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
package/src/plugin/manifest.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { resolve, basename, extname } from 'node:path';
|
|
3
3
|
import JSON5 from 'json5';
|
|
4
|
+
import {
|
|
5
|
+
clearParseCache,
|
|
6
|
+
defaultExportObjectLiteral,
|
|
7
|
+
pageConfigLiteral,
|
|
8
|
+
} from './ast.js';
|
|
4
9
|
import type { CourseConfig, QuizConfig } from '../runtime/types.js';
|
|
5
10
|
|
|
6
11
|
// ---------- Types ----------
|
|
@@ -85,29 +90,22 @@ export function deriveSlug(name: string, isFile = false): string {
|
|
|
85
90
|
return stripPrefix(name);
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
export const PAGE_CONFIG_EXPORT_RE = /export\s+const\s+pageConfig\s*=\s*/;
|
|
94
|
-
|
|
95
|
-
/** Matches `export default ` (RHS is read separately). */
|
|
96
|
-
const DEFAULT_EXPORT_RE = /export\s+default\s*/;
|
|
93
|
+
export type DefaultExportLiteralResult =
|
|
94
|
+
| { kind: 'literal'; text: string }
|
|
95
|
+
| { kind: 'none' }
|
|
96
|
+
| { kind: 'invalid' }
|
|
97
|
+
| { kind: 'parse-error' };
|
|
97
98
|
|
|
98
99
|
/**
|
|
99
|
-
* Locate `export default { ... }` and return
|
|
100
|
-
*
|
|
101
|
-
* Used by both manifest extraction and project
|
|
100
|
+
* Locate `export default { ... }` and return its object-literal text. Returns
|
|
101
|
+
* a discriminated result so callers can tell parse failure from a missing or
|
|
102
|
+
* non-literal default export. Used by both manifest extraction and project
|
|
103
|
+
* validation.
|
|
102
104
|
*/
|
|
103
105
|
export function extractDefaultExportObjectLiteral(
|
|
104
106
|
source: string,
|
|
105
|
-
):
|
|
106
|
-
|
|
107
|
-
if (!match || match.index === undefined) return null;
|
|
108
|
-
const startIndex = source.indexOf('{', match.index);
|
|
109
|
-
if (startIndex < 0) return null;
|
|
110
|
-
return extractObjectLiteral(source, startIndex);
|
|
107
|
+
): DefaultExportLiteralResult {
|
|
108
|
+
return defaultExportObjectLiteral(source);
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
export type CourseConfigRead =
|
|
@@ -128,12 +126,14 @@ export type CourseConfigRead =
|
|
|
128
126
|
export function readCourseConfig(projectRoot: string): CourseConfigRead {
|
|
129
127
|
const configPath = resolve(projectRoot, 'course.config.js');
|
|
130
128
|
if (!existsSync(configPath)) return { ok: false, reason: 'missing' };
|
|
131
|
-
const
|
|
129
|
+
const result = extractDefaultExportObjectLiteral(
|
|
132
130
|
readSourceFileCached(configPath),
|
|
133
131
|
);
|
|
134
|
-
if (
|
|
132
|
+
if (result.kind === 'parse-error')
|
|
133
|
+
return { ok: false, reason: 'parse-error' };
|
|
134
|
+
if (result.kind !== 'literal') return { ok: false, reason: 'no-export' };
|
|
135
135
|
try {
|
|
136
|
-
return { ok: true, config: JSON5.parse(
|
|
136
|
+
return { ok: true, config: JSON5.parse(result.text) };
|
|
137
137
|
} catch (error) {
|
|
138
138
|
return { ok: false, reason: 'parse-error', error };
|
|
139
139
|
}
|
|
@@ -150,13 +150,13 @@ export function readMetaFile(metaPath: string): {
|
|
|
150
150
|
} {
|
|
151
151
|
if (!existsSync(metaPath)) return {};
|
|
152
152
|
|
|
153
|
-
const
|
|
153
|
+
const result = extractDefaultExportObjectLiteral(
|
|
154
154
|
readSourceFileCached(metaPath),
|
|
155
155
|
);
|
|
156
|
-
if (
|
|
156
|
+
if (result.kind !== 'literal') return {};
|
|
157
157
|
|
|
158
158
|
try {
|
|
159
|
-
return JSON5.parse(
|
|
159
|
+
return JSON5.parse(result.text);
|
|
160
160
|
} catch {
|
|
161
161
|
return {};
|
|
162
162
|
}
|
|
@@ -178,30 +178,12 @@ export type PageConfigParseResult =
|
|
|
178
178
|
export function parsePageConfigFromSource(
|
|
179
179
|
content: string,
|
|
180
180
|
): PageConfigParseResult {
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
const scriptContent = moduleScriptMatch[1];
|
|
185
|
-
|
|
186
|
-
const configMatch = scriptContent.match(PAGE_CONFIG_EXPORT_RE);
|
|
187
|
-
if (!configMatch || configMatch.index === undefined) return { kind: 'none' };
|
|
188
|
-
|
|
189
|
-
const afterExport = scriptContent
|
|
190
|
-
.slice(configMatch.index + configMatch[0].length)
|
|
191
|
-
.trimStart();
|
|
192
|
-
// pageConfig assigned to something other than an object literal — flag as invalid.
|
|
193
|
-
if (!afterExport.startsWith('{')) return { kind: 'invalid' };
|
|
194
|
-
|
|
195
|
-
const startIndex = scriptContent.indexOf(
|
|
196
|
-
'{',
|
|
197
|
-
configMatch.index + configMatch[0].length,
|
|
198
|
-
);
|
|
199
|
-
if (startIndex < 0) return { kind: 'invalid' };
|
|
200
|
-
const objectStr = extractObjectLiteral(scriptContent, startIndex);
|
|
201
|
-
if (!objectStr) return { kind: 'invalid' };
|
|
181
|
+
const literal = pageConfigLiteral(content);
|
|
182
|
+
if (literal.kind === 'none') return { kind: 'none' };
|
|
183
|
+
if (literal.kind === 'invalid') return { kind: 'invalid' };
|
|
202
184
|
|
|
203
185
|
try {
|
|
204
|
-
return { kind: 'ok', value: JSON5.parse(
|
|
186
|
+
return { kind: 'ok', value: JSON5.parse(literal.text) };
|
|
205
187
|
} catch {
|
|
206
188
|
return { kind: 'invalid' };
|
|
207
189
|
}
|
|
@@ -223,74 +205,6 @@ export function extractPageConfig(filePath: string): {
|
|
|
223
205
|
return {};
|
|
224
206
|
}
|
|
225
207
|
|
|
226
|
-
/**
|
|
227
|
-
* Extract a balanced `{...}` or `[...]` span starting at the opening bracket,
|
|
228
|
-
* skipping strings and comments. Returns the substring (inclusive) or null if
|
|
229
|
-
* the open char is wrong or no matching close is found. Shared by manifest
|
|
230
|
-
* extraction, _meta/pageConfig parsing, and the validator's tag-prop parser.
|
|
231
|
-
*/
|
|
232
|
-
export function extractObjectLiteral(
|
|
233
|
-
source: string,
|
|
234
|
-
startIndex: number,
|
|
235
|
-
): string | null {
|
|
236
|
-
const open = source[startIndex];
|
|
237
|
-
if (open !== '{' && open !== '[') return null;
|
|
238
|
-
|
|
239
|
-
let depth = 0;
|
|
240
|
-
let inString: string | null = null;
|
|
241
|
-
let escaped = false;
|
|
242
|
-
|
|
243
|
-
for (let i = startIndex; i < source.length; i++) {
|
|
244
|
-
const char = source[i];
|
|
245
|
-
|
|
246
|
-
if (escaped) {
|
|
247
|
-
escaped = false;
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (char === '\\' && inString) {
|
|
252
|
-
escaped = true;
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (inString) {
|
|
257
|
-
if (char === inString) {
|
|
258
|
-
inString = null;
|
|
259
|
-
}
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (char === '"' || char === "'" || char === '`') {
|
|
264
|
-
inString = char;
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Skip single-line comments
|
|
269
|
-
if (char === '/' && i + 1 < source.length && source[i + 1] === '/') {
|
|
270
|
-
const newline = source.indexOf('\n', i);
|
|
271
|
-
i = newline === -1 ? source.length : newline;
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Skip multi-line comments
|
|
276
|
-
if (char === '/' && i + 1 < source.length && source[i + 1] === '*') {
|
|
277
|
-
const end = source.indexOf('*/', i + 2);
|
|
278
|
-
i = end === -1 ? source.length : end + 1;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (char === '{' || char === '[') depth++;
|
|
283
|
-
if (char === '}' || char === ']') {
|
|
284
|
-
depth--;
|
|
285
|
-
if (depth === 0) {
|
|
286
|
-
return source.slice(startIndex, i + 1);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
208
|
/**
|
|
295
209
|
* Get sorted subdirectories of a given path.
|
|
296
210
|
*/
|
|
@@ -314,22 +228,73 @@ function getSvelteFiles(dirPath: string): string[] {
|
|
|
314
228
|
.sort();
|
|
315
229
|
}
|
|
316
230
|
|
|
231
|
+
// ---------- Course structure walker ----------
|
|
232
|
+
|
|
233
|
+
export interface WalkedLesson {
|
|
234
|
+
/** Lesson directory name, or null for a section's implicit flat lesson. */
|
|
235
|
+
name: string | null;
|
|
236
|
+
/** Directory holding the `.svelte` files (the section dir for a flat lesson). */
|
|
237
|
+
dir: string;
|
|
238
|
+
/** `_meta.js` path governing this lesson's title and page order. */
|
|
239
|
+
metaPath: string;
|
|
240
|
+
/** Sorted raw `.svelte` filenames in `dir` (pre-ordering). */
|
|
241
|
+
files: string[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface WalkedSection {
|
|
245
|
+
name: string;
|
|
246
|
+
dir: string;
|
|
247
|
+
metaPath: string;
|
|
248
|
+
lessons: WalkedLesson[];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Enumerate the course's section → lesson → file structure. Section-level
|
|
253
|
+
* `.svelte` files become an implicit flat lesson (`name: null`) ordered before
|
|
254
|
+
* the section's explicit lesson directories. Shared by manifest generation and
|
|
255
|
+
* build-time validation so the two never disagree on which files are pages.
|
|
256
|
+
*/
|
|
257
|
+
export function walkPages(pagesDir: string): WalkedSection[] {
|
|
258
|
+
const sections: WalkedSection[] = [];
|
|
259
|
+
for (const sectionName of getSortedDirs(pagesDir)) {
|
|
260
|
+
const dir = resolve(pagesDir, sectionName);
|
|
261
|
+
const metaPath = resolve(dir, '_meta.js');
|
|
262
|
+
const lessons: WalkedLesson[] = [];
|
|
263
|
+
|
|
264
|
+
const flatFiles = getSvelteFiles(dir);
|
|
265
|
+
if (flatFiles.length > 0) {
|
|
266
|
+
lessons.push({ name: null, dir, metaPath, files: flatFiles });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const lessonName of getSortedDirs(dir)) {
|
|
270
|
+
const lessonDir = resolve(dir, lessonName);
|
|
271
|
+
lessons.push({
|
|
272
|
+
name: lessonName,
|
|
273
|
+
dir: lessonDir,
|
|
274
|
+
metaPath: resolve(lessonDir, '_meta.js'),
|
|
275
|
+
files: getSvelteFiles(lessonDir),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
sections.push({ name: sectionName, dir, metaPath, lessons });
|
|
280
|
+
}
|
|
281
|
+
return sections;
|
|
282
|
+
}
|
|
283
|
+
|
|
317
284
|
// ---------- Main ----------
|
|
318
285
|
|
|
319
286
|
/**
|
|
320
287
|
* Generate a course manifest by scanning the pages/ directory.
|
|
321
288
|
*/
|
|
322
289
|
export function generateManifest(pagesDir: string): Manifest {
|
|
290
|
+
clearParseCache();
|
|
323
291
|
const sections: ManifestSection[] = [];
|
|
324
292
|
const flatPages: ManifestPage[] = [];
|
|
325
293
|
let pageIndex = 0;
|
|
326
294
|
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const sectionPath = resolve(pagesDir, sectionName);
|
|
331
|
-
const sectionMeta = readMetaFile(resolve(sectionPath, '_meta.js'));
|
|
332
|
-
const sectionSlug = deriveSlug(sectionName);
|
|
295
|
+
for (const walkedSection of walkPages(pagesDir)) {
|
|
296
|
+
const sectionMeta = readMetaFile(walkedSection.metaPath);
|
|
297
|
+
const sectionSlug = deriveSlug(walkedSection.name);
|
|
333
298
|
|
|
334
299
|
const section: ManifestSection = {
|
|
335
300
|
title: sectionMeta.title || titleCase(sectionSlug),
|
|
@@ -337,25 +302,29 @@ export function generateManifest(pagesDir: string): Manifest {
|
|
|
337
302
|
lessons: [],
|
|
338
303
|
};
|
|
339
304
|
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
const lessonMeta =
|
|
345
|
-
|
|
305
|
+
for (const walkedLesson of walkedSection.lessons) {
|
|
306
|
+
// The flat lesson uses the section _meta for ordering and has no title —
|
|
307
|
+
// the sidebar renders its pages without a lesson header.
|
|
308
|
+
const isFlat = walkedLesson.name === null;
|
|
309
|
+
const lessonMeta = isFlat
|
|
310
|
+
? sectionMeta
|
|
311
|
+
: readMetaFile(walkedLesson.metaPath);
|
|
312
|
+
const lessonSlug = isFlat ? sectionSlug : deriveSlug(walkedLesson.name!);
|
|
313
|
+
const relDir = isFlat
|
|
314
|
+
? `/pages/${walkedSection.name}`
|
|
315
|
+
: `/pages/${walkedSection.name}/${walkedLesson.name}`;
|
|
346
316
|
|
|
347
317
|
const lesson: ManifestLesson = {
|
|
348
|
-
title: lessonMeta.title || titleCase(lessonSlug),
|
|
318
|
+
title: isFlat ? '' : lessonMeta.title || titleCase(lessonSlug),
|
|
349
319
|
slug: lessonSlug,
|
|
350
320
|
pages: [],
|
|
351
321
|
};
|
|
352
322
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const filePath = resolve(lessonPath, fileName);
|
|
323
|
+
for (const fileName of orderPageFiles(
|
|
324
|
+
walkedLesson.files,
|
|
325
|
+
lessonMeta.pages,
|
|
326
|
+
)) {
|
|
327
|
+
const filePath = resolve(walkedLesson.dir, fileName);
|
|
359
328
|
const pageSlug = deriveSlug(fileName, true);
|
|
360
329
|
|
|
361
330
|
let pageConfig: {
|
|
@@ -371,13 +340,11 @@ export function generateManifest(pagesDir: string): Manifest {
|
|
|
371
340
|
console.warn(`[tessera warning] ${(e as Error).message}`);
|
|
372
341
|
}
|
|
373
342
|
|
|
374
|
-
const relativePath = `/pages/${sectionName}/${lessonName}/${fileName}`;
|
|
375
|
-
|
|
376
343
|
const page: ManifestPage = {
|
|
377
344
|
index: pageIndex,
|
|
378
345
|
title: pageConfig.title || titleCase(pageSlug),
|
|
379
346
|
slug: pageSlug,
|
|
380
|
-
importPath:
|
|
347
|
+
importPath: `${relDir}/${fileName}`,
|
|
381
348
|
quiz: pageConfig.quiz || null,
|
|
382
349
|
...(pageConfig.completesOn === 'view'
|
|
383
350
|
? { completesOn: 'view' as const }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Walk up from this module to the tessera-learn package root (the dir holding
|
|
5
|
+
// its package.json). Resolving by directory depth — resolve(dirname, '..', '..')
|
|
6
|
+
// — is brittle: tsdown may emit plugin code at dist/plugin/ or hoist it into a
|
|
7
|
+
// shared chunk at dist/, and those differ by one level. The package ships src/
|
|
8
|
+
// and styles/, so its package.json is the stable anchor for both.
|
|
9
|
+
export function resolvePackageRoot(): string {
|
|
10
|
+
const dir = import.meta.dirname;
|
|
11
|
+
for (let up = dir; up !== dirname(up); up = dirname(up)) {
|
|
12
|
+
const pkgPath = resolve(up, 'package.json');
|
|
13
|
+
if (existsSync(pkgPath)) {
|
|
14
|
+
try {
|
|
15
|
+
const { name } = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
16
|
+
if (name === 'tessera-learn') return up;
|
|
17
|
+
} catch {
|
|
18
|
+
// unreadable / non-JSON package.json — keep walking up
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Fallback to the historical depth assumption (dist/plugin → package root).
|
|
23
|
+
return resolve(dir, '..', '..');
|
|
24
|
+
}
|
package/src/plugin/quiz.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
-
import { resolve
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
4
3
|
import { createOverridePlugin } from './override-plugin.js';
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = dirname(__filename);
|
|
4
|
+
import { resolvePackageRoot } from './package-root.js';
|
|
8
5
|
|
|
9
6
|
export function tesseraQuizPlugin(): Plugin {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
const builtinQuiz = resolve(
|
|
8
|
+
resolvePackageRoot(),
|
|
9
|
+
'src',
|
|
10
|
+
'components',
|
|
11
|
+
'Quiz.svelte',
|
|
12
|
+
);
|
|
14
13
|
return createOverridePlugin({
|
|
15
14
|
name: 'tessera:quiz',
|
|
16
15
|
virtualId: 'virtual:tessera-quiz',
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { validateProject, reportValidationIssues } from './validation.js';
|
|
2
|
+
|
|
3
|
+
export function runValidate(projectRoot: string): number {
|
|
4
|
+
const { errors, warnings } = validateProject(projectRoot);
|
|
5
|
+
|
|
6
|
+
reportValidationIssues({ errors, warnings });
|
|
7
|
+
|
|
8
|
+
if (errors.length > 0) {
|
|
9
|
+
const summary =
|
|
10
|
+
`Validation failed with ${errors.length} error(s)` +
|
|
11
|
+
(warnings.length > 0 ? ` and ${warnings.length} warning(s)` : '') +
|
|
12
|
+
'.';
|
|
13
|
+
console.error(`\n\x1b[31m${summary}\x1b[0m`);
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (warnings.length > 0) {
|
|
18
|
+
console.log(
|
|
19
|
+
`\n\x1b[33mValidation passed with ${warnings.length} warning(s).\x1b[0m`,
|
|
20
|
+
);
|
|
21
|
+
} else {
|
|
22
|
+
console.log(
|
|
23
|
+
'\x1b[32m[tessera]\x1b[0m Validation passed — no issues found.',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
console.log(
|
|
27
|
+
'\x1b[2m[tessera] Static checks only. For a full runtime accessibility audit, run: tessera a11y\x1b[0m',
|
|
28
|
+
);
|
|
29
|
+
return 0;
|
|
30
|
+
}
|