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.
- package/README.md +6 -3
- package/bin/cli.js +14 -1
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +13 -7
- package/framework/docs/FRAMEWORK_GUIDE.md +23 -22
- package/framework/docs/USER_GUIDE.md +9 -3
- package/framework/index.html +1 -1
- package/framework/js/core/event-bus.js +21 -6
- package/framework/js/drivers/lti-driver.js +50 -94
- package/framework/js/drivers/proxy-driver.js +8 -5
- package/framework/js/main.js +3 -15
- package/framework/js/utilities/access-control.js +11 -45
- package/framework/js/utilities/data-reporter.js +16 -3
- package/framework/version.json +26 -50
- package/lib/build-packaging.d.ts +4 -0
- package/lib/build-packaging.js +39 -2
- package/lib/cloud.js +1 -1
- package/lib/course-writer.js +97 -32
- package/lib/create.js +71 -18
- package/lib/manifest/cmi5-manifest.js +19 -10
- package/lib/manifest/lti-tool-config.js +18 -5
- package/lib/manifest/scorm-12-manifest.js +11 -5
- package/lib/manifest/scorm-2004-manifest.js +16 -9
- package/lib/manifest/xml-utils.js +14 -0
- package/lib/preview-routes-api.js +16 -7
- package/lib/preview-server.js +49 -28
- package/lib/project-utils.js +34 -0
- package/lib/proxy-templates/proxy.html +7 -3
- package/lib/proxy-templates/scorm-bridge.js +15 -9
- package/lib/scaffold.js +33 -4
- package/lib/stub-player.js +19 -2
- package/lib/token.js +26 -29
- package/lib/upgrade.js +76 -2
- package/package.json +33 -26
- package/template/course/course-config.js +9 -7
- package/template/gitattributes +44 -0
- package/template/gitignore +38 -0
- package/template/package.json +13 -4
- package/template/vite.config.js +90 -27
package/template/vite.config.js
CHANGED
|
@@ -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
|
|
155
|
-
externalUrl: config.externalUrl || null,
|
|
156
|
-
|
|
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: !
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
306
|
-
//
|
|
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
|
-
|
|
333
|
-
|
|
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
|
});
|