From 5b391a8f9b389a6919cfc9b0ce82eaf1363b0ef7 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:53:23 +0800 Subject: [PATCH 01/36] fix(locales): invalid interpolation patterns --- client/locales/zh.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/locales/zh.json b/client/locales/zh.json index 6d90f0533c9..ff6e1054d8a 100644 --- a/client/locales/zh.json +++ b/client/locales/zh.json @@ -3306,7 +3306,7 @@ "defaultMessage": "运行代码" }, "course.assessment.submission.runCodeWithLimit": { - "defaultMessage": "运行代码({attempts Left, pleural, one {# attempt} other {# attempts}} left)" + "defaultMessage": "运行代码({attemptsLeft, plural, one {# attempt} other {# attempts}} left)" }, "course.assessment.submission.saveDraft": { "defaultMessage": "保存草稿" @@ -4719,7 +4719,7 @@ "defaultMessage": "无法创建 {numFailed} 个组。" }, "course.group.GroupShow.GroupManager.GroupManager.createMultiplePartialFailure": { - "defaultMessage": "无法创建 {numFailed} {numFailed,复数,一个 {group} 其他 {groups}}。" + "defaultMessage": "无法创建 {numFailed} {numFailed, plural, one {group} other {groups}}。" }, "course.group.GroupShow.GroupManager.GroupManager.createMultipleSuccess": { "defaultMessage": "{numCreated} {numCreated, plural, one {group was} other {groups were}} 已成功创建。" From f48d53689323b6ddebebe9810fcd424cb9105996 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:53:56 +0800 Subject: [PATCH 02/36] fix(AchievementReordering): clarify button label --- .../achievement/components/misc/AchievementReordering.tsx | 2 +- client/locales/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx index e0011aa1349..e1bdb3dee55 100644 --- a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx +++ b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx @@ -26,7 +26,7 @@ const translations = defineMessages({ }, endReorderAchievement: { id: 'course.achievement.AchievementReordering.endReorderAchievement', - defaultMessage: 'Save New Ordering', + defaultMessage: 'Done reordering', }, updateFailed: { id: 'course.achievement.AchievementReordering.updateFailed', diff --git a/client/locales/en.json b/client/locales/en.json index 074a07bcaa1..5e0623dba09 100644 --- a/client/locales/en.json +++ b/client/locales/en.json @@ -306,7 +306,7 @@ "defaultMessage": "New Achievement" }, "course.achievement.AchievementReordering.endReorderAchievement": { - "defaultMessage": "Save New Ordering" + "defaultMessage": "Done reordering" }, "course.achievement.AchievementReordering.startReorderAchievement": { "defaultMessage": "Reorder" From 9770410fbd0e5bc6cfb0c1c4bf401491a57694d8 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:54:38 +0800 Subject: [PATCH 03/36] perf(AchievementReordering): lazy load jQuery, jQuery UI --- client/app/__test__/setup.js | 4 - .../components/misc/AchievementReordering.tsx | 97 ++++++++++++------- .../lib/components/wrappers/ThemeProvider.tsx | 2 +- client/webpack.common.js | 7 -- 4 files changed, 65 insertions(+), 45 deletions(-) diff --git a/client/app/__test__/setup.js b/client/app/__test__/setup.js index f34b92f8144..0fcb817bfdd 100644 --- a/client/app/__test__/setup.js +++ b/client/app/__test__/setup.js @@ -15,8 +15,6 @@ import './mocks/matchMedia'; Enzyme.configure({ adapter: new Adapter() }); require('@babel/polyfill'); -// Our jquery is from CDN and loaded at runtime, so this is required in test. -const jQuery = require('jquery'); const timeZone = 'Asia/Singapore'; const intlCache = createIntlCache(); @@ -47,8 +45,6 @@ const buildContextOptions = (store) => { global.courseId = courseId; global.window = window; global.muiTheme = muiTheme; -global.$ = jQuery; -global.jQuery = jQuery; global.buildContextOptions = buildContextOptions; window.history.pushState({}, '', `/courses/${courseId}`); diff --git a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx index e1bdb3dee55..89288695de2 100644 --- a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx +++ b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx @@ -1,24 +1,16 @@ +import { useRef, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { Button } from '@mui/material'; +import { LoadingButton } from '@mui/lab'; import CourseAPI from 'api/course'; import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; -require('jquery-ui/ui/widgets/sortable'); - interface AchievementReorderingProps { handleReordering: (state: boolean) => void; isReordering: boolean; } -const styles = { - AchievementReorderingButton: { - fontSize: 14, - marginRight: 12, - }, -}; - const translations = defineMessages({ startReorderAchievement: { id: 'course.achievement.AchievementReordering.startReorderAchievement', @@ -38,12 +30,6 @@ const translations = defineMessages({ }, }); -// Serialise the ordered achievements as data for the API call. -function serializedOrdering(): string { - const options = { attribute: 'achievementid', key: 'achievement_order[]' }; - return $('tbody').first().sortable('serialize', options); -} - const AchievementReordering = ( props: AchievementReorderingProps, ): JSX.Element => { @@ -60,35 +46,80 @@ const AchievementReordering = ( } } + const [loadingSortable, setLoadingSortable] = useState(false); + + const sortableCallbacksRef = useRef<{ + enable: () => void; + disable: () => void; + }>(); + return ( - + ); }; diff --git a/client/app/lib/components/wrappers/ThemeProvider.tsx b/client/app/lib/components/wrappers/ThemeProvider.tsx index bca0fb12815..0b2f0e3e264 100644 --- a/client/app/lib/components/wrappers/ThemeProvider.tsx +++ b/client/app/lib/components/wrappers/ThemeProvider.tsx @@ -74,7 +74,7 @@ const ThemeProvider = (props: ThemeProviderProps): JSX.Element => { classes: { root: 'rounded-full', sizeLarge: 'px-5 py-3', - sizeMedium: 'px-3 py-2', + sizeMedium: 'px-2 py-2', sizeSmall: 'min-w-[6rem] px-1 py-1', }, }, diff --git a/client/webpack.common.js b/client/webpack.common.js index fcc8623d3d0..5b96da75f32 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -164,13 +164,6 @@ module.exports = { ], exclude: /node_modules/, }, - { - test: require.resolve('jquery'), - loader: 'expose-loader', - options: { - exposes: ['jQuery', '$'], - }, - }, { test: require.resolve('./app/lib/moment-timezone'), loader: 'expose-loader', From f5d1f8f0fbfecbdc7e82277516aa6932bcc54a7b Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:57:58 +0800 Subject: [PATCH 04/36] fix(moment): import moment from lib/moment only --- client/app/bundles/common/DashboardPage.tsx | 2 +- .../AssessmentMonitoring/components/HeartbeatsTimeline.tsx | 2 +- .../components/HeartbeatsTimelineChart.tsx | 2 +- .../course/assessment/pages/AssessmentMonitoring/utils.ts | 2 +- .../LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx | 3 +-- .../assessment/submission/reducers/liveFeedbackChats/index.ts | 3 +-- .../reference-timelines/components/DayCalendar/DayCalendar.tsx | 2 +- .../reference-timelines/components/DayCalendar/DayColumn.tsx | 3 ++- .../course/reference-timelines/components/SubmitIndicator.tsx | 2 +- .../reference-timelines/components/TimeBar/DurationBar.tsx | 3 ++- .../course/reference-timelines/components/TimeBar/TimeBar.tsx | 3 ++- .../reference-timelines/components/TimeBar/TimeBarHandle.tsx | 3 ++- .../reference-timelines/components/TimePopup/TimePopup.tsx | 2 +- .../reference-timelines/components/TimePopup/TimePopupForm.tsx | 2 +- .../components/TimelinesStack/AssignableTimeline.tsx | 3 ++- .../reference-timelines/components/TimelinesStack/Timeline.tsx | 2 +- .../course/reference-timelines/contexts/LastSavedContext.tsx | 3 ++- client/app/bundles/course/reference-timelines/utils.ts | 3 ++- .../course/reference-timelines/views/DayView/ItemsSidebar.tsx | 3 ++- 19 files changed, 27 insertions(+), 21 deletions(-) diff --git a/client/app/bundles/common/DashboardPage.tsx b/client/app/bundles/common/DashboardPage.tsx index 4798315b944..be171e86977 100644 --- a/client/app/bundles/common/DashboardPage.tsx +++ b/client/app/bundles/common/DashboardPage.tsx @@ -2,7 +2,6 @@ import { defineMessages } from 'react-intl'; import { Navigate } from 'react-router-dom'; import { ArrowForward } from '@mui/icons-material'; import { Avatar, Stack, Typography } from '@mui/material'; -import moment from 'moment'; import { HomeLayoutCourseData } from 'types/home'; import { getCourseLogoUrl } from 'course/helper'; @@ -13,6 +12,7 @@ import { useAppContext } from 'lib/containers/AppContainer'; import { getUrlParameter } from 'lib/helpers/url-helpers'; import useItems from 'lib/hooks/items/useItems'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import NewCourseButton from './components/NewCourseButton'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx index ea2939f22d5..e14221272c6 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; -import moment from 'moment'; import { HeartbeatDetail } from 'types/channels/liveMonitoring'; import { BrowserAuthorizationMethod } from 'course/assessment/components/monitoring/BrowserAuthorizationMethodOptionsFormFields/common'; +import moment from 'lib/moment'; import HeartbeatDetailCard from './HeartbeatDetailCard'; import HeartbeatsTimelineChart from './HeartbeatsTimelineChart'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx index 3e47f456841..68af626e572 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx @@ -10,12 +10,12 @@ import { PointStyle, } from 'chart.js'; import zoomPlugin from 'chartjs-plugin-zoom'; -import moment from 'moment'; import palette from 'theme/palette'; import { HeartbeatDetail } from 'types/channels/liveMonitoring'; import { useAppSelector } from 'lib/hooks/store'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import translations from '../../../translations'; import { select } from '../selectors'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts index 3ab5a503a8a..ca23d6dc429 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts +++ b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import moment from 'lib/moment'; export type Presence = 'alive' | 'late' | 'missing'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx index 7860f3ca075..fa9f6f7b250 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx @@ -1,6 +1,5 @@ import { Dispatch, FC, SetStateAction } from 'react'; import { Typography } from '@mui/material'; -import moment from 'moment'; import { LiveFeedbackChatMessage } from 'types/course/assessment/submission/liveFeedback'; import { @@ -8,7 +7,7 @@ import { justifyPosition, } from 'course/assessment/submission/components/GetHelpChatPage/utils'; import MarkdownText from 'course/assessment/submission/components/MarkdownText'; -import { SHORT_DATE_TIME_FORMAT } from 'lib/moment'; +import moment, { SHORT_DATE_TIME_FORMAT } from 'lib/moment'; interface Props { messages: LiveFeedbackChatMessage[]; diff --git a/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts b/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts index 60f12b53400..b5e115df408 100644 --- a/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts +++ b/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts @@ -5,9 +5,8 @@ import { PayloadAction, } from '@reduxjs/toolkit'; import { shuffle } from 'lodash'; -import moment from 'moment'; -import { SHORT_TIME_FORMAT } from 'lib/moment'; +import moment, { SHORT_TIME_FORMAT } from 'lib/moment'; import { getLocalStorageValue, diff --git a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx index 719499bdd2c..d3ba32d4527 100644 --- a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx +++ b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx @@ -2,9 +2,9 @@ import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList as List } from 'react-window'; import { Button, Typography } from '@mui/material'; -import moment from 'moment'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import translations from '../../translations'; import { diff --git a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx index 4ca273feeb8..7cee1062211 100644 --- a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx +++ b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx @@ -1,7 +1,8 @@ import { CSSProperties, memo } from 'react'; import { areEqual } from 'react-window'; import { Typography } from '@mui/material'; -import moment from 'moment'; + +import moment from 'lib/moment'; import { getSecondsFromDays, isToday, isWeekend } from '../../utils'; diff --git a/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx b/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx index 416fd39637f..8d77d7dca2b 100644 --- a/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx +++ b/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from 'react'; import { Cancel, CheckCircle } from '@mui/icons-material'; import { Chip, Grow, Tooltip, Typography } from '@mui/material'; -import moment from 'moment'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import { useLastSaved } from '../contexts'; import translations from '../translations'; diff --git a/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx b/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx index b5b64dc5b89..82d74dd9123 100644 --- a/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx @@ -1,5 +1,6 @@ import { MouseEventHandler, ReactNode, TouchEventHandler } from 'react'; -import moment from 'moment'; + +import moment from 'lib/moment'; import { DAY_WIDTH_PIXELS, diff --git a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx index f2d206374ed..9dbae1ef05a 100644 --- a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; -import moment from 'moment'; + +import moment from 'lib/moment'; import { DAY_WIDTH_PIXELS, getDaysFromWidth } from '../../utils'; import HorizontallyDraggable from '../HorizontallyDraggable'; diff --git a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx index 9c5c2a28c03..b6cd8a2ac82 100644 --- a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx @@ -5,7 +5,8 @@ import { TouchEventHandler, } from 'react'; import { Typography } from '@mui/material'; -import moment from 'moment'; + +import moment from 'lib/moment'; interface HandleContentProps { side: 'start' | 'end'; diff --git a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx index ceb9814270b..209aca0c70b 100644 --- a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx @@ -1,5 +1,4 @@ import { Typography } from '@mui/material'; -import moment from 'moment'; import { ItemWithTimeData, TimelineData, @@ -8,6 +7,7 @@ import { import { useAppDispatch } from 'lib/hooks/store'; import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import { useSetLastSaved } from '../../contexts'; import { createTime, deleteTime, updateTime } from '../../operations'; diff --git a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx index b7a7309ffa8..bdb650773d4 100644 --- a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx @@ -1,11 +1,11 @@ import { Controller } from 'react-hook-form'; import { Button, Collapse } from '@mui/material'; -import moment from 'moment'; import { date, object, ref } from 'yup'; import FormDateTimePickerField from 'lib/components/form/fields/DateTimePickerField'; import Form from 'lib/components/form/Form'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import formTranslations from 'lib/translations/form'; import { useLastSaved } from '../../contexts'; diff --git a/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx b/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx index 989b619bf31..1256c169631 100644 --- a/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx @@ -1,11 +1,12 @@ import { useState } from 'react'; -import moment from 'moment'; import { ItemWithTimeData, TimeData, TimelineData, } from 'types/course/referenceTimelines'; +import moment from 'lib/moment'; + import { useLastSaved } from '../../contexts'; import { DraftableTimeData } from '../../utils'; import TimePopup from '../TimePopup'; diff --git a/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx b/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx index c24c262ccc8..9cb7223ab5b 100644 --- a/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx @@ -1,10 +1,10 @@ import { ReactNode, useState } from 'react'; import { Add } from '@mui/icons-material'; import { Typography } from '@mui/material'; -import moment from 'moment'; import { TimeData } from 'types/course/referenceTimelines'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import translations from '../../translations'; import { DAY_WIDTH_PIXELS, getSecondsFromDays } from '../../utils'; diff --git a/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx b/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx index c868b65d4ba..873433794ad 100644 --- a/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx +++ b/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx @@ -6,7 +6,8 @@ import { useContext, useState, } from 'react'; -import moment from 'moment'; + +import moment from 'lib/moment'; type FetchStatus = 'loading' | 'success' | 'failure'; diff --git a/client/app/bundles/course/reference-timelines/utils.ts b/client/app/bundles/course/reference-timelines/utils.ts index b10859b880e..9cce968a80b 100644 --- a/client/app/bundles/course/reference-timelines/utils.ts +++ b/client/app/bundles/course/reference-timelines/utils.ts @@ -1,6 +1,7 @@ -import moment from 'moment'; import { TimeData } from 'types/course/referenceTimelines'; +import moment from 'lib/moment'; + const SECONDS_IN_A_DAY = 86_400 as const; export const DAY_WIDTH_PIXELS = 30 as const; diff --git a/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx b/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx index e0821f4979e..12081c2be64 100644 --- a/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx +++ b/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx @@ -1,10 +1,11 @@ import { Typography } from '@mui/material'; -import moment from 'moment'; import { ItemWithTimeData, TimelineData, } from 'types/course/referenceTimelines'; +import moment from 'lib/moment'; + import { getDaysFromSeconds } from '../../utils'; import TimelineSidebarItem from './TimelineSidebarItem'; From a9f62f1348e82d7281f465fb7472e87f98f280df Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Fri, 18 Apr 2025 14:02:07 +0800 Subject: [PATCH 05/36] perf(EditorField): lazy load Ace's language modes --- .../LiveFeedbackHistory/LiveFeedbackFiles.tsx | 5 +- .../components/answers/Programming/index.jsx | 3 - client/app/initializers.js | 1 - .../components/core/fields/EditorField.tsx | 52 ++++++- client/app/lib/initializers/ace-editor.js | 129 ------------------ 5 files changed, 50 insertions(+), 140 deletions(-) delete mode 100644 client/app/lib/initializers/ace-editor.js diff --git a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx index 192f1e1c003..f605762135f 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx @@ -1,5 +1,4 @@ -import { FC, useRef, useState } from 'react'; -import ReactAce from 'react-ace'; +import { ComponentRef, FC, useRef, useState } from 'react'; import { MessageFile } from 'types/course/assessment/submission/liveFeedback'; import ProgrammingFileDownloadChip from 'course/assessment/submission/components/answers/Programming/ProgrammingFileDownloadChip'; @@ -12,7 +11,7 @@ interface Props { const LiveFeedbackFiles: FC = (props) => { const { file } = props; - const editorRef = useRef(null); + const editorRef = useRef>(null); const [selectedLine, setSelectedLine] = useState(1); diff --git a/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx b/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx index 681af3e71ba..27d3d871e3b 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx +++ b/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx @@ -7,9 +7,6 @@ import { getIsSavingAnswer } from 'course/assessment/submission/selectors/answer import { getSubmission } from 'course/assessment/submission/selectors/submissions'; import { useAppSelector } from 'lib/hooks/store'; -import 'ace-builds/src-noconflict/mode-python'; -import 'ace-builds/src-noconflict/theme-github'; - import CodaveriFeedbackStatus from '../../../containers/CodaveriFeedbackStatus'; import ProgrammingImportEditor from '../../../containers/ProgrammingImport/ProgrammingImportEditor'; import { questionShape } from '../../../propTypes'; diff --git a/client/app/initializers.js b/client/app/initializers.js index eb9d60a4816..264d98d39d0 100644 --- a/client/app/initializers.js +++ b/client/app/initializers.js @@ -1,7 +1,6 @@ /* eslint-disable global-require */ function loadModules() { - require('lib/initializers/ace-editor'); // Require web font last so that it doesn't block the load of current module. require('lib/initializers/webfont'); } diff --git a/client/app/lib/components/core/fields/EditorField.tsx b/client/app/lib/components/core/fields/EditorField.tsx index 953023f138c..39c7fd3809b 100644 --- a/client/app/lib/components/core/fields/EditorField.tsx +++ b/client/app/lib/components/core/fields/EditorField.tsx @@ -1,8 +1,15 @@ -import { ComponentProps, ForwardedRef, forwardRef } from 'react'; +import { + ComponentProps, + ForwardedRef, + forwardRef, + useEffect, + useState, +} from 'react'; import AceEditor from 'react-ace'; import { LanguageMode } from 'types/course/assessment/question/programming'; import 'ace-builds/src-noconflict/theme-github'; +import 'ace-builds/src-noconflict/mode-python'; import './AceEditor.css'; @@ -30,11 +37,48 @@ const DEFAULT_FONT_FAMILY = [ 'monospace', ].join(','); +/** + * Loads Ace's mode scripts on demand for `language` and returns `true` if the mode + * has been loaded. + * + * Remember to update the regex in the `webpackInclude` comment below to bundle any + * new languages we support in the future. + */ +const useLazyMode = (language: LanguageMode): boolean => { + const [loading, setLoading] = useState(true); + + useEffect(() => { + setLoading(true); + + let ignore = false; + + (async (): Promise => { + await import( + /* webpackInclude: /ace-builds\/src-noconflict\/mode-(c_cpp|python|r|java|javascript)\./ */ + /* webpackChunkName: "ace-[request]" */ + `ace-builds/src-noconflict/mode-${language}` + ); + + if (ignore) return; + + setLoading(false); + })(); + + return () => { + ignore = true; + }; + }, [language]); + + return loading; +}; + const EditorField = forwardRef( (props: EditorProps, ref: ForwardedRef): JSX.Element => { const { language, value, disabled, onChange, cursorStart, ...otherProps } = props; + const loading = useLazyMode(language); + return ( '); - $editor.css({ - display: 'none', - position: 'relative', - }); - $editor[0].id = `ace_${id}`; - - return $editor; -} - -/** - * Builds the editor within the given container. - * - * @param {jQuery} $container The container to build the editor in. - * @param {Object} options The options for the editor. - * @returns {Object} The Ace editor instance. - */ -function buildEditor($container, options) { - const editor = ace.edit($container[0]); - editor.setTheme(`ace/theme/${options.theme}`); - editor.getSession().setMode(`ace/mode/${options.lang}`); - - editor.setOptions({ readOnly: !!options.readOnly }); - - return editor; -} - -/** - * Builds the Ace configuration object. - * - * @param {jQuery} $container The container for the Ace editor. - * @param {Object} aceEditor The Ace editor created. - * @returns {Object} - */ -function buildAceOptions($container, aceEditor) { - return { - container: $container, - editor: aceEditor, - }; -} - -/** - * Assigns the Ace configuration to the given element. - * - * @param {jQuery} $element The element which has the Ace editor associated with. - * @param {jQuery} $container The container element for the Ace editor. - * @param {Object} aceEditor The Ace editor created from `ace.edit`. - */ -function assignEditor($element, $container, aceEditor) { - $element.data('ace', buildAceOptions($container, aceEditor)); -} - -/** - * Finds or builds an editor container from the given jQuery element. - * - * @param {jQuery} $element The element to find or build an Ace editor for. - * @param {Object} options The options to use when building the element. - * @returns {jQuery} - */ -function findOrBuildEditorContainer($element, options) { - const aceData = $element.data('ace'); - if (aceData) { - return aceData.container; - } - - const $container = buildEditorContainer($element[0].id); - $container.insertAfter($element); - $container.height($element.height()); - - const editor = buildEditor($container, options); - editor.on('change', () => { - $element.val(editor.session.getValue()); - }); - editor.session.setValue($element.val()); - assignEditor($element, $container, editor); - - return $container; -} - -/** - * Redirects all labels from the old textarea to point to the new editor. - * @param {jQuery} $from The textarea to redirect the labels from. - * @param {jQuery} $editor The container for the Ace editor to redirect the labels to. - */ -function reassignLabelsToAce($from, $editor) { - const fromId = $from[0].id; - const $editorTextarea = $('textarea.ace_text-input', $editor); - $editorTextarea[0].id = `ace_textarea_${fromId}`; - $(`label[for="${fromId}"]`).attr('for', $editorTextarea[0].id); -} - -$.fn.ace = function (opt) { - const options = $.extend({}, $.fn.ace.defaults, opt); - return this.each(function () { - const $this = $(this); - const elementOptions = $.extend({}, options, { - lang: this.lang, - readOnly: $this[0].readOnly, - }); - const $editor = findOrBuildEditorContainer($this, elementOptions); - - $this.hide(); - $editor.show(); - - reassignLabelsToAce($this, $editor); - }); -}; - -$.fn.ace.defaults = { - theme: 'github', - lang: null, -}; From 35c18372c8c6f0398c312833d38bc8033063de13 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Fri, 18 Apr 2025 14:03:34 +0800 Subject: [PATCH 06/36] perf(VideoPlayer): import only YouTube adapter for react-player --- .../bundles/course/video/submission/containers/VideoPlayer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/bundles/course/video/submission/containers/VideoPlayer.jsx b/client/app/bundles/course/video/submission/containers/VideoPlayer.jsx index a1c3c8e0b83..cf8332f1a8b 100644 --- a/client/app/bundles/course/video/submission/containers/VideoPlayer.jsx +++ b/client/app/bundles/course/video/submission/containers/VideoPlayer.jsx @@ -82,7 +82,7 @@ class VideoPlayer extends Component { UNSAFE_componentWillMount() { if (VideoPlayer.ReactPlayer !== undefined) return; // Already loaded - import(/* webpackChunkName: "video" */ 'react-player').then( + import(/* webpackChunkName: "video" */ 'react-player/youtube').then( (ReactPlayer) => { VideoPlayer.ReactPlayer = ReactPlayer.default; this.forceUpdate(); From de58b2ffd9abfaea5d3d9fd3a03a6e0a6961103e Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:40:03 +0800 Subject: [PATCH 07/36] perf(AuthenticatedApp): lazy load all routes --- client/app/bundles/course/container/index.ts | 1 - client/app/routers/AuthenticatedApp.tsx | 1735 ++++++++++++++---- 2 files changed, 1405 insertions(+), 331 deletions(-) delete mode 100644 client/app/bundles/course/container/index.ts diff --git a/client/app/bundles/course/container/index.ts b/client/app/bundles/course/container/index.ts deleted file mode 100644 index 379f1d945fc..00000000000 --- a/client/app/bundles/course/container/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as CourseContainer } from './CourseContainer'; diff --git a/client/app/routers/AuthenticatedApp.tsx b/client/app/routers/AuthenticatedApp.tsx index bdf3f382ecf..ae3e58bc9ed 100644 --- a/client/app/routers/AuthenticatedApp.tsx +++ b/client/app/routers/AuthenticatedApp.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable sonarjs/no-duplicate-string */ import { memo } from 'react'; import { withAuthenticationRequired } from 'react-oidc-context'; @@ -8,145 +9,6 @@ import { RouterProvider, } from 'react-router-dom'; -import GlobalAnnouncementIndex from 'bundles/announcements/GlobalAnnouncementIndex'; -import DashboardPage from 'bundles/common/DashboardPage'; -import AchievementShow from 'bundles/course/achievement/pages/AchievementShow'; -import AchievementsIndex from 'bundles/course/achievement/pages/AchievementsIndex'; -import SettingsNavigation from 'bundles/course/admin/components/SettingsNavigation'; -import AnnouncementSettings from 'bundles/course/admin/pages/AnnouncementsSettings'; -import AssessmentSettings from 'bundles/course/admin/pages/AssessmentSettings'; -import CodaveriSettings from 'bundles/course/admin/pages/CodaveriSettings'; -import CommentsSettings from 'bundles/course/admin/pages/CommentsSettings'; -import ComponentSettings from 'bundles/course/admin/pages/ComponentSettings'; -import CourseSettings from 'bundles/course/admin/pages/CourseSettings'; -import ForumsSettings from 'bundles/course/admin/pages/ForumsSettings'; -import LeaderboardSettings from 'bundles/course/admin/pages/LeaderboardSettings'; -import LessonPlanSettings from 'bundles/course/admin/pages/LessonPlanSettings'; -import MaterialsSettings from 'bundles/course/admin/pages/MaterialsSettings'; -import NotificationSettings from 'bundles/course/admin/pages/NotificationSettings'; -import RagWiseSettings from 'bundles/course/admin/pages/RagWiseSettings'; -import SidebarSettings from 'bundles/course/admin/pages/SidebarSettings'; -import VideosSettings from 'bundles/course/admin/pages/VideosSettings'; -import AnnouncementsIndex from 'bundles/course/announcements/pages/AnnouncementsIndex'; -import AssessmentEdit from 'bundles/course/assessment/pages/AssessmentEdit'; -import AssessmentMonitoring from 'bundles/course/assessment/pages/AssessmentMonitoring'; -import AssessmentShow from 'bundles/course/assessment/pages/AssessmentShow'; -import AssessmentsIndex from 'bundles/course/assessment/pages/AssessmentsIndex'; -import AssessmentStatistics from 'bundles/course/assessment/pages/AssessmentStatistics'; -import EditForumPostResponsePage from 'bundles/course/assessment/question/forum-post-responses/EditForumPostResponsePage'; -import NewForumPostResponsePage from 'bundles/course/assessment/question/forum-post-responses/NewForumPostResponsePage'; -import EditMcqMrqPage from 'bundles/course/assessment/question/multiple-responses/EditMcqMrqPage'; -import NewMcqMrqPage from 'bundles/course/assessment/question/multiple-responses/NewMcqMrqPage'; -import EditProgrammingQuestionPage from 'bundles/course/assessment/question/programming/EditProgrammingQuestionPage'; -import NewProgrammingQuestionPage from 'bundles/course/assessment/question/programming/NewProgrammingQuestionPage'; -import ScribingQuestion from 'bundles/course/assessment/question/scribing/ScribingQuestion'; -import EditTextResponse from 'bundles/course/assessment/question/text-responses/EditTextResponsePage'; -import NewTextResponse from 'bundles/course/assessment/question/text-responses/NewTextResponsePage'; -import EditVoicePage from 'bundles/course/assessment/question/voice-responses/EditVoicePage'; -import NewVoicePage from 'bundles/course/assessment/question/voice-responses/NewVoicePage'; -import AssessmentSessionNew from 'bundles/course/assessment/sessions/pages/AssessmentSessionNew'; -import SkillsIndex from 'bundles/course/assessment/skills/pages/SkillsIndex'; -import LogsIndex from 'bundles/course/assessment/submission/pages/LogsIndex'; -import SubmissionEditIndex from 'bundles/course/assessment/submission/pages/SubmissionEditIndex'; -import AssessmentSubmissionsIndex from 'bundles/course/assessment/submission/pages/SubmissionsIndex'; -import SubmissionsIndex from 'bundles/course/assessment/submissions/SubmissionsIndex'; -import CourseShow from 'bundles/course/courses/pages/CourseShow'; -import CommentIndex from 'bundles/course/discussion/topics/pages/CommentIndex'; -import Duplication from 'bundles/course/duplication/pages/Duplication'; -import UserRequests from 'bundles/course/enrol-requests/pages/UserRequests'; -import ExperiencePointsIndex from 'bundles/course/experience-points'; -import ForumShow from 'bundles/course/forum/pages/ForumShow'; -import ForumsIndex from 'bundles/course/forum/pages/ForumsIndex'; -import ForumTopicShow from 'bundles/course/forum/pages/ForumTopicShow'; -import GroupIndex from 'bundles/course/group/pages/GroupIndex'; -import GroupShow from 'bundles/course/group/pages/GroupShow'; -import LeaderboardIndex from 'bundles/course/leaderboard/pages/LeaderboardIndex'; -import LearningMap from 'bundles/course/learning-map/containers/LearningMap'; -import LessonPlanLayout from 'bundles/course/lesson-plan/containers/LessonPlanLayout'; -import LessonPlanEdit from 'bundles/course/lesson-plan/pages/LessonPlanEdit'; -import LessonPlanShow from 'bundles/course/lesson-plan/pages/LessonPlanShow'; -import LevelsIndex from 'bundles/course/level/pages/LevelsIndex'; -import DownloadingFilePage from 'bundles/course/material/files/DownloadingFilePage'; -import ErrorRetrievingFilePage from 'bundles/course/material/files/ErrorRetrievingFilePage'; -import FolderShow from 'bundles/course/material/folders/pages/FolderShow'; -import TimelineDesigner from 'bundles/course/reference-timelines/TimelineDesigner'; -import ResponseEdit from 'bundles/course/survey/pages/ResponseEdit'; -import ResponseIndex from 'bundles/course/survey/pages/ResponseIndex'; -import ResponseShow from 'bundles/course/survey/pages/ResponseShow'; -import SurveyIndex from 'bundles/course/survey/pages/SurveyIndex'; -import SurveyResults from 'bundles/course/survey/pages/SurveyResults'; -import SurveyShow from 'bundles/course/survey/pages/SurveyShow'; -import UserEmailSubscriptions from 'bundles/course/user-email-subscriptions/UserEmailSubscriptions'; -import InvitationsIndex from 'bundles/course/user-invitations/pages/InvitationsIndex'; -import InviteUsers from 'bundles/course/user-invitations/pages/InviteUsers'; -import ExperiencePointsRecords from 'bundles/course/users/pages/ExperiencePointsRecords'; -import ManageStaff from 'bundles/course/users/pages/ManageStaff'; -import ManageStudents from 'bundles/course/users/pages/ManageStudents'; -import PersonalTimes from 'bundles/course/users/pages/PersonalTimes'; -import PersonalTimesShow from 'bundles/course/users/pages/PersonalTimesShow'; -import CourseUserShow from 'bundles/course/users/pages/UserShow'; -import UsersIndex from 'bundles/course/users/pages/UsersIndex'; -import VideoShow from 'bundles/course/video/pages/VideoShow'; -import VideosIndex from 'bundles/course/video/pages/VideosIndex'; -import VideoSubmissionEdit from 'bundles/course/video/submission/pages/VideoSubmissionEdit'; -import VideoSubmissionShow from 'bundles/course/video/submission/pages/VideoSubmissionShow'; -import VideoSubmissionsIndex from 'bundles/course/video/submission/pages/VideoSubmissionsIndex'; -import UserVideoSubmissionsIndex from 'bundles/course/video-submissions/pages/UserVideoSubmissionsIndex'; -import AdminNavigator from 'bundles/system/admin/admin/AdminNavigator'; -import AnnouncementIndex from 'bundles/system/admin/admin/pages/AnnouncementsIndex'; -import CourseIndex from 'bundles/system/admin/admin/pages/CoursesIndex'; -import InstancesIndex from 'bundles/system/admin/admin/pages/InstancesIndex'; -import UserIndex from 'bundles/system/admin/admin/pages/UsersIndex'; -import InstanceAdminNavigator from 'bundles/system/admin/instance/instance/InstanceAdminNavigator'; -import InstanceAnnouncementsIndex from 'bundles/system/admin/instance/instance/pages/InstanceAnnouncementsIndex'; -import InstanceComponentsIndex from 'bundles/system/admin/instance/instance/pages/InstanceComponentsIndex'; -import InstanceCoursesIndex from 'bundles/system/admin/instance/instance/pages/InstanceCoursesIndex'; -import InstanceUserRoleRequestsIndex from 'bundles/system/admin/instance/instance/pages/InstanceUserRoleRequestsIndex'; -import InstanceUsersIndex from 'bundles/system/admin/instance/instance/pages/InstanceUsersIndex'; -import InstanceUsersInvitations from 'bundles/system/admin/instance/instance/pages/InstanceUsersInvitations'; -import InstanceUsersInvite from 'bundles/system/admin/instance/instance/pages/InstanceUsersInvite'; -import AccountSettings from 'bundles/user/AccountSettings'; -import ConfirmEmailPage from 'bundles/users/pages/ConfirmEmailPage'; -import UserShow from 'bundles/users/pages/UserShow'; -import { achievementHandle } from 'course/achievement/handles'; -import StoriesSettings from 'course/admin/pages/StoriesSettings'; -import { announcementsHandle } from 'course/announcements/handles'; -import assessmentAttemptLoader from 'course/assessment/attemptLoader'; -import { - assessmentHandle, - assessmentsHandle, - questionHandle, -} from 'course/assessment/handles'; -import GenerateProgrammingQuestionPage from 'course/assessment/pages/AssessmentGenerate/GenerateProgrammingQuestionPage'; -import QuestionFormOutlet from 'course/assessment/question/components/QuestionFormOutlet'; -import { CourseContainer } from 'course/container'; -import { commentHandle } from 'course/discussion/topics/handles'; -import { - forumHandle, - forumNameHandle, - forumTopicHandle, -} from 'course/forum/handles'; -import { leaderboardHandle } from 'course/leaderboard/handles'; -import { folderHandle } from 'course/material/folders/handles'; -import materialLoader from 'course/material/materialLoader'; -import { videoWatchHistoryHandle } from 'course/statistics/handles'; -import StatisticsIndex from 'course/statistics/pages/StatisticsIndex'; -import AssessmentsStatistics from 'course/statistics/pages/StatisticsIndex/assessments/AssessmentsStatistics'; -import CourseStatistics from 'course/statistics/pages/StatisticsIndex/course/CourseStatistics'; -import StaffStatistics from 'course/statistics/pages/StatisticsIndex/staff/StaffStatistics'; -import StudentsStatistics from 'course/statistics/pages/StatisticsIndex/students/StudentsStatistics'; -import LearnRedirect from 'course/stories/components/LearnRedirect'; -import LearnPage from 'course/stories/pages/LearnPage'; -import MissionControlPage from 'course/stories/pages/MissionControlPage'; -import { surveyHandle, surveyResponseHandle } from 'course/survey/handles'; -import { - courseUserHandle, - courseUserPersonalizedTimelineHandle, - manageUserHandles, -} from 'course/users/handles'; -import videoAttemptLoader from 'course/video/attemptLoader'; -import { videoHandle, videosHandle } from 'course/video/handles'; -import CourselessContainer from 'lib/containers/CourselessContainer'; import useTranslation, { Translated } from 'lib/hooks/useTranslation'; import { reservedRoutes } from './redirects'; @@ -156,61 +18,187 @@ const authenticatedRouter: Translated = (t) => createAppRouter([ { path: 'courses/:courseId', - element: , - loader: CourseContainer.loader, - handle: CourseContainer.handle, + lazy: async () => { + const CourseContainer = ( + await import( + /* webpackChunkName: 'CourseContainer' */ + 'course/container/CourseContainer' + ) + ).default; + + return { + Component: CourseContainer, + handle: CourseContainer.handle, + loader: CourseContainer.loader, + }; + }, shouldRevalidate: ({ currentParams, nextParams }): boolean => { return currentParams.courseId !== nextParams.courseId; }, children: [ { index: true, - element: ( - } to="learn" /> - ), + lazy: async () => { + const [CourseShow, LearnRedirect] = await Promise.all([ + import( + /* webpackChunkName: 'CourseShow' */ + 'course/courses/pages/CourseShow' + ).then((module) => module.default), + import( + /* webpackChunkName: 'LearnRedirect' */ + 'course/stories/components/LearnRedirect' + ).then((module) => module.default), + ]); + + return { + element: ( + } to="learn" /> + ), + }; + }, }, { path: 'home', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'CourseShow' */ + 'course/courses/pages/CourseShow' + ) + ).default, + }), }, { path: 'learn', - handle: LearnPage.handle, - element: , + lazy: async () => { + const LearnPage = ( + await import( + /* webpackChunkName: 'LearnPage' */ + 'course/stories/pages/LearnPage' + ) + ).default; + + return { + Component: LearnPage, + handle: LearnPage.handle, + }; + }, }, { path: 'mission_control', - handle: MissionControlPage.handle, - element: , + lazy: async () => { + const MissionControlPage = ( + await import( + /* webpackChunkName: 'MissionControlPage' */ + 'course/stories/pages/MissionControlPage' + ) + ).default; + + return { + Component: MissionControlPage, + handle: MissionControlPage.handle, + }; + }, }, { path: 'timelines', - handle: TimelineDesigner.handle, - element: , + lazy: async () => { + const TimelineDesigner = ( + await import( + /* webpackChunkName: 'TimelineDesigner' */ + 'course/reference-timelines/TimelineDesigner' + ) + ).default; + + return { + Component: TimelineDesigner, + handle: TimelineDesigner.handle, + }; + }, }, { path: 'announcements', - handle: announcementsHandle, - element: , + lazy: async () => { + const [announcementsHandle, AnnouncementsIndex] = await Promise.all( + [ + import( + /* webpackChunkName: 'announcementsHandle' */ + 'course/announcements/handles' + ).then((module) => module.announcementsHandle), + import( + /* webpackChunkName: 'AnnouncementsIndex' */ + 'course/announcements/pages/AnnouncementsIndex' + ).then((module) => module.default), + ], + ); + + return { + Component: AnnouncementsIndex, + handle: announcementsHandle, + }; + }, }, { path: 'comments', - handle: commentHandle, - element: , + lazy: async () => { + const [commentHandle, CommentIndex] = await Promise.all([ + import( + /* webpackChunkName: 'commentHandle' */ + 'course/discussion/topics/handles' + ).then((module) => module.commentHandle), + import( + /* webpackChunkName: 'CommentIndex' */ + 'course/discussion/topics/pages/CommentIndex' + ).then((module) => module.default), + ]); + + return { + Component: CommentIndex, + handle: commentHandle, + }; + }, }, { path: 'leaderboard', - handle: leaderboardHandle, - element: , + lazy: async () => { + const [leaderboardHandle, LeaderboardIndex] = await Promise.all([ + import( + /* webpackChunkName: 'leaderboardHandle' */ + 'course/leaderboard/handles' + ).then((module) => module.leaderboardHandle), + import( + /* webpackChunkName: 'LeaderboardIndex' */ + 'course/leaderboard/pages/LeaderboardIndex' + ).then((module) => module.default), + ]); + + return { + Component: LeaderboardIndex, + handle: leaderboardHandle, + }; + }, }, { path: 'learning_map', - handle: LearningMap.handle, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'LearningMap' */ + 'course/learning-map/containers/LearningMap' + ) + ).default, + }), }, { path: 'materials/folders', - handle: folderHandle, + lazy: async () => ({ + handle: ( + await import( + /* webpackChunkName: 'folderHandle' */ + 'course/material/folders/handles' + ) + ).folderHandle, + }), // `:folderId` must be split this way so that `folderHandle` is matched // to the stable (non-changing) match of `/materials/folders`. This allows // the crumbs in the Workbin to not disappear when revalidated by the @@ -218,20 +206,57 @@ const authenticatedRouter: Translated = (t) => children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'FolderShow' */ + 'course/material/folders/pages/FolderShow' + ) + ).default, + }), }, { path: ':folderId', children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'FolderShow' */ + 'course/material/folders/pages/FolderShow' + ) + ).default, + }), }, { path: 'files/:materialId', - loader: materialLoader, - errorElement: , - element: , + lazy: async () => { + const [ + materialLoader, + ErrorRetrievingFilePage, + DownloadingFilePage, + ] = await Promise.all([ + import( + /* webpackChunkName: 'materialLoader' */ + 'course/material/materialLoader' + ).then((module) => module.default), + import( + /* webpackChunkName: 'ErrorRetrievingFilePage' */ + 'course/material/files/ErrorRetrievingFilePage' + ).then((module) => module.default), + import( + /* webpackChunkName: 'DownloadingFilePage' */ + 'course/material/files/DownloadingFilePage' + ).then((module) => module.default), + ]); + + return { + loader: materialLoader, + errorElement: , + element: , + }; + }, }, ], }, @@ -239,237 +264,691 @@ const authenticatedRouter: Translated = (t) => }, { path: 'levels', - handle: LevelsIndex.handle, - element: , + lazy: async () => { + const LevelsIndex = ( + await import( + /* webpackChunkName: 'LevelsIndex' */ + 'course/level/pages/LevelsIndex' + ) + ).default; + + return { + Component: LevelsIndex, + handle: LevelsIndex.handle, + }; + }, }, { path: 'statistics', - handle: StatisticsIndex.handle, - element: , + lazy: async () => { + const StatisticsIndex = ( + await import( + /* webpackChunkName: 'StatisticsIndex' */ + 'course/statistics/pages/StatisticsIndex' + ) + ).default; + + return { + Component: StatisticsIndex, + handle: StatisticsIndex.handle, + }; + }, children: [ { path: 'students', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'StudentsStatistics' */ + 'course/statistics/pages/StatisticsIndex/students/StudentsStatistics' + ) + ).default, + }), }, { path: 'staff', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'StaffStatistics' */ + 'course/statistics/pages/StatisticsIndex/staff/StaffStatistics' + ) + ).default, + }), }, { path: 'course', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'CourseStatistics' */ + 'course/statistics/pages/StatisticsIndex/course/CourseStatistics' + ) + ).default, + }), }, { path: 'assessments', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'CourseStatistics' */ + 'course/statistics/pages/StatisticsIndex/assessments/AssessmentsStatistics' + ) + ).default, + }), }, ], }, { path: 'duplication', - handle: Duplication.handle, - element: , + lazy: async () => { + const Duplication = ( + await import( + /* webpackChunkName: 'Duplication' */ + 'course/duplication/pages/Duplication' + ) + ).default; + + return { + Component: Duplication, + handle: Duplication.handle, + }; + }, }, { path: 'enrol_requests', - handle: manageUserHandles.enrolRequests, - element: , + lazy: async () => { + const [manageUserHandles, UserRequests] = await Promise.all([ + import( + /* webpackChunkName: 'userHandles' */ + 'course/users/handles' + ).then((module) => module.manageUserHandles), + import( + /* webpackChunkName: 'UserRequests' */ + 'course/enrol-requests/pages/UserRequests' + ).then((module) => module.default), + ]); + + return { + Component: UserRequests, + handle: manageUserHandles.enrolRequests, + }; + }, }, { path: 'user_invitations', - handle: manageUserHandles.invitations, - element: , + lazy: async () => { + const [manageUserHandles, UserRequests] = await Promise.all([ + import( + /* webpackChunkName: 'userHandles' */ + 'course/users/handles' + ).then((module) => module.manageUserHandles), + import( + /* webpackChunkName: 'InvitationsIndex' */ + 'course/user-invitations/pages/InvitationsIndex' + ).then((module) => module.default), + ]); + + return { + Component: UserRequests, + handle: manageUserHandles.invitations, + }; + }, }, { path: 'students', - handle: manageUserHandles.students, - element: , + lazy: async () => { + const [manageUserHandles, UserRequests] = await Promise.all([ + import( + /* webpackChunkName: 'userHandles' */ + 'course/users/handles' + ).then((module) => module.manageUserHandles), + import( + /* webpackChunkName: 'ManageStudents' */ + 'course/users/pages/ManageStudents' + ).then((module) => module.default), + ]); + + return { + Component: UserRequests, + handle: manageUserHandles.students, + }; + }, }, { path: 'staff', - handle: manageUserHandles.staff, - element: , + lazy: async () => { + const [manageUserHandles, UserRequests] = await Promise.all([ + import( + /* webpackChunkName: 'userHandles' */ + 'course/users/handles' + ).then((module) => module.manageUserHandles), + import( + /* webpackChunkName: 'ManageStaff' */ + 'course/users/pages/ManageStaff' + ).then((module) => module.default), + ]); + + return { + Component: UserRequests, + handle: manageUserHandles.staff, + }; + }, }, { path: 'lesson_plan', - // @ts-ignore `connect` throws error when cannot find `store` as direct parent - element: , - handle: LessonPlanLayout.handle, + lazy: async () => { + const LessonPlanLayout = ( + await import( + /* webpackChunkName: 'LessonPlanLayout' */ + 'course/lesson-plan/containers/LessonPlanLayout' + ) + ).default; + + return { + // @ts-ignore `connect` throws error when cannot find `store` as direct parent + element: , + handle: LessonPlanLayout.handle, + }; + }, children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'LessonPlanShow' */ + 'course/lesson-plan/pages/LessonPlanShow' + ) + ).default, + }), }, { path: 'edit', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'LessonPlanEdit' */ + 'course/lesson-plan/pages/LessonPlanEdit' + ) + ).default, + }), }, ], }, { path: 'experience_points_records', - handle: ExperiencePointsIndex.handle, - element: , + lazy: async () => { + const ExperiencePointsIndex = ( + await import( + /* webpackChunkName: 'ExperiencePointsIndex' */ + 'course/experience-points' + ) + ).default; + + return { + Component: ExperiencePointsIndex, + handle: ExperiencePointsIndex.handle, + }; + }, }, { path: 'users', children: [ { index: true, - handle: UsersIndex.handle, - element: , + lazy: async () => { + const UsersIndex = ( + await import( + /* webpackChunkName: 'UsersIndex' */ + 'course/users/pages/UsersIndex' + ) + ).default; + + return { + Component: UsersIndex, + handle: UsersIndex.handle, + }; + }, }, { path: 'personal_times', - handle: manageUserHandles.personalizedTimelines, - element: , + lazy: async () => { + const [manageUserHandles, PersonalTimes] = await Promise.all([ + import( + /* webpackChunkName: 'userHandles' */ + 'course/users/handles' + ).then((module) => module.manageUserHandles), + import( + /* webpackChunkName: 'PersonalTimes' */ + 'course/users/pages/PersonalTimes' + ).then((module) => module.default), + ]); + + return { + Component: PersonalTimes, + handle: manageUserHandles.personalizedTimelines, + }; + }, }, { path: 'invite', - handle: manageUserHandles.inviteUsers, - element: , + lazy: async () => { + const [manageUserHandles, InviteUsers] = await Promise.all([ + import( + /* webpackChunkName: 'userHandles' */ + 'course/users/handles' + ).then((module) => module.manageUserHandles), + import( + /* webpackChunkName: 'InviteUsers' */ + 'course/user-invitations/pages/InviteUsers' + ).then((module) => module.default), + ]); + + return { + Component: InviteUsers, + handle: manageUserHandles.inviteUsers, + }; + }, }, { path: ':userId', - handle: courseUserHandle, + lazy: async () => ({ + handle: await import( + /* webpackChunkName: 'userHandles' */ + 'course/users/handles' + ).then((module) => module.courseUserHandle), + }), children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'CourseUserShow' */ + 'course/users/pages/UserShow' + ) + ).default, + }), }, { path: 'experience_points_records', - handle: ExperiencePointsRecords.handle, - element: , + lazy: async () => { + const ExperiencePointsRecords = ( + await import( + /* webpackChunkName: 'ExperiencePointsRecords' */ + 'course/users/pages/ExperiencePointsRecords' + ) + ).default; + + return { + Component: ExperiencePointsRecords, + handle: ExperiencePointsRecords.handle, + }; + }, }, ], }, { path: ':userId/personal_times', - handle: courseUserPersonalizedTimelineHandle, - element: , + lazy: async () => { + const [courseUserPersonalizedTimelineHandle, InviteUsers] = + await Promise.all([ + import( + /* webpackChunkName: 'userHandles' */ + 'course/users/handles' + ).then( + (module) => module.courseUserPersonalizedTimelineHandle, + ), + import( + /* webpackChunkName: 'PersonalTimesShow' */ + 'course/users/pages/PersonalTimesShow' + ).then((module) => module.default), + ]); + + return { + Component: InviteUsers, + handle: courseUserPersonalizedTimelineHandle, + }; + }, }, { path: ':userId/video_submissions', - handle: videoWatchHistoryHandle, - element: , + lazy: async () => { + const [videoWatchHistoryHandle, UserVideoSubmissionsIndex] = + await Promise.all([ + import( + /* webpackChunkName: 'videoWatchHistoryHandle' */ + 'course/statistics/handles' + ).then((module) => module.videoWatchHistoryHandle), + import( + /* webpackChunkName: 'UserVideoSubmissionsIndex' */ + 'course/video-submissions/pages/UserVideoSubmissionsIndex' + ).then((module) => module.default), + ]); + + return { + Component: UserVideoSubmissionsIndex, + handle: videoWatchHistoryHandle, + }; + }, }, { path: ':userId/manage_email_subscription', - handle: UserEmailSubscriptions.handle, - element: , + lazy: async () => { + const UserEmailSubscriptions = ( + await import( + /* webpackChunkName: 'UserEmailSubscriptions' */ + 'course/user-email-subscriptions/UserEmailSubscriptions' + ) + ).default; + + return { + Component: UserEmailSubscriptions, + handle: UserEmailSubscriptions.handle, + }; + }, }, ], }, { path: 'admin', - loader: SettingsNavigation.loader, - handle: SettingsNavigation.handle, - element: , + lazy: async () => { + const SettingsNavigation = ( + await import( + /* webpackChunkName: 'SettingsNavigation' */ + 'course/admin/components/SettingsNavigation' + ) + ).default; + + return { + Component: SettingsNavigation, + handle: SettingsNavigation.handle, + loader: SettingsNavigation.loader, + }; + }, children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'CourseSettings' */ + 'course/admin/pages/CourseSettings' + ) + ).default, + }), }, { path: 'components', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'ComponentSettings' */ + 'course/admin/pages/ComponentSettings' + ) + ).default, + }), }, { path: 'sidebar', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'SidebarSettings' */ + 'course/admin/pages/SidebarSettings' + ) + ).default, + }), }, { path: 'notifications', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'NotificationSettings' */ + 'course/admin/pages/NotificationSettings' + ) + ).default, + }), }, { path: 'announcements', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'AnnouncementsSettings' */ + 'course/admin/pages/AnnouncementsSettings' + ) + ).default, + }), }, { path: 'assessments', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'AssessmentSettings' */ + 'course/admin/pages/AssessmentSettings' + ) + ).default, + }), }, { path: 'materials', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'MaterialsSettings' */ + 'course/admin/pages/MaterialsSettings' + ) + ).default, + }), }, { path: 'forums', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'ForumsSettings' */ + 'course/admin/pages/ForumsSettings' + ) + ).default, + }), }, { path: 'leaderboard', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'LeaderboardSettings' */ + 'course/admin/pages/LeaderboardSettings' + ) + ).default, + }), }, { path: 'comments', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'CommentsSettings' */ + 'course/admin/pages/CommentsSettings' + ) + ).default, + }), }, { path: 'videos', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'VideosSettings' */ + 'course/admin/pages/VideosSettings' + ) + ).default, + }), }, { path: 'lesson_plan', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'LessonPlanSettings' */ + 'course/admin/pages/LessonPlanSettings' + ) + ).default, + }), }, { path: 'codaveri', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'CodaveriSettings' */ + 'course/admin/pages/CodaveriSettings' + ) + ).default, + }), }, { path: 'stories', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'StoriesSettings' */ + 'course/admin/pages/StoriesSettings' + ) + ).default, + }), }, { path: 'rag_wise', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'RagWiseSettings' */ + 'course/admin/pages/RagWiseSettings' + ) + ).default, + }), }, ], }, { path: 'surveys', - handle: SurveyIndex.handle, + lazy: async () => ({ + handle: ( + await import( + /* webpackChunkName: 'SurveyIndex' */ + 'course/survey/pages/SurveyIndex' + ) + ).default.handle, + }), children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'SurveyIndex' */ + 'course/survey/pages/SurveyIndex' + ) + ).default, + }), }, { path: ':surveyId', - handle: surveyHandle, + lazy: async () => ({ + handle: ( + await import( + /* webpackChunkName: 'surveyHandles' */ + 'course/survey/handles' + ) + ).surveyHandle, + }), children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'SurveyShow' */ + 'course/survey/pages/SurveyShow' + ) + ).default, + }), }, { path: 'results', - handle: SurveyResults.handle, - element: , + lazy: async () => { + const SurveyResults = ( + await import( + /* webpackChunkName: 'SurveyResults' */ + 'course/survey/pages/SurveyResults' + ) + ).default; + + return { + Component: SurveyResults, + handle: SurveyResults.handle, + }; + }, }, { path: 'responses', children: [ { index: true, - handle: ResponseIndex.handle, - element: , + lazy: async () => { + const ResponseIndex = ( + await import( + /* webpackChunkName: 'ResponseIndex' */ + 'course/survey/pages/ResponseIndex' + ) + ).default; + + return { + Component: ResponseIndex, + handle: ResponseIndex.handle, + }; + }, }, { path: ':responseId', children: [ { index: true, - handle: surveyResponseHandle, - element: , + lazy: async () => { + const [surveyResponseHandle, ResponseShow] = + await Promise.all([ + import( + /* webpackChunkName: 'surveyHandles' */ + 'course/survey/handles' + ).then((module) => module.surveyResponseHandle), + import( + /* webpackChunkName: 'ResponseShow' */ + 'course/survey/pages/ResponseShow' + ).then((module) => module.default), + ]); + + return { + Component: ResponseShow, + handle: surveyResponseHandle, + }; + }, }, { path: 'edit', - handle: ResponseEdit.handle, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'ResponseEdit' */ + 'course/survey/pages/ResponseEdit' + ) + ).default, + }), }, ], }, @@ -481,50 +960,128 @@ const authenticatedRouter: Translated = (t) => }, { path: 'groups', - element: , - handle: GroupIndex.handle, + lazy: async () => { + const GroupIndex = ( + await import( + /* webpackChunkName: 'GroupIndex' */ + 'course/group/pages/GroupIndex' + ) + ).default; + + return { + Component: GroupIndex, + handle: GroupIndex.handle, + }; + }, children: [ { path: ':groupCategoryId', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'GroupShow' */ + 'course/group/pages/GroupShow' + ) + ).default, + }), }, ], }, { path: 'videos', - handle: videosHandle, + lazy: async () => ({ + handle: ( + await import( + /* webpackChunkName: 'videoHandles' */ + 'course/video/handles' + ) + ).videosHandle, + }), children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'VideosIndex' */ + 'course/video/pages/VideosIndex' + ) + ).default, + }), }, { path: ':videoId', - handle: videoHandle, + lazy: async () => ({ + handle: ( + await import( + /* webpackChunkName: 'videoHandles' */ + 'course/video/handles' + ) + ).videoHandle, + }), children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'VideoShow' */ + 'course/video/pages/VideoShow' + ) + ).default, + }), }, { path: 'submissions', children: [ { index: true, - handle: VideoSubmissionsIndex.handle, - element: , + lazy: async () => { + const VideoSubmissionsIndex = ( + await import( + /* webpackChunkName: 'VideoSubmissionsIndex' */ + 'course/video/submission/pages/VideoSubmissionsIndex' + ) + ).default; + + return { + Component: VideoSubmissionsIndex, + handle: VideoSubmissionsIndex.handle, + }; + }, }, { path: ':submissionId', - handle: VideoSubmissionShow.handle, + lazy: async () => ({ + handle: ( + await import( + /* webpackChunkName: 'VideoSubmissionShow' */ + 'course/video/submission/pages/VideoSubmissionShow' + ) + ).default.handle, + }), children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'VideoSubmissionShow' */ + 'course/video/submission/pages/VideoSubmissionShow' + ) + ).default, + }), }, { path: 'edit', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'VideoSubmissionEdit' */ + 'course/video/submission/pages/VideoSubmissionEdit' + ) + ).default, + }), }, ], }, @@ -532,7 +1089,16 @@ const authenticatedRouter: Translated = (t) => }, { path: 'attempt', - loader: videoAttemptLoader(t), + lazy: async () => { + const videoAttemptLoader = ( + await import( + /* webpackChunkName: 'videoAttemptLoader' */ + 'course/video/attemptLoader' + ) + ).default; + + return { loader: videoAttemptLoader(t) }; + }, }, ], }, @@ -540,24 +1106,68 @@ const authenticatedRouter: Translated = (t) => }, { path: 'forums', - handle: forumHandle, + lazy: async () => ({ + handle: ( + await import( + /* webpackChunkName: 'forumHandles' */ + 'course/forum/handles' + ) + ).forumHandle, + }), children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'ForumsIndex' */ + 'course/forum/pages/ForumsIndex' + ) + ).default, + }), }, { path: ':forumId', - handle: forumNameHandle, + lazy: async () => ({ + handle: ( + await import( + /* webpackChunkName: 'forumHandles' */ + 'course/forum/handles' + ) + ).forumNameHandle, + }), children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'ForumShow' */ + 'course/forum/pages/ForumShow' + ) + ).default, + }), }, { path: 'topics/:topicId', - handle: forumTopicHandle, - element: , + lazy: async () => { + const [forumTopicHandle, ForumTopicShow] = + await Promise.all([ + import( + /* webpackChunkName: 'forumHandles' */ + 'course/forum/handles' + ).then((module) => module.forumTopicHandle), + import( + /* webpackChunkName: 'ForumTopicShow' */ + 'course/forum/pages/ForumTopicShow' + ).then((module) => module.default), + ]); + + return { + Component: ForumTopicShow, + handle: forumTopicHandle, + }; + }, }, ], }, @@ -565,88 +1175,249 @@ const authenticatedRouter: Translated = (t) => }, { path: 'achievements', - handle: AchievementsIndex.handle, + lazy: async () => ({ + handle: ( + await import( + /* webpackChunkName: 'AchievementsIndex' */ + 'course/achievement/pages/AchievementsIndex' + ) + ).default.handle, + }), children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'AchievementsIndex' */ + 'course/achievement/pages/AchievementsIndex' + ) + ).default, + }), }, { path: ':achievementId', - handle: achievementHandle, - element: , + lazy: async () => { + const [achievementHandle, AchievementShow] = await Promise.all([ + import( + /* webpackChunkName: 'achievementHandle' */ + 'course/achievement/handles' + ).then((module) => module.achievementHandle), + import( + /* webpackChunkName: 'AchievementShow' */ + 'course/achievement/pages/AchievementShow' + ).then((module) => module.default), + ]); + + return { + Component: AchievementShow, + handle: achievementHandle, + }; + }, }, ], }, { path: 'assessments', - handle: assessmentsHandle, + lazy: async () => ({ + handle: ( + await import( + /* webpackChunkName: 'assessmentHandles' */ + 'course/assessment/handles' + ) + ).assessmentsHandle, + }), children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'AssessmentsIndex' */ + 'bundles/course/assessment/pages/AssessmentsIndex' + ) + ).default, + }), }, { path: 'submissions', - handle: SubmissionsIndex.handle, - element: , + lazy: async () => { + const SubmissionsIndex = ( + await import( + /* webpackChunkName: 'SubmissionsIndex' */ + 'course/assessment/submissions/SubmissionsIndex' + ) + ).default; + + return { + Component: SubmissionsIndex, + handle: SubmissionsIndex.handle, + }; + }, }, { path: 'skills', - handle: SkillsIndex.handle, - element: , + lazy: async () => { + const SkillsIndex = ( + await import( + /* webpackChunkName: 'SkillsIndex' */ + 'course/assessment/skills/pages/SkillsIndex' + ) + ).default; + + return { + Component: SkillsIndex, + handle: SkillsIndex.handle, + }; + }, }, { path: ':assessmentId', - handle: assessmentHandle, + lazy: async () => ({ + handle: ( + await import( + /* webpackChunkName: 'assessmentHandles' */ + 'course/assessment/handles' + ) + ).assessmentHandle, + }), children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'AssessmentShow' */ + 'course/assessment/pages/AssessmentShow' + ) + ).default, + }), }, { path: 'edit', - handle: AssessmentEdit.handle, - element: , + lazy: async () => { + const AssessmentEdit = ( + await import( + /* webpackChunkName: 'AssessmentEdit' */ + 'course/assessment/pages/AssessmentEdit' + ) + ).default; + + return { + Component: AssessmentEdit, + handle: AssessmentEdit.handle, + }; + }, }, { path: 'attempt', - loader: assessmentAttemptLoader(t), + lazy: async () => { + const assessmentAttemptLoader = ( + await import( + /* webpackChunkName: 'assessmentAttemptLoader' */ + 'course/assessment/attemptLoader' + ) + ).default; + + return { loader: assessmentAttemptLoader(t) }; + }, }, { path: 'monitoring', - handle: AssessmentMonitoring.handle, - element: , + lazy: async () => { + const AssessmentMonitoring = ( + await import( + /* webpackChunkName: 'AssessmentMonitoring' */ + 'course/assessment/pages/AssessmentMonitoring' + ) + ).default; + + return { + Component: AssessmentMonitoring, + handle: AssessmentMonitoring.handle, + }; + }, }, { path: 'sessions/new', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'AssessmentSessionNew' */ + 'course/assessment/sessions/pages/AssessmentSessionNew' + ) + ).default, + }), }, { path: 'statistics', - handle: AssessmentStatistics.handle, - element: , + lazy: async () => { + const AssessmentStatistics = ( + await import( + /* webpackChunkName: 'AssessmentStatistics' */ + 'course/assessment/pages/AssessmentStatistics' + ) + ).default; + + return { + Component: AssessmentStatistics, + handle: AssessmentStatistics.handle, + }; + }, }, { path: 'submissions', children: [ { index: true, - handle: AssessmentSubmissionsIndex.handle, - element: , + lazy: async () => { + const AssessmentSubmissionsIndex = ( + await import( + /* webpackChunkName: 'AssessmentSubmissionsIndex' */ + 'course/assessment/submission/pages/SubmissionsIndex' + ) + ).default; + + return { + Component: AssessmentSubmissionsIndex, + handle: AssessmentSubmissionsIndex.handle, + }; + }, }, { path: ':submissionId', children: [ { path: 'edit', - handle: SubmissionEditIndex.handle, - element: , + lazy: async () => { + const SubmissionEditIndex = ( + await import( + /* webpackChunkName: 'SubmissionEditIndex' */ + 'course/assessment/submission/pages/SubmissionEditIndex' + ) + ).default; + + return { + Component: SubmissionEditIndex, + handle: SubmissionEditIndex.handle, + }; + }, }, { path: 'logs', - handle: LogsIndex.handle, - element: , + lazy: async () => { + const SubmissionLogs = ( + await import( + /* webpackChunkName: 'SubmissionLogs' */ + 'course/assessment/submission/pages/LogsIndex' + ) + ).default; + + return { + Component: SubmissionLogs, + handle: SubmissionLogs.handle, + }; + }, }, ], }, @@ -654,20 +1425,54 @@ const authenticatedRouter: Translated = (t) => }, { path: 'question', - element: , - handle: questionHandle, + lazy: async () => { + const [questionHandle, QuestionFormOutlet] = + await Promise.all([ + import( + /* webpackChunkName: 'assessmentHandles' */ + 'course/assessment/handles' + ).then((module) => module.questionHandle), + import( + /* webpackChunkName: 'QuestionFormOutlet' */ + 'course/assessment/question/components/QuestionFormOutlet' + ).then((module) => module.default), + ]); + + return { + Component: QuestionFormOutlet, + handle: questionHandle, + }; + }, children: [ { path: 'forum_post_responses', children: [ { path: 'new', - handle: NewForumPostResponsePage.handle, - element: , + lazy: async () => { + const NewForumPostResponsePage = ( + await import( + /* webpackChunkName: 'NewForumPostResponsePage' */ + 'course/assessment/question/forum-post-responses/NewForumPostResponsePage' + ) + ).default; + + return { + Component: NewForumPostResponsePage, + handle: NewForumPostResponsePage.handle, + }; + }, }, { path: ':questionId/edit', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'EditForumPostResponsePage' */ + 'course/assessment/question/forum-post-responses/EditForumPostResponsePage' + ) + ).default, + }), }, ], }, @@ -676,12 +1481,30 @@ const authenticatedRouter: Translated = (t) => children: [ { path: 'new', - handle: NewTextResponse.handle, - element: , + lazy: async () => { + const NewTextResponse = ( + await import( + /* webpackChunkName: 'NewTextResponsePage' */ + 'course/assessment/question/text-responses/NewTextResponsePage' + ) + ).default; + + return { + Component: NewTextResponse, + handle: NewTextResponse.handle, + }; + }, }, { path: ':questionId/edit', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'EditTextResponsePage' */ + 'course/assessment/question/text-responses/EditTextResponsePage' + ) + ).default, + }), }, ], }, @@ -690,12 +1513,30 @@ const authenticatedRouter: Translated = (t) => children: [ { path: 'new', - handle: NewVoicePage.handle, - element: , + lazy: async () => { + const NewVoicePage = ( + await import( + /* webpackChunkName: 'NewVoicePage' */ + 'course/assessment/question/voice-responses/NewVoicePage' + ) + ).default; + + return { + Component: NewVoicePage, + handle: NewVoicePage.handle, + }; + }, }, { path: ':questionId/edit', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'EditVoicePage' */ + 'course/assessment/question/voice-responses/EditVoicePage' + ) + ).default, + }), }, ], }, @@ -704,12 +1545,30 @@ const authenticatedRouter: Translated = (t) => children: [ { path: 'new', - handle: NewMcqMrqPage.handle, - element: , + lazy: async () => { + const NewMcqMrqPage = ( + await import( + /* webpackChunkName: 'NewMcqMrqPage' */ + 'course/assessment/question/multiple-responses/NewMcqMrqPage' + ) + ).default; + + return { + Component: NewMcqMrqPage, + handle: NewMcqMrqPage.handle, + }; + }, }, { path: ':questionId/edit', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'EditMcqMrqPage' */ + 'course/assessment/question/multiple-responses/EditMcqMrqPage' + ) + ).default, + }), }, ], }, @@ -718,12 +1577,30 @@ const authenticatedRouter: Translated = (t) => children: [ { path: 'new', - handle: ScribingQuestion.handle, - element: , + lazy: async () => { + const ScribingQuestion = ( + await import( + /* webpackChunkName: 'ScribingQuestion' */ + 'course/assessment/question/scribing/ScribingQuestion' + ) + ).default; + + return { + Component: ScribingQuestion, + handle: ScribingQuestion.handle, + }; + }, }, { path: ':questionId/edit', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'ScribingQuestion' */ + 'course/assessment/question/scribing/ScribingQuestion' + ) + ).default, + }), }, ], }, @@ -732,17 +1609,46 @@ const authenticatedRouter: Translated = (t) => children: [ { path: 'new', - handle: NewProgrammingQuestionPage.handle, - element: , + lazy: async () => { + const NewProgrammingQuestionPage = ( + await import( + /* webpackChunkName: 'NewProgrammingQuestionPage' */ + 'course/assessment/question/programming/NewProgrammingQuestionPage' + ) + ).default; + + return { + Component: NewProgrammingQuestionPage, + handle: NewProgrammingQuestionPage.handle, + }; + }, }, { path: 'generate', - handle: GenerateProgrammingQuestionPage.handle, - element: , + lazy: async () => { + const GenerateProgrammingQuestionPage = ( + await import( + /* webpackChunkName: 'GenerateProgrammingQuestionPage' */ + 'course/assessment/pages/AssessmentGenerate/GenerateProgrammingQuestionPage' + ) + ).default; + + return { + Component: GenerateProgrammingQuestionPage, + handle: GenerateProgrammingQuestionPage.handle, + }; + }, }, { path: ':questionId/edit', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'EditProgrammingQuestionPage' */ + 'course/assessment/question/programming/EditProgrammingQuestionPage' + ) + ).default, + }), }, ], }, @@ -756,23 +1662,64 @@ const authenticatedRouter: Translated = (t) => }, { path: '/', - element: , + lazy: async () => { + const CourselessContainer = ( + await import( + /* webpackChunkName: 'CourselessContainer' */ + 'lib/containers/CourselessContainer' + ) + ).default; + + return { + element: , + }; + }, children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'DashboardPage' */ + 'bundles/common/DashboardPage' + ) + ).default, + }), }, ], }, { path: '*', - element: , + lazy: async () => { + const CourselessContainer = ( + await import( + /* webpackChunkName: 'CourselessContainer' */ + 'lib/containers/CourselessContainer' + ) + ).default; + + return { + element: , + }; + }, + children: [ reservedRoutes, { path: 'admin', - handle: AdminNavigator.handle, - element: , + lazy: async () => { + const AdminNavigator = ( + await import( + /* webpackChunkName: 'AdminNavigator' */ + 'bundles/system/admin/admin/AdminNavigator' + ) + ).default; + + return { + Component: AdminNavigator, + handle: AdminNavigator.handle, + }; + }, children: [ { index: true, @@ -780,26 +1727,65 @@ const authenticatedRouter: Translated = (t) => }, { path: 'announcements', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'AnnouncementsIndex' */ + 'bundles/system/admin/admin/pages/AnnouncementsIndex' + ) + ).default, + }), }, { path: 'users', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'UsersIndex' */ + 'bundles/system/admin/admin/pages/UsersIndex' + ) + ).default, + }), }, { path: 'instances', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'InstancesIndex' */ + 'bundles/system/admin/admin/pages/InstancesIndex' + ) + ).default, + }), }, { path: 'courses', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'CoursesIndex' */ + 'bundles/system/admin/admin/pages/CoursesIndex' + ) + ).default, + }), }, ], }, { path: 'admin/instance', - handle: InstanceAdminNavigator.handle, - element: , + lazy: async () => { + const InstanceAdminNavigator = ( + await import( + /* webpackChunkName: 'InstanceAdminNavigator' */ + 'bundles/system/admin/instance/instance/InstanceAdminNavigator' + ) + ).default; + + return { + Component: InstanceAdminNavigator, + handle: InstanceAdminNavigator.handle, + }; + }, children: [ { index: true, @@ -807,54 +1793,132 @@ const authenticatedRouter: Translated = (t) => }, { path: 'announcements', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'InstanceAnnouncementsIndex' */ + 'bundles/system/admin/instance/instance/pages/InstanceAnnouncementsIndex' + ) + ).default, + }), }, { path: 'components', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'InstanceComponentsIndex' */ + 'bundles/system/admin/instance/instance/pages/InstanceComponentsIndex' + ) + ).default, + }), }, { path: 'courses', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'InstanceCoursesIndex' */ + 'bundles/system/admin/instance/instance/pages/InstanceCoursesIndex' + ) + ).default, + }), }, { path: 'users', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'InstanceUsersIndex' */ + 'bundles/system/admin/instance/instance/pages/InstanceUsersIndex' + ) + ).default, + }), }, { path: 'users/invite', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'InstanceUsersInvite' */ + 'bundles/system/admin/instance/instance/pages/InstanceUsersInvite' + ) + ).default, + }), }, { path: 'user_invitations', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'InstanceUsersInvitations' */ + 'bundles/system/admin/instance/instance/pages/InstanceUsersInvitations' + ) + ).default, + }), }, { path: 'role_requests', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'InstanceUserRoleRequestsIndex' */ + 'bundles/system/admin/instance/instance/pages/InstanceUserRoleRequestsIndex' + ) + ).default, + }), }, ], }, { path: 'announcements', - handle: GlobalAnnouncementIndex.handle, - element: , + lazy: async () => { + const GlobalAnnouncementIndex = ( + await import( + /* webpackChunkName: 'GlobalAnnouncementIndex' */ + 'bundles/announcements/GlobalAnnouncementIndex' + ) + ).default; + + return { + Component: GlobalAnnouncementIndex, + handle: GlobalAnnouncementIndex.handle, + }; + }, }, { path: 'users', children: [ { path: ':userId', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: 'UserShow' */ + 'bundles/users/pages/UserShow' + ) + ).default, + }), }, { path: 'confirmation', children: [ { index: true, - loader: ConfirmEmailPage.loader, - element: , - errorElement: , + lazy: async () => { + const ConfirmEmailPage = ( + await import( + /* webpackChunkName: 'ConfirmEmailPage' */ + 'bundles/users/pages/ConfirmEmailPage' + ) + ).default; + + return { + element: , + errorElement: , + loader: ConfirmEmailPage.loader, + }; + }, }, ], }, @@ -862,8 +1926,19 @@ const authenticatedRouter: Translated = (t) => }, { path: 'user/profile/edit', - handle: AccountSettings.handle, - element: , + lazy: async () => { + const AccountSettings = ( + await import( + /* webpackChunkName: 'AccountSettings' */ + 'bundles/user/AccountSettings' + ) + ).default; + + return { + Component: AccountSettings, + handle: AccountSettings.handle, + }; + }, }, { path: 'role_requests', From 4730f44279e2f5ef3781fc82c06736e9542550e6 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:41:18 +0800 Subject: [PATCH 08/36] perf(router): lazy load all routes --- .../PrivacyPolicyPage/PrivacyPolicyPage.tsx | 9 -- .../common/PrivacyPolicyPage/index.tsx | 16 +-- .../TermsOfServicePage/TermsOfServicePage.tsx | 9 -- .../common/TermsOfServicePage/index.tsx | 16 +-- client/app/routers/router.tsx | 111 ++++++++++++++---- 5 files changed, 103 insertions(+), 58 deletions(-) delete mode 100644 client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx delete mode 100644 client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx diff --git a/client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx b/client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx deleted file mode 100644 index 4d655be2330..00000000000 --- a/client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import MarkdownPage from 'lib/components/core/layouts/MarkdownPage'; - -import privacyPolicy from './privacy-policy.md'; - -const PrivacyPolicyPage = (): JSX.Element => ( - -); - -export default PrivacyPolicyPage; diff --git a/client/app/bundles/common/PrivacyPolicyPage/index.tsx b/client/app/bundles/common/PrivacyPolicyPage/index.tsx index 58ce9822021..d9e6bce2ab1 100644 --- a/client/app/bundles/common/PrivacyPolicyPage/index.tsx +++ b/client/app/bundles/common/PrivacyPolicyPage/index.tsx @@ -1,10 +1,8 @@ -import { lazy, Suspense } from 'react'; import { defineMessages } from 'react-intl'; -const PrivacyPolicyPage = lazy( - () => - import(/* webpackChunkName: "PrivacyPolicyPage" */ './PrivacyPolicyPage'), -); +import MarkdownPage from 'lib/components/core/layouts/MarkdownPage'; + +import privacyPolicy from './privacy-policy.md'; const translations = defineMessages({ privacyPolicy: { @@ -13,12 +11,10 @@ const translations = defineMessages({ }, }); -const SuspensedPrivacyPolicyPage = (): JSX.Element => ( - - - +const PrivacyPolicyPage = (): JSX.Element => ( + ); const handle = translations.privacyPolicy; -export default Object.assign(SuspensedPrivacyPolicyPage, { handle }); +export default Object.assign(PrivacyPolicyPage, { handle }); diff --git a/client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx b/client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx deleted file mode 100644 index 938510a6efb..00000000000 --- a/client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import MarkdownPage from 'lib/components/core/layouts/MarkdownPage'; - -import termsOfService from './terms-of-service.md'; - -const TermsOfServicePage = (): JSX.Element => ( - -); - -export default TermsOfServicePage; diff --git a/client/app/bundles/common/TermsOfServicePage/index.tsx b/client/app/bundles/common/TermsOfServicePage/index.tsx index 594e2c2c111..b92d2dc089c 100644 --- a/client/app/bundles/common/TermsOfServicePage/index.tsx +++ b/client/app/bundles/common/TermsOfServicePage/index.tsx @@ -1,10 +1,8 @@ -import { lazy, Suspense } from 'react'; import { defineMessages } from 'react-intl'; -const TermsOfServicePage = lazy( - () => - import(/* webpackChunkName: "TermsOfServicePage" */ './TermsOfServicePage'), -); +import MarkdownPage from 'lib/components/core/layouts/MarkdownPage'; + +import termsOfService from './terms-of-service.md'; const translations = defineMessages({ termsOfService: { @@ -13,12 +11,10 @@ const translations = defineMessages({ }, }); -const SuspensedTermsOfServicePage = (): JSX.Element => ( - - - +const TermsOfServicePage = (): JSX.Element => ( + ); const handle = translations.termsOfService; -export default Object.assign(SuspensedTermsOfServicePage, { handle }); +export default Object.assign(TermsOfServicePage, { handle }); diff --git a/client/app/routers/router.tsx b/client/app/routers/router.tsx index 5f3e57d556c..f9b3b1a7d1b 100644 --- a/client/app/routers/router.tsx +++ b/client/app/routers/router.tsx @@ -1,19 +1,24 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ import { RouteObject } from 'react-router-dom'; import { resetStore } from 'store'; -import ErrorPage from 'bundles/common/ErrorPage'; -import PrivacyPolicyPage from 'bundles/common/PrivacyPolicyPage'; -import TermsOfServicePage from 'bundles/common/TermsOfServicePage'; -import CoursesIndex from 'bundles/course/courses/pages/CoursesIndex'; -import AppContainer from 'lib/containers/AppContainer'; -import CourselessContainer from 'lib/containers/CourselessContainer'; - const createAppRouter = (router: RouteObject[]): RouteObject[] => [ { path: '/', - element: , - loader: AppContainer.loader, - errorElement: , + lazy: async () => { + const AppContainer = ( + await import( + /* webpackChunkName: "AppContainer" */ + 'lib/containers/AppContainer' + ) + ).default; + + return { + Component: AppContainer, + loader: AppContainer.loader, + errorElement: , + }; + }, shouldRevalidate: (props): boolean => { const isChangingCourse = props.currentParams.courseId !== props.nextParams.courseId; @@ -35,36 +40,102 @@ const createAppRouter = (router: RouteObject[]): RouteObject[] => [ ...router, { path: '*', - element: , + lazy: async () => { + const CourselessContainer = ( + await import( + /* webpackChunkName: "CourselessContainer" */ + 'lib/containers/CourselessContainer' + ) + ).default; + + return { + element: , + }; + }, children: [ { path: 'courses', - handle: CoursesIndex.handle, - element: , + lazy: async () => { + const CoursesIndex = ( + await import( + /* webpackChunkName: "CoursesIndex" */ + 'bundles/course/courses/pages/CoursesIndex' + ) + ).default; + + return { + Component: CoursesIndex, + handle: CoursesIndex.handle, + }; + }, }, { path: 'pages', children: [ { path: 'terms_of_service', - handle: TermsOfServicePage.handle, - element: , + lazy: async () => { + const TermsOfServicePage = ( + await import( + /* webpackChunkName: "TermsOfServicePage" */ + 'bundles/common/TermsOfServicePage' + ) + ).default; + + return { + Component: TermsOfServicePage, + handle: TermsOfServicePage.handle, + }; + }, }, { path: 'privacy_policy', - handle: PrivacyPolicyPage.handle, - element: , + lazy: async () => { + const PrivacyPolicyPage = ( + await import( + /* webpackChunkName: "PrivacyPolicyPage" */ + 'bundles/common/PrivacyPolicyPage' + ) + ).default; + + return { + Component: PrivacyPolicyPage, + handle: PrivacyPolicyPage.handle, + }; + }, }, ], }, { path: 'forbidden', - loader: ErrorPage.Forbidden.loader, - element: , + lazy: async () => { + const ErrorPage = ( + await import( + /* webpackChunkName: "ErrorPage" */ + 'bundles/common/ErrorPage' + ) + ).default; + + return { + Component: ErrorPage.Forbidden, + loader: ErrorPage.Forbidden.loader, + }; + }, }, { path: '*', - element: , + lazy: async () => { + const ErrorPage = ( + await import( + /* webpackChunkName: "ErrorPage" */ + 'bundles/common/ErrorPage' + ) + ).default; + + return { + Component: ErrorPage.NotFound, + }; + }, }, ], }, From 733681adcb68de9b8b7b5ab9902ae7f6a43294b1 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:41:25 +0800 Subject: [PATCH 09/36] perf(UnauthenticatedApp): lazy load all routes --- client/app/routers/UnauthenticatedApp.tsx | 157 +++++++++++++++++----- 1 file changed, 126 insertions(+), 31 deletions(-) diff --git a/client/app/routers/UnauthenticatedApp.tsx b/client/app/routers/UnauthenticatedApp.tsx index 07cecff985f..59afe4ef940 100644 --- a/client/app/routers/UnauthenticatedApp.tsx +++ b/client/app/routers/UnauthenticatedApp.tsx @@ -1,18 +1,6 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ import { createBrowserRouter, RouterProvider } from 'react-router-dom'; -import AuthenticationRedirection from 'bundles/authentication/pages/AuthenticationRedirection'; -import LandingPage from 'bundles/common/LandingPage'; -import ConfirmEmailPage from 'bundles/users/pages/ConfirmEmailPage'; -import ForgotPasswordLandingPage from 'bundles/users/pages/ForgotPasswordLandingPage'; -import ForgotPasswordPage from 'bundles/users/pages/ForgotPasswordPage'; -import ResendConfirmationEmailLandingPage from 'bundles/users/pages/ResendConfirmationEmailLandingPage'; -import ResendConfirmationEmailPage from 'bundles/users/pages/ResendConfirmationEmailPage'; -import ResetPasswordPage from 'bundles/users/pages/ResetPasswordPage'; -import SignUpLandingPage from 'bundles/users/pages/SignUpLandingPage'; -import SignUpPage from 'bundles/users/pages/SignUpPage'; -import AuthPagesContainer from 'lib/containers/AuthPagesContainer'; -import CourselessContainer from 'lib/containers/CourselessContainer'; - import { protectedRoutes } from './redirects'; import createAppRouter from './router'; @@ -20,37 +8,94 @@ const unauthenticatedRouter = createAppRouter([ protectedRoutes, { path: '*', - element: , + lazy: async () => { + const CourselessContainer = ( + await import( + /* webpackChunkName: "CourselessContainer" */ + 'lib/containers/CourselessContainer' + ) + ).default; + + return { + element: , + }; + }, children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: "LandingPage" */ + 'bundles/common/LandingPage' + ) + ).default, + }), }, { index: true, path: 'auth', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: "AuthenticationRedirection" */ + 'bundles/authentication/pages/AuthenticationRedirection' + ) + ).default, + }), }, { path: 'users', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: "AuthPagesContainer" */ + 'lib/containers/AuthPagesContainer' + ) + ).default, + }), children: [ { index: true, path: 'sign_in', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: "AuthenticationRedirection" */ + 'bundles/authentication/pages/AuthenticationRedirection' + ) + ).default, + }), }, { path: 'sign_up', children: [ { index: true, - loader: SignUpPage.loader, - element: , + lazy: async () => { + const SignUpPage = ( + await import( + /* webpackChunkName: "SignUpPage" */ + 'bundles/users/pages/SignUpPage' + ) + ).default; + + return { + Component: SignUpPage, + loader: SignUpPage.loader, + }; + }, }, { path: 'completed', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: "SignUpLandingPage" */ + 'bundles/users/pages/SignUpLandingPage' + ) + ).default, + }), }, ], }, @@ -59,20 +104,45 @@ const unauthenticatedRouter = createAppRouter([ children: [ { index: true, - loader: ConfirmEmailPage.loader, - element: , - errorElement: , + lazy: async () => { + const ConfirmEmailPage = ( + await import( + /* webpackChunkName: "ConfirmEmailPage" */ + 'bundles/users/pages/ConfirmEmailPage' + ) + ).default; + + return { + Component: ConfirmEmailPage, + loader: ConfirmEmailPage.loader, + errorElement: , + }; + }, }, { path: 'new', children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: "ResendConfirmationEmailPage" */ + 'bundles/users/pages/ResendConfirmationEmailPage' + ) + ).default, + }), }, { path: 'completed', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: "ResendConfirmationEmailLandingPage" */ + 'bundles/users/pages/ResendConfirmationEmailLandingPage' + ) + ).default, + }), }, ], }, @@ -86,19 +156,44 @@ const unauthenticatedRouter = createAppRouter([ children: [ { index: true, - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: "ForgotPasswordPage" */ + 'bundles/users/pages/ForgotPasswordPage' + ) + ).default, + }), }, { path: 'completed', - element: , + lazy: async () => ({ + Component: ( + await import( + /* webpackChunkName: "ForgotPasswordLandingPage" */ + 'bundles/users/pages/ForgotPasswordLandingPage' + ) + ).default, + }), }, ], }, { path: 'edit', - loader: ResetPasswordPage.loader, - errorElement: , - element: , + lazy: async () => { + const ResetPasswordPage = ( + await import( + /* webpackChunkName: "ResetPasswordPage" */ + 'bundles/users/pages/ResetPasswordPage' + ) + ).default; + + return { + Component: ResetPasswordPage, + loader: ResetPasswordPage.loader, + errorElement: , + }; + }, }, ], }, From d325e82a7310b9fb670bac70c4491498409bad5d Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:42:56 +0800 Subject: [PATCH 10/36] perf(SubmissionEditIndex): lazy load all question type answer components --- .../submission/components/answers/Answer.tsx | 302 ++++++------------ .../answers/AnswerNotImplemented.tsx | 21 ++ .../answers/adapters/FileUploadAdapter.tsx | 17 + .../adapters/ForumPostResponseAdapter.tsx | 20 ++ .../adapters/MultipleChoiceAdapter.tsx | 26 ++ .../adapters/MultipleResponseAdapter.tsx | 26 ++ .../answers/adapters/ProgrammingAdapter.tsx | 18 ++ .../answers/adapters/ScribingAdapter.tsx | 9 + .../answers/adapters/TextResponseAdapter.tsx | 26 ++ .../answers/adapters/VoiceResponseAdapter.tsx | 18 ++ 10 files changed, 272 insertions(+), 211 deletions(-) create mode 100644 client/app/bundles/course/assessment/submission/components/answers/AnswerNotImplemented.tsx create mode 100644 client/app/bundles/course/assessment/submission/components/answers/adapters/FileUploadAdapter.tsx create mode 100644 client/app/bundles/course/assessment/submission/components/answers/adapters/ForumPostResponseAdapter.tsx create mode 100644 client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleChoiceAdapter.tsx create mode 100644 client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleResponseAdapter.tsx create mode 100644 client/app/bundles/course/assessment/submission/components/answers/adapters/ProgrammingAdapter.tsx create mode 100644 client/app/bundles/course/assessment/submission/components/answers/adapters/ScribingAdapter.tsx create mode 100644 client/app/bundles/course/assessment/submission/components/answers/adapters/TextResponseAdapter.tsx create mode 100644 client/app/bundles/course/assessment/submission/components/answers/adapters/VoiceResponseAdapter.tsx diff --git a/client/app/bundles/course/assessment/submission/components/answers/Answer.tsx b/client/app/bundles/course/assessment/submission/components/answers/Answer.tsx index 4a758bd2b12..d6791cb2299 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/Answer.tsx +++ b/client/app/bundles/course/assessment/submission/components/answers/Answer.tsx @@ -1,209 +1,83 @@ -import { defineMessages } from 'react-intl'; -import { Alert, Card, CardContent } from '@mui/material'; +import { lazy, LazyExoticComponent, Suspense } from 'react'; +import { Alert } from '@mui/material'; import { QuestionType } from 'types/course/assessment/question'; import { SubmissionQuestionData } from 'types/course/assessment/submission/question/types'; +import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import useTranslation from 'lib/hooks/useTranslation'; import PastAnswers from '../../containers/PastAnswers'; -import ScribingView from '../../containers/ScribingView'; -import VoiceResponseAnswer from '../../containers/VoiceResponseAnswer'; -import FileUploadAnswer from './FileUpload'; -import ForumPostResponseAnswer from './ForumPostResponse'; -import MultipleChoiceAnswer from './MultipleChoice'; -import MultipleResponseAnswer from './MultipleResponse'; -import ProgrammingAnswer from './Programming'; -import TextResponseAnswer from './TextResponse'; -import { - AnswerPropsMap, - FileUploadAnswerProps, - ForumPostResponseAnswerProps, - McqAnswerProps, - MrqAnswerProps, - ProgrammingAnswerProps, - ScribingAnswerProps, - TextResponseAnswerProps, - VoiceResponseAnswerProps, -} from './types'; +import type { AnswerPropsMap } from './types'; -const translations = defineMessages({ - rendererNotImplemented: { - id: 'course.assessment.submission.Answer.rendererNotImplemented', - defaultMessage: - 'The display for this question type has not been implemented yet.', - }, - missingAnswer: { - id: 'course.assessment.submission.Answer.missingAnswer', - defaultMessage: - 'There is no answer submitted for this question - this might be caused by \ - the addition of this question after the submission is submitted.', - }, -}); +const AnswerNotImplemented = lazy( + () => + import( + /* webpackChunkName: "AnswerNotImplemented" */ + './AnswerNotImplemented' + ), +); -const MultipleChoice = (props: McqAnswerProps): JSX.Element => { - const { - question, - answerId, - readOnly, - graderView, - showMcqMrqSolution, - saveAnswerAndUpdateClientVersion, - } = props; - return ( - - ); -}; - -const MultipleResponse = (props: MrqAnswerProps): JSX.Element => { - const { - question, - answerId, - readOnly, - graderView, - showMcqMrqSolution, - saveAnswerAndUpdateClientVersion, - } = props; - return ( - - ); -}; - -const Programming = (props: ProgrammingAnswerProps): JSX.Element => { - const { question, answerId, readOnly, saveAnswerAndUpdateClientVersion } = - props; - return ( - - ); -}; - -const TextResponse = (props: TextResponseAnswerProps): JSX.Element => { - const { - question, - answerId, - readOnly, - graderView, - saveAnswerAndUpdateClientVersion, - handleUploadTextResponseFiles, - } = props; - return ( - - ); -}; - -const FileUpload = (props: FileUploadAnswerProps): JSX.Element => { - const { question, answerId, readOnly, handleUploadTextResponseFiles } = props; - return ( - - ); -}; - -const Scribing = (props: ScribingAnswerProps): JSX.Element => { - const { question, answerId } = props; - return ; -}; - -const VoiceResponse = (props: VoiceResponseAnswerProps): JSX.Element => { - const { question, answerId, readOnly, saveAnswerAndUpdateClientVersion } = - props; - return ( - - ); -}; - -const ForumPostResponse = ( - props: ForumPostResponseAnswerProps, -): JSX.Element => { - const { question, answerId, readOnly, saveAnswerAndUpdateClientVersion } = - props; - return ( - - ); -}; - -const AnswerNotImplemented = (): JSX.Element => { - const { t } = useTranslation(); - - return ( - - {t(translations.rendererNotImplemented)} - - ); -}; - -export const AnswerMapper = { - MultipleChoice: (props: McqAnswerProps): JSX.Element => ( - +const answerComponents: Record< + Exclude, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + LazyExoticComponent +> = { + MultipleChoice: lazy( + () => + import( + /* webpackChunkName: "MultipleChoiceAdapter" */ + './adapters/MultipleChoiceAdapter' + ), ), - MultipleResponse: (props: MrqAnswerProps): JSX.Element => ( - + MultipleResponse: lazy( + () => + import( + /* webpackChunkName: "MultipleResponseAdapter" */ + './adapters/MultipleResponseAdapter' + ), ), - Programming: (props: ProgrammingAnswerProps): JSX.Element => ( - + Programming: lazy( + () => + import( + /* webpackChunkName: "ProgrammingAdapter" */ + './adapters/ProgrammingAdapter' + ), ), - TextResponse: (props: TextResponseAnswerProps): JSX.Element => ( - + TextResponse: lazy( + () => + import( + /* webpackChunkName: "TextResponseAdapter" */ + './adapters/TextResponseAdapter' + ), ), - FileUpload: (props: FileUploadAnswerProps): JSX.Element => ( - + FileUpload: lazy( + () => + import( + /* webpackChunkName: "FileUploadAdapter" */ + './adapters/FileUploadAdapter' + ), ), - Comprehension: (): JSX.Element => , - Scribing: (props: ScribingAnswerProps): JSX.Element => ( - + Scribing: lazy( + () => + import( + /* webpackChunkName: "ScribingAdapter" */ + './adapters/ScribingAdapter' + ), ), - VoiceResponse: (props: VoiceResponseAnswerProps): JSX.Element => ( - + VoiceResponse: lazy( + () => + import( + /* webpackChunkName: "VoiceResponseAdapter" */ + './adapters/VoiceResponseAdapter' + ), ), - ForumPostResponse: (props: ForumPostResponseAnswerProps): JSX.Element => ( - + ForumPostResponse: lazy( + () => + import( + /* webpackChunkName: "ForumPostResponseAdapter" */ + './adapters/ForumPostResponseAdapter' + ), ), }; @@ -214,32 +88,38 @@ interface AnswerComponentProps { answerProps: AnswerPropsMap[T]; } -const Answer = ( - props: AnswerComponentProps, -): JSX.Element => { - const { answerId, questionType, question, answerProps } = props; +const SuspensefulAnswer = ({ + answerId, + questionType, + question, + answerProps, +}: AnswerComponentProps): JSX.Element => { const { t } = useTranslation(); - if (!answerId) { - return {t(translations.missingAnswer)}; - } - - if (question.viewHistory) { - return ; - } + if (!answerId) + return ( + + {t({ + id: 'course.assessment.submission.Answer.missingAnswer', + defaultMessage: + 'There is no answer submitted for this question - this might be caused by the addition of this question after the submission is submitted.', + })} + + ); - const Component = AnswerMapper[questionType]; + if (question.viewHistory) return ; - if (!Component) { - return ; - } + // @ts-expect-error + const Adapter = answerComponents[questionType]; + if (!Adapter) return ; - // "Any" type is used here as the props are dynamically generated - // depending on the different answer type and typescript - // does not support union typing for the elements. - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return Component(answerProps as any); + return ; }; +const Answer: typeof SuspensefulAnswer = (props) => ( + }> + + +); + export default Answer; diff --git a/client/app/bundles/course/assessment/submission/components/answers/AnswerNotImplemented.tsx b/client/app/bundles/course/assessment/submission/components/answers/AnswerNotImplemented.tsx new file mode 100644 index 00000000000..30b35151d16 --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/AnswerNotImplemented.tsx @@ -0,0 +1,21 @@ +import { Card, CardContent } from '@mui/material'; + +import useTranslation from 'lib/hooks/useTranslation'; + +const AnswerNotImplemented = (): JSX.Element => { + const { t } = useTranslation(); + + return ( + + + {t({ + id: 'course.assessment.submission.Answer.rendererNotImplemented', + defaultMessage: + 'The display for this question type has not been implemented yet.', + })} + + + ); +}; + +export default AnswerNotImplemented; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/FileUploadAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/FileUploadAdapter.tsx new file mode 100644 index 00000000000..09cc52f2064 --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/FileUploadAdapter.tsx @@ -0,0 +1,17 @@ +import FileUploadAnswer from '../FileUpload'; +import type { FileUploadAnswerProps } from '../types'; + +const FileUploadAdapter = (props: FileUploadAnswerProps): JSX.Element => { + const { question, answerId, readOnly, handleUploadTextResponseFiles } = props; + return ( + + ); +}; + +export default FileUploadAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/ForumPostResponseAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/ForumPostResponseAdapter.tsx new file mode 100644 index 00000000000..82bccc54a88 --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/ForumPostResponseAdapter.tsx @@ -0,0 +1,20 @@ +import ForumPostResponseAnswer from '../ForumPostResponse'; +import type { ForumPostResponseAnswerProps } from '../types'; + +const ForumPostResponseAdapter = ( + props: ForumPostResponseAnswerProps, +): JSX.Element => { + const { question, answerId, readOnly, saveAnswerAndUpdateClientVersion } = + props; + return ( + + ); +}; + +export default ForumPostResponseAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleChoiceAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleChoiceAdapter.tsx new file mode 100644 index 00000000000..f26bb95dddf --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleChoiceAdapter.tsx @@ -0,0 +1,26 @@ +import MultipleChoiceAnswer from '../MultipleChoice'; +import type { McqAnswerProps } from '../types'; + +const MultipleChoiceAdapter = (props: McqAnswerProps): JSX.Element => { + const { + question, + answerId, + readOnly, + graderView, + showMcqMrqSolution, + saveAnswerAndUpdateClientVersion, + } = props; + return ( + + ); +}; + +export default MultipleChoiceAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleResponseAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleResponseAdapter.tsx new file mode 100644 index 00000000000..01877b712e1 --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/MultipleResponseAdapter.tsx @@ -0,0 +1,26 @@ +import MultipleResponseAnswer from '../MultipleResponse'; +import type { MrqAnswerProps } from '../types'; + +const MultipleResponseAdapter = (props: MrqAnswerProps): JSX.Element => { + const { + question, + answerId, + readOnly, + graderView, + showMcqMrqSolution, + saveAnswerAndUpdateClientVersion, + } = props; + return ( + + ); +}; + +export default MultipleResponseAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/ProgrammingAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/ProgrammingAdapter.tsx new file mode 100644 index 00000000000..8b3170fdf30 --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/ProgrammingAdapter.tsx @@ -0,0 +1,18 @@ +import ProgrammingAnswer from '../Programming'; +import type { ProgrammingAnswerProps } from '../types'; + +const ProgrammingAdapter = (props: ProgrammingAnswerProps): JSX.Element => { + const { question, answerId, readOnly, saveAnswerAndUpdateClientVersion } = + props; + return ( + + ); +}; + +export default ProgrammingAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/ScribingAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/ScribingAdapter.tsx new file mode 100644 index 00000000000..3bbc73aeb5b --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/ScribingAdapter.tsx @@ -0,0 +1,9 @@ +import ScribingView from '../../../containers/ScribingView'; +import type { ScribingAnswerProps } from '../types'; + +const ScribingAdapter = (props: ScribingAnswerProps): JSX.Element => { + const { question, answerId } = props; + return ; +}; + +export default ScribingAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/TextResponseAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/TextResponseAdapter.tsx new file mode 100644 index 00000000000..5b14509e4ff --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/TextResponseAdapter.tsx @@ -0,0 +1,26 @@ +import TextResponseAnswer from '../TextResponse'; +import type { TextResponseAnswerProps } from '../types'; + +const TextResponseAdapter = (props: TextResponseAnswerProps): JSX.Element => { + const { + question, + answerId, + readOnly, + graderView, + saveAnswerAndUpdateClientVersion, + handleUploadTextResponseFiles, + } = props; + return ( + + ); +}; + +export default TextResponseAdapter; diff --git a/client/app/bundles/course/assessment/submission/components/answers/adapters/VoiceResponseAdapter.tsx b/client/app/bundles/course/assessment/submission/components/answers/adapters/VoiceResponseAdapter.tsx new file mode 100644 index 00000000000..15109b3b536 --- /dev/null +++ b/client/app/bundles/course/assessment/submission/components/answers/adapters/VoiceResponseAdapter.tsx @@ -0,0 +1,18 @@ +import VoiceResponseAnswer from '../../../containers/VoiceResponseAnswer'; +import type { VoiceResponseAnswerProps } from '../types'; + +const VoiceResponseAdapter = (props: VoiceResponseAnswerProps): JSX.Element => { + const { question, answerId, readOnly, saveAnswerAndUpdateClientVersion } = + props; + return ( + + ); +}; + +export default VoiceResponseAdapter; From c2eb7d558d186586f276f9176ab513be53a1ce68 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:44:35 +0800 Subject: [PATCH 11/36] perf(index): remove initializers, webfontloader -> CSS `@import` --- client/app/index.tsx | 2 -- client/app/initializers.js | 22 ---------------------- client/app/lib/initializers/webfont.js | 12 ------------ client/app/theme/index.css | 2 ++ client/package.json | 2 -- client/yarn.lock | 10 ---------- 6 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 client/app/initializers.js delete mode 100644 client/app/lib/initializers/webfont.js diff --git a/client/app/index.tsx b/client/app/index.tsx index 4f9a06b92d1..193c778ab48 100644 --- a/client/app/index.tsx +++ b/client/app/index.tsx @@ -1,8 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import './initializers'; - import App from './App'; import 'theme/index.css'; diff --git a/client/app/initializers.js b/client/app/initializers.js deleted file mode 100644 index 264d98d39d0..00000000000 --- a/client/app/initializers.js +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable global-require */ - -function loadModules() { - // Require web font last so that it doesn't block the load of current module. - require('lib/initializers/webfont'); -} - -if (!global.Intl) { - Promise.all([ - import(/* webpackChunkName: "intl" */ 'intl'), - import(/* webpackChunkName: "intl" */ 'intl/locale-data/jsonp/en'), - import(/* webpackChunkName: "intl" */ 'intl/locale-data/jsonp/zh'), - ]) - .then(() => { - loadModules(); - }) - .catch((e) => { - throw e; - }); -} else { - loadModules(); -} diff --git a/client/app/lib/initializers/webfont.js b/client/app/lib/initializers/webfont.js deleted file mode 100644 index 4aa0a11b776..00000000000 --- a/client/app/lib/initializers/webfont.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Load the google fonts. - * See https://github.com/typekit/webfontloader - */ -import WebFont from 'webfontloader'; - -WebFont.load({ - google: { - families: ['Inter:400,500,600,700,800,i4'], - }, - timeout: 1500, -}); diff --git a/client/app/theme/index.css b/client/app/theme/index.css index 076bddc4918..1d1dfb5dd2d 100644 --- a/client/app/theme/index.css +++ b/client/app/theme/index.css @@ -1,3 +1,5 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100..900;1,100..900&display=swap'); + @tailwind base; @tailwind components; @tailwind utilities; diff --git a/client/package.json b/client/package.json index da4c7900aa6..cc35fc7df66 100644 --- a/client/package.json +++ b/client/package.json @@ -60,7 +60,6 @@ "idb": "^8.0.0", "immer": "^10.0.4", "immutable": "^3.8.2", - "intl": "^1.2.5", "jquery": "^3.7.1", "jquery-ui": "^1.13.3", "js-cookie": "^3.0.5", @@ -105,7 +104,6 @@ "redux-thunk": "^2.4.2", "resize-observer-polyfill": "^1.5.1", "rollbar": "^2.26.3", - "webfontloader": "^1.6.28", "yup": "^0.32.11" }, "devDependencies": { diff --git a/client/yarn.lock b/client/yarn.lock index 1da56906603..8e1447ef6af 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -6370,11 +6370,6 @@ intl-messageformat@10.5.11: "@formatjs/icu-messageformat-parser" "2.7.6" tslib "^2.4.0" -intl@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/intl/-/intl-1.2.5.tgz#82244a2190c4e419f8371f5aa34daa3420e2abde" - integrity sha512-rK0KcPHeBFBcqsErKSpvZnrOmWOj+EmDkyJ57e90YWaQNqbcivcqmKDlHEeNprDWOsKzPsh1BfSpPQdDvclHVw== - ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -11218,11 +11213,6 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -webfontloader@^1.6.28: - version "1.6.28" - resolved "https://registry.yarnpkg.com/webfontloader/-/webfontloader-1.6.28.tgz#db786129253cb6e8eae54c2fb05f870af6675bae" - integrity sha512-Egb0oFEga6f+nSgasH3E0M405Pzn6y3/9tOVanv/DLfa1YBIgcv90L18YyWnvXkRbIM17v5Kv6IT2N6g1x5tvQ== - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From 76778ad1217a723f0616e8bc6876b89f62c6b04c Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:45:09 +0800 Subject: [PATCH 12/36] fix(store): suppress legacy `onSubmit`, `onConfirm` console errors --- client/app/store.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/app/store.ts b/client/app/store.ts index 9b612953ae5..bc0d260f8aa 100644 --- a/client/app/store.ts +++ b/client/app/store.ts @@ -108,6 +108,12 @@ const purgeableRootReducer: Reducer = (state, action) => { export const store = configureStore({ reducer: purgeableRootReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredPaths: [/\.onSubmit$/, /\.onConfirm$/], + }, + }), }); export type AppState = ReturnType; From 09ff8c82a4baf5d6003b9c2b55be18bffcbf586d Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:46:03 +0800 Subject: [PATCH 13/36] perf: replace lodash with lodash-es absolute imports --- client/.babelrc | 8 -------- .../course/assessment/question/commons/utils.ts | 2 +- .../submission/reducers/liveFeedbackChats/index.ts | 2 +- .../assessment/submission/reducers/scribing.js | 2 +- .../conditions/conditions/AssessmentCondition.tsx | 2 +- client/app/lib/components/form/Form.tsx | 2 +- .../TanStackTableBuilder/useTanStackTableBuilder.tsx | 2 +- client/app/lib/containers/AuthPagesContainer.tsx | 2 +- client/app/lib/hooks/useDebounce.ts | 3 +-- client/package.json | 3 ++- client/yarn.lock | 12 ++++++++++++ 11 files changed, 22 insertions(+), 18 deletions(-) diff --git a/client/.babelrc b/client/.babelrc index 91b03cadc80..ad46cff5b3f 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -14,14 +14,6 @@ "ast": true } ], - [ - "babel-plugin-import", - { - "libraryName": "lodash", - "libraryDirectory": "", - "camel2DashComponentName": false - } - ], [ "babel-plugin-import", { diff --git a/client/app/bundles/course/assessment/question/commons/utils.ts b/client/app/bundles/course/assessment/question/commons/utils.ts index 6fafe7ade33..1bc2dadb851 100644 --- a/client/app/bundles/course/assessment/question/commons/utils.ts +++ b/client/app/bundles/course/assessment/question/commons/utils.ts @@ -1,4 +1,4 @@ -import { isNumber } from 'lodash'; +import isNumber from 'lodash-es/isNumber'; const getNumberBetweenTwoSquareBrackets = (str: string): number | undefined => { const match = str.match(/\[(\d+)\]/); diff --git a/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts b/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts index b5e115df408..bcdd7d98d7e 100644 --- a/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts +++ b/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts @@ -4,7 +4,7 @@ import { type EntityState, PayloadAction, } from '@reduxjs/toolkit'; -import { shuffle } from 'lodash'; +import shuffle from 'lodash-es/shuffle'; import moment, { SHORT_TIME_FORMAT } from 'lib/moment'; diff --git a/client/app/bundles/course/assessment/submission/reducers/scribing.js b/client/app/bundles/course/assessment/submission/reducers/scribing.js index beffdc744cf..73572c8f9e2 100644 --- a/client/app/bundles/course/assessment/submission/reducers/scribing.js +++ b/client/app/bundles/course/assessment/submission/reducers/scribing.js @@ -1,5 +1,5 @@ import { produce } from 'immer'; -import { isNumber } from 'lodash'; +import isNumber from 'lodash-es/isNumber'; import actions, { canvasActionTypes, diff --git a/client/app/lib/components/extensions/conditions/conditions/AssessmentCondition.tsx b/client/app/lib/components/extensions/conditions/conditions/AssessmentCondition.tsx index 0d9c8dae133..5496f575108 100644 --- a/client/app/lib/components/extensions/conditions/conditions/AssessmentCondition.tsx +++ b/client/app/lib/components/extensions/conditions/conditions/AssessmentCondition.tsx @@ -3,7 +3,7 @@ import { Controller, useForm } from 'react-hook-form'; import { Launch } from '@mui/icons-material'; import { Alert, Autocomplete, Box, Typography } from '@mui/material'; import { produce } from 'immer'; -import { isNumber } from 'lodash'; +import isNumber from 'lodash-es/isNumber'; import { AssessmentConditionData, AvailableAssessments, diff --git a/client/app/lib/components/form/Form.tsx b/client/app/lib/components/form/Form.tsx index d76cd8483ed..76496cda426 100644 --- a/client/app/lib/components/form/Form.tsx +++ b/client/app/lib/components/form/Form.tsx @@ -20,7 +20,7 @@ import { } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { Button, Slide, Typography } from '@mui/material'; -import { isEmpty } from 'lodash'; +import isEmpty from 'lodash-es/isEmpty'; import { AnyObjectSchema } from 'yup'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; diff --git a/client/app/lib/components/table/TanStackTableBuilder/useTanStackTableBuilder.tsx b/client/app/lib/components/table/TanStackTableBuilder/useTanStackTableBuilder.tsx index ae1d1ae7f95..e9e264b4b77 100644 --- a/client/app/lib/components/table/TanStackTableBuilder/useTanStackTableBuilder.tsx +++ b/client/app/lib/components/table/TanStackTableBuilder/useTanStackTableBuilder.tsx @@ -11,7 +11,7 @@ import { Row, useReactTable, } from '@tanstack/react-table'; -import { isEmpty } from 'lodash'; +import isEmpty from 'lodash-es/isEmpty'; import { RowEqualityData, TableProps } from '../adapters'; import { TableTemplate } from '../builder'; diff --git a/client/app/lib/containers/AuthPagesContainer.tsx b/client/app/lib/containers/AuthPagesContainer.tsx index 17bfdc1cb78..9a683756b69 100644 --- a/client/app/lib/containers/AuthPagesContainer.tsx +++ b/client/app/lib/containers/AuthPagesContainer.tsx @@ -1,6 +1,6 @@ import { Dispatch, SetStateAction, useState } from 'react'; import { Outlet, useLocation, useOutletContext } from 'react-router-dom'; -import { isString } from 'lodash'; +import isString from 'lodash-es/isString'; import Page from 'lib/components/core/layouts/Page'; diff --git a/client/app/lib/hooks/useDebounce.ts b/client/app/lib/hooks/useDebounce.ts index 610ef95e62e..9aaca0fe97b 100644 --- a/client/app/lib/hooks/useDebounce.ts +++ b/client/app/lib/hooks/useDebounce.ts @@ -1,6 +1,5 @@ import { DependencyList, useCallback, useEffect } from 'react'; -import type { DebouncedFunc } from 'lodash'; -import { debounce } from 'lodash'; +import debounce, { type DebouncedFunc } from 'lodash-es/debounce'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const useDebounce = any>( diff --git a/client/package.json b/client/package.json index cc35fc7df66..b3a4ceb2020 100644 --- a/client/package.json +++ b/client/package.json @@ -63,7 +63,7 @@ "jquery": "^3.7.1", "jquery-ui": "^1.13.3", "js-cookie": "^3.0.5", - "lodash": "^4.17.21", + "lodash-es": "^4.17.21", "mirror-creator": "1.1.0", "moment": "^2.30.1", "moment-timezone": "^0.5.44", @@ -125,6 +125,7 @@ "@types/enzyme": "^3.10.18", "@types/jest": "^29.5.12", "@types/jquery": "^3.5.29", + "@types/lodash-es": "^4.17.12", "@types/papaparse": "^5.3.14", "@types/rails__actioncable": "^6.1.10", "@types/react": "^18.2.67", diff --git a/client/yarn.lock b/client/yarn.lock index 8e1447ef6af..95030702c04 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2425,6 +2425,18 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash-es@^4.17.12": + version "4.17.12" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b" + integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.17.16" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.16.tgz#94ae78fab4a38d73086e962d0b65c30d816bfb0a" + integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g== + "@types/lodash@^4.14.175": version "4.14.182" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" From c57ed142a9deec029b536b171430728020ddc908 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:49:49 +0800 Subject: [PATCH 14/36] feat(webpack): allow configuring client port --- client/webpack.dev.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/webpack.dev.js b/client/webpack.dev.js index 1e6c32f175c..f435cd3db17 100644 --- a/client/webpack.dev.js +++ b/client/webpack.dev.js @@ -5,6 +5,7 @@ const common = require('./webpack.common'); const packageJSON = require('./package.json'); const SERVER_PORT = packageJSON.devServer.serverPort; +const CLIENT_PORT = packageJSON.devServer.clientPort; const APP_HOST = packageJSON.devServer.appHost; const BLUE_ANSI = '\x1b[36m%s\x1b[0m'; @@ -24,6 +25,7 @@ module.exports = merge(common, { mode: 'development', devtool: 'eval-cheap-module-source-map', devServer: { + port: CLIENT_PORT, allowedHosts: [`.${APP_HOST}`], historyApiFallback: true, devMiddleware: { From 3acdce4fac852ea046fc2824c96efd715ef50bbf Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:50:34 +0800 Subject: [PATCH 15/36] chore(webpack): add webpack config types --- client/webpack.common.js | 3 +++ client/webpack.dev.js | 3 +++ client/webpack.prod.js | 3 +++ 3 files changed, 9 insertions(+) diff --git a/client/webpack.common.js b/client/webpack.common.js index 5b96da75f32..70becf8ab2b 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -14,6 +14,9 @@ const packageJSON = require('./package.json'); const ENV_DIR = process.env.BABEL_ENV === 'e2e-test' ? './.env.test' : './.env'; +/** + * @type {import('webpack').Configuration} + */ module.exports = { entry: { coursemology: [ diff --git a/client/webpack.dev.js b/client/webpack.dev.js index f435cd3db17..1e3d8799ddd 100644 --- a/client/webpack.dev.js +++ b/client/webpack.dev.js @@ -21,6 +21,9 @@ const bypassProxyIf = [ (request) => request.url.startsWith('/oauth'), ]; +/** + * @type {import('webpack').Configuration} + */ module.exports = merge(common, { mode: 'development', devtool: 'eval-cheap-module-source-map', diff --git a/client/webpack.prod.js b/client/webpack.prod.js index 84d484a03c3..b67e2636cae 100644 --- a/client/webpack.prod.js +++ b/client/webpack.prod.js @@ -5,6 +5,9 @@ const common = require('./webpack.common'); const AVAILABLE_CPUS = +process.env.AVAILABLE_CPUS; +/** + * @type {import('webpack').Configuration} + */ module.exports = merge(common, { mode: 'production', devtool: 'source-map', From 19d0adedae8d23bacd0f8e7f7c51331030123ecc Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:51:07 +0800 Subject: [PATCH 16/36] perf(webpack): remove unused entry points --- client/app/__test__/setup.js | 2 -- client/package.json | 2 -- client/webpack.common.js | 9 +-------- client/yarn.lock | 20 +------------------- 4 files changed, 2 insertions(+), 31 deletions(-) diff --git a/client/app/__test__/setup.js b/client/app/__test__/setup.js index 0fcb817bfdd..ce09d53127a 100644 --- a/client/app/__test__/setup.js +++ b/client/app/__test__/setup.js @@ -14,8 +14,6 @@ import './mocks/matchMedia'; Enzyme.configure({ adapter: new Adapter() }); -require('@babel/polyfill'); - const timeZone = 'Asia/Singapore'; const intlCache = createIntlCache(); const intl = createIntl({ locale: 'en', timeZone }, intlCache); diff --git a/client/package.json b/client/package.json index b3a4ceb2020..f2c6892eb57 100644 --- a/client/package.json +++ b/client/package.json @@ -30,8 +30,6 @@ "extends": "react-app" }, "dependencies": { - "@babel/polyfill": "^7.12.1", - "@babel/runtime": "^7.26.10", "@ckeditor/ckeditor5-build-custom": "github:Coursemology/CKEditor5-build-coursemology#0.3.0", "@ckeditor/ckeditor5-react": "^6.2.0", "@emotion/react": "^11.11.4", diff --git a/client/webpack.common.js b/client/webpack.common.js index 70becf8ab2b..80d83dbc2e3 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -18,14 +18,7 @@ const ENV_DIR = process.env.BABEL_ENV === 'e2e-test' ? './.env.test' : './.env'; * @type {import('webpack').Configuration} */ module.exports = { - entry: { - coursemology: [ - '@babel/polyfill', - 'jquery', - './app/index', - './app/lib/moment-timezone', - ], - }, + entry: './app/index.tsx', output: { path: join(__dirname, 'build'), publicPath: '/', diff --git a/client/yarn.lock b/client/yarn.lock index 95030702c04..84e3af58a2f 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -957,14 +957,6 @@ "@babel/helper-create-regexp-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.24.0" -"@babel/polyfill@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.12.1.tgz#1f2d6371d1261bbd961f3c5d5909150e12d0bd96" - integrity sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g== - dependencies: - core-js "^2.6.5" - regenerator-runtime "^0.13.4" - "@babel/preset-env@^7.20.2", "@babel/preset-env@^7.24.3": version "7.24.3" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.3.tgz#f3f138c844ffeeac372597b29c51b5259e8323a3" @@ -1107,7 +1099,7 @@ core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.0", "@babel/runtime@^7.24.1", "@babel/runtime@^7.26.10", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.0", "@babel/runtime@^7.24.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== @@ -4148,11 +4140,6 @@ core-js-pure@^3.30.2: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.41.0.tgz#349fecad168d60807a31e83c99d73d786fe80811" integrity sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q== -core-js@^2.6.5: - version "2.6.12" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" - integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== - core-js@^3.30.2: version "3.30.2" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.2.tgz#6528abfda65e5ad728143ea23f7a14f0dcf503fc" @@ -9692,11 +9679,6 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.4: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - regenerator-runtime@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" From 9b3a80c39bc57db0a1ccdbcb2da0a38742b07de1 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:51:34 +0800 Subject: [PATCH 17/36] perf(webpack): remove unused expose-loader --- client/app/lib/moment-timezone.js | 2 -- client/package.json | 1 - client/webpack.common.js | 7 ------- client/yarn.lock | 5 ----- 4 files changed, 15 deletions(-) delete mode 100644 client/app/lib/moment-timezone.js diff --git a/client/app/lib/moment-timezone.js b/client/app/lib/moment-timezone.js deleted file mode 100644 index eecb46ced8c..00000000000 --- a/client/app/lib/moment-timezone.js +++ /dev/null @@ -1,2 +0,0 @@ -// Intermediate module for loading as a global in Webpack -module.exports = require('lib/moment').default; diff --git a/client/package.json b/client/package.json index f2c6892eb57..84060cd46dc 100644 --- a/client/package.json +++ b/client/package.json @@ -167,7 +167,6 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-simple-import-sort": "^12.1.0", "eslint-plugin-sonarjs": "^0.24.0", - "expose-loader": "^5.0.0", "favicons": "^7.1.4", "favicons-webpack-plugin": "^6.0.1", "fork-ts-checker-webpack-plugin": "^9.0.2", diff --git a/client/webpack.common.js b/client/webpack.common.js index 80d83dbc2e3..4162225f374 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -160,13 +160,6 @@ module.exports = { ], exclude: /node_modules/, }, - { - test: require.resolve('./app/lib/moment-timezone'), - loader: 'expose-loader', - options: { - exposes: 'moment', - }, - }, { test: /\.md$/, type: 'asset/source', diff --git a/client/yarn.lock b/client/yarn.lock index 84e3af58a2f..2e89aca8ffa 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -5411,11 +5411,6 @@ expect@^29.0.0, expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" -expose-loader@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-5.0.0.tgz#41368903eb1246b7c09fecf32c5cb3f67d0260e6" - integrity sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w== - express@^4.17.3: version "4.21.1" resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" From 619ae49b42b674aa0f669c90d93ff6aac15d56fe Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:51:56 +0800 Subject: [PATCH 18/36] perf(webpack): remove unused webpack-manifest-plugin --- client/package.json | 1 - client/webpack.common.js | 5 ----- client/yarn.lock | 21 --------------------- 3 files changed, 27 deletions(-) diff --git a/client/package.json b/client/package.json index 84060cd46dc..7623266fb5c 100644 --- a/client/package.json +++ b/client/package.json @@ -193,7 +193,6 @@ "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4", - "webpack-manifest-plugin": "^5.0.0", "webpack-merge": "^5.10.0" }, "resolutions": { diff --git a/client/webpack.common.js b/client/webpack.common.js index 4162225f374..faefadfa34e 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -4,7 +4,6 @@ const { ContextReplacementPlugin, DefinePlugin, } = require('webpack'); -const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); const DotenvPlugin = require('dotenv-webpack'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); @@ -70,10 +69,6 @@ module.exports = { plugins: [ new DotenvPlugin({ path: ENV_DIR }), new IgnorePlugin({ resourceRegExp: /__test__/ }), - new WebpackManifestPlugin({ - publicPath: '/webpack/', - writeToFileEmit: true, - }), new HtmlWebpackPlugin({ template: './public/index.html' }), new FaviconsWebpackPlugin({ logo: './favicon.svg', inject: true }), // Do not require all locales in moment diff --git a/client/yarn.lock b/client/yarn.lock index 2e89aca8ffa..dd3776cd9fb 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -10254,11 +10254,6 @@ sockjs@^0.3.24: uuid "^8.3.2" websocket-driver "^0.7.4" -source-list-map@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" @@ -11279,14 +11274,6 @@ webpack-dev-server@^5.0.4: webpack-dev-middleware "^7.1.0" ws "^8.16.0" -webpack-manifest-plugin@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-5.0.0.tgz#084246c1f295d1b3222d36e955546433ca8df803" - integrity sha512-8RQfMAdc5Uw3QbCQ/CBV/AXqOR8mt03B6GJmRbhWopE8GzRfEpn+k0ZuWywxW+5QZsffhmFDY1J6ohqJo+eMuw== - dependencies: - tapable "^2.0.0" - webpack-sources "^2.2.0" - webpack-merge@^5.10.0, webpack-merge@^5.7.3: version "5.10.0" resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" @@ -11296,14 +11283,6 @@ webpack-merge@^5.10.0, webpack-merge@^5.7.3: flat "^5.0.2" wildcard "^2.0.0" -webpack-sources@^2.2.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.3.1.tgz#570de0af163949fe272233c2cefe1b56f74511fd" - integrity sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA== - dependencies: - source-list-map "^2.0.1" - source-map "^0.6.1" - webpack-sources@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" From 35f22305ac3c1ad5d02c4f106f865b41df8b84c2 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:54:29 +0800 Subject: [PATCH 19/36] perf(webpack): improve CSS, Sass/SCSS compilations --- client/css-includes.json | 8 ++++++++ client/postcss.config.js | 1 + client/webpack.common.js | 35 +++++++++-------------------------- 3 files changed, 18 insertions(+), 26 deletions(-) create mode 100644 client/css-includes.json diff --git a/client/css-includes.json b/client/css-includes.json new file mode 100644 index 00000000000..54796f124d0 --- /dev/null +++ b/client/css-includes.json @@ -0,0 +1,8 @@ +[ + "app/theme/index.css", + "node_modules/rc-slider/assets", + "node_modules/react-image-crop/dist/ReactCrop.css", + "node_modules/react-tooltip/dist/react-tooltip.min.css", + "app/lib/components/core/fields/CKEditor.css", + "app/lib/components/core/fields/AceEditor.css" +] diff --git a/client/postcss.config.js b/client/postcss.config.js index 63cef2d77b2..71774a1208c 100644 --- a/client/postcss.config.js +++ b/client/postcss.config.js @@ -1,5 +1,6 @@ module.exports = { plugins: { + 'tailwindcss/nesting': {}, tailwindcss: {}, autoprefixer: {}, ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}), diff --git a/client/webpack.common.js b/client/webpack.common.js index faefadfa34e..9fa28fbe583 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -10,6 +10,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const FaviconsWebpackPlugin = require('favicons-webpack-plugin'); const packageJSON = require('./package.json'); +const cssIncludes = require('./css-includes.json'); const ENV_DIR = process.env.BABEL_ENV === 'e2e-test' ? './.env.test' : './.env'; @@ -91,31 +92,12 @@ module.exports = { rules: [ { test: /\.css$/, - use: ['style-loader', 'css-loader'], - include: [ - resolve(__dirname, 'node_modules/rc-slider/assets'), - resolve( - __dirname, - 'node_modules/react-image-crop/dist/ReactCrop.css', - ), - resolve( - __dirname, - 'node_modules/react-tooltip/dist/react-tooltip.min.css', - ), - resolve(__dirname, 'app/lib/components/core/fields/CKEditor.css'), - resolve(__dirname, 'app/lib/components/core/fields/AceEditor.css'), + use: [ + 'style-loader', + { loader: 'css-loader', options: { sourceMap: false } }, + 'postcss-loader', ], - }, - { - test: /\.css$/, - use: ['style-loader', 'css-loader', 'postcss-loader'], - include: [resolve(__dirname, 'app/theme/index.css')], - }, - { - test: /\.scss$/, - use: ['style-loader', 'css-loader', 'sass-loader'], - include: [resolve(__dirname, 'app/lib/styles')], - exclude: /node_modules/, + include: cssIncludes.map((path) => resolve(__dirname, path)), }, { test: /\.scss$/, @@ -124,15 +106,16 @@ module.exports = { { loader: 'css-loader', options: { - importLoaders: 1, + sourceMap: false, modules: { localIdentName: '[path]___[name]__[local]___[hash:base64:5]', }, }, }, + 'postcss-loader', 'sass-loader', ], - exclude: [/node_modules/, resolve(__dirname, 'app/lib/styles')], + exclude: [/node_modules/], }, { test: /\.(csv|png|svg)$/i, From 8ed1e4b9b11fe3bd934b82d1db1268e171d8ac52 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:55:45 +0800 Subject: [PATCH 20/36] fix(webpack): favicon-webpack-plugin warnings in development mode --- client/webpack.common.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/webpack.common.js b/client/webpack.common.js index 9fa28fbe583..c42e5f7ece8 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -71,7 +71,11 @@ module.exports = { new DotenvPlugin({ path: ENV_DIR }), new IgnorePlugin({ resourceRegExp: /__test__/ }), new HtmlWebpackPlugin({ template: './public/index.html' }), - new FaviconsWebpackPlugin({ logo: './favicon.svg', inject: true }), + new FaviconsWebpackPlugin({ + logo: './favicon.svg', + inject: true, + mode: 'auto', + }), // Do not require all locales in moment new ContextReplacementPlugin(/moment\/locale$/, /^\.\/(en-.*|zh-.*)$/), new ForkTsCheckerWebpackPlugin({ From 236160691b4d11f3042ef0aca465abafc9096b7b Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:56:36 +0800 Subject: [PATCH 21/36] fix(webpack): ENAMETOOLONG error due to too many chunks --- client/webpack.common.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/client/webpack.common.js b/client/webpack.common.js index c42e5f7ece8..8d5741d506e 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -42,28 +42,6 @@ module.exports = { optimization: { splitChunks: { chunks: 'all', - name: (_, chunks, cacheGroupKey) => { - /** - * Workers are not part of the `coursemology` runtime, so their dependencies - * are packed in a separate chunk. This chunk has `name` set to `undefined`. - * When simply `Array.prototype.join`ed, we will get weird chunk names like - * `vendors~coursemology~.js` or `vendors~.js` that `application_helper.rb` - * should inject. Normally, this isn't an issue with `HtmlWebpackPlugin`, but - * since we don't have that and are manually injecting webpack assets in - * `layouts/default.html.slim`, we combine these `undefined` chunks into - * `coursemology`'s runtime. So, we have one `vendors~coursemology.js`. - */ - const allChunksNames = - chunks - .map((chunk) => chunk.name) - .filter((name) => Boolean(name)) - .join('~') || 'coursemology'; - - const prefix = - cacheGroupKey === 'defaultVendors' ? 'vendors' : cacheGroupKey; - - return `${prefix}~${allChunksNames}`; - }, }, moduleIds: 'deterministic', }, From d5e548497e74005bda7c5a9191efcd94f1e1b225 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:57:12 +0800 Subject: [PATCH 22/36] perf: partially remove PropTypes in production --- client/.babelrc | 9 ++++++++- client/package.json | 1 + client/yarn.lock | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/client/.babelrc b/client/.babelrc index ad46cff5b3f..1c9ea75a9bb 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -36,7 +36,14 @@ "env": { "production": { "plugins": [ - ["react-remove-properties", { "properties": ["data-testid"] }] + ["react-remove-properties", { "properties": ["data-testid"] }], + [ + "transform-react-remove-prop-types", + { + "mode": "remove", + "removeImport": true + } + ] ] }, "test": { diff --git a/client/package.json b/client/package.json index 7623266fb5c..3b4f0f9c2b8 100644 --- a/client/package.json +++ b/client/package.json @@ -146,6 +146,7 @@ "babel-plugin-istanbul": "^6.1.1", "babel-plugin-react-remove-properties": "^0.3.0", "babel-plugin-transform-import-meta": "^2.2.1", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "css-loader": "^6.11.0", "cssnano": "^6.1.2", "dotenv-webpack": "^8.0.1", diff --git a/client/yarn.lock b/client/yarn.lock index dd3776cd9fb..80d115948e3 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -3503,6 +3503,11 @@ babel-plugin-transform-import-meta@^2.2.1: "@babel/template" "^7.4.4" tslib "^2.4.0" +babel-plugin-transform-react-remove-prop-types@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" + integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== + babel-preset-current-node-syntax@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" From 678dee10c5215d3987021a441b8e17680e7d78c9 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:58:25 +0800 Subject: [PATCH 23/36] feat(webpack): always clean build folder before building --- client/webpack.prod.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/webpack.prod.js b/client/webpack.prod.js index b67e2636cae..22bb6db533b 100644 --- a/client/webpack.prod.js +++ b/client/webpack.prod.js @@ -14,6 +14,7 @@ module.exports = merge(common, { output: { filename: '[name]-[contenthash].js', publicPath: '/static/', + clean: true, }, optimization: { usedExports: true, From 4db8dbeb8bd57e9f6f4c6955a8a6ee38cf0d8aa0 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:58:37 +0800 Subject: [PATCH 24/36] perf(webpack): enable caching for production builds --- client/webpack.prod.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/webpack.prod.js b/client/webpack.prod.js index 22bb6db533b..077df2f23ea 100644 --- a/client/webpack.prod.js +++ b/client/webpack.prod.js @@ -16,6 +16,9 @@ module.exports = merge(common, { publicPath: '/static/', clean: true, }, + cache: { + type: 'filesystem', + }, optimization: { usedExports: true, minimizer: [ From 05d54a42fd8b2accbd89cf92752b1db8f7732478 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:59:17 +0800 Subject: [PATCH 25/36] perf(webpack): use SWC for faster & more aggressive minification --- client/package.json | 1 + client/webpack.prod.js | 7 ++++ client/yarn.lock | 81 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/client/package.json b/client/package.json index 3b4f0f9c2b8..ff73ac2d44d 100644 --- a/client/package.json +++ b/client/package.json @@ -117,6 +117,7 @@ "@formatjs/cli": "^6.2.7", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@svgr/webpack": "^8.1.0", + "@swc/core": "^1.11.21", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.2", diff --git a/client/webpack.prod.js b/client/webpack.prod.js index 077df2f23ea..9ea7350bf47 100644 --- a/client/webpack.prod.js +++ b/client/webpack.prod.js @@ -24,6 +24,13 @@ module.exports = merge(common, { minimizer: [ new TerserPlugin({ parallel: AVAILABLE_CPUS || true, + minify: TerserPlugin.swcMinify, + extractComments: false, + terserOptions: { + compress: true, + mangle: true, + format: { comments: false }, + }, }), ], }, diff --git a/client/yarn.lock b/client/yarn.lock index 80d115948e3..7522c61e3d4 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2129,6 +2129,87 @@ "@svgr/plugin-jsx" "8.1.0" "@svgr/plugin-svgo" "8.1.0" +"@swc/core-darwin-arm64@1.11.21": + version "1.11.21" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.21.tgz#8bae966479e96481ff4b6ac285d6684cb7e31995" + integrity sha512-v6gjw9YFWvKulCw3ZA1dY+LGMafYzJksm1mD4UZFZ9b36CyHFowYVYug1ajYRIRqEvvfIhHUNV660zTLoVFR8g== + +"@swc/core-darwin-x64@1.11.21": + version "1.11.21" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.11.21.tgz#231ba71b64ddac14e9ca97f4622c9d2fb681f602" + integrity sha512-CUiTiqKlzskwswrx9Ve5NhNoab30L1/ScOfQwr1duvNlFvarC8fvQSgdtpw2Zh3MfnfNPpyLZnYg7ah4kbT9JQ== + +"@swc/core-linux-arm-gnueabihf@1.11.21": + version "1.11.21" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.21.tgz#0c73e3a3751898895db103bee5bad342da23d2ca" + integrity sha512-YyBTAFM/QPqt1PscD8hDmCLnqPGKmUZpqeE25HXY8OLjl2MUs8+O4KjwPZZ+OGxpdTbwuWFyMoxjcLy80JODvg== + +"@swc/core-linux-arm64-gnu@1.11.21": + version "1.11.21" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.21.tgz#678a6aaba5aa42d62fa3fdfa0b0e28a77ebc18d1" + integrity sha512-DQD+ooJmwpNsh4acrftdkuwl5LNxxg8U4+C/RJNDd7m5FP9Wo4c0URi5U0a9Vk/6sQNh9aSGcYChDpqCDWEcBw== + +"@swc/core-linux-arm64-musl@1.11.21": + version "1.11.21" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.21.tgz#f1b66df0a9f0a81c3a574f4b3e560deee56012bc" + integrity sha512-y1L49+snt1a1gLTYPY641slqy55QotPdtRK9Y6jMi4JBQyZwxC8swWYlQWb+MyILwxA614fi62SCNZNznB3XSA== + +"@swc/core-linux-x64-gnu@1.11.21": + version "1.11.21" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.21.tgz#5d3d83763cebc686cc0ef9fa6b3461118a3bfb6c" + integrity sha512-NesdBXv4CvVEaFUlqKj+GA4jJMNUzK2NtKOrUNEtTbXaVyNiXjFCSaDajMTedEB0jTAd9ybB0aBvwhgkJUWkWA== + +"@swc/core-linux-x64-musl@1.11.21": + version "1.11.21" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.21.tgz#5e0cc95bf89b74ba913afeb66cc93f35d59abea0" + integrity sha512-qFV60pwpKVOdmX67wqQzgtSrUGWX9Cibnp1CXyqZ9Mmt8UyYGvmGu7p6PMbTyX7vdpVUvWVRf8DzrW2//wmVHg== + +"@swc/core-win32-arm64-msvc@1.11.21": + version "1.11.21" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.21.tgz#070c70f5684b6f96e3d51f7ca3c70d7f971cffa9" + integrity sha512-DJJe9k6gXR/15ZZVLv1SKhXkFst8lYCeZRNHH99SlBodvu4slhh/MKQ6YCixINRhCwliHrpXPym8/5fOq8b7Ig== + +"@swc/core-win32-ia32-msvc@1.11.21": + version "1.11.21" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.21.tgz#e72ad415d18d7e4660a9ebb8529ffd68c02c61da" + integrity sha512-TqEXuy6wedId7bMwLIr9byds+mKsaXVHctTN88R1UIBPwJA92Pdk0uxDgip0pEFzHB/ugU27g6d8cwUH3h2eIw== + +"@swc/core-win32-x64-msvc@1.11.21": + version "1.11.21" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.21.tgz#7ff6dd290b36c013a98aea877cda288251e48119" + integrity sha512-BT9BNNbMxdpUM1PPAkYtviaV0A8QcXttjs2MDtOeSqqvSJaPtyM+Fof2/+xSwQDmDEFzbGCcn75M5+xy3lGqpA== + +"@swc/core@^1.11.21": + version "1.11.21" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.11.21.tgz#012dc73111e6ecea96cc30b522a50355eca4b35c" + integrity sha512-/Y3BJLcwd40pExmdar8MH2UGGvCBrqNN7hauOMckrEX2Ivcbv3IMhrbGX4od1dnF880Ed8y/E9aStZCIQi0EGw== + dependencies: + "@swc/counter" "^0.1.3" + "@swc/types" "^0.1.21" + optionalDependencies: + "@swc/core-darwin-arm64" "1.11.21" + "@swc/core-darwin-x64" "1.11.21" + "@swc/core-linux-arm-gnueabihf" "1.11.21" + "@swc/core-linux-arm64-gnu" "1.11.21" + "@swc/core-linux-arm64-musl" "1.11.21" + "@swc/core-linux-x64-gnu" "1.11.21" + "@swc/core-linux-x64-musl" "1.11.21" + "@swc/core-win32-arm64-msvc" "1.11.21" + "@swc/core-win32-ia32-msvc" "1.11.21" + "@swc/core-win32-x64-msvc" "1.11.21" + +"@swc/counter@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" + integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== + +"@swc/types@^0.1.21": + version "0.1.21" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.21.tgz#6fcadbeca1d8bc89e1ab3de4948cef12344a38c0" + integrity sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ== + dependencies: + "@swc/counter" "^0.1.3" + "@tailwindcss/container-queries@^0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz#9a759ce2cb8736a4c6a0cb93aeb740573a731974" From 3d74ef2143586aa92876222f88a146cee4538c9b Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 01:00:06 +0800 Subject: [PATCH 26/36] perf(webpack): trim moment-timezone data to only from 2014 --- client/package.json | 1 + client/webpack.prod.js | 4 ++++ client/yarn.lock | 10 +++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/client/package.json b/client/package.json index ff73ac2d44d..e80464afe83 100644 --- a/client/package.json +++ b/client/package.json @@ -179,6 +179,7 @@ "jest-environment-jsdom": "^29.7.0", "jest-localstorage-mock": "^2.4.26", "mkdirp": "^3.0.1", + "moment-timezone-data-webpack-plugin": "^1.5.1", "postcss": "^8.4.38", "postcss-loader": "^8.1.1", "prettier": "^3.2.5", diff --git a/client/webpack.prod.js b/client/webpack.prod.js index 9ea7350bf47..c63d899caff 100644 --- a/client/webpack.prod.js +++ b/client/webpack.prod.js @@ -1,5 +1,6 @@ const { merge } = require('webpack-merge'); const TerserPlugin = require('terser-webpack-plugin'); +const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin'); const common = require('./webpack.common'); @@ -43,4 +44,7 @@ module.exports = merge(common, { }, ], }, + plugins: [ + new MomentTimezoneDataPlugin({ startYear: 2014 }), + ], }); diff --git a/client/yarn.lock b/client/yarn.lock index 7522c61e3d4..a6ed60cde6c 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -5672,7 +5672,7 @@ find-cache-dir@^2.0.0: make-dir "^2.0.0" pkg-dir "^3.0.0" -find-cache-dir@^3.3.2: +find-cache-dir@^3.0.0, find-cache-dir@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== @@ -8079,6 +8079,14 @@ mkdirp@^3.0.1: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== +moment-timezone-data-webpack-plugin@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/moment-timezone-data-webpack-plugin/-/moment-timezone-data-webpack-plugin-1.5.1.tgz#9d35dfd3768db55058e1e809d77a2b64bd6d03a4" + integrity sha512-1le6a35GgYdWMVYFzrfpE/F6Pk4bj0M3QKD6Iv6ba9LqWGoVqHQRHyCTLvLis5E1J98Sz40ET6yhZzMVakwpjg== + dependencies: + find-cache-dir "^3.0.0" + make-dir "^3.0.0" + moment-timezone@^0.5.44: version "0.5.44" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.44.tgz#a64a4e47b68a43deeab5ae4eb4f82da77cdf595f" From 8a5bb3a3404618092028381d0b0eb2de8ab18124 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 01:00:45 +0800 Subject: [PATCH 27/36] perf(webpack): optimise images, SVGs --- client/package.json | 3 + client/webpack.prod.js | 21 +++++ client/yarn.lock | 188 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 211 insertions(+), 1 deletion(-) diff --git a/client/package.json b/client/package.json index e80464afe83..b1c15d89963 100644 --- a/client/package.json +++ b/client/package.json @@ -174,6 +174,7 @@ "fork-ts-checker-webpack-plugin": "^9.0.2", "glob": "^10.3.7", "html-webpack-plugin": "^5.6.0", + "image-minimizer-webpack-plugin": "^4.1.3", "jest": "^29.7.0", "jest-canvas-mock": "^2.5.2", "jest-environment-jsdom": "^29.7.0", @@ -188,7 +189,9 @@ "redux-logger": "^3.0.6", "sass": "^1.76.0", "sass-loader": "^14.1.0", + "sharp": "^0.34.1", "style-loader": "^3.3.4", + "svgo": "^3.3.2", "tailwindcss": "^3.4.1", "terser-webpack-plugin": "^5.3.10", "ts-jest": "^29.1.2", diff --git a/client/webpack.prod.js b/client/webpack.prod.js index c63d899caff..1d8692aa37a 100644 --- a/client/webpack.prod.js +++ b/client/webpack.prod.js @@ -1,6 +1,7 @@ const { merge } = require('webpack-merge'); const TerserPlugin = require('terser-webpack-plugin'); const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin'); +const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin'); const common = require('./webpack.common'); @@ -46,5 +47,25 @@ module.exports = merge(common, { }, plugins: [ new MomentTimezoneDataPlugin({ startYear: 2014 }), + new ImageMinimizerPlugin({ + test: /\.(jpe?g|png|gif)$/i, + minimizer: { + implementation: ImageMinimizerPlugin.sharpMinify, + options: { + encodeOptions: { + png: { + quality: 90, + compressionLevel: 9, + }, + }, + }, + }, + }), + new ImageMinimizerPlugin({ + test: /\.svg$/i, + minimizer: { + implementation: ImageMinimizerPlugin.svgoMinify, + }, + }), ], }); diff --git a/client/yarn.lock b/client/yarn.lock index a6ed60cde6c..097bc15fe4d 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1178,6 +1178,13 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@emnapi/runtime@^1.4.0": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.3.tgz#c0564665c80dc81c448adac23f9dfbed6c838f7d" + integrity sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ== + dependencies: + tslib "^2.4.0" + "@emotion/babel-plugin@^11.11.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" @@ -1474,6 +1481,124 @@ resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw== +"@img/sharp-darwin-arm64@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz#e79a4756bea9a06a7aadb4391ee53cb154a4968c" + integrity sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A== + optionalDependencies: + "@img/sharp-libvips-darwin-arm64" "1.1.0" + +"@img/sharp-darwin-x64@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz#f1f1d386719f6933796415d84937502b7199a744" + integrity sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.1.0" + +"@img/sharp-libvips-darwin-arm64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz#843f7c09c7245dc0d3cfec2b3c83bb08799a704f" + integrity sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA== + +"@img/sharp-libvips-darwin-x64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz#1239c24426c06a8e833815562f78047a3bfbaaf8" + integrity sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ== + +"@img/sharp-libvips-linux-arm64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz#20d276cefd903ee483f0441ba35961679c286315" + integrity sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew== + +"@img/sharp-libvips-linux-arm@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz#067c0b566eae8063738cf1b1db8f8a8573b5465c" + integrity sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA== + +"@img/sharp-libvips-linux-ppc64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz#682334595f2ca00e0a07a675ba170af165162802" + integrity sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ== + +"@img/sharp-libvips-linux-s390x@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz#82fcd68444b3666384235279c145c2b28d8ee302" + integrity sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA== + +"@img/sharp-libvips-linux-x64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz#65b2b908bf47156b0724fde9095676c83a18cf5a" + integrity sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q== + +"@img/sharp-libvips-linuxmusl-arm64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz#72accf924e80b081c8db83b900b444a67c203f01" + integrity sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w== + +"@img/sharp-libvips-linuxmusl-x64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz#1fa052737e203f46bf44192acd01f9faf11522d7" + integrity sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A== + +"@img/sharp-linux-arm64@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz#c36ef964499b8cfc2d2ed88fe68f27ce41522c80" + integrity sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.1.0" + +"@img/sharp-linux-arm@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz#c96e38ff028d645912bb0aa132a7178b96997866" + integrity sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.1.0" + +"@img/sharp-linux-s390x@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz#8ac58d9a49dcb08215e76c8d450717979b7815c3" + integrity sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA== + optionalDependencies: + "@img/sharp-libvips-linux-s390x" "1.1.0" + +"@img/sharp-linux-x64@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz#3d8652efac635f0dba39d5e3b8b49515a2b2dee1" + integrity sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA== + optionalDependencies: + "@img/sharp-libvips-linux-x64" "1.1.0" + +"@img/sharp-linuxmusl-arm64@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz#b267e6a3e06f9e4d345cde471e5480c5c39e6969" + integrity sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64" "1.1.0" + +"@img/sharp-linuxmusl-x64@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz#a8dee4b6227f348c4bbacaa6ac3dc584a1a80391" + integrity sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.1.0" + +"@img/sharp-wasm32@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz#f7dfd66b6c231269042d3d8750c90f28b9ddcba1" + integrity sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg== + dependencies: + "@emnapi/runtime" "^1.4.0" + +"@img/sharp-win32-ia32@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz#4bc293705df76a5f0a02df66ca3dc12e88f61332" + integrity sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw== + +"@img/sharp-win32-x64@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz#8a7922fec949f037c204c79f6b83238d2482384b" + integrity sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -4649,6 +4774,11 @@ detect-libc@^2.0.0, detect-libc@^2.0.2: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== +detect-libc@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -6342,6 +6472,14 @@ ignore@^5.0.5, ignore@^5.2.0, ignore@^5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +image-minimizer-webpack-plugin@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/image-minimizer-webpack-plugin/-/image-minimizer-webpack-plugin-4.1.3.tgz#728e89d153978f49396fe1881aa11fe6cef57f83" + integrity sha512-yJvYlLAZosu2iqlGF81BEUHfUiWRPD05krtoax9Ffst3Yzbn3X7p04VXambwlx3uhbSwH/BeyM5+bJHQksnuyw== + dependencies: + schema-utils "^4.2.0" + serialize-javascript "^6.0.2" + immer@^10.0.4: version "10.0.4" resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.4.tgz#09af41477236b99449f9d705369a4daaf780362b" @@ -10119,6 +10257,11 @@ semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4: dependencies: lru-cache "^6.0.0" +semver@^7.7.1: + version "7.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" + integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== + send@0.19.0: version "0.19.0" resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" @@ -10138,7 +10281,7 @@ send@0.19.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@^6.0.1: +serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== @@ -10240,6 +10383,36 @@ sharp@^0.32.4: tar-fs "^3.0.4" tunnel-agent "^0.6.0" +sharp@^0.34.1: + version "0.34.1" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.1.tgz#e5922894b0cc7ddf159eeabc6d5668e4e8b11d61" + integrity sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg== + dependencies: + color "^4.2.3" + detect-libc "^2.0.3" + semver "^7.7.1" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.34.1" + "@img/sharp-darwin-x64" "0.34.1" + "@img/sharp-libvips-darwin-arm64" "1.1.0" + "@img/sharp-libvips-darwin-x64" "1.1.0" + "@img/sharp-libvips-linux-arm" "1.1.0" + "@img/sharp-libvips-linux-arm64" "1.1.0" + "@img/sharp-libvips-linux-ppc64" "1.1.0" + "@img/sharp-libvips-linux-s390x" "1.1.0" + "@img/sharp-libvips-linux-x64" "1.1.0" + "@img/sharp-libvips-linuxmusl-arm64" "1.1.0" + "@img/sharp-libvips-linuxmusl-x64" "1.1.0" + "@img/sharp-linux-arm" "0.34.1" + "@img/sharp-linux-arm64" "0.34.1" + "@img/sharp-linux-s390x" "0.34.1" + "@img/sharp-linux-x64" "0.34.1" + "@img/sharp-linuxmusl-arm64" "0.34.1" + "@img/sharp-linuxmusl-x64" "0.34.1" + "@img/sharp-wasm32" "0.34.1" + "@img/sharp-win32-ia32" "0.34.1" + "@img/sharp-win32-x64" "0.34.1" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -10677,6 +10850,19 @@ svgo@^3.0.2, svgo@^3.2.0: csso "^5.0.5" picocolors "^1.0.0" +svgo@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.2.tgz#ad58002652dffbb5986fc9716afe52d869ecbda8" + integrity sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^5.1.0" + css-tree "^2.3.1" + css-what "^6.1.0" + csso "^5.0.5" + picocolors "^1.0.0" + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" From 17840d1df8ab98d8aa0f6ab99a6d6820a5ad6df2 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 01:01:17 +0800 Subject: [PATCH 28/36] perf(webpack): generate gzip, brotli compressed bundles --- client/package.json | 1 + client/webpack.prod.js | 16 ++++++++++++++++ client/yarn.lock | 8 ++++++++ 3 files changed, 25 insertions(+) diff --git a/client/package.json b/client/package.json index b1c15d89963..c6ab93cd0a6 100644 --- a/client/package.json +++ b/client/package.json @@ -148,6 +148,7 @@ "babel-plugin-react-remove-properties": "^0.3.0", "babel-plugin-transform-import-meta": "^2.2.1", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", + "compression-webpack-plugin": "^11.1.0", "css-loader": "^6.11.0", "cssnano": "^6.1.2", "dotenv-webpack": "^8.0.1", diff --git a/client/webpack.prod.js b/client/webpack.prod.js index 1d8692aa37a..0f3e442aa08 100644 --- a/client/webpack.prod.js +++ b/client/webpack.prod.js @@ -2,6 +2,8 @@ const { merge } = require('webpack-merge'); const TerserPlugin = require('terser-webpack-plugin'); const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin'); const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin'); +const CompressionPlugin = require('compression-webpack-plugin'); +const zlib = require('zlib'); const common = require('./webpack.common'); @@ -67,5 +69,19 @@ module.exports = merge(common, { implementation: ImageMinimizerPlugin.svgoMinify, }, }), + new CompressionPlugin({ + test: /\.(js|css|html|svg|png)$/, + algorithm: 'gzip', + }), + new CompressionPlugin({ + algorithm: 'brotliCompress', + test: /\.(js|css|html|svg|png)$/, + compressionOptions: { + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: 11, + }, + }, + threshold: 10240, + }), ], }); diff --git a/client/yarn.lock b/client/yarn.lock index 097bc15fe4d..3c1f640c390 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -4262,6 +4262,14 @@ compressible@~2.0.16: dependencies: mime-db ">= 1.43.0 < 2" +compression-webpack-plugin@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-11.1.0.tgz#ee340d2029cf99ccecdea9ad1410b377d15b48b3" + integrity sha512-zDOQYp10+upzLxW+VRSjEpRRwBXJdsb5lBMlRxx1g8hckIFBpe3DTI0en2w7h+beuq89576RVzfiXrkdPGrHhA== + dependencies: + schema-utils "^4.2.0" + serialize-javascript "^6.0.2" + compression@^1.7.4: version "1.7.4" resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" From 66afa777c0ef5dce6c163f93b6bccee0a7c2be2c Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 01:02:58 +0800 Subject: [PATCH 29/36] perf(i18n): lazy load translations --- .gitignore | 2 + .../lib/components/wrappers/I18nProvider.tsx | 57 +++++++++++++++---- client/package.json | 4 +- client/scripts/aggregate-translations.js | 28 --------- client/yarn.lock | 5 -- 5 files changed, 49 insertions(+), 47 deletions(-) delete mode 100644 client/scripts/aggregate-translations.js diff --git a/.gitignore b/.gitignore index f759f0926d2..7e41de355a5 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,5 @@ playwright/.cache/ coverage/ dump.rdb + +compiled-locales diff --git a/client/app/lib/components/wrappers/I18nProvider.tsx b/client/app/lib/components/wrappers/I18nProvider.tsx index 368c9cd044c..b85c279869b 100644 --- a/client/app/lib/components/wrappers/I18nProvider.tsx +++ b/client/app/lib/components/wrappers/I18nProvider.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import { IntlProvider } from 'react-intl'; import { @@ -8,7 +8,7 @@ import { import { useI18nConfig } from 'lib/hooks/session'; import moment from 'lib/moment'; -import translations from '../../../../build/locales/locales.json'; +import LoadingIndicator from '../core/LoadingIndicator'; interface I18nProviderProps { children: ReactNode; @@ -17,26 +17,61 @@ interface I18nProviderProps { const getLocaleWithoutRegionCode = (locale: string): string => locale.toLowerCase().split(/[_-]+/)[0]; -const getMessages = (locale: string): Record | undefined => { - const localeWithoutRegionCode = getLocaleWithoutRegionCode(locale); - - return localeWithoutRegionCode !== DEFAULT_LOCALE - ? translations[localeWithoutRegionCode] || translations[locale] - : undefined; -}; - const I18nProvider = (props: I18nProviderProps): JSX.Element => { const { locale, timeZone } = useI18nConfig(); + const [messages, setMessages] = useState>(); useEffect(() => { moment.tz.setDefault(timeZone?.trim() || DEFAULT_TIME_ZONE); }, [timeZone]); + const localeWithoutRegionCode = getLocaleWithoutRegionCode(locale); + + useEffect(() => { + setMessages(undefined); + + let ignore = false; + + (async (): Promise => { + let loadedMessages: Record; + + try { + loadedMessages = await import( + /* webpackChunkName: "locale-[request]" */ + `../../../../compiled-locales/${localeWithoutRegionCode}.json` + ); + } catch (error) { + if ( + !( + error instanceof Error && + error.message.includes('Cannot find module') + ) + ) + throw error; + + loadedMessages = await import( + /* webpackChunkName: "locale-[request]" */ + `../../../../compiled-locales/${DEFAULT_LOCALE}.json` + ); + } + + if (ignore) return; + + setMessages(loadedMessages); + })(); + + return () => { + ignore = true; + }; + }, [localeWithoutRegionCode]); + + if (!messages) return ; + return ( {props.children} diff --git a/client/package.json b/client/package.json index c6ab93cd0a6..59f4eb7e28d 100644 --- a/client/package.json +++ b/client/package.json @@ -13,7 +13,7 @@ "build:production": "export NODE_ENV=production && yarn run build:translations && webpack --node-env=production --config webpack.prod.js", "build:development": "yarn run build:translations && webpack serve --config webpack.dev.js", "build:profile": "yarn run build:translations && webpack serve --config webpack.profile.js --progress=profile", - "build:translations": "babel-node scripts/aggregate-translations.js", + "build:translations": "formatjs compile-folder --ast locales compiled-locales", "extract-translations": "formatjs extract \"app/**/*.{js,jsx,ts,tsx}\" --ignore='**/*.d.ts' --out-file ./locales/en.json", "lint-src": "eslint . --ext .js --ext .jsx --ext .ts --ext .tsx --cache --ignore-pattern '**/__test__/**' --ignore-pattern 'coverage/**'", "lint-tests": "eslint . --ext .test.js --ext .test.jsx --ext .test.ts --ext .test.tsx --cache", @@ -173,14 +173,12 @@ "favicons": "^7.1.4", "favicons-webpack-plugin": "^6.0.1", "fork-ts-checker-webpack-plugin": "^9.0.2", - "glob": "^10.3.7", "html-webpack-plugin": "^5.6.0", "image-minimizer-webpack-plugin": "^4.1.3", "jest": "^29.7.0", "jest-canvas-mock": "^2.5.2", "jest-environment-jsdom": "^29.7.0", "jest-localstorage-mock": "^2.4.26", - "mkdirp": "^3.0.1", "moment-timezone-data-webpack-plugin": "^1.5.1", "postcss": "^8.4.38", "postcss-loader": "^8.1.1", diff --git a/client/scripts/aggregate-translations.js b/client/scripts/aggregate-translations.js deleted file mode 100644 index d672a933618..00000000000 --- a/client/scripts/aggregate-translations.js +++ /dev/null @@ -1,28 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const globSync = require('glob').sync; -const { sync: mkdirpSync } = require('mkdirp').mkdirp; - -const OUTPUT_DIR = './build/locales/'; - -// Excludes en as it is the default language -const translations = globSync('./locales/[!en]*.json') - .map((filename) => [ - path.basename(filename, '.json'), - fs.readFileSync(filename, 'utf8'), - ]) - .map(([locale, file]) => [locale, JSON.parse(file)]) - .reduce((collection, [locale, messages]) => { - const extractedMessages = {}; - Object.keys(messages).forEach((key) => { - extractedMessages[key] = messages[key].defaultMessage; - }); - return { ...collection, [locale]: { ...extractedMessages } }; - }, {}); - -// Write the messages to this directory -mkdirpSync(OUTPUT_DIR); -fs.writeFileSync( - `${OUTPUT_DIR}locales.json`, - JSON.stringify(translations, null, 2), -); diff --git a/client/yarn.lock b/client/yarn.lock index 3c1f640c390..8f7e69bc3f3 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -8220,11 +8220,6 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" - integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== - moment-timezone-data-webpack-plugin@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/moment-timezone-data-webpack-plugin/-/moment-timezone-data-webpack-plugin-1.5.1.tgz#9d35dfd3768db55058e1e809d77a2b64bd6d03a4" From 8f40e0523f5f28ea1cd95321401096624a19f519 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Sat, 19 Apr 2025 01:03:53 +0800 Subject: [PATCH 30/36] perf(CKEditorRichText): optimise build, upgrade to v45 --- .../lib/components/core/fields/CKEditor.css | 5 +- .../components/core/fields/CKEditorField.tsx | 63 + .../core/fields/CKEditorRichText.tsx | 171 +-- client/css-includes.json | 1 + client/package.json | 5 +- client/yarn.lock | 1263 ++++++++++++++++- 6 files changed, 1360 insertions(+), 148 deletions(-) create mode 100644 client/app/lib/components/core/fields/CKEditorField.tsx diff --git a/client/app/lib/components/core/fields/CKEditor.css b/client/app/lib/components/core/fields/CKEditor.css index cdec3ab717f..cb1f81e2c40 100644 --- a/client/app/lib/components/core/fields/CKEditor.css +++ b/client/app/lib/components/core/fields/CKEditor.css @@ -1,6 +1,5 @@ /* Below are needed to ensure ckeditor popup (eg link) is rendered properly in MUI dialog */ - .ck-body-wrapper { position: absolute; z-index: 1500; @@ -8,3 +7,7 @@ is rendered properly in MUI dialog */ display: none; } } + +.ck-editor__editable { + max-height: 35rem; +} diff --git a/client/app/lib/components/core/fields/CKEditorField.tsx b/client/app/lib/components/core/fields/CKEditorField.tsx new file mode 100644 index 00000000000..01c96b50d31 --- /dev/null +++ b/client/app/lib/components/core/fields/CKEditorField.tsx @@ -0,0 +1,63 @@ +import { CKEditor } from '@ckeditor/ckeditor5-react'; +import type { FileLoader, UploadAdapter, UploadResponse } from 'ckeditor5'; +import ClassicEditor from 'coursemology-ckeditor'; + +import attachmentsAPI from 'api/Attachments'; + +import 'coursemology-ckeditor/build/index.css'; +import './CKEditor.css'; + +class SimpleUploadAdapter implements UploadAdapter { + private loader: FileLoader; + + constructor(loader: FileLoader) { + this.loader = loader; + } + + async upload(): Promise { + const file = await this.loader.file; + if (file === null) return {}; + + const data = (await attachmentsAPI.create(file)).data; + if (!data.success) return {}; + + return { default: `/attachments/${data.id}` }; + } +} + +const CKEditorField = ({ + placeholder, + disabled, + value, + autoFocus, + onChange, + onBlur, + onFocus, +}: { + placeholder?: string; + disabled?: boolean; + value?: string; + autoFocus?: boolean; + onChange?: (value: string) => void; + onBlur?: () => void; + onFocus?: () => void; +}): JSX.Element => ( + onChange?.(editor.getData())} + onFocus={onFocus} + onReady={(editor) => { + editor.plugins.get('FileRepository').createUploadAdapter = ( + loader, + ): UploadAdapter => new SimpleUploadAdapter(loader); + + if (autoFocus) editor.focus(); + }} + /> +); + +export default CKEditorField; diff --git a/client/app/lib/components/core/fields/CKEditorRichText.tsx b/client/app/lib/components/core/fields/CKEditorRichText.tsx index 8f22ac841e9..383c92f9f73 100644 --- a/client/app/lib/components/core/fields/CKEditorRichText.tsx +++ b/client/app/lib/components/core/fields/CKEditorRichText.tsx @@ -1,15 +1,25 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { useState } from 'react'; -import CustomEditor from '@ckeditor/ckeditor5-build-custom'; -import { CKEditor } from '@ckeditor/ckeditor5-react'; -import { FormHelperText, InputLabel } from '@mui/material'; +import { lazy, Suspense, useState } from 'react'; +import { FormHelperText, InputLabel, Skeleton } from '@mui/material'; import { cyan } from '@mui/material/colors'; -import attachmentsAPI from 'api/Attachments'; +const CKEditorField = lazy( + () => import(/* webpackChunkName: "CKEditorField" */ './CKEditorField'), +); -import './CKEditor.css'; - -interface Props { +const CKEditorRichText = ({ + label, + value, + onChange, + disabled, + error, + field, + required, + name, + inputId, + disableMargins, + placeholder, + autofocus, +}: { name: string; onChange: (text: string) => void; value: string; @@ -22,57 +32,15 @@ interface Props { label?: string; placeholder?: string; required?: boolean | undefined; -} - -const uploadAdapter = (loader) => { - return { - upload: () => - new Promise((resolve, reject) => { - loader.file.then((file: File) => { - attachmentsAPI - .create(file) - .then((response) => response.data) - .then((data) => { - if (data.success) { - resolve({ default: `/attachments/${data.id}` }); - } - }) - .catch((err) => { - reject(err); - }); - }); - }), - abort: () => {}, - }; -}; - -const CKEditorRichText = (props: Props) => { - const { - label, - value, - onChange, - disabled, - error, - field, - required, - name, - inputId, - disableMargins, - placeholder, - autofocus, - } = props; - +}): JSX.Element => { const [isFocused, setIsFocused] = useState(false); const textFieldLabelColor = isFocused ? cyan[500] : undefined; return (
{ > {label && ( {label} )} +