tessera-learn 0.0.13 → 0.1.0

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 (56) hide show
  1. package/AGENTS.md +1744 -0
  2. package/README.md +2 -2
  3. package/dist/{validation-B-xTvM9B.js → audit-CzKAXy3Y.js} +591 -268
  4. package/dist/audit-CzKAXy3Y.js.map +1 -0
  5. package/dist/build-commands-D101M_qb.js +27 -0
  6. package/dist/build-commands-D101M_qb.js.map +1 -0
  7. package/dist/inline-config-DYHT51G8.js +29 -0
  8. package/dist/inline-config-DYHT51G8.js.map +1 -0
  9. package/dist/plugin/cli.d.ts +5 -1
  10. package/dist/plugin/cli.d.ts.map +1 -0
  11. package/dist/plugin/cli.js +108 -15
  12. package/dist/plugin/cli.js.map +1 -1
  13. package/dist/plugin/index.d.ts.map +1 -1
  14. package/dist/plugin/index.js +2 -763
  15. package/dist/plugin-y35ym9A3.js +744 -0
  16. package/dist/plugin-y35ym9A3.js.map +1 -0
  17. package/package.json +12 -9
  18. package/src/components/FillInTheBlank.svelte +2 -2
  19. package/src/components/Matching.svelte +2 -2
  20. package/src/components/MultipleChoice.svelte +2 -2
  21. package/src/components/RevealModal.svelte +48 -103
  22. package/src/components/Sorting.svelte +2 -2
  23. package/src/components/util.ts +9 -0
  24. package/src/plugin/a11y/audit.ts +35 -8
  25. package/src/plugin/a11y-cli.ts +35 -22
  26. package/src/plugin/ast.ts +276 -0
  27. package/src/plugin/build-commands.ts +25 -0
  28. package/src/plugin/cli.ts +53 -21
  29. package/src/plugin/index.ts +87 -122
  30. package/src/plugin/inline-config.ts +43 -0
  31. package/src/plugin/manifest.ts +103 -136
  32. package/src/plugin/package-root.ts +24 -0
  33. package/src/plugin/quiz.ts +8 -9
  34. package/src/plugin/validate-cli.ts +30 -0
  35. package/src/plugin/validation.ts +152 -244
  36. package/src/runtime/App.svelte +11 -97
  37. package/src/runtime/Sidebar.svelte +3 -1
  38. package/src/runtime/adapters/cmi5.ts +6 -10
  39. package/src/runtime/adapters/format.ts +6 -0
  40. package/src/runtime/adapters/retry.ts +1 -1
  41. package/src/runtime/adapters/scorm2004.ts +2 -4
  42. package/src/runtime/branding.ts +90 -0
  43. package/src/runtime/defaults.ts +3 -0
  44. package/src/runtime/hooks.svelte.ts +16 -53
  45. package/src/runtime/interaction-format.ts +3 -8
  46. package/src/runtime/progress.svelte.ts +47 -83
  47. package/src/runtime/xapi/derive-actor.ts +41 -48
  48. package/src/runtime/xapi/publisher.ts +14 -14
  49. package/src/runtime/xapi/setup.ts +39 -46
  50. package/dist/audit-BBJpQGqb.js +0 -204
  51. package/dist/audit-BBJpQGqb.js.map +0 -1
  52. package/dist/plugin/a11y-cli.d.ts +0 -1
  53. package/dist/plugin/a11y-cli.js +0 -36
  54. package/dist/plugin/a11y-cli.js.map +0 -1
  55. package/dist/plugin/index.js.map +0 -1
  56. package/dist/validation-B-xTvM9B.js.map +0 -1
@@ -1,7 +1,6 @@
1
1
  import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';
2
2
  import { svelte } from '@sveltejs/vite-plugin-svelte';
