coursecode 0.1.53 → 0.1.55

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 (38) hide show
  1. package/README.md +6 -3
  2. package/bin/cli.js +14 -1
  3. package/framework/docs/COURSE_AUTHORING_GUIDE.md +13 -7
  4. package/framework/docs/FRAMEWORK_GUIDE.md +23 -22
  5. package/framework/docs/USER_GUIDE.md +9 -3
  6. package/framework/index.html +1 -1
  7. package/framework/js/core/event-bus.js +21 -6
  8. package/framework/js/drivers/lti-driver.js +50 -94
  9. package/framework/js/drivers/proxy-driver.js +8 -5
  10. package/framework/js/main.js +3 -15
  11. package/framework/js/utilities/access-control.js +11 -45
  12. package/framework/js/utilities/data-reporter.js +16 -3
  13. package/framework/version.json +26 -50
  14. package/lib/build-packaging.d.ts +4 -0
  15. package/lib/build-packaging.js +39 -2
  16. package/lib/cloud.js +1 -1
  17. package/lib/course-writer.js +97 -32
  18. package/lib/create.js +71 -18
  19. package/lib/manifest/cmi5-manifest.js +19 -10
  20. package/lib/manifest/lti-tool-config.js +18 -5
  21. package/lib/manifest/scorm-12-manifest.js +11 -5
  22. package/lib/manifest/scorm-2004-manifest.js +16 -9
  23. package/lib/manifest/xml-utils.js +14 -0
  24. package/lib/preview-routes-api.js +16 -7
  25. package/lib/preview-server.js +49 -28
  26. package/lib/project-utils.js +34 -0
  27. package/lib/proxy-templates/proxy.html +7 -3
  28. package/lib/proxy-templates/scorm-bridge.js +15 -9
  29. package/lib/scaffold.js +33 -4
  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 +33 -26
  34. package/template/course/course-config.js +9 -7
  35. package/template/gitattributes +44 -0
  36. package/template/gitignore +38 -0
  37. package/template/package.json +13 -4
  38. package/template/vite.config.js +90 -27
@@ -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,19 @@ 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'];
18
+
19
+ function directoryContainsFiles(directory) {
20
+ if (!fs.existsSync(directory)) return false;
21
+ return fs.readdirSync(directory, { withFileTypes: true }).some(entry => (
22
+ entry.isFile() || (entry.isDirectory() && directoryContainsFiles(path.join(directory, entry.name)))
23
+ ));
24
+ }
17
25
 
