tessera-learn 0.2.3 → 0.4.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 +50 -21
- package/README.md +2 -2
- package/dist/{audit--fSWIOgK.js → audit-DsYqXbqm.js} +282 -197
- package/dist/audit-DsYqXbqm.js.map +1 -0
- package/dist/{build-commands-Qyrlsp3n.js → build-commands-BFuiAxaR.js} +4 -4
- package/dist/build-commands-BFuiAxaR.js.map +1 -0
- package/dist/{inline-config-DqAKsCNl.js → inline-config-DVvOCKht.js} +6 -6
- package/dist/inline-config-DVvOCKht.js.map +1 -0
- package/dist/plugin/cli.d.ts +5 -1
- package/dist/plugin/cli.d.ts.map +1 -1
- package/dist/plugin/cli.js +91 -49
- package/dist/plugin/cli.js.map +1 -1
- package/dist/plugin/index.d.ts +287 -2
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +3 -3
- package/dist/{plugin-B-aiL9-V.js → plugin-BuMiDTmU.js} +145 -111
- package/dist/plugin-BuMiDTmU.js.map +1 -0
- package/package.json +7 -7
- package/src/components/DefaultLayout.svelte +2 -5
- package/src/components/MultipleChoice.svelte +1 -2
- package/src/components/Quiz.svelte +18 -26
- package/src/plugin/ast.ts +9 -2
- package/src/plugin/build-commands.ts +7 -4
- package/src/plugin/cli.ts +96 -46
- package/src/plugin/csp.ts +59 -0
- package/src/plugin/duplicate-cli.ts +37 -1
- package/src/plugin/export.ts +56 -27
- package/src/plugin/index.ts +138 -93
- package/src/plugin/inline-config.ts +4 -2
- package/src/plugin/manifest.ts +24 -23
- package/src/plugin/new-cli.ts +2 -0
- package/src/plugin/validate-cli.ts +5 -2
- package/src/plugin/validation.ts +255 -238
- package/src/runtime/App.svelte +14 -9
- package/src/runtime/Sidebar.svelte +3 -1
- package/src/runtime/adapters/cmi5.ts +59 -402
- package/src/runtime/adapters/discovery.ts +11 -0
- package/src/runtime/adapters/index.ts +27 -60
- package/src/runtime/adapters/lms-error.ts +61 -0
- package/src/runtime/adapters/scorm-base.ts +15 -14
- package/src/runtime/adapters/scorm12.ts +6 -25
- package/src/runtime/adapters/scorm2004.ts +12 -54
- package/src/runtime/adapters/web.ts +11 -4
- package/src/runtime/adapters/xapi-launch-base.ts +346 -0
- package/src/runtime/adapters/xapi.ts +26 -0
- package/src/runtime/fingerprint.ts +28 -0
- package/src/runtime/interaction-format.ts +0 -1
- package/src/runtime/persistence.ts +4 -0
- package/src/runtime/types.ts +22 -1
- package/src/runtime/xapi/publisher.ts +16 -15
- package/src/runtime/xapi/setup.ts +24 -15
- package/src/virtual.d.ts +4 -1
- package/templates/course/course.config.js +1 -0
- package/dist/audit--fSWIOgK.js.map +0 -1
- package/dist/build-commands-Qyrlsp3n.js.map +0 -1
- package/dist/inline-config-DqAKsCNl.js.map +0 -1
- package/dist/plugin-B-aiL9-V.js.map +0 -1
package/dist/plugin/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { c as validateProject, n as runAudit, r as resolvePackageRoot
|
|
2
|
+
import { c as reportValidationIssues, i as VALID_EXPORT_STANDARDS, l as validateProject, n as runAudit, r as resolvePackageRoot } from "../audit-DsYqXbqm.js";
|
|
3
3
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
5
6
|
//#region src/plugin/validate-cli.ts
|
|
6
|
-
function runValidate(projectRoot, { showA11yTip = true } = {}) {
|
|
7
|
-
const { errors, warnings } = validateProject(projectRoot);
|
|
7
|
+
function runValidate(projectRoot, { showA11yTip = true, standardOverride } = {}) {
|
|
8
|
+
const { errors, warnings } = validateProject(projectRoot, standardOverride);
|
|
8
9
|
reportValidationIssues({
|
|
9
10
|
errors,
|
|
10
11
|
warnings
|
|
@@ -207,15 +208,37 @@ function runNew(name, cwd) {
|
|
|
207
208
|
console.error(`[tessera new] Course "${name}" already exists.`);
|
|
208
209
|
return 1;
|
|
209
210
|
}
|
|
210
|
-
copyTemplate(join(resolvePackageRoot(), "templates", "course"), courseDir, {
|
|
211
|
+
copyTemplate(join(resolvePackageRoot(), "templates", "course"), courseDir, {
|
|
212
|
+
PROJECT_TITLE: toTitleCase(name),
|
|
213
|
+
COURSE_ID: `urn:uuid:${randomUUID()}`
|
|
214
|
+
});
|
|
211
215
|
const rel = relative(workspaceRoot, courseDir);
|
|
212
216
|
console.log(`\nCreated ${rel}.\n\nNext steps:\n pnpm tessera dev ${name}\n`);
|
|
213
217
|
return 0;
|
|
214
218
|
}
|
|
215
219
|
//#endregion
|
|
216
220
|
//#region src/plugin/duplicate-cli.ts
|
|
221
|
+
const ID_LINE = /^([ \t]*'?id'?[ \t]*:[ \t]*)('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`|[^\n,}]*)/m;
|
|
222
|
+
const DEFAULT_OBJECT = /export\s+default\s*\{/;
|
|
223
|
+
function reidentifyCourse(courseRoot) {
|
|
224
|
+
const configPath = join(courseRoot, "course.config.js");
|
|
225
|
+
if (!existsSync(configPath)) return;
|
|
226
|
+
const text = readFileSync(configPath, "utf-8");
|
|
227
|
+
const newId = `'urn:uuid:${randomUUID()}'`;
|
|
228
|
+
if (ID_LINE.test(text)) {
|
|
229
|
+
writeFileSync(configPath, text.replace(ID_LINE, `$1${newId}`));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const obj = DEFAULT_OBJECT.exec(text);
|
|
233
|
+
if (obj) {
|
|
234
|
+
const at = obj.index + obj[0].length;
|
|
235
|
+
writeFileSync(configPath, `${text.slice(0, at)}\n id: ${newId},${text.slice(at)}`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
console.warn(`[tessera duplicate] Could not set a unique id in ${configPath} — the copy shares the source's identity. Add a unique "id" manually.`);
|
|
239
|
+
}
|
|
217
240
|
const HELP = "Usage: tessera duplicate <source> <new>\n\nCopy courses/<source>/ to courses/<new>/ within the current workspace.";
|
|
218
|
-
const SKIP = new Set([
|
|
241
|
+
const SKIP = /* @__PURE__ */ new Set([
|
|
219
242
|
"node_modules",
|
|
220
243
|
"dist",
|
|
221
244
|
"a11y-report.json",
|
|
@@ -254,6 +277,7 @@ function runDuplicate(source, target, cwd) {
|
|
|
254
277
|
recursive: true,
|
|
255
278
|
filter: (src) => src === srcDir || !skip(src)
|
|
256
279
|
});
|
|
280
|
+
reidentifyCourse(destDir);
|
|
257
281
|
const rel = relative(workspaceRoot, destDir);
|
|
258
282
|
const srcRel = relative(workspaceRoot, srcDir);
|
|
259
283
|
console.log(`\nCreated ${rel} (duplicated from ${srcRel}).\n\nRemember to update the title in ${rel}/course.config.js.\n\nNext steps:\n pnpm tessera dev ${target}\n`);
|
|
@@ -274,8 +298,25 @@ Commands:
|
|
|
274
298
|
|
|
275
299
|
Run a command from inside a course folder, or name the course explicitly.
|
|
276
300
|
|
|
301
|
+
export/validate options:
|
|
302
|
+
--standard <web|scorm12|scorm2004|cmi5|xapi> Override course.config.js export.standard
|
|
303
|
+
|
|
277
304
|
a11y/check options:
|
|
278
305
|
--threshold <minor|moderate|serious|critical> Failing impact (default: serious)`;
|
|
306
|
+
function parseExportFlags(flags) {
|
|
307
|
+
let standardOverride;
|
|
308
|
+
for (let i = 0; i < flags.length; i++) {
|
|
309
|
+
const arg = flags[i];
|
|
310
|
+
let value;
|
|
311
|
+
if (arg === "--standard") value = flags[++i];
|
|
312
|
+
else if (arg.startsWith("--standard=")) value = arg.slice(11);
|
|
313
|
+
else return { error: `Unknown argument: ${arg}` };
|
|
314
|
+
if (value === void 0 || value.startsWith("-")) return { error: "--standard requires a value" };
|
|
315
|
+
if (!VALID_EXPORT_STANDARDS.includes(value)) return { error: `--standard must be one of ${VALID_EXPORT_STANDARDS.join(", ")}, got "${value}"` };
|
|
316
|
+
standardOverride = value;
|
|
317
|
+
}
|
|
318
|
+
return standardOverride ? { standardOverride } : {};
|
|
319
|
+
}
|
|
279
320
|
function splitCourseArg(rest) {
|
|
280
321
|
if (rest.length > 0 && !rest[0].startsWith("-")) return {
|
|
281
322
|
course: rest[0],
|
|
@@ -286,60 +327,61 @@ function splitCourseArg(rest) {
|
|
|
286
327
|
flags: rest
|
|
287
328
|
};
|
|
288
329
|
}
|
|
330
|
+
const COURSE_COMMANDS = {
|
|
331
|
+
dev: async (courseRoot, workspaceRoot) => (await import("../build-commands-BFuiAxaR.js")).runDev(courseRoot, workspaceRoot),
|
|
332
|
+
export: async (courseRoot, workspaceRoot, flags) => {
|
|
333
|
+
const { standardOverride, error } = parseExportFlags(flags);
|
|
334
|
+
if (error) {
|
|
335
|
+
console.error(`[tessera] ${error}`);
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
return (await import("../build-commands-BFuiAxaR.js")).runBuild(courseRoot, workspaceRoot, standardOverride);
|
|
339
|
+
},
|
|
340
|
+
validate: (courseRoot, _workspaceRoot, flags) => {
|
|
341
|
+
const { standardOverride, error } = parseExportFlags(flags);
|
|
342
|
+
if (error) {
|
|
343
|
+
console.error(`[tessera] ${error}`);
|
|
344
|
+
return 1;
|
|
345
|
+
}
|
|
346
|
+
return runValidate(courseRoot, { standardOverride });
|
|
347
|
+
},
|
|
348
|
+
a11y: (courseRoot, workspaceRoot, flags) => runA11y(courseRoot, workspaceRoot, flags),
|
|
349
|
+
check: (courseRoot, workspaceRoot, flags) => {
|
|
350
|
+
const validateCode = runValidate(courseRoot, { showA11yTip: false });
|
|
351
|
+
if (validateCode !== 0) return validateCode;
|
|
352
|
+
return runA11y(courseRoot, workspaceRoot, flags);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
289
355
|
async function main(argv, cwd = process.cwd()) {
|
|
290
356
|
const [sub, ...rest] = argv;
|
|
291
357
|
if (sub === "new") return runNew(rest[0], cwd);
|
|
292
358
|
if (sub === "duplicate") return runDuplicate(rest[0], rest[1], cwd);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
case "export":
|
|
296
|
-
case "validate":
|
|
297
|
-
case "a11y":
|
|
298
|
-
case "check": {
|
|
299
|
-
if (rest.includes("--help") || rest.includes("-h")) {
|
|
300
|
-
console.log(USAGE);
|
|
301
|
-
return 0;
|
|
302
|
-
}
|
|
303
|
-
const { course, flags } = splitCourseArg(rest);
|
|
304
|
-
const resolved = resolveCourse(cwd, course);
|
|
305
|
-
if (!resolved.ok) {
|
|
306
|
-
console.error(`[tessera] ${resolved.error}`);
|
|
307
|
-
return 1;
|
|
308
|
-
}
|
|
309
|
-
const { courseRoot, workspaceRoot } = resolved;
|
|
310
|
-
switch (sub) {
|
|
311
|
-
case "dev": {
|
|
312
|
-
const { runDev } = await import("../build-commands-Qyrlsp3n.js");
|
|
313
|
-
return runDev(courseRoot, workspaceRoot);
|
|
314
|
-
}
|
|
315
|
-
case "export": {
|
|
316
|
-
const { runBuild } = await import("../build-commands-Qyrlsp3n.js");
|
|
317
|
-
return runBuild(courseRoot, workspaceRoot);
|
|
318
|
-
}
|
|
319
|
-
case "validate": return runValidate(courseRoot);
|
|
320
|
-
case "check": {
|
|
321
|
-
const validateCode = runValidate(courseRoot, { showA11yTip: false });
|
|
322
|
-
if (validateCode !== 0) return validateCode;
|
|
323
|
-
return runA11y(courseRoot, workspaceRoot, flags);
|
|
324
|
-
}
|
|
325
|
-
case "a11y": return runA11y(courseRoot, workspaceRoot, flags);
|
|
326
|
-
}
|
|
327
|
-
return 0;
|
|
328
|
-
}
|
|
329
|
-
case "--help":
|
|
330
|
-
case "-h":
|
|
359
|
+
if (sub !== void 0 && Object.hasOwn(COURSE_COMMANDS, sub)) {
|
|
360
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
331
361
|
console.log(USAGE);
|
|
332
362
|
return 0;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
console.error(`
|
|
363
|
+
}
|
|
364
|
+
const { course, flags } = splitCourseArg(rest);
|
|
365
|
+
const resolved = resolveCourse(cwd, course);
|
|
366
|
+
if (!resolved.ok) {
|
|
367
|
+
console.error(`[tessera] ${resolved.error}`);
|
|
338
368
|
return 1;
|
|
369
|
+
}
|
|
370
|
+
return COURSE_COMMANDS[sub](resolved.courseRoot, resolved.workspaceRoot, flags);
|
|
371
|
+
}
|
|
372
|
+
if (sub === "--help" || sub === "-h") {
|
|
373
|
+
console.log(USAGE);
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
if (sub === void 0) {
|
|
377
|
+
console.error(`No command given.\n\n${USAGE}`);
|
|
378
|
+
return 1;
|
|
339
379
|
}
|
|
380
|
+
console.error(`Unknown command: ${sub}\n\n${USAGE}`);
|
|
381
|
+
return 1;
|
|
340
382
|
}
|
|
341
383
|
if (import.meta.main) main(process.argv.slice(2)).then((code) => process.exit(code));
|
|
342
384
|
//#endregion
|
|
343
|
-
export { main, splitCourseArg };
|
|
385
|
+
export { main, parseExportFlags, splitCourseArg };
|
|
344
386
|
|
|
345
387
|
//# sourceMappingURL=cli.js.map
|
package/dist/plugin/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../../src/plugin/validate-cli.ts","../../src/plugin/a11y-cli.ts","../../src/plugin/project-name.ts","../../src/plugin/course-root.ts","../../src/plugin/template-copy.ts","../../src/plugin/new-cli.ts","../../src/plugin/duplicate-cli.ts","../../src/plugin/cli.ts"],"sourcesContent":["import { basename } from 'node:path';\nimport { validateProject, reportValidationIssues } from './validation.js';\n\nexport function runValidate(\n projectRoot: string,\n { showA11yTip = true }: { showA11yTip?: boolean } = {},\n): number {\n const { errors, warnings } = validateProject(projectRoot);\n\n reportValidationIssues({ errors, warnings });\n\n if (errors.length > 0) {\n const summary =\n `Validation failed with ${errors.length} error(s)` +\n (warnings.length > 0 ? ` and ${warnings.length} warning(s)` : '') +\n '.';\n console.error(`\\n\\x1b[31m${summary}\\x1b[0m`);\n return 1;\n }\n\n if (warnings.length > 0) {\n console.log(\n `\\n\\x1b[33mValidation passed with ${warnings.length} warning(s).\\x1b[0m`,\n );\n } else {\n console.log(\n '\\x1b[32m[tessera]\\x1b[0m Validation passed — no issues found.',\n );\n }\n if (showA11yTip) {\n console.log(\n `\\x1b[2m[tessera] Static checks only. For a full runtime accessibility audit, run: pnpm a11y ${basename(projectRoot)}\\x1b[0m`,\n );\n }\n return 0;\n}\n","import { runAudit, type AuditOptions, type ImpactLevel } from './a11y/audit.js';\n\nconst VALID_THRESHOLDS: ImpactLevel[] = [\n 'minor',\n 'moderate',\n 'serious',\n 'critical',\n];\n\nexport type ParsedA11yArgs =\n | { ok: true; args: AuditOptions }\n | { ok: false; error: string };\n\nexport function parseA11yArgs(argv: string[]): ParsedA11yArgs {\n let threshold: ImpactLevel | undefined;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === '--threshold') {\n const value = argv[++i] as ImpactLevel;\n if (!VALID_THRESHOLDS.includes(value)) {\n return {\n ok: false,\n error: `--threshold must be one of: ${VALID_THRESHOLDS.join(', ')}`,\n };\n }\n threshold = value;\n } else {\n return { ok: false, error: `Unknown argument: ${arg}` };\n }\n }\n\n const args: AuditOptions = {};\n if (threshold !== undefined) args.threshold = threshold;\n return { ok: true, args };\n}\n\nexport async function runA11y(\n projectRoot: string,\n workspaceRoot: string,\n argv: string[],\n): Promise<number> {\n const parsed = parseA11yArgs(argv);\n if (!parsed.ok) {\n console.error(`[tessera a11y] ${parsed.error}`);\n return 1;\n }\n return runAudit(projectRoot, workspaceRoot, parsed.args);\n}\n","// Pure, dependency-free helpers shared by the `tessera new` subcommand and the\n// `create-tessera` scaffolder. Kept import-free so create-tessera can bundle it\n// at build time without dragging in the rest of the plugin (vite, svelte, …).\n\n// npm package name rules: 1-214 chars, lowercase, must start with [a-z0-9],\n// allowed chars [a-z0-9._-], no leading dot or underscore.\nexport function validateProjectName(\n name: string,\n label = 'Project name',\n): string | null {\n if (!name) return `${label} is required`;\n if (name.length > 214) return `${label} must be 214 characters or fewer`;\n if (name !== name.toLowerCase()) return `${label} must be lowercase`;\n if (!/^[a-z0-9]/.test(name)) {\n return `${label} must start with a letter or digit`;\n }\n if (!/^[a-z0-9._-]+$/.test(name)) {\n return `${label} may only contain lowercase letters, digits, \"-\", \"_\", and \".\"`;\n }\n return null;\n}\n\nexport function toTitleCase(slug: string): string {\n return slug\n .split(/[-_.\\s]+/)\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n","import { existsSync, readdirSync, statSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { validateProjectName } from './project-name.js';\n\nexport interface ResolvedCourse {\n ok: true;\n courseRoot: string;\n workspaceRoot: string;\n}\n\nexport type ResolveResult = ResolvedCourse | { ok: false; error: string };\n\nfunction isDir(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\nfunction isCourse(dir: string): boolean {\n return existsSync(join(dir, 'course.config.js'));\n}\n\n// A workspace is the nearest ancestor holding a courses/ directory. The walk\n// includes the starting dir, so the workspace root resolves to itself.\nexport function findWorkspaceRoot(cwd: string): string | null {\n for (let dir = resolve(cwd); ; dir = dirname(dir)) {\n if (isDir(join(dir, 'courses'))) return dir;\n const parent = dirname(dir);\n if (parent === dir) return null;\n }\n}\n\nfunction scanCourses(workspaceRoot: string): {\n courses: string[];\n malformed: string[];\n} {\n const coursesDir = join(workspaceRoot, 'courses');\n const courses: string[] = [];\n const malformed: string[] = [];\n try {\n for (const e of readdirSync(coursesDir, { withFileTypes: true })) {\n if (!e.isDirectory()) continue;\n const dir = join(coursesDir, e.name);\n if (isCourse(dir)) courses.push(e.name);\n else if (isDir(join(dir, 'pages'))) malformed.push(e.name);\n }\n } catch {\n return { courses, malformed };\n }\n return { courses: courses.sort(), malformed: malformed.sort() };\n}\n\nexport function listCourses(workspaceRoot: string): string[] {\n return scanCourses(workspaceRoot).courses;\n}\n\nexport function listMalformedCourses(workspaceRoot: string): string[] {\n return scanCourses(workspaceRoot).malformed;\n}\n\nconst NOT_A_WORKSPACE =\n 'Not inside a Tessera workspace — no `courses/` directory was found at or above the current directory.';\n\nfunction malformedHint(malformed: string[]): string {\n if (malformed.length === 0) return '';\n return (\n `\\nSkipped (missing course.config.js):\\n` +\n malformed.map((c) => ` courses/${c}`).join('\\n')\n );\n}\n\nfunction listHint(workspaceRoot: string): string {\n const { courses, malformed } = scanCourses(workspaceRoot);\n if (courses.length === 0) {\n return (\n '\\nNo courses found. Create one with `tessera new <name>`.' +\n malformedHint(malformed)\n );\n }\n return (\n `\\nAvailable courses:\\n${courses.map((c) => ` ${c}`).join('\\n')}` +\n '\\nName one (`tessera <command> <course>`) or cd into its folder.' +\n malformedHint(malformed)\n );\n}\n\n// A name argument always wins; otherwise the cwd must itself be a course. There\n// is deliberately no \"single course → use it implicitly\" rule, so a bare command\n// never changes meaning when a second course is added.\nexport function resolveCourse(cwd: string, name?: string): ResolveResult {\n const here = resolve(cwd);\n\n if (name) {\n const nameError = validateProjectName(name, 'the name');\n if (nameError) {\n return {\n ok: false,\n error: `Invalid course name \"${name}\" — ${nameError}.`,\n };\n }\n const workspaceRoot = findWorkspaceRoot(here);\n if (!workspaceRoot) return { ok: false, error: NOT_A_WORKSPACE };\n const courseRoot = join(workspaceRoot, 'courses', name);\n if (!isCourse(courseRoot)) {\n return {\n ok: false,\n error: `Course \"${name}\" not found in courses/.${listHint(workspaceRoot)}`,\n };\n }\n return { ok: true, courseRoot, workspaceRoot };\n }\n\n if (isCourse(here)) {\n const workspaceRoot = findWorkspaceRoot(here) ?? dirname(dirname(here));\n return { ok: true, courseRoot: here, workspaceRoot };\n }\n\n const workspaceRoot = findWorkspaceRoot(here);\n if (!workspaceRoot) return { ok: false, error: NOT_A_WORKSPACE };\n return {\n ok: false,\n error: `No course specified.${listHint(workspaceRoot)}`,\n };\n}\n","import {\n copyFileSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n writeFileSync,\n} from 'node:fs';\nimport { join } from 'node:path';\n\n// npm's tarball packing strips/renames leading-dot files, so templates store\n// them prefixed and we restore the dot on copy. (create-vite convention.)\nconst RENAME: Record<string, string> = {\n _gitignore: '.gitignore',\n _gitkeep: '.gitkeep',\n};\n\n// Text files get token substitution; everything else is copied byte-for-byte.\nconst TEXT = /\\.(svelte|js|ts|json|css|md|html)$/;\n\nfunction applyTokens(s: string, tokens: Record<string, string>): string {\n return s.replace(/__([A-Z_]+)__/g, (m, key) =>\n key in tokens ? tokens[key] : m,\n );\n}\n\nexport function copyTemplate(\n srcDir: string,\n destDir: string,\n tokens: Record<string, string>,\n): void {\n mkdirSync(destDir, { recursive: true });\n for (const entry of readdirSync(srcDir, { withFileTypes: true })) {\n const src = join(srcDir, entry.name);\n const dest = join(destDir, RENAME[entry.name] ?? entry.name);\n if (entry.isDirectory()) {\n copyTemplate(src, dest, tokens);\n } else if (TEXT.test(entry.name)) {\n writeFileSync(dest, applyTokens(readFileSync(src, 'utf-8'), tokens));\n } else {\n copyFileSync(src, dest);\n }\n }\n}\n","import { existsSync } from 'node:fs';\nimport { join, relative, resolve } from 'node:path';\nimport { findWorkspaceRoot } from './course-root.js';\nimport { validateProjectName, toTitleCase } from './project-name.js';\nimport { copyTemplate } from './template-copy.js';\nimport { resolvePackageRoot } from './package-root.js';\n\n// `tessera new <name>` — stamp a new course into courses/<name> inside the\n// current workspace. No install step: the workspace already owns the deps.\nexport function runNew(name: string | undefined, cwd: string): number {\n if (name === '--help' || name === '-h') {\n console.log(\n 'Usage: tessera new <name>\\n\\n' +\n 'Scaffold a new course into courses/<name> inside the current workspace.',\n );\n return 0;\n }\n if (!name) {\n console.error('Usage: tessera new <name>');\n return 1;\n }\n\n const nameError = validateProjectName(name, 'Course name');\n if (nameError) {\n console.error(`[tessera new] ${nameError}`);\n return 1;\n }\n\n const workspaceRoot = findWorkspaceRoot(resolve(cwd));\n if (!workspaceRoot) {\n console.error(\n '[tessera new] Not inside a Tessera workspace — run this from a workspace (a directory with a `courses/` folder).',\n );\n return 1;\n }\n\n const courseDir = join(workspaceRoot, 'courses', name);\n if (existsSync(courseDir)) {\n console.error(`[tessera new] Course \"${name}\" already exists.`);\n return 1;\n }\n\n const templateDir = join(resolvePackageRoot(), 'templates', 'course');\n copyTemplate(templateDir, courseDir, {\n PROJECT_TITLE: toTitleCase(name),\n });\n\n const rel = relative(workspaceRoot, courseDir);\n console.log(`\\nCreated ${rel}.\\n\\nNext steps:\\n pnpm tessera dev ${name}\\n`);\n return 0;\n}\n","import { cpSync, existsSync } from 'node:fs';\nimport { basename, join, relative } from 'node:path';\nimport { resolveCourse } from './course-root.js';\nimport { validateProjectName } from './project-name.js';\n\nconst HELP =\n 'Usage: tessera duplicate <source> <new>\\n\\n' +\n 'Copy courses/<source>/ to courses/<new>/ within the current workspace.';\n\n// Generated/build artifacts that should never travel with a verbatim copy. The\n// a11y throwaway build and Vite's cache live under node_modules, so they're\n// already pruned by the node_modules skip; the rest are belt-and-suspenders.\nconst SKIP = new Set(['node_modules', 'dist', 'a11y-report.json', '.vite']);\n\nfunction skip(srcPath: string): boolean {\n const name = basename(srcPath);\n return SKIP.has(name) || name.startsWith('.tessera');\n}\n\n// `tessera duplicate <source> <new>` — copy an existing course verbatim within\n// the current workspace. Unlike `new`, there is no template stamping: the JS\n// config (including its title) is copied untouched.\nexport function runDuplicate(\n source: string | undefined,\n target: string | undefined,\n cwd: string,\n): number {\n if (\n source === '--help' ||\n source === '-h' ||\n target === '--help' ||\n target === '-h'\n ) {\n console.log(HELP);\n return 0;\n }\n if (!source || !target) {\n console.error('Usage: tessera duplicate <source> <new>');\n return 1;\n }\n\n const nameError = validateProjectName(target, 'Course name');\n if (nameError) {\n console.error(`[tessera duplicate] ${nameError}`);\n return 1;\n }\n\n const resolved = resolveCourse(cwd, source);\n if (!resolved.ok) {\n console.error(`[tessera duplicate] ${resolved.error}`);\n return 1;\n }\n const { courseRoot: srcDir, workspaceRoot } = resolved;\n\n const destDir = join(workspaceRoot, 'courses', target);\n if (existsSync(destDir)) {\n console.error(`[tessera duplicate] Course \"${target}\" already exists.`);\n return 1;\n }\n\n cpSync(srcDir, destDir, {\n recursive: true,\n filter: (src) => src === srcDir || !skip(src),\n });\n\n const rel = relative(workspaceRoot, destDir);\n const srcRel = relative(workspaceRoot, srcDir);\n console.log(\n `\\nCreated ${rel} (duplicated from ${srcRel}).\\n\\n` +\n `Remember to update the title in ${rel}/course.config.js.\\n\\n` +\n `Next steps:\\n pnpm tessera dev ${target}\\n`,\n );\n return 0;\n}\n","#!/usr/bin/env node\nimport { runValidate } from './validate-cli.js';\nimport { runA11y } from './a11y-cli.js';\nimport { runNew } from './new-cli.js';\nimport { runDuplicate } from './duplicate-cli.js';\nimport { resolveCourse } from './course-root.js';\n\nconst USAGE = `Usage: tessera <command> [course] [options]\n\nCommands:\n new <name> Scaffold a new course into courses/<name>\n duplicate <source> <new> Copy courses/<source> to courses/<new>\n dev [course] Start the Vite dev server\n export [course] Build and package the course for its LMS standard\n validate [course] Fast static structure checks\n a11y [course] Runtime accessibility audit (builds + drives Playwright)\n check [course] Run validate, then a11y\n\nRun a command from inside a course folder, or name the course explicitly.\n\na11y/check options:\n --threshold <minor|moderate|serious|critical> Failing impact (default: serious)`;\n\n// The course is a leading positional: `tessera <cmd> [course] [flags]`. Only the\n// first token can be the course, and only when it isn't a flag — otherwise a flag\n// value (e.g. the `serious` in `--threshold serious`) would be misread as a name.\nexport function splitCourseArg(rest: string[]): {\n course?: string;\n flags: string[];\n} {\n if (rest.length > 0 && !rest[0].startsWith('-')) {\n return { course: rest[0], flags: rest.slice(1) };\n }\n return { course: undefined, flags: rest };\n}\n\nexport async function main(\n argv: string[],\n cwd: string = process.cwd(),\n): Promise<number> {\n const [sub, ...rest] = argv;\n\n if (sub === 'new') return runNew(rest[0], cwd);\n if (sub === 'duplicate') return runDuplicate(rest[0], rest[1], cwd);\n\n switch (sub) {\n case 'dev':\n case 'export':\n case 'validate':\n case 'a11y':\n case 'check': {\n if (rest.includes('--help') || rest.includes('-h')) {\n console.log(USAGE);\n return 0;\n }\n const { course, flags } = splitCourseArg(rest);\n const resolved = resolveCourse(cwd, course);\n if (!resolved.ok) {\n console.error(`[tessera] ${resolved.error}`);\n return 1;\n }\n const { courseRoot, workspaceRoot } = resolved;\n\n switch (sub) {\n case 'dev': {\n const { runDev } = await import('./build-commands.js');\n return runDev(courseRoot, workspaceRoot);\n }\n case 'export': {\n const { runBuild } = await import('./build-commands.js');\n return runBuild(courseRoot, workspaceRoot);\n }\n case 'validate':\n return runValidate(courseRoot);\n case 'check': {\n const validateCode = runValidate(courseRoot, { showA11yTip: false });\n if (validateCode !== 0) return validateCode;\n return runA11y(courseRoot, workspaceRoot, flags);\n }\n case 'a11y':\n return runA11y(courseRoot, workspaceRoot, flags);\n }\n return 0;\n }\n case '--help':\n case '-h':\n console.log(USAGE);\n return 0;\n case undefined:\n console.error(`No command given.\\n\\n${USAGE}`);\n return 1;\n default:\n console.error(`Unknown command: ${sub}\\n\\n${USAGE}`);\n return 1;\n }\n}\n\n// import.meta.main is true only when this module is the program entry point,\n// and resolves symlinks itself (pnpm/npm bin shims) — Node >= 24.\nif (import.meta.main) {\n void main(process.argv.slice(2)).then((code) => process.exit(code));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../../src/plugin/validate-cli.ts","../../src/plugin/a11y-cli.ts","../../src/plugin/project-name.ts","../../src/plugin/course-root.ts","../../src/plugin/template-copy.ts","../../src/plugin/new-cli.ts","../../src/plugin/duplicate-cli.ts","../../src/plugin/cli.ts"],"sourcesContent":["import { basename } from 'node:path';\nimport { validateProject, reportValidationIssues } from './validation.js';\n\nexport function runValidate(\n projectRoot: string,\n {\n showA11yTip = true,\n standardOverride,\n }: { showA11yTip?: boolean; standardOverride?: string } = {},\n): number {\n const { errors, warnings } = validateProject(projectRoot, standardOverride);\n\n reportValidationIssues({ errors, warnings });\n\n if (errors.length > 0) {\n const summary =\n `Validation failed with ${errors.length} error(s)` +\n (warnings.length > 0 ? ` and ${warnings.length} warning(s)` : '') +\n '.';\n console.error(`\\n\\x1b[31m${summary}\\x1b[0m`);\n return 1;\n }\n\n if (warnings.length > 0) {\n console.log(\n `\\n\\x1b[33mValidation passed with ${warnings.length} warning(s).\\x1b[0m`,\n );\n } else {\n console.log(\n '\\x1b[32m[tessera]\\x1b[0m Validation passed — no issues found.',\n );\n }\n if (showA11yTip) {\n console.log(\n `\\x1b[2m[tessera] Static checks only. For a full runtime accessibility audit, run: pnpm a11y ${basename(projectRoot)}\\x1b[0m`,\n );\n }\n return 0;\n}\n","import { runAudit, type AuditOptions, type ImpactLevel } from './a11y/audit.js';\n\nconst VALID_THRESHOLDS: ImpactLevel[] = [\n 'minor',\n 'moderate',\n 'serious',\n 'critical',\n];\n\nexport type ParsedA11yArgs =\n | { ok: true; args: AuditOptions }\n | { ok: false; error: string };\n\nexport function parseA11yArgs(argv: string[]): ParsedA11yArgs {\n let threshold: ImpactLevel | undefined;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === '--threshold') {\n const value = argv[++i] as ImpactLevel;\n if (!VALID_THRESHOLDS.includes(value)) {\n return {\n ok: false,\n error: `--threshold must be one of: ${VALID_THRESHOLDS.join(', ')}`,\n };\n }\n threshold = value;\n } else {\n return { ok: false, error: `Unknown argument: ${arg}` };\n }\n }\n\n const args: AuditOptions = {};\n if (threshold !== undefined) args.threshold = threshold;\n return { ok: true, args };\n}\n\nexport async function runA11y(\n projectRoot: string,\n workspaceRoot: string,\n argv: string[],\n): Promise<number> {\n const parsed = parseA11yArgs(argv);\n if (!parsed.ok) {\n console.error(`[tessera a11y] ${parsed.error}`);\n return 1;\n }\n return runAudit(projectRoot, workspaceRoot, parsed.args);\n}\n","// Pure, dependency-free helpers shared by the `tessera new` subcommand and the\n// `create-tessera` scaffolder. Kept import-free so create-tessera can bundle it\n// at build time without dragging in the rest of the plugin (vite, svelte, …).\n\n// npm package name rules: 1-214 chars, lowercase, must start with [a-z0-9],\n// allowed chars [a-z0-9._-], no leading dot or underscore.\nexport function validateProjectName(\n name: string,\n label = 'Project name',\n): string | null {\n if (!name) return `${label} is required`;\n if (name.length > 214) return `${label} must be 214 characters or fewer`;\n if (name !== name.toLowerCase()) return `${label} must be lowercase`;\n if (!/^[a-z0-9]/.test(name)) {\n return `${label} must start with a letter or digit`;\n }\n if (!/^[a-z0-9._-]+$/.test(name)) {\n return `${label} may only contain lowercase letters, digits, \"-\", \"_\", and \".\"`;\n }\n return null;\n}\n\nexport function toTitleCase(slug: string): string {\n return slug\n .split(/[-_.\\s]+/)\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n","import { existsSync, readdirSync, statSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { validateProjectName } from './project-name.js';\n\nexport interface ResolvedCourse {\n ok: true;\n courseRoot: string;\n workspaceRoot: string;\n}\n\nexport type ResolveResult = ResolvedCourse | { ok: false; error: string };\n\nfunction isDir(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\nfunction isCourse(dir: string): boolean {\n return existsSync(join(dir, 'course.config.js'));\n}\n\n// A workspace is the nearest ancestor holding a courses/ directory. The walk\n// includes the starting dir, so the workspace root resolves to itself.\nexport function findWorkspaceRoot(cwd: string): string | null {\n for (let dir = resolve(cwd); ; dir = dirname(dir)) {\n if (isDir(join(dir, 'courses'))) return dir;\n const parent = dirname(dir);\n if (parent === dir) return null;\n }\n}\n\nfunction scanCourses(workspaceRoot: string): {\n courses: string[];\n malformed: string[];\n} {\n const coursesDir = join(workspaceRoot, 'courses');\n const courses: string[] = [];\n const malformed: string[] = [];\n try {\n for (const e of readdirSync(coursesDir, { withFileTypes: true })) {\n if (!e.isDirectory()) continue;\n const dir = join(coursesDir, e.name);\n if (isCourse(dir)) courses.push(e.name);\n else if (isDir(join(dir, 'pages'))) malformed.push(e.name);\n }\n } catch {\n return { courses, malformed };\n }\n return { courses: courses.sort(), malformed: malformed.sort() };\n}\n\nexport function listCourses(workspaceRoot: string): string[] {\n return scanCourses(workspaceRoot).courses;\n}\n\nexport function listMalformedCourses(workspaceRoot: string): string[] {\n return scanCourses(workspaceRoot).malformed;\n}\n\nconst NOT_A_WORKSPACE =\n 'Not inside a Tessera workspace — no `courses/` directory was found at or above the current directory.';\n\nfunction malformedHint(malformed: string[]): string {\n if (malformed.length === 0) return '';\n return (\n `\\nSkipped (missing course.config.js):\\n` +\n malformed.map((c) => ` courses/${c}`).join('\\n')\n );\n}\n\nfunction listHint(workspaceRoot: string): string {\n const { courses, malformed } = scanCourses(workspaceRoot);\n if (courses.length === 0) {\n return (\n '\\nNo courses found. Create one with `tessera new <name>`.' +\n malformedHint(malformed)\n );\n }\n return (\n `\\nAvailable courses:\\n${courses.map((c) => ` ${c}`).join('\\n')}` +\n '\\nName one (`tessera <command> <course>`) or cd into its folder.' +\n malformedHint(malformed)\n );\n}\n\n// A name argument always wins; otherwise the cwd must itself be a course. There\n// is deliberately no \"single course → use it implicitly\" rule, so a bare command\n// never changes meaning when a second course is added.\nexport function resolveCourse(cwd: string, name?: string): ResolveResult {\n const here = resolve(cwd);\n\n if (name) {\n const nameError = validateProjectName(name, 'the name');\n if (nameError) {\n return {\n ok: false,\n error: `Invalid course name \"${name}\" — ${nameError}.`,\n };\n }\n const workspaceRoot = findWorkspaceRoot(here);\n if (!workspaceRoot) return { ok: false, error: NOT_A_WORKSPACE };\n const courseRoot = join(workspaceRoot, 'courses', name);\n if (!isCourse(courseRoot)) {\n return {\n ok: false,\n error: `Course \"${name}\" not found in courses/.${listHint(workspaceRoot)}`,\n };\n }\n return { ok: true, courseRoot, workspaceRoot };\n }\n\n if (isCourse(here)) {\n const workspaceRoot = findWorkspaceRoot(here) ?? dirname(dirname(here));\n return { ok: true, courseRoot: here, workspaceRoot };\n }\n\n const workspaceRoot = findWorkspaceRoot(here);\n if (!workspaceRoot) return { ok: false, error: NOT_A_WORKSPACE };\n return {\n ok: false,\n error: `No course specified.${listHint(workspaceRoot)}`,\n };\n}\n","import {\n copyFileSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n writeFileSync,\n} from 'node:fs';\nimport { join } from 'node:path';\n\n// npm's tarball packing strips/renames leading-dot files, so templates store\n// them prefixed and we restore the dot on copy. (create-vite convention.)\nconst RENAME: Record<string, string> = {\n _gitignore: '.gitignore',\n _gitkeep: '.gitkeep',\n};\n\n// Text files get token substitution; everything else is copied byte-for-byte.\nconst TEXT = /\\.(svelte|js|ts|json|css|md|html)$/;\n\nfunction applyTokens(s: string, tokens: Record<string, string>): string {\n return s.replace(/__([A-Z_]+)__/g, (m, key) =>\n key in tokens ? tokens[key] : m,\n );\n}\n\nexport function copyTemplate(\n srcDir: string,\n destDir: string,\n tokens: Record<string, string>,\n): void {\n mkdirSync(destDir, { recursive: true });\n for (const entry of readdirSync(srcDir, { withFileTypes: true })) {\n const src = join(srcDir, entry.name);\n const dest = join(destDir, RENAME[entry.name] ?? entry.name);\n if (entry.isDirectory()) {\n copyTemplate(src, dest, tokens);\n } else if (TEXT.test(entry.name)) {\n writeFileSync(dest, applyTokens(readFileSync(src, 'utf-8'), tokens));\n } else {\n copyFileSync(src, dest);\n }\n }\n}\n","import { existsSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\nimport { join, relative, resolve } from 'node:path';\nimport { findWorkspaceRoot } from './course-root.js';\nimport { validateProjectName, toTitleCase } from './project-name.js';\nimport { copyTemplate } from './template-copy.js';\nimport { resolvePackageRoot } from './package-root.js';\n\n// `tessera new <name>` — stamp a new course into courses/<name> inside the\n// current workspace. No install step: the workspace already owns the deps.\nexport function runNew(name: string | undefined, cwd: string): number {\n if (name === '--help' || name === '-h') {\n console.log(\n 'Usage: tessera new <name>\\n\\n' +\n 'Scaffold a new course into courses/<name> inside the current workspace.',\n );\n return 0;\n }\n if (!name) {\n console.error('Usage: tessera new <name>');\n return 1;\n }\n\n const nameError = validateProjectName(name, 'Course name');\n if (nameError) {\n console.error(`[tessera new] ${nameError}`);\n return 1;\n }\n\n const workspaceRoot = findWorkspaceRoot(resolve(cwd));\n if (!workspaceRoot) {\n console.error(\n '[tessera new] Not inside a Tessera workspace — run this from a workspace (a directory with a `courses/` folder).',\n );\n return 1;\n }\n\n const courseDir = join(workspaceRoot, 'courses', name);\n if (existsSync(courseDir)) {\n console.error(`[tessera new] Course \"${name}\" already exists.`);\n return 1;\n }\n\n const templateDir = join(resolvePackageRoot(), 'templates', 'course');\n copyTemplate(templateDir, courseDir, {\n PROJECT_TITLE: toTitleCase(name),\n COURSE_ID: `urn:uuid:${randomUUID()}`,\n });\n\n const rel = relative(workspaceRoot, courseDir);\n console.log(`\\nCreated ${rel}.\\n\\nNext steps:\\n pnpm tessera dev ${name}\\n`);\n return 0;\n}\n","import { cpSync, existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\nimport { basename, join, relative } from 'node:path';\nimport { resolveCourse } from './course-root.js';\nimport { validateProjectName } from './project-name.js';\n\n// The top-level `id` property as scaffolders write it: the key (bare or quoted)\n// first on its own line, then its value. Anchoring to line start keeps `id:`\n// inside a comment or a string value from matching, and CourseConfig has no\n// nested `id`, so the first match is always the course identity.\nconst ID_LINE =\n /^([ \\t]*'?id'?[ \\t]*:[ \\t]*)('(?:[^'\\\\]|\\\\.)*'|\"(?:[^\"\\\\]|\\\\.)*\"|`(?:[^`\\\\]|\\\\.)*`|[^\\n,}]*)/m;\nconst DEFAULT_OBJECT = /export\\s+default\\s*\\{/;\n\n// A verbatim copy inherits the source's `id`; mint a fresh one so the duplicate\n// is a distinct course (own storage key + LRS activity id).\nfunction reidentifyCourse(courseRoot: string): void {\n const configPath = join(courseRoot, 'course.config.js');\n if (!existsSync(configPath)) return;\n const text = readFileSync(configPath, 'utf-8');\n const newId = `'urn:uuid:${randomUUID()}'`;\n\n if (ID_LINE.test(text)) {\n writeFileSync(configPath, text.replace(ID_LINE, `$1${newId}`));\n return;\n }\n const obj = DEFAULT_OBJECT.exec(text);\n if (obj) {\n const at = obj.index + obj[0].length;\n writeFileSync(\n configPath,\n `${text.slice(0, at)}\\n id: ${newId},${text.slice(at)}`,\n );\n return;\n }\n console.warn(\n `[tessera duplicate] Could not set a unique id in ${configPath} — the copy shares the source's identity. Add a unique \"id\" manually.`,\n );\n}\n\nconst HELP =\n 'Usage: tessera duplicate <source> <new>\\n\\n' +\n 'Copy courses/<source>/ to courses/<new>/ within the current workspace.';\n\n// Generated/build artifacts that should never travel with a verbatim copy. The\n// a11y throwaway build and Vite's cache live under node_modules, so they're\n// already pruned by the node_modules skip; the rest are belt-and-suspenders.\nconst SKIP = new Set(['node_modules', 'dist', 'a11y-report.json', '.vite']);\n\nfunction skip(srcPath: string): boolean {\n const name = basename(srcPath);\n return SKIP.has(name) || name.startsWith('.tessera');\n}\n\n// `tessera duplicate <source> <new>` — copy an existing course verbatim within\n// the current workspace. Unlike `new`, there is no template stamping: the JS\n// config (including its title) is copied untouched.\nexport function runDuplicate(\n source: string | undefined,\n target: string | undefined,\n cwd: string,\n): number {\n if (\n source === '--help' ||\n source === '-h' ||\n target === '--help' ||\n target === '-h'\n ) {\n console.log(HELP);\n return 0;\n }\n if (!source || !target) {\n console.error('Usage: tessera duplicate <source> <new>');\n return 1;\n }\n\n const nameError = validateProjectName(target, 'Course name');\n if (nameError) {\n console.error(`[tessera duplicate] ${nameError}`);\n return 1;\n }\n\n const resolved = resolveCourse(cwd, source);\n if (!resolved.ok) {\n console.error(`[tessera duplicate] ${resolved.error}`);\n return 1;\n }\n const { courseRoot: srcDir, workspaceRoot } = resolved;\n\n const destDir = join(workspaceRoot, 'courses', target);\n if (existsSync(destDir)) {\n console.error(`[tessera duplicate] Course \"${target}\" already exists.`);\n return 1;\n }\n\n cpSync(srcDir, destDir, {\n recursive: true,\n filter: (src) => src === srcDir || !skip(src),\n });\n reidentifyCourse(destDir);\n\n const rel = relative(workspaceRoot, destDir);\n const srcRel = relative(workspaceRoot, srcDir);\n console.log(\n `\\nCreated ${rel} (duplicated from ${srcRel}).\\n\\n` +\n `Remember to update the title in ${rel}/course.config.js.\\n\\n` +\n `Next steps:\\n pnpm tessera dev ${target}\\n`,\n );\n return 0;\n}\n","#!/usr/bin/env node\nimport { runValidate } from './validate-cli.js';\nimport { runA11y } from './a11y-cli.js';\nimport { runNew } from './new-cli.js';\nimport { runDuplicate } from './duplicate-cli.js';\nimport { resolveCourse } from './course-root.js';\nimport { VALID_EXPORT_STANDARDS } from './validation.js';\n\nconst USAGE = `Usage: tessera <command> [course] [options]\n\nCommands:\n new <name> Scaffold a new course into courses/<name>\n duplicate <source> <new> Copy courses/<source> to courses/<new>\n dev [course] Start the Vite dev server\n export [course] Build and package the course for its LMS standard\n validate [course] Fast static structure checks\n a11y [course] Runtime accessibility audit (builds + drives Playwright)\n check [course] Run validate, then a11y\n\nRun a command from inside a course folder, or name the course explicitly.\n\nexport/validate options:\n --standard <web|scorm12|scorm2004|cmi5|xapi> Override course.config.js export.standard\n\na11y/check options:\n --threshold <minor|moderate|serious|critical> Failing impact (default: serious)`;\n\n// Validate here, against the config validator's list, so an unknown standard\n// fails before Vite spins up.\nexport function parseExportFlags(flags: string[]): {\n standardOverride?: string;\n error?: string;\n} {\n let standardOverride: string | undefined;\n for (let i = 0; i < flags.length; i++) {\n const arg = flags[i];\n let value: string | undefined;\n if (arg === '--standard') {\n value = flags[++i];\n } else if (arg.startsWith('--standard=')) {\n value = arg.slice('--standard='.length);\n } else {\n return { error: `Unknown argument: ${arg}` };\n }\n if (value === undefined || value.startsWith('-')) {\n return { error: '--standard requires a value' };\n }\n if (!VALID_EXPORT_STANDARDS.includes(value)) {\n return {\n error: `--standard must be one of ${VALID_EXPORT_STANDARDS.join(', ')}, got \"${value}\"`,\n };\n }\n standardOverride = value;\n }\n return standardOverride ? { standardOverride } : {};\n}\n\n// The course is a leading positional: `tessera <cmd> [course] [flags]`. Only the\n// first token can be the course, and only when it isn't a flag — otherwise a flag\n// value (e.g. the `serious` in `--threshold serious`) would be misread as a name.\nexport function splitCourseArg(rest: string[]): {\n course?: string;\n flags: string[];\n} {\n if (rest.length > 0 && !rest[0].startsWith('-')) {\n return { course: rest[0], flags: rest.slice(1) };\n }\n return { course: undefined, flags: rest };\n}\n\ntype CourseCommand = (\n courseRoot: string,\n workspaceRoot: string,\n flags: string[],\n) => number | Promise<number>;\n\nconst COURSE_COMMANDS: Record<string, CourseCommand> = {\n dev: async (courseRoot, workspaceRoot) =>\n (await import('./build-commands.js')).runDev(courseRoot, workspaceRoot),\n export: async (courseRoot, workspaceRoot, flags) => {\n const { standardOverride, error } = parseExportFlags(flags);\n if (error) {\n console.error(`[tessera] ${error}`);\n return 1;\n }\n return (await import('./build-commands.js')).runBuild(\n courseRoot,\n workspaceRoot,\n standardOverride,\n );\n },\n validate: (courseRoot, _workspaceRoot, flags) => {\n const { standardOverride, error } = parseExportFlags(flags);\n if (error) {\n console.error(`[tessera] ${error}`);\n return 1;\n }\n return runValidate(courseRoot, { standardOverride });\n },\n a11y: (courseRoot, workspaceRoot, flags) =>\n runA11y(courseRoot, workspaceRoot, flags),\n check: (courseRoot, workspaceRoot, flags) => {\n const validateCode = runValidate(courseRoot, { showA11yTip: false });\n if (validateCode !== 0) return validateCode;\n return runA11y(courseRoot, workspaceRoot, flags);\n },\n};\n\nexport async function main(\n argv: string[],\n cwd: string = process.cwd(),\n): Promise<number> {\n const [sub, ...rest] = argv;\n\n if (sub === 'new') return runNew(rest[0], cwd);\n if (sub === 'duplicate') return runDuplicate(rest[0], rest[1], cwd);\n\n if (sub !== undefined && Object.hasOwn(COURSE_COMMANDS, sub)) {\n if (rest.includes('--help') || rest.includes('-h')) {\n console.log(USAGE);\n return 0;\n }\n const { course, flags } = splitCourseArg(rest);\n const resolved = resolveCourse(cwd, course);\n if (!resolved.ok) {\n console.error(`[tessera] ${resolved.error}`);\n return 1;\n }\n return COURSE_COMMANDS[sub](\n resolved.courseRoot,\n resolved.workspaceRoot,\n flags,\n );\n }\n\n if (sub === '--help' || sub === '-h') {\n console.log(USAGE);\n return 0;\n }\n if (sub === undefined) {\n console.error(`No command given.\\n\\n${USAGE}`);\n return 1;\n }\n console.error(`Unknown command: ${sub}\\n\\n${USAGE}`);\n return 1;\n}\n\n// import.meta.main is true only when this module is the program entry point,\n// and resolves symlinks itself (pnpm/npm bin shims) — Node >= 24.\nif (import.meta.main) {\n void main(process.argv.slice(2)).then((code) => process.exit(code));\n}\n"],"mappings":"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"}
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -1,5 +1,259 @@
|
|
|
1
1
|
import { Plugin } from "vite";
|
|
2
|
+
import { SvelteMap, SvelteSet } from "svelte/reactivity";
|
|
2
3
|
|
|
4
|
+
//#region src/runtime/xapi/types.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* xAPI types used by the publisher and registry. These mirror the relevant
|
|
7
|
+
* subset of the xAPI 1.0.3 spec — the publisher only models Agents (not
|
|
8
|
+
* Groups) for v1, and only the statement fields actually exercised by
|
|
9
|
+
* Tessera or surfaced to authors.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Identified xAPI Agent. Exactly one of `mbox` / `mbox_sha1sum` / `openid` /
|
|
13
|
+
* `account` must be present (the IFI rule). The publisher validates this on
|
|
14
|
+
* any actor it resolves; values that fail produce a runtime error rather
|
|
15
|
+
* than a silent LRS 400.
|
|
16
|
+
*/
|
|
17
|
+
interface XAPIAgent {
|
|
18
|
+
name?: string;
|
|
19
|
+
mbox?: string;
|
|
20
|
+
mbox_sha1sum?: string;
|
|
21
|
+
openid?: string;
|
|
22
|
+
account?: {
|
|
23
|
+
homePage: string;
|
|
24
|
+
name: string;
|
|
25
|
+
};
|
|
26
|
+
objectType?: 'Agent';
|
|
27
|
+
member?: unknown;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/runtime/types.d.ts
|
|
31
|
+
/**
|
|
32
|
+
* Quiz enum domains as runtime tuples. The unions below derive from these, and
|
|
33
|
+
* the build-time validator imports them too — so the accepted value set has a
|
|
34
|
+
* single source and can't drift between the types and the validator.
|
|
35
|
+
*/
|
|
36
|
+
declare const FEEDBACK_MODES: readonly ["review", "immediate", "never"];
|
|
37
|
+
declare const RETRY_MODES: readonly ["full", "incorrect-only"];
|
|
38
|
+
/**
|
|
39
|
+
* Per-page quiz configuration. Single source of truth — the build plugin
|
|
40
|
+
* extracts this from `pageConfig.quiz` and embeds it in the manifest;
|
|
41
|
+
* the runtime reads it from there. Keep field shapes in sync.
|
|
42
|
+
*/
|
|
43
|
+
interface QuizConfig {
|
|
44
|
+
graded?: boolean;
|
|
45
|
+
gatesProgress?: boolean;
|
|
46
|
+
maxAttempts?: number;
|
|
47
|
+
feedbackMode?: (typeof FEEDBACK_MODES)[number];
|
|
48
|
+
retryMode?: (typeof RETRY_MODES)[number];
|
|
49
|
+
}
|
|
50
|
+
interface CourseConfig {
|
|
51
|
+
title: string;
|
|
52
|
+
/** Stable, unique course identity (e.g. 'urn:uuid:…'). Seeds the web
|
|
53
|
+
* localStorage key and the cmi5/xAPI LRS activity id; scaffolders generate one.
|
|
54
|
+
* Absent → both fall back to a fixed value, colliding across courses. */
|
|
55
|
+
id?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
author?: string;
|
|
58
|
+
version?: string;
|
|
59
|
+
/** Resume policy. 'auto' (default) restores saved progress unless the page
|
|
60
|
+
* structure changed since it was saved; 'never' always starts fresh. */
|
|
61
|
+
resume?: 'auto' | 'never';
|
|
62
|
+
/** BCP-47 language tag for <html lang>. Defaults to 'en'. WCAG 3.1.1. */
|
|
63
|
+
language?: string;
|
|
64
|
+
/** Accessibility checker configuration. */
|
|
65
|
+
a11y?: A11yConfig;
|
|
66
|
+
branding?: {
|
|
67
|
+
logo?: string;
|
|
68
|
+
primaryColor?: string;
|
|
69
|
+
fontFamily?: string;
|
|
70
|
+
};
|
|
71
|
+
navigation: {
|
|
72
|
+
mode: 'free' | 'sequential';
|
|
73
|
+
canAccess?: AccessFn;
|
|
74
|
+
};
|
|
75
|
+
completion: ManualCompletion | QuizCompletion | PercentageCompletion;
|
|
76
|
+
/** Optional under "manual"; required under "quiz". */
|
|
77
|
+
scoring: {
|
|
78
|
+
passingScore: number;
|
|
79
|
+
};
|
|
80
|
+
export: {
|
|
81
|
+
standard: 'web' | 'scorm12' | 'scorm2004' | 'cmi5' | 'xapi';
|
|
82
|
+
/** Web export only: extend the baseline Content-Security-Policy. Each key is
|
|
83
|
+
* a directive; its sources are appended (unioned) onto the baseline. `false`
|
|
84
|
+
* drops the CSP meta entirely (for deployments that set a CSP header).
|
|
85
|
+
* Ignored unless `standard` is 'web'. */
|
|
86
|
+
csp?: false | Record<string, string[]>;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Optional xAPI destination(s) for custom statement publishing via
|
|
90
|
+
* `useXAPI()`. A single object or an array of destinations. Under cmi5
|
|
91
|
+
* export, the sentinel `endpoint: 'lms'` re-uses the LMS launch's
|
|
92
|
+
* credentials and shares the cmi5 adapter's queue.
|
|
93
|
+
*/
|
|
94
|
+
xapi?: XAPIConfig | XAPIConfig[];
|
|
95
|
+
}
|
|
96
|
+
/** Accessibility checker configuration. */
|
|
97
|
+
interface A11yConfig {
|
|
98
|
+
/** Build-gate severity for promotable Tier-1 rules + Tier-1a warnings. */
|
|
99
|
+
level?: 'warn' | 'error';
|
|
100
|
+
/** axe ruleset tags for the Tier-2 runtime auditor. */
|
|
101
|
+
standard?: 'wcag2a' | 'wcag2aa' | 'wcag21aa';
|
|
102
|
+
/** Per-rule escape hatch matched literally against each diagnostic's ID. */
|
|
103
|
+
ignore?: string[];
|
|
104
|
+
}
|
|
105
|
+
interface ManualCompletion {
|
|
106
|
+
mode: 'manual';
|
|
107
|
+
/**
|
|
108
|
+
* Set to "page" to opt into a build-time check that at least one page
|
|
109
|
+
* declares `completesOn: "view"`. Omit to skip the check; both completion
|
|
110
|
+
* paths still work at runtime.
|
|
111
|
+
*/
|
|
112
|
+
trigger?: 'page';
|
|
113
|
+
/** When set, markComplete() also flips successStatus. Omit for unknown. */
|
|
114
|
+
requireSuccessStatus?: 'passed' | 'failed';
|
|
115
|
+
}
|
|
116
|
+
interface QuizCompletion {
|
|
117
|
+
mode: 'quiz';
|
|
118
|
+
}
|
|
119
|
+
interface PercentageCompletion {
|
|
120
|
+
mode: 'percentage';
|
|
121
|
+
percentageThreshold?: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* cmi5 launch-inherited destination. Only valid under `export.standard:
|
|
125
|
+
* 'cmi5'`. Auth, actor, activityId, and registration are taken from the
|
|
126
|
+
* launch URL, so no other fields are accepted.
|
|
127
|
+
*/
|
|
128
|
+
interface XAPILMSConfig {
|
|
129
|
+
endpoint: 'lms';
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Explicit LRS destination. The author provides every field. `actor` is
|
|
133
|
+
* optional under SCORM (synthesized from `cmi.core.student_id` /
|
|
134
|
+
* `cmi.learner_id`) and required under web.
|
|
135
|
+
*/
|
|
136
|
+
interface XAPIExplicitConfig {
|
|
137
|
+
/** Absolute http(s) URL of the LRS Statements endpoint base. */
|
|
138
|
+
endpoint: string;
|
|
139
|
+
/**
|
|
140
|
+
* Basic-auth credential value (the part after "Basic "), or a function
|
|
141
|
+
* that resolves one. Function form is re-invoked once on 401 to cover
|
|
142
|
+
* short-lived tokens.
|
|
143
|
+
*/
|
|
144
|
+
auth: string | (() => string | Promise<string>);
|
|
145
|
+
/**
|
|
146
|
+
* Identified Agent or a resolver function. Required for web export;
|
|
147
|
+
* optional under SCORM where it can be synthesized from the LMS data
|
|
148
|
+
* model. Optional under cmi5 where it can be inherited from the launch.
|
|
149
|
+
*/
|
|
150
|
+
actor?: XAPIAgent | (() => XAPIAgent | Promise<XAPIAgent>);
|
|
151
|
+
/** xAPI activity IRI scoped to this destination. */
|
|
152
|
+
activityId: string;
|
|
153
|
+
/** Optional UUID v4 — primarily a cmi5 launch concept. */
|
|
154
|
+
registration?: string;
|
|
155
|
+
/**
|
|
156
|
+
* Override for the SCORM-derived actor's `account.homePage`. Defaults
|
|
157
|
+
* to the activityId origin when activityId is http(s); required when
|
|
158
|
+
* activityId uses a non-http(s) scheme.
|
|
159
|
+
*/
|
|
160
|
+
actorAccountHomePage?: string;
|
|
161
|
+
}
|
|
162
|
+
type XAPIConfig = XAPILMSConfig | XAPIExplicitConfig;
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/plugin/manifest.d.ts
|
|
165
|
+
interface ManifestPage {
|
|
166
|
+
index: number;
|
|
167
|
+
title: string;
|
|
168
|
+
slug: string;
|
|
169
|
+
importPath: string;
|
|
170
|
+
quiz: QuizConfig | null;
|
|
171
|
+
completesOn?: 'view';
|
|
172
|
+
}
|
|
173
|
+
interface ManifestLesson {
|
|
174
|
+
title: string;
|
|
175
|
+
slug: string;
|
|
176
|
+
pages: ManifestPage[];
|
|
177
|
+
}
|
|
178
|
+
interface ManifestSection {
|
|
179
|
+
title: string;
|
|
180
|
+
slug: string;
|
|
181
|
+
lessons: ManifestLesson[];
|
|
182
|
+
}
|
|
183
|
+
interface Manifest {
|
|
184
|
+
sections: ManifestSection[];
|
|
185
|
+
pages: ManifestPage[];
|
|
186
|
+
totalPages: number;
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/runtime/progress.svelte.d.ts
|
|
190
|
+
declare class ProgressState {
|
|
191
|
+
#private;
|
|
192
|
+
constructor(quizGradedIndices: ReadonlySet<number>, config: CourseConfig, totalPages: number);
|
|
193
|
+
visitedPages: SvelteSet<number>;
|
|
194
|
+
quizScores: SvelteMap<number, number>;
|
|
195
|
+
/**
|
|
196
|
+
* Chunk progress — for pages that reveal content in stages (Continue buttons).
|
|
197
|
+
* Maps pageIndex → highest revealed chunk index (0-based).
|
|
198
|
+
*/
|
|
199
|
+
chunkProgress: SvelteMap<number, number>;
|
|
200
|
+
/**
|
|
201
|
+
* Per-page standalone question scores from `useQuestion`. pageIndex → (questionId → score 0-100).
|
|
202
|
+
* Tracked separately from `quizScores` because <Quiz> blocks score as a unit
|
|
203
|
+
* while standalone questions score individually and average per page.
|
|
204
|
+
*/
|
|
205
|
+
standaloneQuestionScores: SvelteMap<number, Map<string, number>>;
|
|
206
|
+
/**
|
|
207
|
+
* Set of page indices that have at least one graded standalone question.
|
|
208
|
+
* Pages in this set contribute to course success status via their standalone average.
|
|
209
|
+
*/
|
|
210
|
+
gradedStandalonePages: SvelteSet<number>;
|
|
211
|
+
/**
|
|
212
|
+
* Monotonic counter incremented on every persistable state mutation. App.svelte
|
|
213
|
+
* subscribes to this single signal to schedule a coalesced save.
|
|
214
|
+
*/
|
|
215
|
+
version: number;
|
|
216
|
+
get manuallyCompleted(): boolean;
|
|
217
|
+
/** Idempotent — only the first call per session has an effect. */
|
|
218
|
+
markCompleteManually(): void;
|
|
219
|
+
markVisited(pageIndex: number): void;
|
|
220
|
+
quizCompleted(pageIndex: number, score: number): void;
|
|
221
|
+
/** Record the highest chunk index revealed on a page. Only advances forward. */
|
|
222
|
+
markChunk(pageIndex: number, chunkIndex: number): void;
|
|
223
|
+
/** Highest chunk revealed on a page, or -1 if none. */
|
|
224
|
+
getChunk(pageIndex: number): number;
|
|
225
|
+
markStandaloneQuestion(pageIndex: number, questionId: string, score: number, graded: boolean): void;
|
|
226
|
+
/** Average of standalone question scores on a page, or 0 if none. */
|
|
227
|
+
getPageStandaloneAverage(pageIndex: number): number;
|
|
228
|
+
completionStatus: "incomplete" | "complete";
|
|
229
|
+
successStatus: "passed" | "failed" | "unknown";
|
|
230
|
+
/**
|
|
231
|
+
* Effective graded score for LMS reporting — same union and averaging as
|
|
232
|
+
* successStatus, so score and success status can't disagree.
|
|
233
|
+
*/
|
|
234
|
+
gradedScore(): {
|
|
235
|
+
average: number;
|
|
236
|
+
attempted: boolean;
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/runtime/access.d.ts
|
|
241
|
+
interface AccessContext {
|
|
242
|
+
pageIndex: number;
|
|
243
|
+
page: ManifestPage;
|
|
244
|
+
manifest: Manifest;
|
|
245
|
+
progress: ProgressState;
|
|
246
|
+
config: CourseConfig;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Predicate deciding whether a page is accessible to the learner.
|
|
250
|
+
*
|
|
251
|
+
* Runs synchronously on every derived re-evaluation — keep it cheap. It is a
|
|
252
|
+
* runtime-side check only: the LMS does not enforce these rules. Authors who
|
|
253
|
+
* need true sequencing must rely on the LMS standard's own activity rules.
|
|
254
|
+
*/
|
|
255
|
+
type AccessFn = (ctx: AccessContext) => boolean;
|
|
256
|
+
//#endregion
|
|
3
257
|
//#region src/plugin/a11y/audit.d.ts
|
|
4
258
|
interface AuditOptions {
|
|
5
259
|
/** Minimum violation impact that fails the run (CI gate). Default 'serious'. */
|
|
@@ -14,7 +268,38 @@ type ImpactLevel = 'minor' | 'moderate' | 'serious' | 'critical';
|
|
|
14
268
|
declare function runAudit(projectRoot: string, workspaceRoot: string, options?: AuditOptions): Promise<number>;
|
|
15
269
|
//#endregion
|
|
16
270
|
//#region src/plugin/index.d.ts
|
|
17
|
-
declare function tesseraPlugin(
|
|
271
|
+
declare function tesseraPlugin(options?: {
|
|
272
|
+
standardOverride?: string;
|
|
273
|
+
}): (Plugin<any> | Plugin<any>[])[];
|
|
274
|
+
/** Fill runtime defaults into a parsed course.config.js. Exported for tests. */
|
|
275
|
+
declare function mergeCourseConfig(userConfig: Partial<CourseConfig>): {
|
|
276
|
+
title: string;
|
|
277
|
+
resume: "auto" | "never";
|
|
278
|
+
navigation: {
|
|
279
|
+
mode: string;
|
|
280
|
+
canAccess?: AccessFn;
|
|
281
|
+
};
|
|
282
|
+
completion: {};
|
|
283
|
+
scoring: {
|
|
284
|
+
passingScore: number;
|
|
285
|
+
};
|
|
286
|
+
export: {
|
|
287
|
+
standard: string;
|
|
288
|
+
csp?: false | Record<string, string[]>;
|
|
289
|
+
};
|
|
290
|
+
id?: string | undefined;
|
|
291
|
+
description?: string | undefined;
|
|
292
|
+
author?: string | undefined;
|
|
293
|
+
version?: string | undefined;
|
|
294
|
+
language?: string | undefined;
|
|
295
|
+
a11y?: A11yConfig | undefined;
|
|
296
|
+
branding?: {
|
|
297
|
+
logo?: string;
|
|
298
|
+
primaryColor?: string;
|
|
299
|
+
fontFamily?: string;
|
|
300
|
+
} | undefined;
|
|
301
|
+
xapi?: (XAPIConfig | XAPIConfig[]) | undefined;
|
|
302
|
+
};
|
|
18
303
|
//#endregion
|
|
19
|
-
export { type AuditOptions, type ImpactLevel, runAudit, tesseraPlugin };
|
|
304
|
+
export { type AuditOptions, type ImpactLevel, mergeCourseConfig, runAudit, tesseraPlugin };
|
|
20
305
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/plugin/a11y/audit.ts","../../src/plugin/index.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/runtime/xapi/types.ts","../../src/runtime/types.ts","../../src/plugin/manifest.ts","../../src/runtime/progress.svelte.ts","../../src/runtime/access.ts","../../src/plugin/a11y/audit.ts","../../src/plugin/index.ts"],"mappings":";;;;;;;;AAaA;;;;;;;;UAAiB,SAAA;EACf,IAAA;EACA,IAAA;EACA,YAAA;EACA,MAAA;EACA,OAAA;IAAY,QAAA;IAAkB,IAAA;EAAA;EAC9B,UAAA;EAGA,MAAA;AAAA;;;;;AATF;;;cCLa,cAAA;AAAA,cACA,WAAA;;ADaL;;;;UCGS,UAAA;EACf,MAAA;EACA,aAAA;EACA,WAAA;EACA,YAAA,WAAuB,cAAA;EACvB,SAAA,WAAoB,WAAW;AAAA;AAAA,UAGhB,YAAA;EACf,KAAA;EAzB4D;AAgB9D;;EAaE,EAAA;EACA,WAAA;EACA,MAAA;EACA,OAAA;EAbA;;EAgBA,MAAA;EAdA;EAgBA,QAAA;EAhB+B;EAkB/B,IAAA,GAAO,UAAA;EACP,QAAA;IACE,IAAA;IACA,YAAA;IACA,UAAA;EAAA;EAEF,UAAA;IACE,IAAA;IACA,SAAA,GAAY,QAAA;EAAA;EAEd,UAAA,EAAY,gBAAA,GAAmB,cAAA,GAAiB,oBAAA;EAmB5B;EAjBpB,OAAA;IACE,YAAA;EAAA;EAEF,MAAA;IACE,QAAA;IAxBF;;;;IA6BE,GAAA,WAAc,MAAA;EAAA;EApBhB;;;;;;EA4BA,IAAA,GAAO,UAAA,GAAa,UAAA;AAAA;;UAIL,UAAA;EAvBgB;EAyB/B,KAAA;EAvBA;EAyBA,QAAA;EAtBA;EAwBA,MAAA;AAAA;AAAA,UAGe,gBAAA;EACf,IAAA;EAdO;;;AAAuB;AAIhC;EAgBE,OAAA;;EAEA,oBAAA;AAAA;AAAA,UAGe,cAAA;EACf,IAAI;AAAA;AAAA,UAGW,oBAAA;EACf,IAAA;EACA,mBAAmB;AAAA;;;;;;UAQJ,aAAA;EACf,QAAQ;AAAA;;;AAdJ;AAGN;;UAmBiB,kBAAA;EAlBf;EAoBA,QAAA;EAXe;;;;AACP;EAgBR,IAAA,2BAA+B,OAAA;EARE;;;;;EAcjC,KAAA,GAAQ,SAAA,UAAmB,SAAA,GAAY,OAAA,CAAQ,SAAA;EAAR;EAEvC,UAAA;EAF8C;EAI9C,YAAA;EAVA;;;;;EAgBA,oBAAA;AAAA;AAAA,KAGU,UAAA,GAAa,aAAA,GAAgB,kBAAkB;;;UC1I1C,YAAA;EACf,KAAA;EACA,KAAA;EACA,IAAA;EACA,UAAA;EACA,IAAA,EAAM,UAAU;EAChB,WAAA;AAAA;AAAA,UAGe,cAAA;EACf,KAAA;EACA,IAAA;EACA,KAAA,EAAO,YAAY;AAAA;AAAA,UAGJ,eAAA;EACf,KAAA;EACA,IAAA;EACA,OAAA,EAAS,cAAc;AAAA;AAAA,UAGR,QAAA;EACf,QAAA,EAAU,eAAA;EACV,KAAA,EAAO,YAAY;EACnB,UAAA;AAAA;;;cClCW,aAAA;EAAA;cAMT,iBAAA,EAAmB,WAAA,UACnB,MAAA,EAAQ,YAAA,EACR,UAAA;EAOF,YAAA,EAAY,SAAA;EACZ,UAAA,EAAU,SAAA;EHPc;;;;EGYxB,aAAA,EAAa,SAAA;EHPb;;;;;EGaA,wBAAA,EAAwB,SAAA,SAAA,GAAA;EHTlB;;;;EGgBN,qBAAA,EAAqB,SAAA;EF9BgD;;;AAAA;EEuCrE,OAAA;EAAA,IAEI,iBAAA;;EAKJ,oBAAA;EAMA,WAAA,CAAY,SAAA;EAMZ,aAAA,CAAc,SAAA,UAAmB,KAAA;EFzCR;EE+CzB,SAAA,CAAU,SAAA,UAAmB,UAAA;EF1CE;EEkD/B,QAAA,CAAS,SAAA;EAIT,sBAAA,CACE,SAAA,UACA,UAAA,UACA,KAAA,UACA,MAAA;EF5DF;EE2EA,wBAAA,CAAyB,SAAA;EAQzB,gBAAA;EAqBA,aAAA;EFtGoB;;AAAW;AAGjC;EEmHE,WAAA;IAAiB,OAAA;IAAiB,SAAA;EAAA;AAAA;;;UC/InB,aAAA;EACf,SAAA;EACA,IAAA,EAAM,YAAA;EACN,QAAA,EAAU,QAAA;EACV,QAAA,EAAU,aAAA;EACV,MAAA,EAAQ,YAAA;AAAA;;;;;;;;KAUE,QAAA,IAAY,GAAkB,EAAb,aAAa;;;UCbzB,YAAA;;EAEf,SAAA,GAAY,WAAW;AAAA;AAAA,KAGb,WAAA;;;;;;iBAoUU,QAAA,CACpB,WAAA,UACA,aAAA,UACA,OAAA,GAAS,YAAA,GACR,OAAO;;;iBC1NM,aAAA,CAAc,OAAA;EAAW,gBAAA;AAAA,KAAgC,MAAA,QAAA,MAAA;;iBAoQzD,iBAAA,CAAkB,UAAA,EAAY,OAAA,CAAQ,YAAA;;;;;gBAAD,QAAA;EAAA"}
|
package/dist/plugin/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as runAudit } from "../audit
|
|
2
|
-
import {
|
|
3
|
-
export { runAudit, tesseraPlugin };
|
|
1
|
+
import { n as runAudit } from "../audit-DsYqXbqm.js";
|
|
2
|
+
import { n as tesseraPlugin, t as mergeCourseConfig } from "../plugin-BuMiDTmU.js";
|
|
3
|
+
export { mergeCourseConfig, runAudit, tesseraPlugin };
|