3
- import { fileURLToPath } from 'node:url';
4
- import { dirname, resolve, relative, isAbsolute } from 'node:path';
3
+ import { resolve, relative, isAbsolute } from 'node:path';
5
4
  import {
6
5
  existsSync,
7
6
  readdirSync,
@@ -14,6 +13,10 @@ import {
14
13
  import { generateManifest, readCourseConfig } from './manifest.js';
15
14
  import type { Manifest } from './manifest.js';
16
15
  import type { CourseConfig } from '../runtime/types.js';
16
+ import {
17
+ DEFAULT_PASSING_SCORE,
18
+ DEFAULT_PERCENTAGE_THRESHOLD,
19
+ } from '../runtime/defaults.js';
17
20
  import {
18
21
  validateProject,
19
22
  reportValidationIssues,
@@ -25,6 +28,7 @@ import {
25
28
  import { runExport } from './export.js';
26
29
  import { tesseraLayoutPlugin } from './layout.js';
27
30
  import { tesseraQuizPlugin } from './quiz.js';
31
+ import { resolvePackageRoot } from './package-root.js';
28
32
 
29
33
  import { AUDIT_ENV_FLAG } from './a11y/audit.js';
30
34
 
@@ -35,19 +39,14 @@ function isAuditBuild(): boolean {
35
39
  return process.env[AUDIT_ENV_FLAG] === '1';
36
40
  }
37
41
 
38
- const __filename = fileURLToPath(import.meta.url);
39
- const __dirname = dirname(__filename);
40
-
41
42
  // Resolve the runtime directory where App.svelte lives
42
43
  function resolveRuntimeDir(): string {
43
- const packageRoot = resolve(__dirname, '..', '..');
44
- return resolve(packageRoot, 'src', 'runtime');
44
+ return resolve(resolvePackageRoot(), 'src', 'runtime');
45
45
  }
46
46
 
47
47
  // Resolve the framework styles directory
48
48
  function resolveStylesDir(): string {
49
- const packageRoot = resolve(__dirname, '..', '..');
50
- return resolve(packageRoot, 'styles');
49
+ return resolve(resolvePackageRoot(), 'styles');
51
50
  }
52
51
 
53
52
  // Tier-1a state shared between the svelte() onwarn handler and the sibling
@@ -84,6 +83,37 @@ function projectFileRel(
84
83
  return rel;
85
84
  }
86
85
 
86
+ type VirtualLoadCtx = { projectRoot: string; isBuild: boolean };
87
+
88
+ function virtualModule(
89
+ name: string,
90
+ virtualId: string,
91
+ load: (
92
+ this: import('vite').Rollup.PluginContext,
93
+ ctx: VirtualLoadCtx,
94
+ ) => string | null,
95
+ ): Plugin {
96
+ const resolvedId = '\0' + virtualId;
97
+ let projectRoot = '';
98
+ let isBuild = false;
99
+ return {
100
+ name,
101
+ enforce: 'pre',
102
+ configResolved(config: ResolvedConfig) {
103
+ projectRoot = config.root;
104
+ isBuild = config.command === 'build';
105
+ },
106
+ resolveId(id) {
107
+ return id === virtualId ? resolvedId : null;
108
+ },
109
+ load(id) {
110
+ return id === resolvedId
111
+ ? load.call(this, { projectRoot, isBuild })
112
+ : null;
113
+ },
114
+ };
115
+ }
116
+
87
117
  export function tesseraPlugin() {
88
118
  const manifestRef: { current: Manifest | null; root: string } = {
89
119
  current: null,
@@ -117,6 +147,7 @@ export function tesseraPlugin() {
117
147
  tesseraA11yCompilerPlugin(a11y),
118
148
  tesseraValidationPlugin(),
119
149
  tesseraEntryPlugin(),
150
+ tesseraConfigDefaultsPlugin(),
120
151
  tesseraConfigPlugin(),
121
152
  tesseraPagesPlugin(),
122
153
  tesseraManifestPlugin(manifestRef),
@@ -287,7 +318,6 @@ mount(App, {
287
318
  // ---------- Config Plugin ----------
288
319
 
289
320
  const VIRTUAL_CONFIG_ID = 'virtual:tessera-config';
290
- const RESOLVED_CONFIG_ID = '\0' + VIRTUAL_CONFIG_ID;
291
321
 
292
322
  function completionDefaults(mode: string | undefined): {
293
323
  completion: Record<string, unknown>;
@@ -297,72 +327,55 @@ function completionDefaults(mode: string | undefined): {
297
327
  return { completion: { mode: 'manual' }, passingScore: 0 };
298
328
  }
299
329
  return {
300
- completion: { mode: 'percentage', percentageThreshold: 100 },
301
- passingScore: 70,
330
+ completion: {
331
+ mode: 'percentage',
332
+ percentageThreshold: DEFAULT_PERCENTAGE_THRESHOLD,
333
+ },
334
+ passingScore: DEFAULT_PASSING_SCORE,
302
335
  };
303
336
  }
304
337
 
305
- function tesseraConfigPlugin(): Plugin {
306
- let projectRoot: string;
307
-
338
+ function tesseraConfigDefaultsPlugin(): Plugin {
308
339
  return {
309
- name: 'tessera:config',
340
+ name: 'tessera:config-defaults',
310
341
  enforce: 'pre',
311
-
312
342
  config(config) {
313
343
  const root = config.root || process.cwd();
314
-
315
344
  return {
316
345
  base: './',
317
- build: {
318
- assetsDir: 'tessera',
319
- },
320
- resolve: {
321
- alias: {
322
- $assets: resolve(root, 'assets'),
323
- },
324
- },
346
+ build: { assetsDir: 'tessera' },
347
+ resolve: { alias: { $assets: resolve(root, 'assets') } },
325
348
  // tessera-learn ships .ts/.svelte.ts source; Vite's dep optimizer
326
349
  // doesn't run vite-plugin-svelte's preprocessor, so skip pre-bundling.
327
- optimizeDeps: {
328
- exclude: ['tessera-learn'],
329
- },
350
+ optimizeDeps: { exclude: ['tessera-learn'] },
330
351
  };
331
352
  },
353
+ };
354
+ }
332
355
 
333
- configResolved(config: ResolvedConfig) {
334
- projectRoot = config.root;
335
- },
336
-
337
- resolveId(id) {
338
- if (id === VIRTUAL_CONFIG_ID) return RESOLVED_CONFIG_ID;
339
- return null;
340
- },
341
-
342
- load(id) {
343
- if (id === RESOLVED_CONFIG_ID) {
344
- const configPath = resolve(projectRoot, 'course.config.js');
345
- if (existsSync(configPath)) this.addWatchFile(configPath);
346
- const read = readCourseConfig(projectRoot);
347
- const userConfig: Partial<CourseConfig> = read.ok ? read.config : {};
348
-
349
- const { completion, passingScore } = completionDefaults(
350
- userConfig.completion?.mode,
351
- );
352
- const merged = {
353
- title: userConfig.title || 'Untitled Course',
354
- ...userConfig,
355
- navigation: { mode: 'free', ...userConfig.navigation },
356
- completion: { ...completion, ...userConfig.completion },
357
- scoring: { passingScore, ...userConfig.scoring },
358
- export: { standard: 'web', ...userConfig.export },
359
- };
360
-
361
- return `export default ${JSON.stringify(merged)};`;
362
- }
363
- return null;
356
+ function tesseraConfigPlugin(): Plugin {
357
+ return virtualModule(
358
+ 'tessera:config',
359
+ VIRTUAL_CONFIG_ID,
360
+ function ({ projectRoot }) {
361
+ const configPath = resolve(projectRoot, 'course.config.js');
362
+ if (existsSync(configPath)) this.addWatchFile(configPath);
363
+ const read = readCourseConfig(projectRoot);
364
+ const userConfig: Partial<CourseConfig> = read.ok ? read.config : {};
365
+ const { completion, passingScore } = completionDefaults(
366
+ userConfig.completion?.mode,
367
+ );
368
+ const merged = {
369
+ title: userConfig.title || 'Untitled Course',
370
+ ...userConfig,
371
+ navigation: { mode: 'free', ...userConfig.navigation },
372
+ completion: { ...completion, ...userConfig.completion },
373
+ scoring: { passingScore, ...userConfig.scoring },
374
+ export: { standard: 'web', ...userConfig.export },
375
+ };
376
+ return `export default ${JSON.stringify(merged)};`;
364
377
  },
365
- };
378
+ );
366
379
  }
367
380
 
368
381
  // ---------- Manifest Watch Helpers ----------
@@ -386,7 +399,6 @@ function addWatchFiles(
386
399
  // ---------- Pages Plugin ----------
387
400
 
388
401
  const VIRTUAL_PAGES_ID = 'virtual:tessera-pages';
389
- const RESOLVED_PAGES_ID = '\0' + VIRTUAL_PAGES_ID;
390
402
 
391
403
  /**
392
404
  * Provides a virtual module that exports an import.meta.glob map for all .svelte
@@ -394,22 +406,9 @@ const RESOLVED_PAGES_ID = '\0' + VIRTUAL_PAGES_ID;
394
406
  * pages/ directory, and Vite can statically analyze it for code splitting.
395
407
  */
396
408
  function tesseraPagesPlugin(): Plugin {
397
- return {
398
- name: 'tessera:pages',
399
- enforce: 'pre',
400
-
401
- resolveId(id) {
402
- if (id === VIRTUAL_PAGES_ID) return RESOLVED_PAGES_ID;
403
- return null;
404
- },
405
-
406
- load(id) {
407
- if (id === RESOLVED_PAGES_ID) {
408
- return `export default import.meta.glob('/pages/**/*.svelte');`;
409
- }
410
- return null;
411
- },
412
- };
409
+ return virtualModule('tessera:pages', VIRTUAL_PAGES_ID, () => {
410
+ return `export default import.meta.glob('/pages/**/*.svelte');`;
411
+ });
413
412
  }
414
413
 
415
414
  // ---------- Validation Plugin ----------
@@ -620,29 +619,12 @@ function tesseraManifestPlugin(manifestRef: {
620
619
  }
621
620
 
622
621
  const VIRTUAL_ADAPTER_ID = 'virtual:tessera-adapter';
623
- const RESOLVED_ADAPTER_ID = '\0' + VIRTUAL_ADAPTER_ID;
624
622
 
625
623
  function tesseraAdapterPlugin(): Plugin {
626
- let projectRoot: string;
627
- let isBuild = false;
628
-
629
- return {
630
- name: 'tessera:adapter',
631
- enforce: 'pre',
632
-
633
- configResolved(config: ResolvedConfig) {
634
- projectRoot = config.root;
635
- isBuild = config.command === 'build';
636
- },
637
-
638
- resolveId(id) {
639
- if (id === VIRTUAL_ADAPTER_ID) return RESOLVED_ADAPTER_ID;
640
- return null;
641
- },
642
-
643
- load(id) {
644
- if (id !== RESOLVED_ADAPTER_ID) return null;
645
-
624
+ return virtualModule(
625
+ 'tessera:adapter',
626
+ VIRTUAL_ADAPTER_ID,
627
+ ({ projectRoot, isBuild }) => {
646
628
  // In dev, defer to the runtime selector so its WebAdapter fallback
647
629
  // for unreachable LMS APIs keeps working.
648
630
  if (!isBuild) {
@@ -701,33 +683,16 @@ export function createAdapter(config) {
701
683
  `;
702
684
  }
703
685
  },
704
- };
686
+ );
705
687
  }
706
688
 
707
689
  const VIRTUAL_XAPI_SETUP_ID = 'virtual:tessera-xapi-setup';
708
- const RESOLVED_XAPI_SETUP_ID = '\0' + VIRTUAL_XAPI_SETUP_ID;
709
690
 
710
691
  function tesseraXAPISetupPlugin(): Plugin {
711
- let projectRoot: string;
712
- let isBuild = false;
713
-
714
- return {
715
- name: 'tessera:xapi-setup',
716
- enforce: 'pre',
717
-
718
- configResolved(config: ResolvedConfig) {
719
- projectRoot = config.root;
720
- isBuild = config.command === 'build';
721
- },
722
-
723
- resolveId(id) {
724
- if (id === VIRTUAL_XAPI_SETUP_ID) return RESOLVED_XAPI_SETUP_ID;
725
- return null;
726
- },
727
-
728
- load(id) {
729
- if (id !== RESOLVED_XAPI_SETUP_ID) return null;
730
-
692
+ return virtualModule(
693
+ 'tessera:xapi-setup',
694
+ VIRTUAL_XAPI_SETUP_ID,
695
+ ({ projectRoot, isBuild }) => {
731
696
  if (!isBuild) {
732
697
  return `export { buildXAPIClient } from 'tessera-learn/runtime/xapi/setup.js';`;
733
698
  }
@@ -754,7 +719,7 @@ function tesseraXAPISetupPlugin(): Plugin {
754
719
 
755
720
  return `export async function buildXAPIClient() { return null; }`;
756
721
  },
757
- };
722
+ );
758
723
  }
759
724
 
760
725
  function tesseraFirstPagePreloadPlugin(manifestRef: {
@@ -0,0 +1,43 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import type { ConfigEnv, InlineConfig } from 'vite';
5
+ import { tesseraPlugin } from './index.js';
6
+
7
+ // Base Vite config for every Tessera command (dev, export, a11y build).
8
+ // configFile:false disables Vite's own discovery — there is no vite.config.js —
9
+ // and tesseraPlugin() supplies the Svelte compiler, so this is the full plugin set.
10
+ export function buildInlineConfig(projectRoot: string): InlineConfig {
11
+ return {
12
+ root: projectRoot,
13
+ configFile: false,
14
+ plugins: [tesseraPlugin()],
15
+ };
16
+ }
17
+
18
+ // Optional author-owned escape hatch, never scaffolded or reconciled. A *partial*
19
+ // Vite config the caller merges on top of buildInlineConfig(), so tesseraPlugin()
20
+ // stays wired in and the author only writes the delta.
21
+ export async function loadUserConfig(
22
+ projectRoot: string,
23
+ env: ConfigEnv,
24
+ ): Promise<InlineConfig | null> {
25
+ const configPath = resolve(projectRoot, 'tessera.config.js');
26
+ if (!existsSync(configPath)) return null;
27
+ const mod = await import(pathToFileURL(configPath).href);
28
+ const config = mod.default ?? mod;
29
+ // mergeConfig throws on a function, so resolve Vite's callback form first.
30
+ return (
31
+ typeof config === 'function' ? await config(env) : config
32
+ ) as InlineConfig;
33
+ }
34
+
35
+ export async function resolveTesseraConfig(
36
+ projectRoot: string,
37
+ env: ConfigEnv,
38
+ ): Promise<InlineConfig> {
39
+ const vite = await import('vite');
40
+ const base = buildInlineConfig(projectRoot);
41
+ const user = await loadUserConfig(projectRoot, env);
42
+ return user ? vite.mergeConfig(base, user) : base;
43
+ }