18
26
  async function loadBuildUtils() {
19
27
  if (
@@ -21,7 +29,8 @@ async function loadBuildUtils() {
21
29
  contentDiscoveryPlugin &&
22
30
  createStandardPackage &&
23
31
  createExternalPackagesForClients &&
24
- validateExternalHostingConfig
32
+ validateExternalHostingConfig &&
33
+ loadExternalAccessConfig
25
34
  ) {
26
35
  return;
27
36
  }
@@ -31,7 +40,8 @@ async function loadBuildUtils() {
31
40
  ({
32
41
  createStandardPackage,
33
42
  createExternalPackagesForClients,
34
- validateExternalHostingConfig
43
+ validateExternalHostingConfig,
44
+ loadExternalAccessConfig
35
45
  } = await import('coursecode/build-packaging'));
36
46
  ({ default: contentDiscoveryPlugin } = await import('coursecode/vite-plugin-content-discovery'));
37
47
  } catch {
@@ -43,7 +53,8 @@ async function loadBuildUtils() {
43
53
  ({
44
54
  createStandardPackage,
45
55
  createExternalPackagesForClients,
46
- validateExternalHostingConfig
56
+ validateExternalHostingConfig,
57
+ loadExternalAccessConfig
47
58
  } = await import(toUrl('build-packaging.js')));
48
59
  ({ default: contentDiscoveryPlugin } = await import(toUrl('vite-plugin-content-discovery.js')));
49
60
  }
@@ -89,6 +100,34 @@ function validatePackage(format = 'scorm2004') {
89
100
  warnings.push('No assets directory found');
90
101
  }
91
102
 
103
+ // Authoring inputs must never be published with the learner runtime.
104
+ const forbiddenCoursePaths = [
105
+ 'course/course-config.js',
106
+ 'course/references',
107
+ 'course/slides',
108
+ 'course/assessments',
109
+ 'course/theme.css'
110
+ ];
111
+ for (const relativePath of forbiddenCoursePaths) {
112
+ if (fs.existsSync(path.join(DIST_DIR, relativePath))) {
113
+ errors.push(`Authoring source leaked into package: ${relativePath}`);
114
+ }
115
+ }
116
+
117
+ const invalidCopyLayouts = [
118
+ '.vite',
119
+ 'schemas',
120
+ 'common/schemas',
121
+ 'course/course',
122
+ 'course/template',
123
+ 'js/vendor/framework'
124
+ ];
125
+ for (const relativePath of invalidCopyLayouts) {
126
+ if (fs.existsSync(path.join(DIST_DIR, relativePath))) {
127
+ errors.push(`Static-copy output has an unexpected nested source path: ${relativePath}`);
128
+ }
129
+ }
130
+
92
131
  // Validate manifest/tool config content by format
93
132
  if (format === 'cmi5') {
94
133
  // cmi5: Validate cmi5.xml structure
@@ -142,6 +181,12 @@ async function loadCourseConfig() {
142
181
  const module = await import(configUrl);
143
182
  const config = module.courseConfig || module.default || {};
144
183
 
184
+ const masteryScore = config.lms?.masteryScore;
185
+ if (masteryScore !== undefined && (!Number.isFinite(masteryScore) || masteryScore < 0 || masteryScore > 100)) {
186
+ throw new Error('lms.masteryScore must be a number between 0 and 100');
187
+ }
188
+
189
+ const lmsFormat = process.env.LMS_FORMAT || config.format || 'cmi5';
145
190
  return {
146
191
  title: config.metadata?.title || 'SCORM Course',
147
192
  description: config.metadata?.description || 'SCORM 2004 4th Edition Course',
@@ -151,9 +196,10 @@ async function loadCourseConfig() {
151
196
  windowWidth: config.environment?.window?.width || 1024,
152
197
  windowHeight: config.environment?.window?.height || 768,
153
198
  // 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,
199
+ lmsFormat,
200
+ externalUrl: process.env.LTI_EXTERNAL_URL || config.externalUrl || null,
201
+ masteryScore: masteryScore ?? null,
202
+ accessControl: loadExternalAccessConfig(ROOT_DIR, config),
157
203
  galleryConfig: config.navigation?.documentGallery || null
158
204
  };
159
205
  }
@@ -184,14 +230,31 @@ function scanDistFiles() {
184
230
  return files;
185
231
  }
186
232
 
233
+ function removeHiddenBuildArtifacts(dir) {
234
+ if (!fs.existsSync(dir)) return;
235
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
236
+ const fullPath = path.join(dir, entry.name);
237
+ if (entry.isDirectory()) {
238
+ removeHiddenBuildArtifacts(fullPath);
239
+ } else if (entry.name === '.DS_Store' || entry.name === '.gitkeep' || entry.name.startsWith('._')) {
240
+ fs.rmSync(fullPath, { force: true });
241
+ }
242
+ }
243
+ }
244
+
187
245
  /**
188
246
  * SCORM post-build plugin
189
247
  */
190
- function scormPostBuild(isDev) {
248
+ function scormPostBuild(isDev, { isWatchBuild }) {
249
+ let initialBuild = true;
191
250
  return {
192
251
  name: 'scorm-post-build',
193
252
  enforce: 'post',
194
253
  buildStart() {
254
+ if (initialBuild && isWatchBuild && fs.existsSync(DIST_DIR)) {
255
+ fs.rmSync(DIST_DIR, { recursive: true, force: true });
256
+ }
257
+ initialBuild = false;
195
258
  if (isDev) console.log('🔨 Building...');
196
259
  },
197
260
  closeBundle: async () => {
@@ -199,6 +262,7 @@ function scormPostBuild(isDev) {
199
262
  // Move index.html to root
200
263
  const indexSource = path.join(DIST_DIR, 'framework', 'index.html');
201
264
  const indexDest = path.join(DIST_DIR, 'index.html');
265
+ removeHiddenBuildArtifacts(DIST_DIR);
202
266
 
203
267
  // Load course config once for meta tag injection and manifest generation
204
268
  const config = await loadCourseConfig();
@@ -267,6 +331,7 @@ function scormPostBuild(isDev) {
267
331
  export default defineConfig(async ({ mode }) => {
268
332
  await loadBuildUtils();
269
333
  const isDev = mode === 'development';
334
+ const isWatchBuild = process.argv.includes('--watch');
270
335
  const courseConfig = await loadCourseConfig();
271
336
 
272
337
  return {
@@ -292,18 +357,20 @@ export default defineConfig(async ({ mode }) => {
292
357
 
293
358
  build: {
294
359
  outDir: 'dist',
295
- emptyOutDir: !isDev,
296
- manifest: true,
360
+ emptyOutDir: !isWatchBuild,
361
+ // Stable browser contract for LMS launches. SCORM defines the LMS API,
362
+ // not an obsolete browser requirement; IE11/SystemJS is unsupported.
363
+ target: SUPPORTED_BROWSER_TARGETS,
297
364
  // LMS environments (SCORM/cmi5) can be restrictive with dynamic imports,
298
365
  // so we intentionally keep main.js as a single chunk
299
366
  chunkSizeWarningLimit: 600,
300
- rollupOptions: {
367
+ rolldownOptions: {
301
368
  input: {
302
369
  main: path.resolve(__dirname, 'framework/index.html')
303
370
  },
304
371
  // 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.
372
+ // LTI OIDC/JWT validation remains server-side, so no browser crypto
373
+ // verification dependency is required here.
307
374
  output: {
308
375
  entryFileNames: 'assets/[name].js',
309
376
  chunkFileNames: 'assets/[name].js',
@@ -316,26 +383,22 @@ export default defineConfig(async ({ mode }) => {
316
383
  // Content discovery - generates manifest at build time
317
384
  contentDiscoveryPlugin({ galleryConfig: courseConfig.galleryConfig }),
318
385
 
319
- ...(!isDev ? [
320
- legacy({
321
- targets: ['ie >= 11'],
322
- renderLegacyChunks: true,
323
- modernPolyfills: true,
324
- ignoreBrowserslistConfig: true
325
- })
326
- ] : []),
327
-
328
386
  viteStaticCopy({
329
387
  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' }
388
+ { src: 'schemas/*.{xml,xsd,dtd}', dest: '.', rename: { stripBase: 1 } },
389
+ { src: 'schemas/common/*', dest: 'common', rename: { stripBase: 2 } },
390
+ // Publish runtime assets only. Never ship authoring references,
391
+ // configuration source, assessment source, or hidden project files.
392
+ // A blank course is valid and may not contain any assets yet.
393
+ ...(directoryContainsFiles(path.join(ROOT_DIR, 'course', 'assets'))
394
+ ? [{ src: 'course/assets', dest: 'course', rename: { stripBase: 1 } }]
395
+ : []),
396
+ { src: 'framework/js/vendor/**/*', dest: 'js/vendor', rename: { stripBase: 3 } }
334
397
  ],
335
398
  watch: isDev ? { rerun: true } : undefined
336
399
  }),
337
400
 
338
- scormPostBuild(isDev)
401
+ scormPostBuild(isDev, { isWatchBuild })
339
402
  ]
340
403
  };
341
404
  });