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
@@ -0,0 +1,276 @@
1
+ import { Parser } from 'acorn';
2
+ import { tsPlugin } from '@sveltejs/acorn-typescript';
3
+ import { parse } from 'svelte/compiler';
4
+
5
+ /**
6
+ * Shared parsing layer for the build-time validator and manifest generator.
7
+ *
8
+ * `.svelte` files go through `svelte/compiler`'s `parse`; plain JS files
9
+ * (`course.config.js`, `_meta.js`) and the module-script fallback go through
10
+ * acorn (with `acorn-typescript` for `as const` / `satisfies T`). Static
11
+ * *values* are still recovered with JSON5 by the callers — only structure
12
+ * parsing lives here.
13
+ */
14
+
15
+ export type PropValue =
16
+ | { kind: 'string'; value: string }
17
+ | { kind: 'expr'; raw: string }
18
+ | { kind: 'bool' };
19
+
20
+ export interface ComponentMatch {
21
+ name: string;
22
+ props: Map<string, PropValue>;
23
+ hasSpread: boolean;
24
+ }
25
+
26
+ export type NamedObjectLiteral =
27
+ | { kind: 'none' }
28
+ | { kind: 'invalid' }
29
+ | { kind: 'literal'; text: string };
30
+
31
+ interface Node {
32
+ type: string;
33
+ start: number;
34
+ end: number;
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ interface CacheEntry {
39
+ root: Node | null;
40
+ error: string | null;
41
+ }
42
+
43
+ const rootCache = new Map<string, CacheEntry>();
44
+
45
+ /** Drop every cached root. Call at the start of a run to scope the cache. */
46
+ export function clearParseCache(): void {
47
+ rootCache.clear();
48
+ }
49
+
50
+ function parseRoot(source: string): CacheEntry {
51
+ const cached = rootCache.get(source);
52
+ if (cached !== undefined) return cached;
53
+ let entry: CacheEntry;
54
+ try {
55
+ entry = {
56
+ root: parse(source, { modern: true }) as unknown as Node,
57
+ error: null,
58
+ };
59
+ } catch (error) {
60
+ const message = error instanceof Error ? error.message : String(error);
61
+ const firstLine = message.split('\n')[0].trim();
62
+ entry = { root: null, error: firstLine || 'parse error' };
63
+ }
64
+ rootCache.set(source, entry);
65
+ return entry;
66
+ }
67
+
68
+ function collectComponents(root: Node, names: ReadonlySet<string>): Node[] {
69
+ const found: Node[] = [];
70
+ const seen = new Set<object>();
71
+ const walk = (value: unknown): void => {
72
+ if (!value || typeof value !== 'object') return;
73
+ if (seen.has(value)) return;
74
+ seen.add(value);
75
+ if (Array.isArray(value)) {
76
+ for (const item of value) walk(item);
77
+ return;
78
+ }
79
+ const node = value as Node;
80
+ if (node.type === 'Component' && names.has(node.name as string)) {
81
+ found.push(node);
82
+ }
83
+ for (const key of Object.keys(node)) {
84
+ if (key === 'type') continue;
85
+ walk(node[key]);
86
+ }
87
+ };
88
+ walk(root);
89
+ return found.sort((a, b) => a.start - b.start);
90
+ }
91
+
92
+ function readProps(source: string, node: Node): ComponentMatch {
93
+ const props = new Map<string, PropValue>();
94
+ let hasSpread = false;
95
+ const attributes = (node.attributes as Node[]) ?? [];
96
+ for (const attr of attributes) {
97
+ if (attr.type === 'SpreadAttribute') {
98
+ hasSpread = true;
99
+ continue;
100
+ }
101
+ if (attr.type === 'BindDirective') {
102
+ const expr = (attr as { expression?: Node }).expression;
103
+ if (expr) {
104
+ props.set(attr.name as string, {
105
+ kind: 'expr',
106
+ raw: source.slice(expr.start, expr.end).trim(),
107
+ });
108
+ }
109
+ continue;
110
+ }
111
+ if (attr.type !== 'Attribute') continue;
112
+ const name = attr.name as string;
113
+ const value = attr.value;
114
+ if (value === true) {
115
+ props.set(name, { kind: 'bool' });
116
+ } else if (Array.isArray(value)) {
117
+ if (value.length === 0) {
118
+ props.set(name, { kind: 'string', value: '' });
119
+ } else {
120
+ const first = value[0] as Node;
121
+ const last = value[value.length - 1] as Node;
122
+ props.set(name, {
123
+ kind: 'string',
124
+ value: source.slice(first.start, last.end),
125
+ });
126
+ }
127
+ } else if (
128
+ value &&
129
+ typeof value === 'object' &&
130
+ (value as Node).type === 'ExpressionTag'
131
+ ) {
132
+ const expr = (value as { expression: Node }).expression;
133
+ props.set(name, {
134
+ kind: 'expr',
135
+ raw: source.slice(expr.start, expr.end).trim(),
136
+ });
137
+ if (source[attr.start] === '{') hasSpread = true;
138
+ }
139
+ }
140
+ return { name: node.name as string, props, hasSpread };
141
+ }
142
+
143
+ /**
144
+ * Return a one-line message if `source` is not valid Svelte, else null. Lets
145
+ * the validator surface a real syntax error itself rather than only failing
146
+ * later in the compiler (and the compile-less CLI would otherwise miss it).
147
+ */
148
+ export function getParseError(source: string): string | null {
149
+ return parseRoot(source).error;
150
+ }
151
+
152
+ /**
153
+ * Find every question/media component in a `.svelte` source, anywhere in the
154
+ * markup, with its props. Returns null if the source can't be parsed — callers
155
+ * then skip component validation, matching the old "skip when unsure" stance.
156
+ */
157
+ export function findComponents(
158
+ source: string,
159
+ names: ReadonlySet<string>,
160
+ ): ComponentMatch[] | null {
161
+ const { root } = parseRoot(source);
162
+ if (!root) return null;
163
+ return collectComponents(root, names).map((node) => readProps(source, node));
164
+ }
165
+
166
+ const TsParser = Parser.extend(
167
+ tsPlugin() as unknown as Parameters<typeof Parser.extend>[0],
168
+ );
169
+
170
+ function parseJsModule(source: string): Node | null {
171
+ try {
172
+ return TsParser.parse(source, {
173
+ ecmaVersion: 'latest',
174
+ sourceType: 'module',
175
+ }) as unknown as Node;
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ function unwrapTsCast(node: Node | null): Node | null {
182
+ let current = node;
183
+ while (
184
+ current &&
185
+ (current.type === 'TSAsExpression' ||
186
+ current.type === 'TSSatisfiesExpression' ||
187
+ current.type === 'TSTypeAssertion' ||
188
+ current.type === 'TSNonNullExpression')
189
+ ) {
190
+ current = (current as { expression?: Node }).expression ?? null;
191
+ }
192
+ return current;
193
+ }
194
+
195
+ function findPageConfigInProgram(
196
+ program: Node,
197
+ source: string,
198
+ ): NamedObjectLiteral {
199
+ const body = (program.body as Node[]) ?? [];
200
+ for (const node of body) {
201
+ if (node.type !== 'ExportNamedDeclaration') continue;
202
+ const declaration = node.declaration as Node | null;
203
+ if (!declaration || declaration.type !== 'VariableDeclaration') continue;
204
+ for (const decl of declaration.declarations as Node[]) {
205
+ const id = decl.id as Node;
206
+ if (id.type !== 'Identifier' || id.name !== 'pageConfig') continue;
207
+ const init = unwrapTsCast(decl.init as Node | null);
208
+ if (init && init.type === 'ObjectExpression') {
209
+ return { kind: 'literal', text: source.slice(init.start, init.end) };
210
+ }
211
+ return { kind: 'invalid' };
212
+ }
213
+ }
214
+ return { kind: 'none' };
215
+ }
216
+
217
+ /**
218
+ * Locate the `export default { ... }` object literal in a plain JS source.
219
+ * Returns a discriminated result so callers can tell parse failure from a
220
+ * missing or non-literal default export.
221
+ */
222
+ export function defaultExportObjectLiteral(
223
+ jsSource: string,
224
+ ): NamedObjectLiteral | { kind: 'parse-error' } {
225
+ const program = parseJsModule(jsSource);
226
+ if (!program) return { kind: 'parse-error' };
227
+ for (const node of (program.body as Node[]) ?? []) {
228
+ if (node.type !== 'ExportDefaultDeclaration') continue;
229
+ const decl = unwrapTsCast(
230
+ (node as { declaration?: Node }).declaration ?? null,
231
+ );
232
+ if (decl && decl.type === 'ObjectExpression') {
233
+ return { kind: 'literal', text: jsSource.slice(decl.start, decl.end) };
234
+ }
235
+ return { kind: 'invalid' };
236
+ }
237
+ return { kind: 'none' };
238
+ }
239
+
240
+ const MODULE_SCRIPT_OPEN_RE =
241
+ /<script\s+(?:context\s*=\s*["']module["']|module)[^>]*>/;
242
+ const SCRIPT_CLOSE = '</script>';
243
+
244
+ function pageConfigFromModuleScriptFallback(
245
+ svelteSource: string,
246
+ ): NamedObjectLiteral {
247
+ const open = svelteSource.match(MODULE_SCRIPT_OPEN_RE);
248
+ if (!open || open.index === undefined) return { kind: 'none' };
249
+ const bodyStart = open.index + open[0].length;
250
+ // Try every `</script>` candidate from earliest; the first one whose body
251
+ // parses as JS is the real close (an earlier hit is inside a string literal).
252
+ let from = bodyStart;
253
+ while (true) {
254
+ const closeIdx = svelteSource.indexOf(SCRIPT_CLOSE, from);
255
+ if (closeIdx < 0) return { kind: 'none' };
256
+ const body = svelteSource.slice(bodyStart, closeIdx);
257
+ const program = parseJsModule(body);
258
+ if (program) return findPageConfigInProgram(program, body);
259
+ from = closeIdx + SCRIPT_CLOSE.length;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Locate `export const pageConfig = { ... }` in a Svelte page's module script
265
+ * and return the object-literal text. Walks the page-level AST so TypeScript
266
+ * (`lang="ts"`) module scripts are handled by Svelte's own parser.
267
+ */
268
+ export function pageConfigLiteral(svelteSource: string): NamedObjectLiteral {
269
+ const { root } = parseRoot(svelteSource);
270
+ if (root) {
271
+ const program = (root.module as { content?: Node } | null)?.content;
272
+ if (!program) return { kind: 'none' };
273
+ return findPageConfigInProgram(program, svelteSource);
274
+ }
275
+ return pageConfigFromModuleScriptFallback(svelteSource);
276
+ }
@@ -0,0 +1,25 @@
1
+ import { resolveTesseraConfig } from './inline-config.js';
2
+
3
+ export async function runDev(projectRoot: string): Promise<number> {
4
+ const vite = await import('vite');
5
+ const config = await resolveTesseraConfig(projectRoot, {
6
+ command: 'serve',
7
+ mode: 'development',
8
+ });
9
+ const server = await vite.createServer(config);
10
+ await server.listen();
11
+ server.printUrls();
12
+ server.bindCLIShortcuts({ print: true });
13
+ // Never resolve: the CLI wrapper would process.exit and kill the server.
14
+ return new Promise<number>(() => {});
15
+ }
16
+
17
+ export async function runBuild(projectRoot: string): Promise<number> {
18
+ const vite = await import('vite');
19
+ const config = await resolveTesseraConfig(projectRoot, {
20
+ command: 'build',
21
+ mode: 'production',
22
+ });
23
+ await vite.build(config);
24
+ return 0;
25
+ }
package/src/plugin/cli.ts CHANGED
@@ -1,28 +1,60 @@
1
1
  #!/usr/bin/env node
2
- import { validateProject, reportValidationIssues } from './validation.js';
2
+ import { runValidate } from './validate-cli.js';
3
+ import { runA11y } from './a11y-cli.js';
3
4
 
4
- const projectRoot = process.cwd();
5
- const { errors, warnings } = validateProject(projectRoot);
5
+ const USAGE = `Usage: tessera <command> [options]
6
6
 
7
- reportValidationIssues({ errors, warnings });
7
+ Commands:
8
+ dev Start the Vite dev server
9
+ export Build and package the course for its LMS standard
10
+ validate Fast static structure checks
11
+ a11y [options] Runtime accessibility audit (builds + drives Playwright)
12
+ check [options] Run validate, then a11y
8
13
 
9
- if (errors.length > 0) {
10
- const summary =
11
- `Validation failed with ${errors.length} error(s)` +
12
- (warnings.length > 0 ? ` and ${warnings.length} warning(s)` : '') +
13
- '.';
14
- console.error(`\n\x1b[31m${summary}\x1b[0m`);
15
- process.exit(1);
14
+ a11y/check options:
15
+ --threshold <minor|moderate|serious|critical> Failing impact (default: serious)
16
+ --build Force a fresh build first`;
17
+
18
+ export async function main(argv: string[]): Promise<number> {
19
+ const [sub, ...rest] = argv;
20
+ switch (sub) {
21
+ case 'dev': {
22
+ const { runDev } = await import('./build-commands.js');
23
+ return runDev(process.cwd());
24
+ }
25
+ case 'export': {
26
+ const { runBuild } = await import('./build-commands.js');
27
+ return runBuild(process.cwd());
28
+ }
29
+ case 'validate':
30
+ return runValidate(process.cwd());
31
+ case 'a11y':
32
+ case 'check': {
33
+ if (rest.includes('--help') || rest.includes('-h')) {
34
+ console.log(USAGE);
35
+ return 0;
36
+ }
37
+ if (sub === 'check') {
38
+ const validateCode = runValidate(process.cwd());
39
+ if (validateCode !== 0) return validateCode;
40
+ }
41
+ return runA11y(rest);
42
+ }
43
+ case '--help':
44
+ case '-h':
45
+ console.log(USAGE);
46
+ return 0;
47
+ case undefined:
48
+ console.error(`No command given.\n\n${USAGE}`);
49
+ return 1;
50
+ default:
51
+ console.error(`Unknown command: ${sub}\n\n${USAGE}`);
52
+ return 1;
53
+ }
16
54
  }
17
55
 
18
- if (warnings.length > 0) {
19
- console.log(
20
- `\n\x1b[33mValidation passed with ${warnings.length} warning(s).\x1b[0m`,
21
- );
22
- } else {
23
- console.log('\x1b[32m[tessera]\x1b[0m Validation passed — no issues found.');
56
+ // import.meta.main is true only when this module is the program entry point,
57
+ // and resolves symlinks itself (pnpm/npm bin shims) — Node >= 24.
58
+ if (import.meta.main) {
59
+ void main(process.argv.slice(2)).then((code) => process.exit(code));
24
60
  }
25
- console.log(
26
- '\x1b[2m[tessera] Static checks only. For a full runtime accessibility audit, run: npm run accessibility-check\x1b[0m',
27
- );
28
- process.exit(0);