coursecode 0.1.52 → 0.1.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +11 -6
  2. package/bin/cli.js +1 -0
  3. package/framework/css/components/loading.css +11 -3
  4. package/framework/docs/COURSE_AUTHORING_GUIDE.md +8 -6
  5. package/framework/docs/FRAMEWORK_GUIDE.md +22 -21
  6. package/framework/docs/USER_GUIDE.md +9 -3
  7. package/framework/index.html +2 -2
  8. package/framework/js/app/AppUI.js +2 -1
  9. package/framework/js/core/event-bus.js +21 -6
  10. package/framework/js/drivers/lti-driver.js +50 -94
  11. package/framework/js/drivers/proxy-driver.js +8 -5
  12. package/framework/js/main.js +7 -18
  13. package/framework/js/utilities/access-control.js +11 -45
  14. package/framework/js/utilities/data-reporter.js +16 -3
  15. package/framework/version.json +26 -50
  16. package/lib/build-packaging.d.ts +4 -0
  17. package/lib/build-packaging.js +39 -2
  18. package/lib/cloud.js +1 -1
  19. package/lib/course-writer.js +97 -32
  20. package/lib/manifest/cmi5-manifest.js +19 -10
  21. package/lib/manifest/lti-tool-config.js +18 -5
  22. package/lib/manifest/scorm-12-manifest.js +11 -5
  23. package/lib/manifest/scorm-2004-manifest.js +16 -9
  24. package/lib/manifest/xml-utils.js +14 -0
  25. package/lib/preview-routes-api.js +16 -7
  26. package/lib/preview-server.js +49 -28
  27. package/lib/project-utils.js +34 -0
  28. package/lib/proxy-templates/proxy.html +7 -3
  29. package/lib/proxy-templates/scorm-bridge.js +15 -9
  30. package/lib/stub-player.js +19 -2
  31. package/lib/token.js +26 -29
  32. package/lib/upgrade.js +76 -2
  33. package/package.json +32 -25
  34. package/template/course/course-config.js +9 -7
  35. package/template/package.json +13 -4
  36. package/template/vite.config.js +80 -27
package/lib/upgrade.js CHANGED
@@ -113,6 +113,46 @@ export function addMissingRuntimeDependencies(projectPkg, templatePkg) {
113
113
  return added;
114
114
  }
115
115
 
