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.
- package/AGENTS.md +1744 -0
- package/README.md +2 -2
- package/dist/{validation-B-xTvM9B.js → audit-CzKAXy3Y.js} +591 -268
- package/dist/audit-CzKAXy3Y.js.map +1 -0
- package/dist/build-commands-D101M_qb.js +27 -0
- package/dist/build-commands-D101M_qb.js.map +1 -0
- package/dist/inline-config-DYHT51G8.js +29 -0
- package/dist/inline-config-DYHT51G8.js.map +1 -0
- package/dist/plugin/cli.d.ts +5 -1
- package/dist/plugin/cli.d.ts.map +1 -0
- package/dist/plugin/cli.js +108 -15
- package/dist/plugin/cli.js.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2 -763
- package/dist/plugin-y35ym9A3.js +744 -0
- package/dist/plugin-y35ym9A3.js.map +1 -0
- package/package.json +12 -9
- package/src/components/FillInTheBlank.svelte +2 -2
- package/src/components/Matching.svelte +2 -2
- package/src/components/MultipleChoice.svelte +2 -2
- package/src/components/RevealModal.svelte +48 -103
- package/src/components/Sorting.svelte +2 -2
- package/src/components/util.ts +9 -0
- package/src/plugin/a11y/audit.ts +35 -8
- package/src/plugin/a11y-cli.ts +35 -22
- package/src/plugin/ast.ts +276 -0
- package/src/plugin/build-commands.ts +25 -0
- package/src/plugin/cli.ts +53 -21
- package/src/plugin/index.ts +87 -122
- package/src/plugin/inline-config.ts +43 -0
- package/src/plugin/manifest.ts +103 -136
- package/src/plugin/package-root.ts +24 -0
- package/src/plugin/quiz.ts +8 -9
- package/src/plugin/validate-cli.ts +30 -0
- package/src/plugin/validation.ts +152 -244
- package/src/runtime/App.svelte +11 -97
- package/src/runtime/Sidebar.svelte +3 -1
- package/src/runtime/adapters/cmi5.ts +6 -10
- package/src/runtime/adapters/format.ts +6 -0
- package/src/runtime/adapters/retry.ts +1 -1
- package/src/runtime/adapters/scorm2004.ts +2 -4
- package/src/runtime/branding.ts +90 -0
- package/src/runtime/defaults.ts +3 -0
- package/src/runtime/hooks.svelte.ts +16 -53
- package/src/runtime/interaction-format.ts +3 -8
- package/src/runtime/progress.svelte.ts +47 -83
- package/src/runtime/xapi/derive-actor.ts +41 -48
- package/src/runtime/xapi/publisher.ts +14 -14
- package/src/runtime/xapi/setup.ts +39 -46
- package/dist/audit-BBJpQGqb.js +0 -204
- package/dist/audit-BBJpQGqb.js.map +0 -1
- package/dist/plugin/a11y-cli.d.ts +0 -1
- package/dist/plugin/a11y-cli.js +0 -36
- package/dist/plugin/a11y-cli.js.map +0 -1
- package/dist/plugin/index.js.map +0 -1
- 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 {
|
|
2
|
+
import { runValidate } from './validate-cli.js';
|
|
3
|
+
import { runA11y } from './a11y-cli.js';
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
-
const { errors, warnings } = validateProject(projectRoot);
|
|
5
|
+
const USAGE = `Usage: tessera <command> [options]
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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);
|