diff --git a/.gitignore b/.gitignore index 9e2c5aa0d..dc27a187d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,6 @@ yarn-error.log* **/runner-results parallel-weights.json -**/src/tempcodes \ No newline at end of file +**/src/tempcodes +# Generated from apps/app/scripts/copy-vendor-assets.mjs during install/build. +apps/app/public/vendor/ diff --git a/apps/api/package.json b/apps/api/package.json index 9aeaeda02..b6a7cc950 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -9,7 +9,7 @@ "main": "dist/src/index.js", "scripts": { "build:assets": "npx tsx scripts/copy-assets-to-dist.ts", - "build": "tsc --build && npm run build:assets", + "build": "tsc --build tsconfig.build.json && npm run build:assets", "dev": "tsx watch src/index.ts", "lint": "eslint --fix", "lint:check": "eslint", diff --git a/apps/api/tsconfig.build.json b/apps/api/tsconfig.build.json new file mode 100644 index 000000000..e46f41e18 --- /dev/null +++ b/apps/api/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.tsbuildinfo/apps/api.build.tsbuildinfo" + }, + "references": [ + { + "path": "../../packages/shared" + } + ], + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] +} diff --git a/apps/app/eslint.config.mjs b/apps/app/eslint.config.mjs index d680e0156..3be2ee38f 100644 --- a/apps/app/eslint.config.mjs +++ b/apps/app/eslint.config.mjs @@ -6,6 +6,11 @@ import { createBaseConfig, reactConfig } from "@doenet-tools/eslint-config"; export default tseslint.config( ...createBaseConfig(import.meta.dirname), ...reactConfig, + { + // Vendor files are copied from node_modules for runtime use, so they + // should not be linted as part of the app source tree. + ignores: ["public/vendor/**"], + }, { rules: { "@typescript-eslint/triple-slash-reference": "off", diff --git a/apps/app/package.json b/apps/app/package.json index e4763e1d8..64fb3eda7 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -7,9 +7,10 @@ "./src/types": "./src/types.ts" }, "scripts": { - "build:assets": "vite build", - "build": "tsc --build && npm run build:assets", - "dev": "cross-env NODE_ENV=production vite", + "prepare:vendor": "node scripts/copy-vendor-assets.mjs", + "build:assets": "npm run prepare:vendor && vite build", + "build": "tsc --build tsconfig.build.json && npm run build:assets", + "dev": "npm run prepare:vendor && cross-env NODE_ENV=production vite", "lint": "eslint --fix", "lint:check": "eslint", "test": "cypress open", diff --git a/apps/app/scripts/copy-vendor-assets.mjs b/apps/app/scripts/copy-vendor-assets.mjs new file mode 100644 index 000000000..e2f2e2392 --- /dev/null +++ b/apps/app/scripts/copy-vendor-assets.mjs @@ -0,0 +1,25 @@ +import { cp, mkdir, readdir, rm } from "fs/promises"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const appDir = dirname(scriptDir); +const repoRoot = dirname(dirname(appDir)); + +// The v0.6 -> v0.7 syntax upgrader is only used from editor settings, but its +// published bundle includes a large worker. Copying the runtime files into +// public/vendor keeps that code out of Vite's main app bundle. +const sourceDir = join(repoRoot, "node_modules", "@doenet", "v06-to-v07"); +const targetDir = join(appDir, "public", "vendor", "doenet", "v06-to-v07"); + +await rm(targetDir, { recursive: true, force: true }); +await mkdir(targetDir, { recursive: true }); + +const files = await readdir(sourceDir); +for (const file of files) { + // The package entrypoint dynamically imports the hashed worker file, so ship + // both files together and let the browser load them on demand at runtime. + if (file === "index.js" || /^lib_doenetml_worker_bg-.*\.js$/.test(file)) { + await cp(join(sourceDir, file), join(targetDir, file)); + } +} diff --git a/apps/app/src/index.tsx b/apps/app/src/index.tsx index 1b3f1e097..5aa11b968 100644 --- a/apps/app/src/index.tsx +++ b/apps/app/src/index.tsx @@ -24,28 +24,15 @@ import { loader as sharedActivitiesLoader, SharedActivities, } from "./paths/SharedActivities"; -import { - loader as activityViewerLoader, - ActivityViewer, -} from "./paths/ActivityViewer"; import { loader as assignedLoader, Assigned } from "./paths/Assigned"; import { loader as assignmentResponseOverviewLoader, AssignmentData as AssignmentResponseOverview, } from "./paths/AssignmentResponseOverview"; -import { - loader as assignmentResponseStudentLoader, - AssignmentResponseStudent, -} from "./paths/AssignmentResponseStudent"; import { action as enterClassCodeAction, EnterClassCode, } from "./paths/EnterClassCode"; -import { - loader as assignmentViewerLoader, - action as assignmentViewerAction, - AssignmentViewer, -} from "./paths/AssignmentViewer"; import { loader as studentsLoader, Students } from "./paths/Students"; import { loader as studentAssignmentScoresLoader, @@ -63,12 +50,6 @@ import { loader as editorHeaderLoader, EditorHeader, } from "./paths/editor/EditorHeader"; -import { - DoenetMLComparison, - loader as doenetMLComparisonLoader, - action as doenetMLComparisonAction, -} from "./paths/DoenetMLComparison"; -import { mathjaxConfig } from "@doenet/doenetml-iframe"; import { SignIn, action as signInAction } from "./paths/SignIn"; import { ConfirmSignIn, @@ -83,14 +64,6 @@ import { LibraryActivities, loader as libraryActivitiesLoader, } from "./paths/LibraryActivities"; -import { - DocEditorViewMode, - loader as docEditorViewModeLoader, -} from "./paths/editor/DocEditorViewMode"; -import { - loader as docEditorEditModeLoader, - DocEditorEditMode, -} from "./paths/editor/DocEditorEditMode"; import { CompoundEditorViewMode, loader as compoundEditorViewModeLoader, @@ -105,10 +78,6 @@ import { } from "./paths/editor/EditorSettingsMode"; import axios, { AxiosError } from "axios"; import { loadShareStatus } from "./popups/ShareMyContentModal"; -import { - DocEditorHistoryMode, - loader as docEditorHistoryModeLoader, -} from "./paths/editor/DocEditorHistoryMode"; import { DocEditorRemixMode, loader as docEditorRemixModeLoader, @@ -122,12 +91,64 @@ import { loader as sharedWithMeLoader, } from "./paths/SharedWithMe"; import { editorUrl } from "./utils/url"; -import { ScratchPad, loader as scratchPadLoader } from "./paths/ScratchPad"; import { About } from "./paths/About"; -import { RawViewer, loader as rawViewerLoader } from "./paths/RawViewer"; import { GetInvolved } from "./paths/GetInvolved"; import { Events } from "./paths/Events"; import { QuickLinks } from "./paths/QuickLinks"; +import { mathjaxConfig } from "./utils/mathjaxConfig"; + +async function loadActivityViewerRoute() { + const route = await import("./paths/ActivityViewer"); + return { loader: route.loader, Component: route.ActivityViewer }; +} + +async function loadAssignmentResponseStudentRoute() { + const route = await import("./paths/AssignmentResponseStudent"); + return { loader: route.loader, Component: route.AssignmentResponseStudent }; +} + +async function loadAssignmentViewerRoute() { + const route = await import("./paths/AssignmentViewer"); + return { + loader: route.loader, + action: route.action, + Component: route.AssignmentViewer, + }; +} + +async function loadDoenetMLComparisonRoute() { + const route = await import("./paths/DoenetMLComparison"); + return { + loader: route.loader, + action: route.action, + Component: route.DoenetMLComparison, + }; +} + +async function loadDocEditorEditModeRoute() { + const route = await import("./paths/editor/DocEditorEditMode"); + return { loader: route.loader, Component: route.DocEditorEditMode }; +} + +async function loadDocEditorViewModeRoute() { + const route = await import("./paths/editor/DocEditorViewMode"); + return { loader: route.loader, Component: route.DocEditorViewMode }; +} + +async function loadDocEditorHistoryModeRoute() { + const route = await import("./paths/editor/DocEditorHistoryMode"); + return { loader: route.loader, Component: route.DocEditorHistoryMode }; +} + +async function loadScratchPadRoute() { + const route = await import("./paths/ScratchPad"); + return { loader: route.loader, Component: route.ScratchPad }; +} + +async function loadRawViewerRoute() { + const route = await import("./paths/RawViewer"); + return { loader: route.loader, Component: route.RawViewer }; +} const router = createBrowserRouter([ { @@ -243,10 +264,9 @@ const router = createBrowserRouter([ }, { path: "activityViewer/:contentId", - loader: activityViewerLoader, + lazy: loadActivityViewerRoute, action: genericAction, errorElement: , - element: , }, { path: "documentEditor/:contentId", @@ -257,14 +277,12 @@ const router = createBrowserRouter([ children: [ { path: "edit", - loader: docEditorEditModeLoader, - element: , + lazy: loadDocEditorEditModeRoute, errorElement: , }, { path: "view", - loader: docEditorViewModeLoader, - element: , + lazy: loadDocEditorViewModeRoute, errorElement: , }, { @@ -276,9 +294,8 @@ const router = createBrowserRouter([ }, { path: "history", - loader: docEditorHistoryModeLoader, + lazy: loadDocEditorHistoryModeRoute, action: genericAction, - element: , errorElement: , }, { @@ -335,9 +352,7 @@ const router = createBrowserRouter([ }, { path: "activityCompare/:contentId/:compareId", - loader: doenetMLComparisonLoader, - action: doenetMLComparisonAction, - element: , + lazy: loadDoenetMLComparisonRoute, errorElement: , }, { @@ -355,8 +370,7 @@ const router = createBrowserRouter([ }, { path: "assignedData/:contentId", - loader: assignmentResponseStudentLoader, - element: , + lazy: loadAssignmentResponseStudentRoute, errorElement: , }, { @@ -368,8 +382,7 @@ const router = createBrowserRouter([ }, { path: "assignmentData/:contentId/:studentUserId", - loader: assignmentResponseStudentLoader, - element: , + lazy: loadAssignmentResponseStudentRoute, errorElement: , }, { @@ -386,9 +399,7 @@ const router = createBrowserRouter([ }, { path: "code/:classCode", - loader: assignmentViewerLoader, - action: assignmentViewerAction, - element: , + lazy: loadAssignmentViewerRoute, errorElement: , }, { @@ -417,17 +428,15 @@ const router = createBrowserRouter([ }, { path: "scratchPad", - loader: scratchPadLoader, + lazy: loadScratchPadRoute, action: genericAction, errorElement: , - element: , }, ], }, { path: "/embed/:viewId", - element: , - loader: rawViewerLoader, + lazy: loadRawViewerRoute, errorElement: ( diff --git a/apps/app/src/paths/AssignmentResponseOverview.tsx b/apps/app/src/paths/AssignmentResponseOverview.tsx index d09e04d86..dee559ea9 100644 --- a/apps/app/src/paths/AssignmentResponseOverview.tsx +++ b/apps/app/src/paths/AssignmentResponseOverview.tsx @@ -46,14 +46,13 @@ import { DoenetmlVersion, UserInfoWithEmail, } from "../types"; -import { isActivitySource } from "@doenet/assignment-viewer"; import { compileActivityFromContent, contentTypeToName, getIconInfo, } from "../utils/activity"; import { BiDownArrowAlt, BiUpArrowAlt } from "react-icons/bi"; -import { ActivitySource } from "@doenet-tools/shared"; +import { ActivitySource, isActivitySource } from "@doenet-tools/shared"; import { EditAssignmentSettings } from "../widgets/editor/EditAssignmentSettings"; import { DateTime } from "luxon"; import { NameBar } from "../widgets/NameBar"; diff --git a/apps/app/src/paths/editor/DoenetMLVersionComponents.tsx b/apps/app/src/paths/editor/DoenetMLVersionComponents.tsx index 98032056a..a65fa3a91 100644 --- a/apps/app/src/paths/editor/DoenetMLVersionComponents.tsx +++ b/apps/app/src/paths/editor/DoenetMLVersionComponents.tsx @@ -18,7 +18,7 @@ import { import { DoenetmlVersion } from "../../types"; import axios from "axios"; import { optimistic } from "../../utils/optimistic_ui"; -import { updateSyntaxFromV06toV07 } from "@doenet/v06-to-v07"; +import { updateSyntaxFromV06toV07 } from "../../utils/v06ToV07"; /** * Renders a DoenetML version selector. @@ -159,6 +159,8 @@ async function performSyntaxUpgrade( const source: string = data.source; + // This upgrade helper is loaded from public/vendor on demand so the large + // converter bundle doesn't inflate the default app asset build. const update = await updateSyntaxFromV06toV07(source); const upgraded = update.xml; diff --git a/apps/app/src/utils/mathjaxConfig.ts b/apps/app/src/utils/mathjaxConfig.ts new file mode 100644 index 000000000..e6eb9974b --- /dev/null +++ b/apps/app/src/utils/mathjaxConfig.ts @@ -0,0 +1,18 @@ +export const mathjaxConfig = { + tex: { + tags: "ams", + macros: { + lt: "<", + gt: ">", + amp: "&", + var: ["\\mathrm{#1}", 1], + csch: "\\operatorname{csch}", + sech: "\\operatorname{sech}", + erf: "\\operatorname{erf}", + }, + displayMath: [["\\[", "\\]"]], + }, + output: { + displayOverflow: "linebreak", + }, +}; diff --git a/apps/app/src/utils/v06ToV07.ts b/apps/app/src/utils/v06ToV07.ts new file mode 100644 index 000000000..6a8bcc739 --- /dev/null +++ b/apps/app/src/utils/v06ToV07.ts @@ -0,0 +1,36 @@ +type SyntaxUpdateResult = { + xml: string; + vfile: { + messages: unknown[]; + }; +}; + +type SyntaxUpdaterModule = { + updateSyntaxFromV06toV07: ( + source: string, + options?: unknown, + ) => Promise; +}; + +let syntaxUpdaterModule: Promise | null = null; +// Load the syntax upgrader from public/vendor instead of bundling it into the +// app, since it is only needed when someone explicitly runs the upgrade action. +const syntaxUpdaterModulePath = "/vendor/doenet/v06-to-v07/index.js"; + +async function loadSyntaxUpdater() { + syntaxUpdaterModule ??= import( + /* @vite-ignore */ + // Keep this as a runtime URL so Vite doesn't try to pull the large + // converter bundle and its worker back into the app asset build. + syntaxUpdaterModulePath + ) as Promise; + + return await syntaxUpdaterModule; +} + +export async function updateSyntaxFromV06toV07(source: string) { + // Cache the module promise so repeated upgrades in one session do not + // re-fetch the vendored bundle. + const module = await loadSyntaxUpdater(); + return await module.updateSyntaxFromV06toV07(source); +} diff --git a/apps/app/tsconfig.build.json b/apps/app/tsconfig.build.json new file mode 100644 index 000000000..3a4b82906 --- /dev/null +++ b/apps/app/tsconfig.build.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "tsBuildInfoFile": "../../.tsbuildinfo/apps/app.build.tsbuildinfo", + "types": ["vite/client", "node"] + }, + "references": [ + { + "path": "../../packages/shared" + } + ], + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": [ + "src/**/*.cy.ts", + "src/**/*.cy.tsx", + "src/**/*.test.ts", + "src/**/*.test.tsx" + ] +} diff --git a/package.json b/package.json index e2a5fb4a5..61d930024 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "packages/*" ], "scripts": { - "postinstall": "npm run prisma:generate --workspace @doenet-tools/api", + "postinstall": "npm run prisma:generate --workspace @doenet-tools/api && npm run prepare:vendor --workspace @doenet-tools/app", "setup": "node scripts/setup.js", "db:setup": "npm run prisma:migrate-dev --workspace @doenet-tools/api && npm run prisma:seed --workspace @doenet-tools/api", "format": "prettier . --write", diff --git a/tsconfig.json b/tsconfig.json index 7ccadb4be..0ca0772c2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,10 +2,9 @@ { "files": [], "references": [ - { "path": "apps/web" }, - { "path": "apps/app" }, - { "path": "apps/api" }, { "path": "packages/shared" }, - { "path": "packages/e2e-tests" } + { "path": "apps/web" }, + { "path": "apps/app/tsconfig.build.json" }, + { "path": "apps/api/tsconfig.build.json" } ] }