116
+ const OBSOLETE_MANAGED_DEV_DEPENDENCIES = ['@vitejs/plugin-legacy'];
117
+
118
+ /**
119
+ * Synchronize the build tooling that accompanies a replaced template config.
120
+ * This only runs with `coursecode upgrade --configs`, keeping the default
121
+ * framework-only upgrade from changing a project's custom build stack.
122
+ */
123
+ export function syncManagedBuildTooling(projectPkg, templatePkg) {
124
+ const projectDevDeps = { ...(projectPkg.devDependencies || {}) };
125
+ const templateDevDeps = templatePkg.devDependencies || {};
126
+ const updated = [];
127
+ const removed = [];
128
+
129
+ for (const [name, version] of Object.entries(templateDevDeps)) {
130
+ if (projectDevDeps[name] !== version) {
131
+ projectDevDeps[name] = version;
132
+ updated.push(`${name}@${version}`);
133
+ }
134
+ }
135
+
136
+ for (const name of OBSOLETE_MANAGED_DEV_DEPENDENCIES) {
137
+ if (projectDevDeps[name]) {
138
+ delete projectDevDeps[name];
139
+ removed.push(name);
140
+ }
141
+ }
142
+
143
+ projectPkg.devDependencies = Object.fromEntries(
144
+ Object.entries(projectDevDeps).sort(([a], [b]) => a.localeCompare(b))
145
+ );
146
+
147
+ const nodeEngine = templatePkg.engines?.node;
148
+ const engineUpdated = Boolean(nodeEngine && projectPkg.engines?.node !== nodeEngine);
149
+ if (engineUpdated) {
150
+ projectPkg.engines = { ...(projectPkg.engines || {}), node: nodeEngine };
151
+ }
152
+
153
+ return { updated, removed, engineUpdated };
154
+ }
155
+
116
156
  export async function upgrade(options = {}) {
117
157
  const cwd = process.cwd();
118
158
  const rcPath = path.join(cwd, '.coursecoderc.json');
@@ -175,7 +215,8 @@ export async function upgrade(options = {}) {
175
215
  if (options.configs) {
176
216
  dryRunMsg += `
177
217
  - vite.config.js (replace, backup original)
178
- - eslint.config.js (replace, backup original)`;
218
+ - eslint.config.js (replace, backup original)
219
+ - package.json (sync managed Vite/ESLint tooling and remove plugin-legacy)`;
179
220
  }
180
221
 
181
222
  dryRunMsg += `
@@ -266,10 +307,18 @@ export async function upgrade(options = {}) {
266
307
  // the project's existing version ranges.
267
308
  const pkgPath = path.join(cwd, 'package.json');
268
309
  let addedRuntimeDeps = [];
310
+ let toolingChanges = null;
269
311
  if (fs.existsSync(pkgPath)) {
270
312
  const projectPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
271
313
  addedRuntimeDeps = addMissingRuntimeDependencies(projectPkg, templatePkg);
272
- if (addedRuntimeDeps.length > 0) {
314
+ if (options.configs) {
315
+ toolingChanges = syncManagedBuildTooling(projectPkg, templatePkg);
316
+ }
317
+ const packageChanged = addedRuntimeDeps.length > 0 ||
318
+ toolingChanges?.updated.length > 0 ||
319
+ toolingChanges?.removed.length > 0 ||
320
+ toolingChanges?.engineUpdated;
321
+ if (packageChanged) {
273
322
  fs.writeFileSync(pkgPath, JSON.stringify(projectPkg, null, 2) + '\n');
274
323
  }
275
324
  }
@@ -290,6 +339,31 @@ export async function upgrade(options = {}) {
290
339
  Run npm install to update node_modules and your lockfile.`;
291
340
  }
292
341
 
342
+ if (toolingChanges && (
343
+ toolingChanges.updated.length > 0 ||
344
+ toolingChanges.removed.length > 0 ||
345
+ toolingChanges.engineUpdated
346
+ )) {
347
+ successMsg += `
348
+
349
+ Managed build tooling synchronized for the new config:`;
350
+ if (toolingChanges.updated.length > 0) {
351
+ successMsg += `
352
+ - Updated: ${toolingChanges.updated.join(', ')}`;
353
+ }
354
+ if (toolingChanges.removed.length > 0) {
355
+ successMsg += `
356
+ - Removed: ${toolingChanges.removed.join(', ')}`;
357
+ }
358
+ if (toolingChanges.engineUpdated) {
359
+ successMsg += `
360
+ - Updated Node engine requirement to ${templatePkg.engines.node}`;
361
+ }
362
+ successMsg += `
363
+
364
+ Run npm install to update node_modules and your lockfile.`;
365
+ }
366
+
293
367
  if (configUpdates.length > 0) {
294
368
  successMsg += `
295
369
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursecode",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
4
4
  "description": "Multi-format course authoring framework with CLI tools (SCORM 2004, SCORM 1.2, cmi5, LTI 1.3)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,20 +57,21 @@
57
57
  "build:scorm12": "LMS_FORMAT=scorm1.2 npx vite build --config vite.framework-dev.config.js",
58
58
  "build:cmi5": "LMS_FORMAT=cmi5 npx vite build --config vite.framework-dev.config.js",
59
59
  "build:lti": "LMS_FORMAT=lti npx vite build --config vite.framework-dev.config.js",
60
- "package": "npm run lint && PACKAGE=true npx vite build --config vite.framework-dev.config.js",
61
- "package:scorm2004": "npm run lint && LMS_FORMAT=scorm2004 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
62
- "package:scorm12": "npm run lint && LMS_FORMAT=scorm1.2 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
63
- "package:cmi5": "npm run lint && LMS_FORMAT=cmi5 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
64
- "package:lti": "npm run lint && LMS_FORMAT=lti PACKAGE=true npx vite build --config vite.framework-dev.config.js",
60
+ "package": "npm run lint && npm run test:coverage && PACKAGE=true npx vite build --config vite.framework-dev.config.js",
61
+ "package:scorm2004": "npm run lint && npm run test:coverage && LMS_FORMAT=scorm2004 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
62
+ "package:scorm12": "npm run lint && npm run test:coverage && LMS_FORMAT=scorm1.2 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
63
+ "package:cmi5": "npm run lint && npm run test:coverage && LMS_FORMAT=cmi5 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
64
+ "package:lti": "npm run lint && npm run test:coverage && LMS_FORMAT=lti PACKAGE=true npx vite build --config vite.framework-dev.config.js",
65
65
  "test": "vitest run --config tests/vitest.config.js",
66
66
  "test:e2e": "vitest run --config tests/vitest.e2e.config.js",
67
- "test:all": "npm test && npm run test:e2e",
67
+ "test:drivers": "vitest run --config tests/vitest.e2e.scorm2004.config.js && vitest run --config tests/vitest.e2e.scorm12.config.js && vitest run --config tests/vitest.e2e.cmi5.config.js && vitest run --config tests/vitest.e2e.lti.config.js",
68
+ "test:all": "npm test && npm run test:e2e && npm run test:drivers",
68
69
  "test:cloud": "vitest run --config tests/vitest.cloud.config.js",
69
70
  "test:watch": "vitest --config tests/vitest.config.js",
70
71
  "test:coverage": "vitest run --config tests/vitest.config.js --coverage",
71
- "prerelease:check": "npm run lint && npm run lint:responsive && npm run lint:responsive:structure && npm run build",
72
- "prerelease:check:full": "npm run prerelease:check && npm run smoke:responsive -- --profile=expanded",
73
- "prepublishOnly": "echo '📦 Preparing coursecode for npm...'"
72
+ "prerelease:check": "npm run lint && npm run lint:responsive && npm run lint:responsive:structure && npm run test:coverage && npm run build",
73
+ "prerelease:check:full": "npm run prerelease:check && npm run test:e2e && npm run test:drivers && npm run smoke:responsive -- --profile=expanded",
74
+ "prepublishOnly": "npm run prerelease:check"
74
75
  },
75
76
  "repository": {
76
77
  "type": "git",
@@ -98,34 +99,40 @@
98
99
  },
99
100
  "homepage": "https://github.com/course-code-framework/coursecode/blob/main/framework/docs/USER_GUIDE.md",
100
101
  "engines": {
101
- "node": ">=18.0.0"
102
+ "node": ">=20.19.0"
102
103
  },
103
104
  "dependencies": {
104
- "@modelcontextprotocol/sdk": "^1.0.0",
105
- "acorn": "^8.15.0",
105
+ "@modelcontextprotocol/sdk": "^1.29.0",
106
+ "acorn": "^8.17.0",
106
107
  "archiver": "^7.0.1",
107
108
  "commander": "^14.0.3",
108
109
  "lz-string": "^1.5.0",
109
- "mammoth": "^1.8.0",
110
+ "mammoth": "^1.12.0",
110
111
  "marked": "^17.0.6",
111
112
  "marked-gfm-heading-id": "^4.1.4",
112
113
  "node-pptx-parser": "^1.0.1",
113
- "pdf2json": "^4.0.2",
114
+ "pdf2json": "^4.0.3",
114
115
  "pdf2md": "^1.0.2",
115
- "puppeteer-core": "^24.42.0",
116
+ "puppeteer-core": "^24.43.1",
116
117
  "win-ca": "^3.5.1",
117
- "ws": "^8.18.0"
118
+ "ws": "^8.21.0"
118
119
  },
119
120
  "devDependencies": {
120
- "@vitejs/plugin-legacy": "^7.2.1",
121
- "@vitest/coverage-v8": "^4.1.5",
121
+ "@vitest/coverage-v8": "^4.1.10",
122
122
  "@xapi/cmi5": "^1.4.0",
123
- "eslint": "^10.2.1",
124
- "globals": "^17.5.0",
125
- "jose": "^6.2.2",
123
+ "eslint": "^10.6.0",
124
+ "globals": "^17.7.0",
126
125
  "pptxgenjs": "^4.0.1",
127
- "vite": "~7.2.2",
128
- "vite-plugin-static-copy": "^3.4.0",
129
- "vitest": "^4.1.5"
126
+ "vite": "~8.1.4",
127
+ "vite-plugin-static-copy": "^4.1.1",
128
+ "vitest": "^4.1.10"
129
+ },
130
+ "overrides": {
131
+ "uuid": "^11.1.1"
132
+ },
133
+ "pnpm": {
134
+ "overrides": {
135
+ "uuid": "^11.1.1"
136
+ }
130
137
  }
131
138
  }
@@ -92,13 +92,15 @@
92
92
  * EXTERNAL HOSTING (CDN deployment with proxy packages):
93
93
  * format: 'scorm1.2-proxy' | 'scorm2004-proxy' | 'cmi5-remote'
94
94
  * externalUrl: 'https://cdn.example.com/courses/my-course' // Where course is hosted
95
- * accessControl: {
96
- * clients: {
97
- * 'acme-corp': { token: 'generated-token-here' },
98
- * 'globex': { token: 'another-token-here' }
99
- * }
100
- * }
101
- * Generate tokens: coursecode token --add <clientId>
95
+ * accessControl: { enforcement: 'server' }
96
+ * Client tokens are stored outside course source in the gitignored file
97
+ * .coursecode/access-control.json (created by coursecode token --add <clientId>).
98
+ * The CDN/backend MUST validate credentials before serving index.html/assets.
99
+ *
100
+ * LMS PACKAGE MASTERY (optional):
101
+ * lms: { masteryScore: 80 } // 0-100; omitted by default
102
+ * Only set this when the whole package has one LMS-level threshold. Individual
103
+ * assessment passing scores remain in their assessment settings.
102
104
  * Build output: One package per client (e.g., acme-corp_proxy.zip)
103
105
  *
104
106
  * CLOUD DEPLOYMENT:
@@ -4,6 +4,9 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "CourseCode project",
7
+ "engines": {
8
+ "node": ">=20.19.0"
9
+ },
7
10
  "scripts": {
8
11
  "dev": "vite build --mode development --watch",
9
12
  "build": "npm run lint && vite build",
@@ -13,16 +16,22 @@
13
16
  },
14
17
  "dependencies": {
15
18
  "@xapi/cmi5": "^1.4.0",
16
- "jose": "^6.2.2",
17
19
  "lz-string": "^1.5.0",
18
20
  "marked": "^17.0.6",
19
21
  "marked-gfm-heading-id": "^4.1.4"
20
22
  },
21
23
  "devDependencies": {
22
- "@vitejs/plugin-legacy": "~7.2.1",
23
24
  "archiver": "^7.0.1",
24
25
  "eslint": "^9.39.4",
25
- "vite": "~7.2.2",
26
- "vite-plugin-static-copy": "^3.4.0"
26
+ "vite": "~8.1.4",
27
+ "vite-plugin-static-copy": "^4.1.1"
28
+ },
29
+ "overrides": {
30
+ "uuid": "^11.1.1"
31
+ },
32
+ "pnpm": {
33
+ "overrides": {
34
+ "uuid": "^11.1.1"
35
+ }
27
36
  }
28
37
  }
@@ -1,5 +1,4 @@
1
1
  import { defineConfig } from 'vite';
2
- import legacy from '@vitejs/plugin-legacy';
3
2
  import { viteStaticCopy } from 'vite-plugin-static-copy';
4
3
  import path from 'path';
5
4
  import { fileURLToPath, pathToFileURL } from 'url';
@@ -10,10 +9,12 @@ let contentDiscoveryPlugin;
10
9
  let createStandardPackage;
11
10
  let createExternalPackagesForClients;
12
11
  let validateExternalHostingConfig;
12
+ let loadExternalAccessConfig;
13
13
 
14
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
15
  const ROOT_DIR = path.resolve(__dirname);
16
16
  const DIST_DIR = path.join(ROOT_DIR, 'dist');
17
+ const SUPPORTED_BROWSER_TARGETS = ['chrome111', 'edge111', 'firefox114', 'safari16.4'];
17
18
 
18
19
  async function loadBuildUtils() {
19
20
  if (
@@ -21,7 +22,8 @@ async function loadBuildUtils() {
21
22
  contentDiscoveryPlugin &&
22
23
  createStandardPackage &&
23
24
  createExternalPackagesForClients &&
24
- validateExternalHostingConfig
25
+ validateExternalHostingConfig &&
26
+ loadExternalAccessConfig
25
27
  ) {
26
28
  return;
27
29
  }
@@ -31,7 +33,8 @@ async function loadBuildUtils() {
31
33
  ({
32
34
  createStandardPackage,
33
35
  createExternalPackagesForClients,
34
- validateExternalHostingConfig
36
+ validateExternalHostingConfig,
37
+ loadExternalAccessConfig
35
38
  } = await import('coursecode/build-packaging'));
36
39
  ({ default: contentDiscoveryPlugin } = await import('coursecode/vite-plugin-content-discovery'));
37
40
  } catch {
@@ -43,7 +46,8 @@ async function loadBuildUtils() {
43
46
  ({
44
47
  createStandardPackage,
45
48
  createExternalPackagesForClients,
46
- validateExternalHostingConfig
49
+ validateExternalHostingConfig,
50
+ loadExternalAccessConfig
47
51
  } = await import(toUrl('build-packaging.js')));
48
52
  ({ default: contentDiscoveryPlugin } = await import(toUrl('vite-plugin-content-discovery.js')));
49
53
  }
@@ -89,6 +93,34 @@ function validatePackage(format = 'scorm2004') {
89
93
  warnings.push('No assets directory found');
90
94
  }
91
95
 
96
+ // Authoring inputs must never be published with the learner runtime.
97
+ const forbiddenCoursePaths = [
98
+ 'course/course-config.js',
99
+ 'course/references',
100
+ 'course/slides',
101
+ 'course/assessments',
102
+ 'course/theme.css'
103
+ ];
104
+ for (const relativePath of forbiddenCoursePaths) {
105
+ if (fs.existsSync(path.join(DIST_DIR, relativePath))) {
106
+ errors.push(`Authoring source leaked into package: ${relativePath}`);
107
+ }
108
+ }
109
+
110
+ const invalidCopyLayouts = [
111
+ '.vite',
112
+ 'schemas',
113
+ 'common/schemas',
114
+ 'course/course',
115
+ 'course/template',
116
+ 'js/vendor/framework'
117
+ ];
118
+ for (const relativePath of invalidCopyLayouts) {
119
+ if (fs.existsSync(path.join(DIST_DIR, relativePath))) {
120
+ errors.push(`Static-copy output has an unexpected nested source path: ${relativePath}`);
121
+ }
122
+ }
123
+
92
124
  // Validate manifest/tool config content by format
93
125
  if (format === 'cmi5') {
94
126
  // cmi5: Validate cmi5.xml structure
@@ -142,6 +174,12 @@ async function loadCourseConfig() {
142
174
  const module = await import(configUrl);
143
175
  const config = module.courseConfig || module.default || {};
144
176
 
177
+ const masteryScore = config.lms?.masteryScore;
178
+ if (masteryScore !== undefined && (!Number.isFinite(masteryScore) || masteryScore < 0 || masteryScore > 100)) {
179
+ throw new Error('lms.masteryScore must be a number between 0 and 100');
180
+ }
181
+
182
+ const lmsFormat = process.env.LMS_FORMAT || config.format || 'cmi5';
145
183
  return {
146
184
  title: config.metadata?.title || 'SCORM Course',
147
185
  description: config.metadata?.description || 'SCORM 2004 4th Edition Course',
@@ -151,9 +189,10 @@ async function loadCourseConfig() {
151
189
  windowWidth: config.environment?.window?.width || 1024,
152
190
  windowHeight: config.environment?.window?.height || 768,
153
191
  // LMS format: CLI override (env var) takes precedence over config file
154
- lmsFormat: process.env.LMS_FORMAT || config.format || 'cmi5',
155
- externalUrl: config.externalUrl || null,
156
- accessControl: config.accessControl || null,
192
+ lmsFormat,
193
+ externalUrl: process.env.LTI_EXTERNAL_URL || config.externalUrl || null,
194
+ masteryScore: masteryScore ?? null,
195
+ accessControl: loadExternalAccessConfig(ROOT_DIR, config),
157
196
  galleryConfig: config.navigation?.documentGallery || null
158
197
  };
159
198
  }
@@ -184,14 +223,31 @@ function scanDistFiles() {
184
223
  return files;
185
224
  }
186
225
 
226
+ function removeHiddenBuildArtifacts(dir) {
227
+ if (!fs.existsSync(dir)) return;
228
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
229
+ const fullPath = path.join(dir, entry.name);
230
+ if (entry.isDirectory()) {
231
+ removeHiddenBuildArtifacts(fullPath);
232
+ } else if (entry.name === '.DS_Store' || entry.name === '.gitkeep' || entry.name.startsWith('._')) {
233
+ fs.rmSync(fullPath, { force: true });
234
+ }
235
+ }
236
+ }
237
+
187
238
  /**
188
239
  * SCORM post-build plugin
189
240
  */
190
- function scormPostBuild(isDev) {
241
+ function scormPostBuild(isDev, { isWatchBuild }) {
242
+ let initialBuild = true;
191
243
  return {
192
244
  name: 'scorm-post-build',
193
245
  enforce: 'post',
194
246
  buildStart() {
247
+ if (initialBuild && isWatchBuild && fs.existsSync(DIST_DIR)) {
248
+ fs.rmSync(DIST_DIR, { recursive: true, force: true });
249
+ }
250
+ initialBuild = false;
195
251
  if (isDev) console.log('🔨 Building...');
196
252
  },
197
253
  closeBundle: async () => {
@@ -199,6 +255,7 @@ function scormPostBuild(isDev) {
199
255
  // Move index.html to root
200
256
  const indexSource = path.join(DIST_DIR, 'framework', 'index.html');
201
257
  const indexDest = path.join(DIST_DIR, 'index.html');
258
+ removeHiddenBuildArtifacts(DIST_DIR);
202
259
 
203
260
  // Load course config once for meta tag injection and manifest generation
204
261
  const config = await loadCourseConfig();
@@ -267,6 +324,7 @@ function scormPostBuild(isDev) {
267
324
  export default defineConfig(async ({ mode }) => {
268
325
  await loadBuildUtils();
269
326
  const isDev = mode === 'development';
327
+ const isWatchBuild = process.argv.includes('--watch');
270
328
  const courseConfig = await loadCourseConfig();
271
329
 
272
330
  return {
@@ -292,18 +350,20 @@ export default defineConfig(async ({ mode }) => {
292
350
 
293
351
  build: {
294
352
  outDir: 'dist',
295
- emptyOutDir: !isDev,
296
- manifest: true,
353
+ emptyOutDir: !isWatchBuild,
354
+ // Stable browser contract for LMS launches. SCORM defines the LMS API,
355
+ // not an obsolete browser requirement; IE11/SystemJS is unsupported.
356
+ target: SUPPORTED_BROWSER_TARGETS,
297
357
  // LMS environments (SCORM/cmi5) can be restrictive with dynamic imports,
298
358
  // so we intentionally keep main.js as a single chunk
299
359
  chunkSizeWarningLimit: 600,
300
- rollupOptions: {
360
+ rolldownOptions: {
301
361
  input: {
302
362
  main: path.resolve(__dirname, 'framework/index.html')
303
363
  },
304
364
  // All drivers are bundled as lazy chunks in the universal build.
305
- // jose is dynamically imported by lti-driver.js only when LTI format
306
- // is selected at runtime, so it's safe to include in all builds.
365
+ // LTI OIDC/JWT validation remains server-side, so no browser crypto
366
+ // verification dependency is required here.
307
367
  output: {
308
368
  entryFileNames: 'assets/[name].js',
309
369
  chunkFileNames: 'assets/[name].js',
@@ -316,26 +376,19 @@ export default defineConfig(async ({ mode }) => {
316
376
  // Content discovery - generates manifest at build time
317
377
  contentDiscoveryPlugin({ galleryConfig: courseConfig.galleryConfig }),
318
378
 
319
- ...(!isDev ? [
320
- legacy({
321
- targets: ['ie >= 11'],
322
- renderLegacyChunks: true,
323
- modernPolyfills: true,
324
- ignoreBrowserslistConfig: true
325
- })
326
- ] : []),
327
-
328
379
  viteStaticCopy({
329
380
  targets: [
330
- { src: 'schemas/*.{xml,xsd,dtd}', dest: '.' },
331
- { src: 'schemas/common/*', dest: 'common' },
332
- { src: 'course', dest: '.' },
333
- { src: 'framework/js/vendor/**/*', dest: 'js/vendor' }
381
+ { src: 'schemas/*.{xml,xsd,dtd}', dest: '.', rename: { stripBase: 1 } },
382
+ { src: 'schemas/common/*', dest: 'common', rename: { stripBase: 2 } },
383
+ // Publish runtime assets only. Never ship authoring references,
384
+ // configuration source, assessment source, or hidden project files.
385
+ { src: 'course/assets', dest: 'course', rename: { stripBase: 1 } },
386
+ { src: 'framework/js/vendor/**/*', dest: 'js/vendor', rename: { stripBase: 3 } }
334
387
  ],
335
388
  watch: isDev ? { rerun: true } : undefined
336
389
  }),
337
390
 
338
- scormPostBuild(isDev)
391
+ scormPostBuild(isDev, { isWatchBuild })
339
392
  ]
340
393
  };
341
394
  });