tessera-learn 0.3.0 → 0.4.1
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/AGENTS.md +17 -12
- package/README.md +1 -1
- package/dist/{audit-DkXqQTqn.js → audit-DsYqXbqm.js} +211 -183
- package/dist/audit-DsYqXbqm.js.map +1 -0
- package/dist/{build-commands-CyzuCDXg.js → build-commands-BFuiAxaR.js} +4 -4
- package/dist/build-commands-BFuiAxaR.js.map +1 -0
- package/dist/{inline-config-BEXyRqsJ.js → inline-config-DVvOCKht.js} +6 -6
- package/dist/inline-config-DVvOCKht.js.map +1 -0
- package/dist/plugin/cli.d.ts +5 -1
- package/dist/plugin/cli.d.ts.map +1 -1
- package/dist/plugin/cli.js +38 -7
- package/dist/plugin/cli.js.map +1 -1
- package/dist/plugin/index.d.ts +7 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2 -2
- package/dist/{plugin-CFUFgwHB.js → plugin-BuMiDTmU.js} +29 -38
- package/dist/plugin-BuMiDTmU.js.map +1 -0
- package/package.json +5 -5
- package/src/components/MultipleChoice.svelte +1 -2
- package/src/plugin/build-commands.ts +7 -4
- package/src/plugin/cli.ts +54 -3
- package/src/plugin/index.ts +31 -42
- package/src/plugin/inline-config.ts +4 -2
- package/src/plugin/manifest.ts +21 -0
- package/src/plugin/validate-cli.ts +5 -2
- package/src/plugin/validation.ts +214 -233
- package/src/runtime/App.svelte +4 -1
- package/src/runtime/adapters/scorm-base.ts +15 -14
- package/src/runtime/adapters/scorm12.ts +6 -25
- package/src/runtime/adapters/scorm2004.ts +12 -55
- package/src/runtime/adapters/web.ts +5 -13
- package/src/runtime/fingerprint.ts +28 -0
- package/src/runtime/interaction-format.ts +0 -1
- package/src/runtime/persistence.ts +4 -0
- package/src/runtime/types.ts +3 -0
- package/src/runtime/xapi/publisher.ts +11 -14
- package/dist/audit-DkXqQTqn.js.map +0 -1
- package/dist/build-commands-CyzuCDXg.js.map +0 -1
- package/dist/inline-config-BEXyRqsJ.js.map +0 -1
- package/dist/plugin-CFUFgwHB.js.map +0 -1
package/src/plugin/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { runA11y } from './a11y-cli.js';
|
|
|
4
4
|
import { runNew } from './new-cli.js';
|
|
5
5
|
import { runDuplicate } from './duplicate-cli.js';
|
|
6
6
|
import { resolveCourse } from './course-root.js';
|
|
7
|
+
import { VALID_EXPORT_STANDARDS } from './validation.js';
|
|
7
8
|
|
|
8
9
|
const USAGE = `Usage: tessera <command> [course] [options]
|
|
9
10
|
|
|
@@ -18,9 +19,42 @@ Commands:
|
|
|
18
19
|
|
|
19
20
|
Run a command from inside a course folder, or name the course explicitly.
|
|
20
21
|
|
|
22
|
+
export/validate options:
|
|
23
|
+
--standard <web|scorm12|scorm2004|cmi5|xapi> Override course.config.js export.standard
|
|
24
|
+
|
|
21
25
|
a11y/check options:
|
|
22
26
|
--threshold <minor|moderate|serious|critical> Failing impact (default: serious)`;
|
|
23
27
|
|
|
28
|
+
// Validate here, against the config validator's list, so an unknown standard
|
|
29
|
+
// fails before Vite spins up.
|
|
30
|
+
export function parseExportFlags(flags: string[]): {
|
|
31
|
+
standardOverride?: string;
|
|
32
|
+
error?: string;
|
|
33
|
+
} {
|
|
34
|
+
let standardOverride: string | undefined;
|
|
35
|
+
for (let i = 0; i < flags.length; i++) {
|
|
36
|
+
const arg = flags[i];
|
|
37
|
+
let value: string | undefined;
|
|
38
|
+
if (arg === '--standard') {
|
|
39
|
+
value = flags[++i];
|
|
40
|
+
} else if (arg.startsWith('--standard=')) {
|
|
41
|
+
value = arg.slice('--standard='.length);
|
|
42
|
+
} else {
|
|
43
|
+
return { error: `Unknown argument: ${arg}` };
|
|
44
|
+
}
|
|
45
|
+
if (value === undefined || value.startsWith('-')) {
|
|
46
|
+
return { error: '--standard requires a value' };
|
|
47
|
+
}
|
|
48
|
+
if (!VALID_EXPORT_STANDARDS.includes(value)) {
|
|
49
|
+
return {
|
|
50
|
+
error: `--standard must be one of ${VALID_EXPORT_STANDARDS.join(', ')}, got "${value}"`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
standardOverride = value;
|
|
54
|
+
}
|
|
55
|
+
return standardOverride ? { standardOverride } : {};
|
|
56
|
+
}
|
|
57
|
+
|
|
24
58
|
// The course is a leading positional: `tessera <cmd> [course] [flags]`. Only the
|
|
25
59
|
// first token can be the course, and only when it isn't a flag — otherwise a flag
|
|
26
60
|
// value (e.g. the `serious` in `--threshold serious`) would be misread as a name.
|
|
@@ -43,9 +77,26 @@ type CourseCommand = (
|
|
|
43
77
|
const COURSE_COMMANDS: Record<string, CourseCommand> = {
|
|
44
78
|
dev: async (courseRoot, workspaceRoot) =>
|
|
45
79
|
(await import('./build-commands.js')).runDev(courseRoot, workspaceRoot),
|
|
46
|
-
export: async (courseRoot, workspaceRoot) =>
|
|
47
|
-
|
|
48
|
-
|
|
80
|
+
export: async (courseRoot, workspaceRoot, flags) => {
|
|
81
|
+
const { standardOverride, error } = parseExportFlags(flags);
|
|
82
|
+
if (error) {
|
|
83
|
+
console.error(`[tessera] ${error}`);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
return (await import('./build-commands.js')).runBuild(
|
|
87
|
+
courseRoot,
|
|
88
|
+
workspaceRoot,
|
|
89
|
+
standardOverride,
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
validate: (courseRoot, _workspaceRoot, flags) => {
|
|
93
|
+
const { standardOverride, error } = parseExportFlags(flags);
|
|
94
|
+
if (error) {
|
|
95
|
+
console.error(`[tessera] ${error}`);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
return runValidate(courseRoot, { standardOverride });
|
|
99
|
+
},
|
|
49
100
|
a11y: (courseRoot, workspaceRoot, flags) =>
|
|
50
101
|
runA11y(courseRoot, workspaceRoot, flags),
|
|
51
102
|
check: (courseRoot, workspaceRoot, flags) => {
|
package/src/plugin/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
generateManifest,
|
|
15
15
|
readCourseConfig,
|
|
16
|
+
readResolvedConfig,
|
|
16
17
|
type CourseConfigRead,
|
|
17
18
|
} from './manifest.js';
|
|
18
19
|
import type { Manifest } from './manifest.js';
|
|
@@ -119,7 +120,8 @@ function virtualModule(
|
|
|
119
120
|
};
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
export function tesseraPlugin() {
|
|
123
|
+
export function tesseraPlugin(options: { standardOverride?: string } = {}) {
|
|
124
|
+
const { standardOverride } = options;
|
|
123
125
|
const manifestRef: { current: Manifest | null; root: string } = {
|
|
124
126
|
current: null,
|
|
125
127
|
root: '',
|
|
@@ -150,18 +152,18 @@ export function tesseraPlugin() {
|
|
|
150
152
|
},
|
|
151
153
|
}),
|
|
152
154
|
tesseraA11yCompilerPlugin(a11y),
|
|
153
|
-
tesseraValidationPlugin(),
|
|
154
|
-
tesseraEntryPlugin(),
|
|
155
|
+
tesseraValidationPlugin(standardOverride),
|
|
156
|
+
tesseraEntryPlugin(standardOverride),
|
|
155
157
|
tesseraConfigDefaultsPlugin(),
|
|
156
|
-
tesseraConfigPlugin(),
|
|
158
|
+
tesseraConfigPlugin(standardOverride),
|
|
157
159
|
tesseraPagesPlugin(),
|
|
158
160
|
tesseraManifestPlugin(manifestRef),
|
|
159
161
|
tesseraLayoutPlugin(),
|
|
160
162
|
tesseraQuizPlugin(),
|
|
161
|
-
tesseraAdapterPlugin(),
|
|
162
|
-
tesseraXAPISetupPlugin(),
|
|
163
|
+
tesseraAdapterPlugin(standardOverride),
|
|
164
|
+
tesseraXAPISetupPlugin(standardOverride),
|
|
163
165
|
tesseraFirstPagePreloadPlugin(manifestRef),
|
|
164
|
-
tesseraExportPlugin(),
|
|
166
|
+
tesseraExportPlugin(standardOverride),
|
|
165
167
|
];
|
|
166
168
|
}
|
|
167
169
|
|
|
@@ -172,7 +174,7 @@ const RESOLVED_ENTRY_ID = '\0' + VIRTUAL_ENTRY_ID;
|
|
|
172
174
|
const VIRTUAL_MAIN_ID = '/virtual:tessera-main';
|
|
173
175
|
const RESOLVED_MAIN_ID = '\0virtual:tessera-main';
|
|
174
176
|
|
|
175
|
-
function tesseraEntryPlugin(): Plugin {
|
|
177
|
+
function tesseraEntryPlugin(standardOverride?: string): Plugin {
|
|
176
178
|
const runtimeDir = resolveRuntimeDir();
|
|
177
179
|
const stylesDir = resolveStylesDir();
|
|
178
180
|
const appSveltePath = resolve(runtimeDir, 'App.svelte');
|
|
@@ -193,7 +195,7 @@ function tesseraEntryPlugin(): Plugin {
|
|
|
193
195
|
// For build mode: write index.html so Rollup can find it
|
|
194
196
|
buildStart() {
|
|
195
197
|
if (isBuild) {
|
|
196
|
-
const read =
|
|
198
|
+
const read = readResolvedConfig(projectRoot, standardOverride);
|
|
197
199
|
writeFileSync(
|
|
198
200
|
resolve(projectRoot, 'index.html'),
|
|
199
201
|
generateIndexHtml(readLanguage(read), cspMeta(read)),
|
|
@@ -265,19 +267,12 @@ function readLanguage(read: CourseConfigRead): string {
|
|
|
265
267
|
return isPlausibleLanguageTag(lang) ? lang : 'en';
|
|
266
268
|
}
|
|
267
269
|
|
|
268
|
-
// Fail closed on an unreadable config: cspMeta only emits for exactly 'web', so
|
|
269
|
-
// an unknown standard withholds the CSP rather than guess the one mode it breaks.
|
|
270
|
-
function readExportStandard(read: CourseConfigRead): string {
|
|
271
|
-
if (!read.ok) return 'unknown';
|
|
272
|
-
return read.config.export?.standard || 'web';
|
|
273
|
-
}
|
|
274
|
-
|
|
275
270
|
// Web export only — never on LMS packages (whose iframe JS bridges a meta CSP
|
|
276
271
|
// could break) and never on the dev server (a meta connect-src would block
|
|
277
272
|
// Vite's HMR websocket). `export.csp` extends the baseline per-directive, or
|
|
278
273
|
// `false` drops the meta for deployments that set a CSP header themselves.
|
|
279
|
-
function cspMeta(read: CourseConfigRead): string {
|
|
280
|
-
if (
|
|
274
|
+
function cspMeta(read: CourseConfigRead & { standard: string }): string {
|
|
275
|
+
if (read.standard !== 'web') return '';
|
|
281
276
|
const csp = read.ok ? read.config.export?.csp : undefined;
|
|
282
277
|
if (csp === false) return '';
|
|
283
278
|
return `\n <meta http-equiv="Content-Security-Policy" content="${buildCsp(csp)}" />`;
|
|
@@ -392,6 +387,7 @@ export function mergeCourseConfig(userConfig: Partial<CourseConfig>) {
|
|
|
392
387
|
return {
|
|
393
388
|
...userConfig,
|
|
394
389
|
title: userConfig.title || 'Untitled Course',
|
|
390
|
+
resume: userConfig.resume ?? 'auto',
|
|
395
391
|
navigation: { mode: 'free', ...userConfig.navigation },
|
|
396
392
|
completion: { ...completion, ...userConfig.completion },
|
|
397
393
|
scoring: { passingScore, ...userConfig.scoring },
|
|
@@ -399,14 +395,16 @@ export function mergeCourseConfig(userConfig: Partial<CourseConfig>) {
|
|
|
399
395
|
};
|
|
400
396
|
}
|
|
401
397
|
|
|
402
|
-
function tesseraConfigPlugin(): Plugin {
|
|
398
|
+
function tesseraConfigPlugin(standardOverride?: string): Plugin {
|
|
403
399
|
return virtualModule(
|
|
404
400
|
'tessera:config',
|
|
405
401
|
VIRTUAL_CONFIG_ID,
|
|
406
402
|
function ({ projectRoot }) {
|
|
407
403
|
const configPath = resolve(projectRoot, 'course.config.js');
|
|
408
404
|
if (existsSync(configPath)) this.addWatchFile(configPath);
|
|
409
|
-
|
|
405
|
+
// The runtime reads export.standard too, so readResolvedConfig must apply
|
|
406
|
+
// the override here — the bundled config, not just the manifest/adapter.
|
|
407
|
+
const read = readResolvedConfig(projectRoot, standardOverride);
|
|
410
408
|
const userConfig: Partial<CourseConfig> = read.ok ? read.config : {};
|
|
411
409
|
return `export default ${JSON.stringify(mergeCourseConfig(userConfig))};`;
|
|
412
410
|
},
|
|
@@ -448,7 +446,7 @@ function tesseraPagesPlugin(): Plugin {
|
|
|
448
446
|
|
|
449
447
|
// ---------- Validation Plugin ----------
|
|
450
448
|
|
|
451
|
-
function tesseraValidationPlugin(): Plugin {
|
|
449
|
+
function tesseraValidationPlugin(standardOverride?: string): Plugin {
|
|
452
450
|
let projectRoot: string;
|
|
453
451
|
let isBuild = false;
|
|
454
452
|
|
|
@@ -461,14 +459,14 @@ function tesseraValidationPlugin(): Plugin {
|
|
|
461
459
|
isBuild = config.command === 'build';
|
|
462
460
|
// Run validation during dev (configResolved fires before server starts)
|
|
463
461
|
if (!isBuild) {
|
|
464
|
-
runValidation(projectRoot);
|
|
462
|
+
runValidation(projectRoot, standardOverride);
|
|
465
463
|
}
|
|
466
464
|
},
|
|
467
465
|
|
|
468
466
|
buildStart() {
|
|
469
467
|
// Run validation during build (buildStart fires once before bundling)
|
|
470
468
|
if (isBuild) {
|
|
471
|
-
runValidation(projectRoot);
|
|
469
|
+
runValidation(projectRoot, standardOverride);
|
|
472
470
|
}
|
|
473
471
|
},
|
|
474
472
|
};
|
|
@@ -506,8 +504,8 @@ function tesseraA11yCompilerPlugin(a11y: A11yCompilerState): Plugin {
|
|
|
506
504
|
};
|
|
507
505
|
}
|
|
508
506
|
|
|
509
|
-
function runValidation(projectRoot: string): void {
|
|
510
|
-
const result = validateProject(projectRoot);
|
|
507
|
+
function runValidation(projectRoot: string, standardOverride?: string): void {
|
|
508
|
+
const result = validateProject(projectRoot, standardOverride);
|
|
511
509
|
reportValidationIssues(result);
|
|
512
510
|
if (result.errors.length > 0) {
|
|
513
511
|
throw new Error(
|
|
@@ -518,7 +516,7 @@ function runValidation(projectRoot: string): void {
|
|
|
518
516
|
|
|
519
517
|
// ---------- Export Plugin ----------
|
|
520
518
|
|
|
521
|
-
function tesseraExportPlugin(): Plugin {
|
|
519
|
+
function tesseraExportPlugin(standardOverride?: string): Plugin {
|
|
522
520
|
let projectRoot: string;
|
|
523
521
|
let isBuild = false;
|
|
524
522
|
|
|
@@ -535,7 +533,7 @@ function tesseraExportPlugin(): Plugin {
|
|
|
535
533
|
if (!isBuild) return;
|
|
536
534
|
if (isAuditBuild()) return;
|
|
537
535
|
|
|
538
|
-
const read =
|
|
536
|
+
const read = readResolvedConfig(projectRoot, standardOverride);
|
|
539
537
|
if (!read.ok) {
|
|
540
538
|
// Validation already required a parseable course.config.js — getting
|
|
541
539
|
// here means it vanished or broke mid-build. Surface that loudly
|
|
@@ -705,7 +703,7 @@ export function createAdapter() {
|
|
|
705
703
|
`;
|
|
706
704
|
}
|
|
707
705
|
|
|
708
|
-
function tesseraAdapterPlugin(): Plugin {
|
|
706
|
+
function tesseraAdapterPlugin(standardOverride?: string): Plugin {
|
|
709
707
|
return virtualModule(
|
|
710
708
|
'tessera:adapter',
|
|
711
709
|
VIRTUAL_ADAPTER_ID,
|
|
@@ -716,11 +714,7 @@ function tesseraAdapterPlugin(): Plugin {
|
|
|
716
714
|
return `export { createAdapter } from 'tessera-learn/runtime/adapters/index.js';`;
|
|
717
715
|
}
|
|
718
716
|
|
|
719
|
-
let standard =
|
|
720
|
-
const read = readCourseConfig(projectRoot);
|
|
721
|
-
if (read.ok && typeof read.config.export?.standard === 'string') {
|
|
722
|
-
standard = read.config.export.standard;
|
|
723
|
-
}
|
|
717
|
+
let standard = readResolvedConfig(projectRoot, standardOverride).standard;
|
|
724
718
|
|
|
725
719
|
// The audit renders headless with no LMS in the frame chain; the SCORM/
|
|
726
720
|
// cmi5 adapters throw when their API is absent, so render with WebAdapter.
|
|
@@ -743,7 +737,7 @@ export function createAdapter(config, options) {
|
|
|
743
737
|
|
|
744
738
|
const VIRTUAL_XAPI_SETUP_ID = 'virtual:tessera-xapi-setup';
|
|
745
739
|
|
|
746
|
-
function tesseraXAPISetupPlugin(): Plugin {
|
|
740
|
+
function tesseraXAPISetupPlugin(standardOverride?: string): Plugin {
|
|
747
741
|
return virtualModule(
|
|
748
742
|
'tessera:xapi-setup',
|
|
749
743
|
VIRTUAL_XAPI_SETUP_ID,
|
|
@@ -757,14 +751,9 @@ function tesseraXAPISetupPlugin(): Plugin {
|
|
|
757
751
|
return `export async function buildXAPIClient() { return null; }`;
|
|
758
752
|
}
|
|
759
753
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
if (read.ok) {
|
|
764
|
-
if (typeof read.config.export?.standard === 'string')
|
|
765
|
-
standard = read.config.export.standard;
|
|
766
|
-
hasXapi = read.config.xapi != null;
|
|
767
|
-
}
|
|
754
|
+
const read = readResolvedConfig(projectRoot, standardOverride);
|
|
755
|
+
const standard = read.standard;
|
|
756
|
+
const hasXapi = read.ok && read.config.xapi != null;
|
|
768
757
|
|
|
769
758
|
// The launch standards (cmi5, plain xAPI) own a publisher the runtime
|
|
770
759
|
// can share for `endpoint: 'lms'`, so wire the client regardless of
|
|
@@ -15,11 +15,12 @@ import { tesseraPlugin } from './index.js';
|
|
|
15
15
|
export function buildInlineConfig(
|
|
16
16
|
projectRoot: string,
|
|
17
17
|
workspaceRoot: string,
|
|
18
|
+
standardOverride?: string,
|
|
18
19
|
): InlineConfig {
|
|
19
20
|
return {
|
|
20
21
|
root: projectRoot,
|
|
21
22
|
configFile: false,
|
|
22
|
-
plugins: [tesseraPlugin()],
|
|
23
|
+
plugins: [tesseraPlugin({ standardOverride })],
|
|
23
24
|
resolve: { alias: { $shared: resolve(workspaceRoot, 'shared') } },
|
|
24
25
|
server: { fs: { allow: [workspaceRoot] } },
|
|
25
26
|
};
|
|
@@ -46,9 +47,10 @@ export async function resolveTesseraConfig(
|
|
|
46
47
|
projectRoot: string,
|
|
47
48
|
workspaceRoot: string,
|
|
48
49
|
env: ConfigEnv,
|
|
50
|
+
standardOverride?: string,
|
|
49
51
|
): Promise<InlineConfig> {
|
|
50
52
|
const vite = await import('vite');
|
|
51
|
-
const base = buildInlineConfig(projectRoot, workspaceRoot);
|
|
53
|
+
const base = buildInlineConfig(projectRoot, workspaceRoot, standardOverride);
|
|
52
54
|
const user = await loadUserConfig(projectRoot, env);
|
|
53
55
|
return user ? vite.mergeConfig(base, user) : base;
|
|
54
56
|
}
|
package/src/plugin/manifest.ts
CHANGED
|
@@ -121,6 +121,27 @@ export function readCourseConfig(projectRoot: string): CourseConfigRead {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Resolve a project's effective export standard once: the CLI `--standard`
|
|
126
|
+
* override wins, else `export.standard`, else `'web'`. An unreadable config with
|
|
127
|
+
* no override fails closed with `'unknown'` so callers withhold standard-specific
|
|
128
|
+
* output rather than guess. The returned `config` already has the override
|
|
129
|
+
* applied, so consumers read it back directly. Exported for tests.
|
|
130
|
+
*/
|
|
131
|
+
export function readResolvedConfig(
|
|
132
|
+
projectRoot: string,
|
|
133
|
+
standardOverride?: string,
|
|
134
|
+
): CourseConfigRead & { standard: string } {
|
|
135
|
+
const read = readCourseConfig(projectRoot);
|
|
136
|
+
if (!read.ok) return { ...read, standard: standardOverride ?? 'unknown' };
|
|
137
|
+
// The CLI validates --standard against the allowed set before it reaches here.
|
|
138
|
+
const override = standardOverride as CourseConfig['export']['standard'];
|
|
139
|
+
const config: Partial<CourseConfig> = override
|
|
140
|
+
? { ...read.config, export: { ...read.config.export, standard: override } }
|
|
141
|
+
: read.config;
|
|
142
|
+
return { ok: true, config, standard: config.export?.standard || 'web' };
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
/**
|
|
125
146
|
* Read a _meta.js file and extract its default export object.
|
|
126
147
|
* Uses the same JSON5 approach as pageConfig extraction — find the object literal
|
|
@@ -3,9 +3,12 @@ import { validateProject, reportValidationIssues } from './validation.js';
|
|
|
3
3
|
|
|
4
4
|
export function runValidate(
|
|
5
5
|
projectRoot: string,
|
|
6
|
-
{
|
|
6
|
+
{
|
|
7
|
+
showA11yTip = true,
|
|
8
|
+
standardOverride,
|
|
9
|
+
}: { showA11yTip?: boolean; standardOverride?: string } = {},
|
|
7
10
|
): number {
|
|
8
|
-
const { errors, warnings } = validateProject(projectRoot);
|
|
11
|
+
const { errors, warnings } = validateProject(projectRoot, standardOverride);
|
|
9
12
|
|
|
10
13
|
reportValidationIssues({ errors, warnings });
|
|
11
14
|
|