tessera-learn 0.0.10 → 0.0.13
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 +1 -0
- package/dist/audit-BBJpQGqb.js +204 -0
- package/dist/audit-BBJpQGqb.js.map +1 -0
- package/dist/plugin/a11y-cli.d.ts +1 -0
- package/dist/plugin/a11y-cli.js +36 -0
- package/dist/plugin/a11y-cli.js.map +1 -0
- package/dist/plugin/cli.js +6 -3
- package/dist/plugin/cli.js.map +1 -1
- package/dist/plugin/index.d.ts +16 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +171 -140
- package/dist/plugin/index.js.map +1 -1
- package/dist/{validation-BxWAMMnJ.js → validation-B-xTvM9B.js} +417 -81
- package/dist/validation-B-xTvM9B.js.map +1 -0
- package/package.json +17 -2
- package/src/components/Accordion.svelte +3 -1
- package/src/components/AccordionItem.svelte +1 -5
- package/src/components/Audio.svelte +22 -5
- package/src/components/Callout.svelte +5 -1
- package/src/components/Carousel.svelte +24 -8
- package/src/components/DefaultLayout.svelte +41 -12
- package/src/components/FillInTheBlank.svelte +75 -103
- package/src/components/Image.svelte +14 -10
- package/src/components/LockedBanner.svelte +5 -5
- package/src/components/Matching.svelte +48 -19
- package/src/components/MediaTracks.svelte +21 -0
- package/src/components/MultipleChoice.svelte +81 -102
- package/src/components/Quiz.svelte +63 -21
- package/src/components/ResultIcon.svelte +20 -4
- package/src/components/RevealModal.svelte +25 -22
- package/src/components/Sorting.svelte +61 -26
- package/src/components/Transcript.svelte +37 -0
- package/src/components/Video.svelte +25 -20
- package/src/components/util.ts +4 -1
- package/src/components/video-embed.ts +25 -0
- package/src/index.ts +2 -7
- package/src/plugin/a11y/audit.ts +299 -0
- package/src/plugin/a11y/contrast.ts +67 -0
- package/src/plugin/a11y-cli.ts +35 -0
- package/src/plugin/cli.ts +6 -8
- package/src/plugin/export.ts +60 -50
- package/src/plugin/index.ts +244 -101
- package/src/plugin/layout.ts +6 -51
- package/src/plugin/manifest.ts +90 -24
- package/src/plugin/override-plugin.ts +68 -0
- package/src/plugin/quiz.ts +9 -54
- package/src/plugin/validation.ts +768 -183
- package/src/runtime/App.svelte +128 -64
- package/src/runtime/LoadingBar.svelte +12 -3
- package/src/runtime/Sidebar.svelte +24 -8
- package/src/runtime/access.ts +15 -3
- package/src/runtime/adapters/cmi5.ts +68 -116
- package/src/runtime/adapters/format.ts +67 -0
- package/src/runtime/adapters/index.ts +45 -34
- package/src/runtime/adapters/retry.ts +25 -84
- package/src/runtime/adapters/scorm-base.ts +19 -15
- package/src/runtime/adapters/scorm12.ts +8 -9
- package/src/runtime/adapters/scorm2004.ts +22 -30
- package/src/runtime/adapters/web.ts +1 -1
- package/src/runtime/hooks.svelte.ts +152 -328
- package/src/runtime/interaction-format.ts +30 -12
- package/src/runtime/interaction.ts +44 -11
- package/src/runtime/navigation.svelte.ts +29 -40
- package/src/runtime/persistence.ts +2 -2
- package/src/runtime/progress.svelte.ts +22 -9
- package/src/runtime/quiz-engine.svelte.ts +361 -0
- package/src/runtime/quiz-policy.ts +28 -179
- package/src/runtime/types.ts +24 -2
- package/src/runtime/xapi/agent-rules.ts +11 -3
- package/src/runtime/xapi/client.ts +5 -5
- package/src/runtime/xapi/derive-actor.ts +2 -2
- package/src/runtime/xapi/publisher.ts +33 -40
- package/src/runtime/xapi/setup.ts +18 -15
- package/src/runtime/xapi/validation.ts +15 -6
- package/src/virtual.d.ts +4 -1
- package/styles/base.css +32 -11
- package/styles/layout.css +39 -18
- package/styles/theme.css +15 -3
- package/dist/validation-BxWAMMnJ.js.map +0 -1
package/src/plugin/index.ts
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';
|
|
2
2
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { dirname, resolve } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { dirname, resolve, relative, isAbsolute } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
statSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
cpSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
} from 'node:fs';
|
|
14
|
+
import { generateManifest, readCourseConfig } from './manifest.js';
|
|
8
15
|
import type { Manifest } from './manifest.js';
|
|
9
|
-
import {
|
|
16
|
+
import type { CourseConfig } from '../runtime/types.js';
|
|
17
|
+
import {
|
|
18
|
+
validateProject,
|
|
19
|
+
reportValidationIssues,
|
|
20
|
+
normalizeA11y,
|
|
21
|
+
isPlausibleLanguageTag,
|
|
22
|
+
isIgnored,
|
|
23
|
+
type A11ySettings,
|
|
24
|
+
} from './validation.js';
|
|
10
25
|
import { runExport } from './export.js';
|
|
11
26
|
import { tesseraLayoutPlugin } from './layout.js';
|
|
12
27
|
import { tesseraQuizPlugin } from './quiz.js';
|
|
13
28
|
|
|
29
|
+
import { AUDIT_ENV_FLAG } from './a11y/audit.js';
|
|
30
|
+
|
|
31
|
+
export { runAudit } from './a11y/audit.js';
|
|
32
|
+
export type { AuditOptions, ImpactLevel } from './a11y/audit.js';
|
|
33
|
+
|
|
34
|
+
function isAuditBuild(): boolean {
|
|
35
|
+
return process.env[AUDIT_ENV_FLAG] === '1';
|
|
36
|
+
}
|
|
37
|
+
|
|
14
38
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
39
|
const __dirname = dirname(__filename);
|
|
16
40
|
|
|
@@ -26,12 +50,71 @@ function resolveStylesDir(): string {
|
|
|
26
50
|
return resolve(packageRoot, 'styles');
|
|
27
51
|
}
|
|
28
52
|
|
|
53
|
+
// Tier-1a state shared between the svelte() onwarn handler and the sibling
|
|
54
|
+
// gate plugin. onwarn fires during transform (after the Tier-1b buildStart
|
|
55
|
+
// gate), so a11y warnings are collected here and flushed/gated at buildEnd.
|
|
56
|
+
interface A11yCompilerState {
|
|
57
|
+
warnings: string[];
|
|
58
|
+
projectRoot: string;
|
|
59
|
+
isBuild: boolean;
|
|
60
|
+
settings: A11ySettings;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Svelte's onwarn filename is relative to the vite root (e.g. `pages/x.svelte`)
|
|
64
|
+
// in build and may be absolute or a virtual id elsewhere. Return the
|
|
65
|
+
// project-relative path for a real author file, or null to skip framework /
|
|
66
|
+
// node_modules / virtual modules — Tier 0 owns the framework's own warnings.
|
|
67
|
+
function projectFileRel(
|
|
68
|
+
filename: string | undefined,
|
|
69
|
+
projectRoot: string,
|
|
70
|
+
): string | null {
|
|
71
|
+
if (!filename || !projectRoot) return null;
|
|
72
|
+
if (
|
|
73
|
+
filename.startsWith('\0') ||
|
|
74
|
+
filename.includes('virtual:') ||
|
|
75
|
+
filename.includes('node_modules')
|
|
76
|
+
) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const abs = isAbsolute(filename) ? filename : resolve(projectRoot, filename);
|
|
80
|
+
const rel = relative(projectRoot, abs);
|
|
81
|
+
if (rel.startsWith('..') || isAbsolute(rel) || rel.includes('node_modules')) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return rel;
|
|
85
|
+
}
|
|
86
|
+
|
|
29
87
|
export function tesseraPlugin() {
|
|
30
|
-
const manifestRef: { current: Manifest | null; root: string } = {
|
|
88
|
+
const manifestRef: { current: Manifest | null; root: string } = {
|
|
89
|
+
current: null,
|
|
90
|
+
root: '',
|
|
91
|
+
};
|
|
92
|
+
const a11y: A11yCompilerState = {
|
|
93
|
+
warnings: [],
|
|
94
|
+
projectRoot: '',
|
|
95
|
+
isBuild: false,
|
|
96
|
+
settings: normalizeA11y(undefined),
|
|
97
|
+
};
|
|
31
98
|
return [
|
|
32
99
|
svelte({
|
|
33
100
|
compilerOptions: { css: 'external' },
|
|
101
|
+
onwarn(warning, defaultHandler) {
|
|
102
|
+
if (warning.code?.startsWith('a11y')) {
|
|
103
|
+
const rel = projectFileRel(warning.filename, a11y.projectRoot);
|
|
104
|
+
if (rel !== null) {
|
|
105
|
+
const msg = `[${warning.code}] ${rel}: ${warning.message}`;
|
|
106
|
+
if (a11y.isBuild) {
|
|
107
|
+
a11y.warnings.push(msg);
|
|
108
|
+
} else if (!a11y.settings.ignore.includes(warning.code)) {
|
|
109
|
+
reportValidationIssues({ errors: [], warnings: [msg] });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return; // suppress the raw Vite print; we re-emit via the reporter
|
|
113
|
+
}
|
|
114
|
+
defaultHandler?.(warning);
|
|
115
|
+
},
|
|
34
116
|
}),
|
|
117
|
+
tesseraA11yCompilerPlugin(a11y),
|
|
35
118
|
tesseraValidationPlugin(),
|
|
36
119
|
tesseraEntryPlugin(),
|
|
37
120
|
tesseraConfigPlugin(),
|
|
@@ -58,6 +141,7 @@ function tesseraEntryPlugin(): Plugin {
|
|
|
58
141
|
const stylesDir = resolveStylesDir();
|
|
59
142
|
const appSveltePath = resolve(runtimeDir, 'App.svelte');
|
|
60
143
|
let projectRoot: string;
|
|
144
|
+
let outDir: string;
|
|
61
145
|
let isBuild = false;
|
|
62
146
|
|
|
63
147
|
return {
|
|
@@ -66,13 +150,18 @@ function tesseraEntryPlugin(): Plugin {
|
|
|
66
150
|
|
|
67
151
|
configResolved(config: ResolvedConfig) {
|
|
68
152
|
projectRoot = config.root;
|
|
153
|
+
outDir = resolve(config.root, config.build.outDir);
|
|
69
154
|
isBuild = config.command === 'build';
|
|
70
155
|
},
|
|
71
156
|
|
|
72
157
|
// For build mode: write index.html so Rollup can find it
|
|
73
158
|
buildStart() {
|
|
74
159
|
if (isBuild) {
|
|
75
|
-
writeFileSync(
|
|
160
|
+
writeFileSync(
|
|
161
|
+
resolve(projectRoot, 'index.html'),
|
|
162
|
+
generateIndexHtml(readLanguage(projectRoot)),
|
|
163
|
+
'utf-8',
|
|
164
|
+
);
|
|
76
165
|
}
|
|
77
166
|
},
|
|
78
167
|
|
|
@@ -81,12 +170,14 @@ function tesseraEntryPlugin(): Plugin {
|
|
|
81
170
|
if (isBuild) {
|
|
82
171
|
const htmlPath = resolve(projectRoot, 'index.html');
|
|
83
172
|
if (existsSync(htmlPath)) {
|
|
84
|
-
try {
|
|
173
|
+
try {
|
|
174
|
+
unlinkSync(htmlPath);
|
|
175
|
+
} catch {}
|
|
85
176
|
}
|
|
86
177
|
|
|
87
|
-
// Copy assets/
|
|
178
|
+
// Copy assets/ into the build's assets/ so $assets/ references resolve
|
|
88
179
|
const assetsDir = resolve(projectRoot, 'assets');
|
|
89
|
-
const distAssetsDir = resolve(
|
|
180
|
+
const distAssetsDir = resolve(outDir, 'assets');
|
|
90
181
|
if (existsSync(assetsDir)) {
|
|
91
182
|
mkdirSync(distAssetsDir, { recursive: true });
|
|
92
183
|
cpSync(assetsDir, distAssetsDir, { recursive: true });
|
|
@@ -99,7 +190,7 @@ function tesseraEntryPlugin(): Plugin {
|
|
|
99
190
|
return () => {
|
|
100
191
|
server.middlewares.use(async (req, res, next) => {
|
|
101
192
|
if (req.url === '/' || req.url === '/index.html') {
|
|
102
|
-
const html = generateIndexHtml();
|
|
193
|
+
const html = generateIndexHtml(readLanguage(projectRoot));
|
|
103
194
|
const transformed = await server.transformIndexHtml(req.url, html);
|
|
104
195
|
res.setHeader('Content-Type', 'text/html');
|
|
105
196
|
res.statusCode = 200;
|
|
@@ -113,7 +204,8 @@ function tesseraEntryPlugin(): Plugin {
|
|
|
113
204
|
|
|
114
205
|
resolveId(id) {
|
|
115
206
|
if (id === VIRTUAL_ENTRY_ID) return RESOLVED_ENTRY_ID;
|
|
116
|
-
if (id === VIRTUAL_MAIN_ID || id === 'virtual:tessera-main')
|
|
207
|
+
if (id === VIRTUAL_MAIN_ID || id === 'virtual:tessera-main')
|
|
208
|
+
return RESOLVED_MAIN_ID;
|
|
117
209
|
return null;
|
|
118
210
|
},
|
|
119
211
|
|
|
@@ -126,9 +218,18 @@ function tesseraEntryPlugin(): Plugin {
|
|
|
126
218
|
};
|
|
127
219
|
}
|
|
128
220
|
|
|
129
|
-
|
|
221
|
+
// 'en' fallback applied here: the config default-merge runs later than buildStart.
|
|
222
|
+
// Only a validated BCP-47 tag is interpolated into <html lang>, so a malformed
|
|
223
|
+
// value (caught separately as a warning) can't ship a broken attribute.
|
|
224
|
+
function readLanguage(projectRoot: string): string {
|
|
225
|
+
const read = readCourseConfig(projectRoot);
|
|
226
|
+
const lang = read.ok ? read.config.language : undefined;
|
|
227
|
+
return isPlausibleLanguageTag(lang) ? lang : 'en';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function generateIndexHtml(lang: string): string {
|
|
130
231
|
return `<!DOCTYPE html>
|
|
131
|
-
<html lang="
|
|
232
|
+
<html lang="${lang}">
|
|
132
233
|
<head>
|
|
133
234
|
<meta charset="UTF-8" />
|
|
134
235
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
@@ -141,15 +242,19 @@ function generateIndexHtml(): string {
|
|
|
141
242
|
</html>`;
|
|
142
243
|
}
|
|
143
244
|
|
|
144
|
-
function generateEntryScript(
|
|
245
|
+
function generateEntryScript(
|
|
246
|
+
appSveltePath: string,
|
|
247
|
+
frameworkStylesDir: string,
|
|
248
|
+
projectRoot: string,
|
|
249
|
+
): string {
|
|
145
250
|
const normalizedPath = appSveltePath.replace(/\\/g, '/');
|
|
146
251
|
|
|
147
252
|
// Framework CSS imports (theme → base → layout)
|
|
148
253
|
const frameworkCssOrder = ['theme.css', 'base.css', 'layout.css'];
|
|
149
254
|
const frameworkImports = frameworkCssOrder
|
|
150
|
-
.map(file => resolve(frameworkStylesDir, file).replace(/\\/g, '/'))
|
|
151
|
-
.filter(path => existsSync(path))
|
|
152
|
-
.map(path => `import '${path}';`)
|
|
255
|
+
.map((file) => resolve(frameworkStylesDir, file).replace(/\\/g, '/'))
|
|
256
|
+
.filter((path) => existsSync(path))
|
|
257
|
+
.map((path) => `import '${path}';`)
|
|
153
258
|
.join('\n');
|
|
154
259
|
|
|
155
260
|
// User CSS imports from project's styles/ directory
|
|
@@ -157,11 +262,11 @@ function generateEntryScript(appSveltePath: string, frameworkStylesDir: string,
|
|
|
157
262
|
let userImports = '';
|
|
158
263
|
if (existsSync(userStylesDir)) {
|
|
159
264
|
const userCssFiles = readdirSync(userStylesDir)
|
|
160
|
-
.filter(f => f.endsWith('.css'))
|
|
265
|
+
.filter((f) => f.endsWith('.css'))
|
|
161
266
|
.sort();
|
|
162
267
|
userImports = userCssFiles
|
|
163
|
-
.map(f => resolve(userStylesDir, f).replace(/\\/g, '/'))
|
|
164
|
-
.map(path => `import '${path}';`)
|
|
268
|
+
.map((f) => resolve(userStylesDir, f).replace(/\\/g, '/'))
|
|
269
|
+
.map((path) => `import '${path}';`)
|
|
165
270
|
.join('\n');
|
|
166
271
|
}
|
|
167
272
|
|
|
@@ -191,7 +296,10 @@ function completionDefaults(mode: string | undefined): {
|
|
|
191
296
|
if (mode === 'manual') {
|
|
192
297
|
return { completion: { mode: 'manual' }, passingScore: 0 };
|
|
193
298
|
}
|
|
194
|
-
return {
|
|
299
|
+
return {
|
|
300
|
+
completion: { mode: 'percentage', percentageThreshold: 100 },
|
|
301
|
+
passingScore: 70,
|
|
302
|
+
};
|
|
195
303
|
}
|
|
196
304
|
|
|
197
305
|
function tesseraConfigPlugin(): Plugin {
|
|
@@ -206,9 +314,12 @@ function tesseraConfigPlugin(): Plugin {
|
|
|
206
314
|
|
|
207
315
|
return {
|
|
208
316
|
base: './',
|
|
317
|
+
build: {
|
|
318
|
+
assetsDir: 'tessera',
|
|
319
|
+
},
|
|
209
320
|
resolve: {
|
|
210
321
|
alias: {
|
|
211
|
-
|
|
322
|
+
$assets: resolve(root, 'assets'),
|
|
212
323
|
},
|
|
213
324
|
},
|
|
214
325
|
// tessera-learn ships .ts/.svelte.ts source; Vite's dep optimizer
|
|
@@ -231,17 +342,13 @@ function tesseraConfigPlugin(): Plugin {
|
|
|
231
342
|
load(id) {
|
|
232
343
|
if (id === RESOLVED_CONFIG_ID) {
|
|
233
344
|
const configPath = resolve(projectRoot, 'course.config.js');
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this.addWatchFile(configPath);
|
|
238
|
-
const objectStr = extractDefaultExportObjectLiteral(readFileSync(configPath, 'utf-8'));
|
|
239
|
-
if (objectStr) {
|
|
240
|
-
try { userConfig = JSON5.parse(objectStr); } catch {}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
345
|
+
if (existsSync(configPath)) this.addWatchFile(configPath);
|
|
346
|
+
const read = readCourseConfig(projectRoot);
|
|
347
|
+
const userConfig: Partial<CourseConfig> = read.ok ? read.config : {};
|
|
243
348
|
|
|
244
|
-
const { completion, passingScore } = completionDefaults(
|
|
349
|
+
const { completion, passingScore } = completionDefaults(
|
|
350
|
+
userConfig.completion?.mode,
|
|
351
|
+
);
|
|
245
352
|
const merged = {
|
|
246
353
|
title: userConfig.title || 'Untitled Course',
|
|
247
354
|
...userConfig,
|
|
@@ -261,7 +368,10 @@ function tesseraConfigPlugin(): Plugin {
|
|
|
261
368
|
// ---------- Manifest Watch Helpers ----------
|
|
262
369
|
|
|
263
370
|
/** Register all _meta.js and .svelte files under pagesDir as watch files for build mode. */
|
|
264
|
-
function addWatchFiles(
|
|
371
|
+
function addWatchFiles(
|
|
372
|
+
ctx: { addWatchFile(id: string): void },
|
|
373
|
+
dir: string,
|
|
374
|
+
): void {
|
|
265
375
|
if (!existsSync(dir)) return;
|
|
266
376
|
for (const entry of readdirSync(dir)) {
|
|
267
377
|
const full = resolve(dir, entry);
|
|
@@ -330,19 +440,44 @@ function tesseraValidationPlugin(): Plugin {
|
|
|
330
440
|
};
|
|
331
441
|
}
|
|
332
442
|
|
|
333
|
-
|
|
334
|
-
|
|
443
|
+
// Tier 1a: flush + gate the Svelte compiler's a11y warnings at buildEnd, after
|
|
444
|
+
// every module is transformed. svelte() accepts `onwarn` but not arbitrary
|
|
445
|
+
// Rollup hooks, so the gate lives here and shares the onwarn closure.
|
|
446
|
+
function tesseraA11yCompilerPlugin(a11y: A11yCompilerState): Plugin {
|
|
447
|
+
return {
|
|
448
|
+
name: 'tessera:a11y-compiler',
|
|
449
|
+
enforce: 'pre',
|
|
335
450
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
451
|
+
configResolved(config: ResolvedConfig) {
|
|
452
|
+
a11y.projectRoot = config.root;
|
|
453
|
+
a11y.isBuild = config.command === 'build';
|
|
454
|
+
const read = readCourseConfig(config.root);
|
|
455
|
+
a11y.settings = normalizeA11y(read.ok ? read.config.a11y : undefined);
|
|
456
|
+
},
|
|
339
457
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
458
|
+
buildEnd() {
|
|
459
|
+
if (!a11y.isBuild || a11y.warnings.length === 0) return;
|
|
460
|
+
const ignored = new Set(a11y.settings.ignore);
|
|
461
|
+
const warnings = a11y.warnings.filter((msg) => !isIgnored(msg, ignored));
|
|
462
|
+
a11y.warnings = [];
|
|
463
|
+
if (warnings.length === 0) return;
|
|
464
|
+
if (a11y.settings.level === 'error') {
|
|
465
|
+
reportValidationIssues({ errors: warnings, warnings: [] });
|
|
466
|
+
throw new Error(
|
|
467
|
+
`Tessera: ${warnings.length} a11y issue(s) with a11y.level: 'error'. Fix the errors above to continue.`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
reportValidationIssues({ errors: [], warnings });
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function runValidation(projectRoot: string): void {
|
|
476
|
+
const result = validateProject(projectRoot);
|
|
477
|
+
reportValidationIssues(result);
|
|
478
|
+
if (result.errors.length > 0) {
|
|
344
479
|
throw new Error(
|
|
345
|
-
`Tessera validation failed with ${errors.length} error(s). Fix the errors above to continue
|
|
480
|
+
`Tessera validation failed with ${result.errors.length} error(s). Fix the errors above to continue.`,
|
|
346
481
|
);
|
|
347
482
|
}
|
|
348
483
|
}
|
|
@@ -364,34 +499,32 @@ function tesseraExportPlugin(): Plugin {
|
|
|
364
499
|
|
|
365
500
|
async closeBundle() {
|
|
366
501
|
if (!isBuild) return;
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
let config: any;
|
|
386
|
-
try {
|
|
387
|
-
config = JSON5.parse(objectStr);
|
|
388
|
-
} catch (err) {
|
|
502
|
+
if (isAuditBuild()) return;
|
|
503
|
+
|
|
504
|
+
const read = readCourseConfig(projectRoot);
|
|
505
|
+
if (!read.ok) {
|
|
506
|
+
// Validation already required a parseable course.config.js — getting
|
|
507
|
+
// here means it vanished or broke mid-build. Surface that loudly
|
|
508
|
+
// rather than shipping a bundle with no LMS export silently.
|
|
509
|
+
if (read.reason === 'missing') {
|
|
510
|
+
throw new Error(
|
|
511
|
+
'[tessera:export] course.config.js not found at closeBundle. The file must exist for the export step to run.',
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
if (read.reason === 'no-export') {
|
|
515
|
+
throw new Error(
|
|
516
|
+
'[tessera:export] course.config.js: could not locate `export default { ... }`. Cannot determine export.standard.',
|
|
517
|
+
);
|
|
518
|
+
}
|
|
389
519
|
throw new Error(
|
|
390
|
-
`[tessera:export] course.config.js: failed to parse export-default object literal — ${(
|
|
520
|
+
`[tessera:export] course.config.js: failed to parse export-default object literal — ${(read.error as Error).message}`,
|
|
391
521
|
);
|
|
392
522
|
}
|
|
393
523
|
|
|
394
|
-
await runExport(
|
|
524
|
+
await runExport(
|
|
525
|
+
projectRoot,
|
|
526
|
+
read.config as Parameters<typeof runExport>[1],
|
|
527
|
+
);
|
|
395
528
|
},
|
|
396
529
|
};
|
|
397
530
|
}
|
|
@@ -401,10 +534,12 @@ function tesseraExportPlugin(): Plugin {
|
|
|
401
534
|
const VIRTUAL_MANIFEST_ID = 'virtual:tessera-manifest';
|
|
402
535
|
const RESOLVED_MANIFEST_ID = '\0' + VIRTUAL_MANIFEST_ID;
|
|
403
536
|
|
|
404
|
-
function tesseraManifestPlugin(manifestRef: {
|
|
537
|
+
function tesseraManifestPlugin(manifestRef: {
|
|
538
|
+
current: Manifest | null;
|
|
539
|
+
root: string;
|
|
540
|
+
}): Plugin {
|
|
405
541
|
let projectRoot: string;
|
|
406
542
|
let pagesDir: string;
|
|
407
|
-
let server: ViteDevServer | null = null;
|
|
408
543
|
|
|
409
544
|
function buildManifest(): Manifest {
|
|
410
545
|
const m = generateManifest(pagesDir);
|
|
@@ -423,8 +558,6 @@ function tesseraManifestPlugin(manifestRef: { current: Manifest | null; root: st
|
|
|
423
558
|
},
|
|
424
559
|
|
|
425
560
|
configureServer(devServer: ViteDevServer) {
|
|
426
|
-
server = devServer;
|
|
427
|
-
|
|
428
561
|
// Watch the pages directory for changes
|
|
429
562
|
devServer.watcher.on('all', (event, filePath) => {
|
|
430
563
|
if (!filePath.startsWith(pagesDir)) return;
|
|
@@ -446,7 +579,9 @@ function tesseraManifestPlugin(manifestRef: { current: Manifest | null; root: st
|
|
|
446
579
|
devServer.ws.send({ type: 'full-reload' });
|
|
447
580
|
}
|
|
448
581
|
|
|
449
|
-
console.log(
|
|
582
|
+
console.log(
|
|
583
|
+
`[tessera] Manifest rebuilt (${event}: ${filePath.replace(projectRoot, '')})`,
|
|
584
|
+
);
|
|
450
585
|
}
|
|
451
586
|
});
|
|
452
587
|
},
|
|
@@ -474,7 +609,7 @@ function tesseraManifestPlugin(manifestRef: { current: Manifest | null; root: st
|
|
|
474
609
|
// scanning .svelte importPath strings as module imports.
|
|
475
610
|
// Replace Infinity with 1e9 since JSON.stringify drops it.
|
|
476
611
|
const json = JSON.stringify(manifestRef.current, (_key, value) =>
|
|
477
|
-
value === Infinity ? 1e9 : value
|
|
612
|
+
value === Infinity ? 1e9 : value,
|
|
478
613
|
);
|
|
479
614
|
const b64 = Buffer.from(json).toString('base64');
|
|
480
615
|
return `export default JSON.parse(atob("${b64}"));`;
|
|
@@ -515,17 +650,15 @@ function tesseraAdapterPlugin(): Plugin {
|
|
|
515
650
|
}
|
|
516
651
|
|
|
517
652
|
let standard = 'web';
|
|
518
|
-
const
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
if (objectStr) {
|
|
522
|
-
try {
|
|
523
|
-
const parsed = JSON5.parse(objectStr);
|
|
524
|
-
if (typeof parsed?.export?.standard === 'string') standard = parsed.export.standard;
|
|
525
|
-
} catch {}
|
|
526
|
-
}
|
|
653
|
+
const read = readCourseConfig(projectRoot);
|
|
654
|
+
if (read.ok && typeof read.config.export?.standard === 'string') {
|
|
655
|
+
standard = read.config.export.standard;
|
|
527
656
|
}
|
|
528
657
|
|
|
658
|
+
// The audit renders headless with no LMS in the frame chain; the SCORM/
|
|
659
|
+
// cmi5 adapters throw when their API is absent, so render with WebAdapter.
|
|
660
|
+
if (isAuditBuild()) standard = 'web';
|
|
661
|
+
|
|
529
662
|
switch (standard) {
|
|
530
663
|
case 'scorm12':
|
|
531
664
|
return `
|
|
@@ -599,18 +732,18 @@ function tesseraXAPISetupPlugin(): Plugin {
|
|
|
599
732
|
return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;
|
|
600
733
|
}
|
|
601
734
|
|
|
735
|
+
// The audit runs offline — don't wire real LRS destinations into it.
|
|
736
|
+
if (isAuditBuild()) {
|
|
737
|
+
return `export async function buildXAPIClient() { return null; }`;
|
|
738
|
+
}
|
|
739
|
+
|
|
602
740
|
let standard = 'web';
|
|
603
741
|
let hasXapi = false;
|
|
604
|
-
const
|
|
605
|
-
if (
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const parsed = JSON5.parse(objectStr);
|
|
610
|
-
if (typeof parsed?.export?.standard === 'string') standard = parsed.export.standard;
|
|
611
|
-
hasXapi = parsed?.xapi != null;
|
|
612
|
-
} catch {}
|
|
613
|
-
}
|
|
742
|
+
const read = readCourseConfig(projectRoot);
|
|
743
|
+
if (read.ok) {
|
|
744
|
+
if (typeof read.config.export?.standard === 'string')
|
|
745
|
+
standard = read.config.export.standard;
|
|
746
|
+
hasXapi = read.config.xapi != null;
|
|
614
747
|
}
|
|
615
748
|
|
|
616
749
|
// cmi5 needs the publisher regardless of explicit xapi config (cmi5
|
|
@@ -624,7 +757,10 @@ function tesseraXAPISetupPlugin(): Plugin {
|
|
|
624
757
|
};
|
|
625
758
|
}
|
|
626
759
|
|
|
627
|
-
function tesseraFirstPagePreloadPlugin(manifestRef: {
|
|
760
|
+
function tesseraFirstPagePreloadPlugin(manifestRef: {
|
|
761
|
+
current: Manifest | null;
|
|
762
|
+
root: string;
|
|
763
|
+
}): Plugin {
|
|
628
764
|
return {
|
|
629
765
|
name: 'tessera:first-page-preload',
|
|
630
766
|
apply: 'build',
|
|
@@ -633,17 +769,24 @@ function tesseraFirstPagePreloadPlugin(manifestRef: { current: Manifest | null;
|
|
|
633
769
|
handler(_html, ctx) {
|
|
634
770
|
const firstPagePath = manifestRef.current?.pages[0]?.importPath;
|
|
635
771
|
if (!firstPagePath || !ctx.bundle) return;
|
|
636
|
-
const normalized = resolve(
|
|
772
|
+
const normalized = resolve(
|
|
773
|
+
manifestRef.root,
|
|
774
|
+
firstPagePath.replace(/^\//, ''),
|
|
775
|
+
).replace(/\\/g, '/');
|
|
637
776
|
const chunk = Object.values(ctx.bundle).find(
|
|
638
777
|
(c): c is import('vite').Rollup.OutputChunk =>
|
|
639
|
-
c.type === 'chunk' &&
|
|
778
|
+
c.type === 'chunk' &&
|
|
779
|
+
!!c.facadeModuleId &&
|
|
780
|
+
c.facadeModuleId.replace(/\\/g, '/') === normalized,
|
|
640
781
|
);
|
|
641
782
|
if (!chunk) return;
|
|
642
|
-
return [
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
783
|
+
return [
|
|
784
|
+
{
|
|
785
|
+
tag: 'link',
|
|
786
|
+
attrs: { rel: 'modulepreload', href: `./${chunk.fileName}` },
|
|
787
|
+
injectTo: 'head',
|
|
788
|
+
},
|
|
789
|
+
];
|
|
647
790
|
},
|
|
648
791
|
},
|
|
649
792
|
};
|
package/src/plugin/layout.ts
CHANGED
|
@@ -1,55 +1,10 @@
|
|
|
1
|
-
import type { Plugin
|
|
2
|
-
import {
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
|
-
|
|
5
|
-
const VIRTUAL_LAYOUT_ID = 'virtual:tessera-layout';
|
|
6
|
-
const RESOLVED_LAYOUT_ID = '\0' + VIRTUAL_LAYOUT_ID;
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import { createOverridePlugin } from './override-plugin.js';
|
|
7
3
|
|
|
8
4
|
export function tesseraLayoutPlugin(): Plugin {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return {
|
|
5
|
+
return createOverridePlugin({
|
|
12
6
|
name: 'tessera:layout',
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
projectRoot = config.root;
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
resolveId(id) {
|
|
20
|
-
if (id === VIRTUAL_LAYOUT_ID) return RESOLVED_LAYOUT_ID;
|
|
21
|
-
return null;
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
load(id) {
|
|
25
|
-
if (id !== RESOLVED_LAYOUT_ID) return null;
|
|
26
|
-
const layoutPath = resolve(projectRoot, 'layout.svelte');
|
|
27
|
-
if (existsSync(layoutPath)) {
|
|
28
|
-
// Register the file with Vite so edits trigger HMR / build --watch
|
|
29
|
-
// re-runs. Only add when the file actually exists — calling
|
|
30
|
-
// addWatchFile on a non-existent path makes Vite's importAnalysis
|
|
31
|
-
// try to resolve it as a real import.
|
|
32
|
-
this.addWatchFile(layoutPath);
|
|
33
|
-
const normalized = layoutPath.replace(/\\/g, '/');
|
|
34
|
-
return `export { default } from '${normalized}';`;
|
|
35
|
-
}
|
|
36
|
-
return `export default null;`;
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
configureServer(server: ViteDevServer) {
|
|
40
|
-
const layoutPath = resolve(projectRoot, 'layout.svelte');
|
|
41
|
-
// Only react to add/unlink: those flip the virtual module's load() output
|
|
42
|
-
// between `export default null` and `export { default } from '...'`. A
|
|
43
|
-
// `change` event leaves that output identical and is handled by Svelte's
|
|
44
|
-
// own HMR for the underlying file — full-reloading on every edit would
|
|
45
|
-
// wipe in-page state for no reason.
|
|
46
|
-
server.watcher.on('all', (event, filePath) => {
|
|
47
|
-
if (filePath !== layoutPath) return;
|
|
48
|
-
if (event !== 'add' && event !== 'unlink') return;
|
|
49
|
-
const mod = server.moduleGraph.getModuleById(RESOLVED_LAYOUT_ID);
|
|
50
|
-
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
51
|
-
server.ws.send({ type: 'full-reload' });
|
|
52
|
-
});
|
|
53
|
-
},
|
|
54
|
-
};
|
|
7
|
+
virtualId: 'virtual:tessera-layout',
|
|
8
|
+
projectFile: 'layout.svelte',
|
|
9
|
+
});
|
|
55
10
|
}
|