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/manifest.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { resolve, basename, extname } from 'node:path';
|
|
3
3
|
import JSON5 from 'json5';
|
|
4
|
-
import type { QuizConfig } from '../runtime/types.js';
|
|
4
|
+
import type { CourseConfig, QuizConfig } from '../runtime/types.js';
|
|
5
5
|
|
|
6
6
|
// ---------- Types ----------
|
|
7
7
|
|
|
@@ -48,7 +48,10 @@ export function ensureSvelteSuffix(name: string): string {
|
|
|
48
48
|
* sharing the read avoids the second disk hit (and matters most on cold-cache
|
|
49
49
|
* CI runs and large courses).
|
|
50
50
|
*/
|
|
51
|
-
const fileContentCache = new Map<
|
|
51
|
+
const fileContentCache = new Map<
|
|
52
|
+
string,
|
|
53
|
+
{ mtimeMs: number; content: string }
|
|
54
|
+
>();
|
|
52
55
|
|
|
53
56
|
export function readSourceFileCached(filePath: string): string {
|
|
54
57
|
const stat = statSync(filePath);
|
|
@@ -70,7 +73,7 @@ export function stripPrefix(name: string): string {
|
|
|
70
73
|
export function titleCase(slug: string): string {
|
|
71
74
|
return slug
|
|
72
75
|
.split('-')
|
|
73
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
76
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
74
77
|
.join(' ');
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -97,7 +100,9 @@ const DEFAULT_EXPORT_RE = /export\s+default\s*/;
|
|
|
97
100
|
* or null if no balanced object literal follows the `export default` keyword.
|
|
98
101
|
* Used by both manifest extraction and project validation.
|
|
99
102
|
*/
|
|
100
|
-
export function extractDefaultExportObjectLiteral(
|
|
103
|
+
export function extractDefaultExportObjectLiteral(
|
|
104
|
+
source: string,
|
|
105
|
+
): string | null {
|
|
101
106
|
const match = source.match(DEFAULT_EXPORT_RE);
|
|
102
107
|
if (!match || match.index === undefined) return null;
|
|
103
108
|
const startIndex = source.indexOf('{', match.index);
|
|
@@ -105,15 +110,49 @@ export function extractDefaultExportObjectLiteral(source: string): string | null
|
|
|
105
110
|
return extractObjectLiteral(source, startIndex);
|
|
106
111
|
}
|
|
107
112
|
|
|
113
|
+
export type CourseConfigRead =
|
|
114
|
+
| { ok: true; config: Partial<CourseConfig> }
|
|
115
|
+
| {
|
|
116
|
+
ok: false;
|
|
117
|
+
reason: 'missing' | 'no-export' | 'parse-error';
|
|
118
|
+
error?: unknown;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read and JSON5-parse the `export default { ... }` literal from a project's
|
|
123
|
+
* course.config.js. Shared by the build plugin and the validator so the read,
|
|
124
|
+
* cache, and parse rules live in one place. The discriminated `reason` lets
|
|
125
|
+
* callers that care (export, validation) emit precise errors while callers
|
|
126
|
+
* that just need a value can fall back on `!ok`.
|
|
127
|
+
*/
|
|
128
|
+
export function readCourseConfig(projectRoot: string): CourseConfigRead {
|
|
129
|
+
const configPath = resolve(projectRoot, 'course.config.js');
|
|
130
|
+
if (!existsSync(configPath)) return { ok: false, reason: 'missing' };
|
|
131
|
+
const objectStr = extractDefaultExportObjectLiteral(
|
|
132
|
+
readSourceFileCached(configPath),
|
|
133
|
+
);
|
|
134
|
+
if (!objectStr) return { ok: false, reason: 'no-export' };
|
|
135
|
+
try {
|
|
136
|
+
return { ok: true, config: JSON5.parse(objectStr) };
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return { ok: false, reason: 'parse-error', error };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
108
142
|
/**
|
|
109
143
|
* Read a _meta.js file and extract its default export object.
|
|
110
144
|
* Uses the same JSON5 approach as pageConfig extraction — find the object literal
|
|
111
145
|
* after `export default` and parse it.
|
|
112
146
|
*/
|
|
113
|
-
export function readMetaFile(metaPath: string): {
|
|
147
|
+
export function readMetaFile(metaPath: string): {
|
|
148
|
+
title?: string;
|
|
149
|
+
pages?: string[];
|
|
150
|
+
} {
|
|
114
151
|
if (!existsSync(metaPath)) return {};
|
|
115
152
|
|
|
116
|
-
const objectStr = extractDefaultExportObjectLiteral(
|
|
153
|
+
const objectStr = extractDefaultExportObjectLiteral(
|
|
154
|
+
readSourceFileCached(metaPath),
|
|
155
|
+
);
|
|
117
156
|
if (!objectStr) return {};
|
|
118
157
|
|
|
119
158
|
try {
|
|
@@ -128,12 +167,17 @@ export type PageConfigParseResult =
|
|
|
128
167
|
/** No module script, or no `pageConfig =` export. Treat as "no config". */
|
|
129
168
|
| { kind: 'none' }
|
|
130
169
|
/** Found and successfully parsed. */
|
|
131
|
-
| {
|
|
170
|
+
| {
|
|
171
|
+
kind: 'ok';
|
|
172
|
+
value: { title?: string; quiz?: QuizConfig; completesOn?: 'view' };
|
|
173
|
+
}
|
|
132
174
|
/** Found but couldn't parse as a static object literal — non-literal RHS or JSON5 failure. */
|
|
133
175
|
| { kind: 'invalid' };
|
|
134
176
|
|
|
135
177
|
/** Source-level pageConfig extraction shared by manifest generation and build-time validation. */
|
|
136
|
-
export function parsePageConfigFromSource(
|
|
178
|
+
export function parsePageConfigFromSource(
|
|
179
|
+
content: string,
|
|
180
|
+
): PageConfigParseResult {
|
|
137
181
|
const moduleScriptMatch = content.match(MODULE_SCRIPT_RE);
|
|
138
182
|
if (!moduleScriptMatch) return { kind: 'none' };
|
|
139
183
|
|
|
@@ -148,7 +192,10 @@ export function parsePageConfigFromSource(content: string): PageConfigParseResul
|
|
|
148
192
|
// pageConfig assigned to something other than an object literal — flag as invalid.
|
|
149
193
|
if (!afterExport.startsWith('{')) return { kind: 'invalid' };
|
|
150
194
|
|
|
151
|
-
const startIndex = scriptContent.indexOf(
|
|
195
|
+
const startIndex = scriptContent.indexOf(
|
|
196
|
+
'{',
|
|
197
|
+
configMatch.index + configMatch[0].length,
|
|
198
|
+
);
|
|
152
199
|
if (startIndex < 0) return { kind: 'invalid' };
|
|
153
200
|
const objectStr = extractObjectLiteral(scriptContent, startIndex);
|
|
154
201
|
if (!objectStr) return { kind: 'invalid' };
|
|
@@ -161,23 +208,33 @@ export function parsePageConfigFromSource(content: string): PageConfigParseResul
|
|
|
161
208
|
}
|
|
162
209
|
|
|
163
210
|
/** Extract pageConfig from a .svelte file. Throws on parse failure. */
|
|
164
|
-
export function extractPageConfig(filePath: string): {
|
|
211
|
+
export function extractPageConfig(filePath: string): {
|
|
212
|
+
title?: string;
|
|
213
|
+
quiz?: QuizConfig;
|
|
214
|
+
completesOn?: 'view';
|
|
215
|
+
} {
|
|
165
216
|
const result = parsePageConfigFromSource(readSourceFileCached(filePath));
|
|
166
217
|
if (result.kind === 'ok') return result.value;
|
|
167
218
|
if (result.kind === 'invalid') {
|
|
168
219
|
throw new Error(
|
|
169
|
-
`${filePath}: pageConfig must be a static object literal (no variables, function calls, or computed values)
|
|
220
|
+
`${filePath}: pageConfig must be a static object literal (no variables, function calls, or computed values)`,
|
|
170
221
|
);
|
|
171
222
|
}
|
|
172
223
|
return {};
|
|
173
224
|
}
|
|
174
225
|
|
|
175
226
|
/**
|
|
176
|
-
* Extract
|
|
177
|
-
*
|
|
227
|
+
* Extract a balanced `{...}` or `[...]` span starting at the opening bracket,
|
|
228
|
+
* skipping strings and comments. Returns the substring (inclusive) or null if
|
|
229
|
+
* the open char is wrong or no matching close is found. Shared by manifest
|
|
230
|
+
* extraction, _meta/pageConfig parsing, and the validator's tag-prop parser.
|
|
178
231
|
*/
|
|
179
|
-
export function extractObjectLiteral(
|
|
180
|
-
|
|
232
|
+
export function extractObjectLiteral(
|
|
233
|
+
source: string,
|
|
234
|
+
startIndex: number,
|
|
235
|
+
): string | null {
|
|
236
|
+
const open = source[startIndex];
|
|
237
|
+
if (open !== '{' && open !== '[') return null;
|
|
181
238
|
|
|
182
239
|
let depth = 0;
|
|
183
240
|
let inString: string | null = null;
|
|
@@ -222,8 +279,8 @@ export function extractObjectLiteral(source: string, startIndex: number): string
|
|
|
222
279
|
continue;
|
|
223
280
|
}
|
|
224
281
|
|
|
225
|
-
if (char === '{') depth++;
|
|
226
|
-
if (char === '}') {
|
|
282
|
+
if (char === '{' || char === '[') depth++;
|
|
283
|
+
if (char === '}' || char === ']') {
|
|
227
284
|
depth--;
|
|
228
285
|
if (depth === 0) {
|
|
229
286
|
return source.slice(startIndex, i + 1);
|
|
@@ -240,7 +297,7 @@ export function extractObjectLiteral(source: string, startIndex: number): string
|
|
|
240
297
|
function getSortedDirs(dirPath: string): string[] {
|
|
241
298
|
if (!existsSync(dirPath)) return [];
|
|
242
299
|
return readdirSync(dirPath)
|
|
243
|
-
.filter(name => {
|
|
300
|
+
.filter((name) => {
|
|
244
301
|
const full = resolve(dirPath, name);
|
|
245
302
|
return statSync(full).isDirectory() && !name.startsWith('.');
|
|
246
303
|
})
|
|
@@ -253,7 +310,7 @@ function getSortedDirs(dirPath: string): string[] {
|
|
|
253
310
|
function getSvelteFiles(dirPath: string): string[] {
|
|
254
311
|
if (!existsSync(dirPath)) return [];
|
|
255
312
|
return readdirSync(dirPath)
|
|
256
|
-
.filter(name => name.endsWith('.svelte'))
|
|
313
|
+
.filter((name) => name.endsWith('.svelte'))
|
|
257
314
|
.sort();
|
|
258
315
|
}
|
|
259
316
|
|
|
@@ -301,7 +358,11 @@ export function generateManifest(pagesDir: string): Manifest {
|
|
|
301
358
|
const filePath = resolve(lessonPath, fileName);
|
|
302
359
|
const pageSlug = deriveSlug(fileName, true);
|
|
303
360
|
|
|
304
|
-
let pageConfig: {
|
|
361
|
+
let pageConfig: {
|
|
362
|
+
title?: string;
|
|
363
|
+
quiz?: QuizConfig;
|
|
364
|
+
completesOn?: 'view';
|
|
365
|
+
} = {};
|
|
305
366
|
try {
|
|
306
367
|
pageConfig = extractPageConfig(filePath);
|
|
307
368
|
} catch (e) {
|
|
@@ -318,7 +379,9 @@ export function generateManifest(pagesDir: string): Manifest {
|
|
|
318
379
|
slug: pageSlug,
|
|
319
380
|
importPath: relativePath,
|
|
320
381
|
quiz: pageConfig.quiz || null,
|
|
321
|
-
...(pageConfig.completesOn === 'view'
|
|
382
|
+
...(pageConfig.completesOn === 'view'
|
|
383
|
+
? { completesOn: 'view' as const }
|
|
384
|
+
: {}),
|
|
322
385
|
};
|
|
323
386
|
|
|
324
387
|
lesson.pages.push(page);
|
|
@@ -342,17 +405,20 @@ export function generateManifest(pagesDir: string): Manifest {
|
|
|
342
405
|
/**
|
|
343
406
|
* Order .svelte files: listed in `pages` array first (in order), then unlisted appended alphabetically.
|
|
344
407
|
*/
|
|
345
|
-
export function orderPageFiles(
|
|
408
|
+
export function orderPageFiles(
|
|
409
|
+
allFiles: string[],
|
|
410
|
+
pagesArray?: string[],
|
|
411
|
+
): string[] {
|
|
346
412
|
if (!pagesArray || pagesArray.length === 0) {
|
|
347
413
|
return allFiles;
|
|
348
414
|
}
|
|
349
415
|
|
|
350
416
|
const listed = pagesArray.map(ensureSvelteSuffix);
|
|
351
417
|
const listedSet = new Set(listed);
|
|
352
|
-
const unlisted = allFiles.filter(f => !listedSet.has(f)).sort();
|
|
418
|
+
const unlisted = allFiles.filter((f) => !listedSet.has(f)).sort();
|
|
353
419
|
|
|
354
420
|
// Only include listed files that actually exist
|
|
355
|
-
const validListed = listed.filter(f => allFiles.includes(f));
|
|
421
|
+
const validListed = listed.filter((f) => allFiles.includes(f));
|
|
356
422
|
|
|
357
423
|
return [...validListed, ...unlisted];
|
|
358
424
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';
|
|
2
|
+
import { normalizePath } from 'vite';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export interface OverridePluginOptions {
|
|
7
|
+
name: string;
|
|
8
|
+
virtualId: string;
|
|
9
|
+
projectFile: string;
|
|
10
|
+
/** Built-in re-exported when the project file is absent; null export otherwise. */
|
|
11
|
+
builtinFile?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A virtual module that resolves to a project-root override file when present,
|
|
16
|
+
* and to the built-in (or a null export) otherwise. Shared by the layout and
|
|
17
|
+
* quiz plugins — they differ only in the virtual id, file name, and built-in.
|
|
18
|
+
*/
|
|
19
|
+
export function createOverridePlugin({
|
|
20
|
+
name,
|
|
21
|
+
virtualId,
|
|
22
|
+
projectFile,
|
|
23
|
+
builtinFile,
|
|
24
|
+
}: OverridePluginOptions): Plugin {
|
|
25
|
+
const resolvedId = '\0' + virtualId;
|
|
26
|
+
const fallback = builtinFile
|
|
27
|
+
? `export { default } from '${normalizePath(builtinFile)}';`
|
|
28
|
+
: 'export default null;';
|
|
29
|
+
let filePath: string;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
name,
|
|
33
|
+
enforce: 'pre',
|
|
34
|
+
|
|
35
|
+
configResolved(config: ResolvedConfig) {
|
|
36
|
+
filePath = resolve(config.root, projectFile);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
resolveId(id) {
|
|
40
|
+
if (id === virtualId) return resolvedId;
|
|
41
|
+
return null;
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
load(id) {
|
|
45
|
+
if (id !== resolvedId) return null;
|
|
46
|
+
if (existsSync(filePath)) {
|
|
47
|
+
// Only watch when it exists — addWatchFile on a missing path makes
|
|
48
|
+
// Vite's importAnalysis try to resolve it as a real import.
|
|
49
|
+
this.addWatchFile(filePath);
|
|
50
|
+
return `export { default } from '${normalizePath(filePath)}';`;
|
|
51
|
+
}
|
|
52
|
+
return fallback;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
configureServer(server: ViteDevServer) {
|
|
56
|
+
// Only add/unlink flips load()'s output between the override and the
|
|
57
|
+
// fallback; a `change` leaves it identical and Svelte's own HMR handles
|
|
58
|
+
// the underlying file.
|
|
59
|
+
server.watcher.on('all', (event, changed) => {
|
|
60
|
+
if (changed !== filePath) return;
|
|
61
|
+
if (event !== 'add' && event !== 'unlink') return;
|
|
62
|
+
const mod = server.moduleGraph.getModuleById(resolvedId);
|
|
63
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
64
|
+
server.ws.send({ type: 'full-reload' });
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
package/src/plugin/quiz.ts
CHANGED
|
@@ -1,65 +1,20 @@
|
|
|
1
|
-
import type { Plugin
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
3
2
|
import { resolve, dirname } from 'node:path';
|
|
4
3
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
|
|
6
|
-
const VIRTUAL_QUIZ_ID = 'virtual:tessera-quiz';
|
|
7
|
-
const RESOLVED_QUIZ_ID = '\0' + VIRTUAL_QUIZ_ID;
|
|
4
|
+
import { createOverridePlugin } from './override-plugin.js';
|
|
8
5
|
|
|
9
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
7
|
const __dirname = dirname(__filename);
|
|
11
8
|
|
|
12
|
-
/**
|
|
13
|
-
* Resolve the project's quiz shell.
|
|
14
|
-
* `projectRoot/quiz.svelte` overrides the built-in `<Quiz>` if it exists,
|
|
15
|
-
* otherwise the built-in is used. Mirrors `tesseraLayoutPlugin` (Phase 3A).
|
|
16
|
-
*/
|
|
17
9
|
export function tesseraQuizPlugin(): Plugin {
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
// `dist/plugin/quiz.js` after build and `src/plugin/quiz.ts` in source —
|
|
21
|
-
// both layouts put `Quiz.svelte` two levels up under `src/components/`.
|
|
10
|
+
// The plugin lives in `dist/plugin/quiz.js` after build and `src/plugin/quiz.ts`
|
|
11
|
+
// in source — both put `Quiz.svelte` two levels up under `src/components/`.
|
|
22
12
|
const packageRoot = resolve(__dirname, '..', '..');
|
|
23
13
|
const builtinQuiz = resolve(packageRoot, 'src', 'components', 'Quiz.svelte');
|
|
24
|
-
|
|
25
|
-
return {
|
|
14
|
+
return createOverridePlugin({
|
|
26
15
|
name: 'tessera:quiz',
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
resolveId(id) {
|
|
34
|
-
if (id === VIRTUAL_QUIZ_ID) return RESOLVED_QUIZ_ID;
|
|
35
|
-
return null;
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
load(id) {
|
|
39
|
-
if (id !== RESOLVED_QUIZ_ID) return null;
|
|
40
|
-
const userQuizPath = resolve(projectRoot, 'quiz.svelte');
|
|
41
|
-
if (existsSync(userQuizPath)) {
|
|
42
|
-
// Watch the user file so add/remove flips through HMR (see below).
|
|
43
|
-
this.addWatchFile(userQuizPath);
|
|
44
|
-
const normalized = userQuizPath.replace(/\\/g, '/');
|
|
45
|
-
return `export { default } from '${normalized}';`;
|
|
46
|
-
}
|
|
47
|
-
const normalized = builtinQuiz.replace(/\\/g, '/');
|
|
48
|
-
return `export { default } from '${normalized}';`;
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
configureServer(server: ViteDevServer) {
|
|
52
|
-
const userQuizPath = resolve(projectRoot, 'quiz.svelte');
|
|
53
|
-
// Only react to add/unlink — those flip the load() output between the
|
|
54
|
-
// user quiz and the built-in. A `change` event leaves the resolved
|
|
55
|
-
// module identical and is handled by Svelte's own HMR.
|
|
56
|
-
server.watcher.on('all', (event, filePath) => {
|
|
57
|
-
if (filePath !== userQuizPath) return;
|
|
58
|
-
if (event !== 'add' && event !== 'unlink') return;
|
|
59
|
-
const mod = server.moduleGraph.getModuleById(RESOLVED_QUIZ_ID);
|
|
60
|
-
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
61
|
-
server.ws.send({ type: 'full-reload' });
|
|
62
|
-
});
|
|
63
|
-
},
|
|
64
|
-
};
|
|
16
|
+
virtualId: 'virtual:tessera-quiz',
|
|
17
|
+
projectFile: 'quiz.svelte',
|
|
18
|
+
builtinFile: builtinQuiz,
|
|
19
|
+
});
|
|
65
20
|
}
|