tessera-learn 0.0.13 → 0.2.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 +1794 -0
- package/README.md +5 -5
- package/dist/{validation-B-xTvM9B.js → audit-BA5o0ick.js} +605 -269
- package/dist/audit-BA5o0ick.js.map +1 -0
- package/dist/build-commands-C0OnV-Vg.js +27 -0
- package/dist/build-commands-C0OnV-Vg.js.map +1 -0
- package/dist/inline-config-CroQ-_2Y.js +31 -0
- package/dist/inline-config-CroQ-_2Y.js.map +1 -0
- package/dist/plugin/cli.d.ts +9 -1
- package/dist/plugin/cli.d.ts.map +1 -0
- package/dist/plugin/cli.js +326 -17
- package/dist/plugin/cli.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2 -763
- package/dist/plugin-W_rk3Pit.js +731 -0
- package/dist/plugin-W_rk3Pit.js.map +1 -0
- package/package.json +21 -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 +40 -8
- package/src/plugin/a11y-cli.ts +39 -22
- package/src/plugin/ast.ts +276 -0
- package/src/plugin/build-commands.ts +31 -0
- package/src/plugin/cli.ts +96 -21
- package/src/plugin/course-root.ts +98 -0
- package/src/plugin/duplicate-cli.ts +74 -0
- package/src/plugin/index.ts +87 -122
- package/src/plugin/inline-config.ts +54 -0
- package/src/plugin/manifest.ts +103 -136
- package/src/plugin/new-cli.ts +51 -0
- package/src/plugin/package-root.ts +24 -0
- package/src/plugin/project-name.ts +29 -0
- package/src/plugin/quiz.ts +8 -9
- package/src/plugin/template-copy.ts +43 -0
- 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/templates/course/course.config.js +11 -0
- package/templates/course/layout.svelte +116 -0
- package/templates/course/pages/01-getting-started/01-welcome/_meta.js +1 -0
- package/templates/course/pages/01-getting-started/01-welcome/welcome.svelte +19 -0
- package/templates/course/pages/01-getting-started/_meta.js +1 -0
- package/templates/course/styles/custom.css +5 -0
- 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
|
@@ -3,54 +3,48 @@ import type { SCORM12API } from '../adapters/scorm12.js';
|
|
|
3
3
|
import type { SCORM2004API } from '../adapters/scorm2004.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* override should treat it as a config error (the build-time validator
|
|
10
|
-
* already enforces this; this is a runtime fallback for completeness).
|
|
6
|
+
* Origin of an http(s) URL, else null. Shared with the config validator, which
|
|
7
|
+
* predicts this result to know when `actorAccountHomePage` becomes required —
|
|
8
|
+
* one helper keeps the two in lockstep.
|
|
11
9
|
*/
|
|
12
|
-
export function
|
|
10
|
+
export function httpOrigin(url: string): string | null {
|
|
13
11
|
try {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return null;
|
|
12
|
+
const parsed = new URL(url);
|
|
13
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
|
|
14
|
+
? parsed.origin
|
|
15
|
+
: null;
|
|
19
16
|
} catch {
|
|
20
17
|
return null;
|
|
21
18
|
}
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
/**
|
|
25
|
-
* Synthesize an Identified Agent
|
|
22
|
+
* Synthesize an Identified Agent from the SCORM learner fields.
|
|
26
23
|
*
|
|
27
|
-
* { account: { homePage, name:
|
|
28
|
-
* name: cmi.core.student_name,
|
|
29
|
-
* objectType: 'Agent' }
|
|
24
|
+
* { account: { homePage, name: <id> }, name: <name>, objectType: 'Agent' }
|
|
30
25
|
*
|
|
31
26
|
* The `account` IFI satisfies xAPI's Identified Agent rule. `homePage`
|
|
32
|
-
* defaults to the activityId origin so analytics keyed on actor identity
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* Returns null if `student_id` is missing — caller should not construct
|
|
37
|
-
* a publisher in that case (the LRS would 400 on every send anyway).
|
|
27
|
+
* defaults to the activityId origin so analytics keyed on actor identity stay
|
|
28
|
+
* stable across LMS hosts; the author's `actorAccountHomePage` overrides when
|
|
29
|
+
* the authority namespace is elsewhere. Returns null if the id is missing —
|
|
30
|
+
* the caller should not construct a publisher (the LRS would 400 every send).
|
|
38
31
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
function synthesizeActor(
|
|
33
|
+
readId: () => string,
|
|
34
|
+
readName: () => string,
|
|
41
35
|
activityId: string,
|
|
42
36
|
actorAccountHomePage?: string,
|
|
43
37
|
): XAPIAgent | null {
|
|
44
38
|
let id = '';
|
|
45
39
|
let name = '';
|
|
46
40
|
try {
|
|
47
|
-
id =
|
|
41
|
+
id = readId() || '';
|
|
48
42
|
} catch {}
|
|
49
43
|
try {
|
|
50
|
-
name =
|
|
44
|
+
name = readName() || '';
|
|
51
45
|
} catch {}
|
|
52
46
|
if (!id) return null;
|
|
53
|
-
const homePage = actorAccountHomePage ??
|
|
47
|
+
const homePage = actorAccountHomePage ?? httpOrigin(activityId);
|
|
54
48
|
if (!homePage) return null;
|
|
55
49
|
const agent: XAPIAgent = {
|
|
56
50
|
account: { homePage, name: id },
|
|
@@ -60,31 +54,30 @@ export function synthesizeSCORM12Actor(
|
|
|
60
54
|
return agent;
|
|
61
55
|
}
|
|
62
56
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
57
|
+
/** SCORM 1.2 actor from `cmi.core.student_id` / `cmi.core.student_name`. */
|
|
58
|
+
export function synthesizeSCORM12Actor(
|
|
59
|
+
api: SCORM12API,
|
|
60
|
+
activityId: string,
|
|
61
|
+
actorAccountHomePage?: string,
|
|
62
|
+
): XAPIAgent | null {
|
|
63
|
+
return synthesizeActor(
|
|
64
|
+
() => api.LMSGetValue('cmi.core.student_id'),
|
|
65
|
+
() => api.LMSGetValue('cmi.core.student_name'),
|
|
66
|
+
activityId,
|
|
67
|
+
actorAccountHomePage,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** SCORM 2004 actor from `cmi.learner_id` / `cmi.learner_name` (renamed 2004 fields). */
|
|
68
72
|
export function synthesizeSCORM2004Actor(
|
|
69
73
|
api: SCORM2004API,
|
|
70
74
|
activityId: string,
|
|
71
75
|
actorAccountHomePage?: string,
|
|
72
76
|
): XAPIAgent | null {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
name = api.GetValue('cmi.learner_name') || '';
|
|
80
|
-
} catch {}
|
|
81
|
-
if (!id) return null;
|
|
82
|
-
const homePage = actorAccountHomePage ?? defaultAccountHomePage(activityId);
|
|
83
|
-
if (!homePage) return null;
|
|
84
|
-
const agent: XAPIAgent = {
|
|
85
|
-
account: { homePage, name: id },
|
|
86
|
-
objectType: 'Agent',
|
|
87
|
-
};
|
|
88
|
-
if (name) agent.name = name;
|
|
89
|
-
return agent;
|
|
77
|
+
return synthesizeActor(
|
|
78
|
+
() => api.GetValue('cmi.learner_id'),
|
|
79
|
+
() => api.GetValue('cmi.learner_name'),
|
|
80
|
+
activityId,
|
|
81
|
+
actorAccountHomePage,
|
|
82
|
+
);
|
|
90
83
|
}
|
|
@@ -482,18 +482,22 @@ export class XAPIPublisher {
|
|
|
482
482
|
}));
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
+
#buildHeaders(token: string): Headers {
|
|
486
|
+
const headers = new Headers();
|
|
487
|
+
if (token) headers.set('Authorization', `Basic ${token}`);
|
|
488
|
+
headers.set('X-Experience-API-Version', X_API_VERSION);
|
|
489
|
+
headers.set('Content-Type', 'application/json');
|
|
490
|
+
return headers;
|
|
491
|
+
}
|
|
492
|
+
|
|
485
493
|
#fetchWithToken(
|
|
486
494
|
token: string,
|
|
487
495
|
body: string,
|
|
488
496
|
keepalive: boolean,
|
|
489
497
|
): Promise<SendOutcome> {
|
|
490
|
-
const headers = new Headers();
|
|
491
|
-
if (token) headers.set('Authorization', `Basic ${token}`);
|
|
492
|
-
headers.set('X-Experience-API-Version', X_API_VERSION);
|
|
493
|
-
headers.set('Content-Type', 'application/json');
|
|
494
498
|
return fetch(this.#statementsUrl, {
|
|
495
499
|
method: 'POST',
|
|
496
|
-
headers,
|
|
500
|
+
headers: this.#buildHeaders(token),
|
|
497
501
|
body,
|
|
498
502
|
keepalive,
|
|
499
503
|
})
|
|
@@ -521,18 +525,14 @@ export class XAPIPublisher {
|
|
|
521
525
|
) {
|
|
522
526
|
this.#cachedAuth = null;
|
|
523
527
|
return this.#resolveAuth(true)
|
|
524
|
-
.then((newToken) =>
|
|
525
|
-
|
|
526
|
-
if (newToken) retryHeaders.set('Authorization', `Basic ${newToken}`);
|
|
527
|
-
retryHeaders.set('X-Experience-API-Version', X_API_VERSION);
|
|
528
|
-
retryHeaders.set('Content-Type', 'application/json');
|
|
529
|
-
return fetch(this.#statementsUrl, {
|
|
528
|
+
.then((newToken) =>
|
|
529
|
+
fetch(this.#statementsUrl, {
|
|
530
530
|
method: 'POST',
|
|
531
|
-
headers:
|
|
531
|
+
headers: this.#buildHeaders(newToken),
|
|
532
532
|
body,
|
|
533
533
|
keepalive,
|
|
534
|
-
})
|
|
535
|
-
|
|
534
|
+
}),
|
|
535
|
+
)
|
|
536
536
|
.then((retryResp): SendOutcome => {
|
|
537
537
|
if (retryResp.ok || retryResp.status === 409) {
|
|
538
538
|
return { ok: true, status: retryResp.status };
|
|
@@ -90,6 +90,10 @@ function makeSCORMDevFallbackPublisher(
|
|
|
90
90
|
* adapter present in non-cmi5 export modes — the validator should have
|
|
91
91
|
* caught this at build time).
|
|
92
92
|
*/
|
|
93
|
+
type ActorResolution =
|
|
94
|
+
| { kind: 'actor'; value: XAPIAgent | (() => XAPIAgent | Promise<XAPIAgent>) }
|
|
95
|
+
| { kind: 'scorm-fallback'; standard: 'scorm12' | 'scorm2004' };
|
|
96
|
+
|
|
93
97
|
function resolveDestination(
|
|
94
98
|
entry: XAPIConfig,
|
|
95
99
|
config: CourseConfig,
|
|
@@ -114,24 +118,18 @@ function resolveDestination(
|
|
|
114
118
|
|
|
115
119
|
// Explicit endpoint.
|
|
116
120
|
const explicit = entry as XAPIExplicitConfig;
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.
|
|
123
|
-
|
|
124
|
-
const std = (
|
|
125
|
-
actorOrResolver as { __scormDevFallback: 'scorm12' | 'scorm2004' }
|
|
126
|
-
).__scormDevFallback;
|
|
127
|
-
return { kind: 'explicit', publisher: makeSCORMDevFallbackPublisher(std) };
|
|
121
|
+
const resolution = resolveExplicitActor(explicit, config, adapter);
|
|
122
|
+
if (resolution === null) return null;
|
|
123
|
+
if (resolution.kind === 'scorm-fallback') {
|
|
124
|
+
return {
|
|
125
|
+
kind: 'explicit',
|
|
126
|
+
publisher: makeSCORMDevFallbackPublisher(resolution.standard),
|
|
127
|
+
};
|
|
128
128
|
}
|
|
129
129
|
const publisher = new XAPIPublisher({
|
|
130
130
|
endpoint: explicit.endpoint,
|
|
131
131
|
auth: explicit.auth,
|
|
132
|
-
actor:
|
|
133
|
-
| XAPIAgent
|
|
134
|
-
| (() => XAPIAgent | Promise<XAPIAgent>),
|
|
132
|
+
actor: resolution.value,
|
|
135
133
|
activityId: explicit.activityId,
|
|
136
134
|
registration: explicit.registration,
|
|
137
135
|
});
|
|
@@ -149,50 +147,45 @@ function resolveExplicitActor(
|
|
|
149
147
|
explicit: XAPIExplicitConfig,
|
|
150
148
|
config: CourseConfig,
|
|
151
149
|
adapter: PersistenceAdapter | null,
|
|
152
|
-
):
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
| null {
|
|
157
|
-
if (explicit.actor !== undefined) return explicit.actor;
|
|
158
|
-
// No author-supplied actor — try mode-specific derivation.
|
|
150
|
+
): ActorResolution | null {
|
|
151
|
+
if (explicit.actor !== undefined) {
|
|
152
|
+
return { kind: 'actor', value: explicit.actor };
|
|
153
|
+
}
|
|
159
154
|
if (config.export?.standard === 'cmi5' && adapter instanceof CMI5Adapter) {
|
|
160
155
|
const inner = adapter.getPublisher();
|
|
161
|
-
if (inner)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
156
|
+
if (!inner) return null;
|
|
157
|
+
try {
|
|
158
|
+
return { kind: 'actor', value: inner.getActor() };
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
168
161
|
}
|
|
169
|
-
return null;
|
|
170
162
|
}
|
|
171
163
|
if (config.export?.standard === 'scorm12') {
|
|
172
164
|
if (adapter instanceof SCORM12Adapter) {
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
165
|
+
return {
|
|
166
|
+
kind: 'actor',
|
|
167
|
+
value: synthesizeSCORM12Actor(
|
|
168
|
+
adapter.getAPI(),
|
|
169
|
+
explicit.activityId,
|
|
170
|
+
explicit.actorAccountHomePage,
|
|
171
|
+
) as XAPIAgent,
|
|
172
|
+
};
|
|
178
173
|
}
|
|
179
|
-
|
|
180
|
-
// dev-fallback path: install a stub publisher that surfaces an
|
|
181
|
-
// explicit error rather than silently no-oping. Authors get the
|
|
182
|
-
// same dev/prod parity in SCORM that they get in cmi5.
|
|
183
|
-
return { __scormDevFallback: 'scorm12' };
|
|
174
|
+
return { kind: 'scorm-fallback', standard: 'scorm12' };
|
|
184
175
|
}
|
|
185
176
|
if (config.export?.standard === 'scorm2004') {
|
|
186
177
|
if (adapter instanceof SCORM2004Adapter) {
|
|
187
|
-
return
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
178
|
+
return {
|
|
179
|
+
kind: 'actor',
|
|
180
|
+
value: synthesizeSCORM2004Actor(
|
|
181
|
+
adapter.getAPI(),
|
|
182
|
+
explicit.activityId,
|
|
183
|
+
explicit.actorAccountHomePage,
|
|
184
|
+
) as XAPIAgent,
|
|
185
|
+
};
|
|
192
186
|
}
|
|
193
|
-
return {
|
|
187
|
+
return { kind: 'scorm-fallback', standard: 'scorm2004' };
|
|
194
188
|
}
|
|
195
|
-
// Web export with no actor — build-time validator should have errored.
|
|
196
189
|
console.warn(
|
|
197
190
|
'Tessera xAPI: explicit destination has no actor and no derivation source — skipping.',
|
|
198
191
|
);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
title: '__PROJECT_TITLE__',
|
|
3
|
+
language: 'en',
|
|
4
|
+
navigation: { mode: 'free' },
|
|
5
|
+
completion: { mode: 'percentage', percentageThreshold: 100 },
|
|
6
|
+
scoring: { passingScore: 70 },
|
|
7
|
+
branding: {
|
|
8
|
+
primaryColor: '#0066cc',
|
|
9
|
+
},
|
|
10
|
+
export: { standard: 'web' },
|
|
11
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import '$shared/tokens.css';
|
|
3
|
+
import { useNavigation, useProgress } from 'tessera-learn';
|
|
4
|
+
|
|
5
|
+
let { page } = $props();
|
|
6
|
+
const nav = useNavigation();
|
|
7
|
+
const progress = useProgress();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<div class="course">
|
|
11
|
+
<header>
|
|
12
|
+
<h1>__PROJECT_TITLE__</h1>
|
|
13
|
+
<span class="progress"
|
|
14
|
+
>{progress.visitedPages.size} / {nav.pages.length}</span
|
|
15
|
+
>
|
|
16
|
+
</header>
|
|
17
|
+
|
|
18
|
+
<main>
|
|
19
|
+
{@render page()}
|
|
20
|
+
</main>
|
|
21
|
+
|
|
22
|
+
<footer>
|
|
23
|
+
<button
|
|
24
|
+
class="nav-btn"
|
|
25
|
+
disabled={!nav.canGoPrev}
|
|
26
|
+
onclick={() => nav.prev()}
|
|
27
|
+
>
|
|
28
|
+
← Previous
|
|
29
|
+
</button>
|
|
30
|
+
<button
|
|
31
|
+
class="nav-btn nav-btn--primary"
|
|
32
|
+
disabled={!nav.canGoNext}
|
|
33
|
+
onclick={() => nav.next()}
|
|
34
|
+
>
|
|
35
|
+
Next →
|
|
36
|
+
</button>
|
|
37
|
+
</footer>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<style>
|
|
41
|
+
.course {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
min-height: 100vh;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
header {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: space-between;
|
|
51
|
+
gap: var(--tessera-spacing-md);
|
|
52
|
+
padding: var(--tessera-spacing-md) var(--tessera-spacing-xl);
|
|
53
|
+
border-bottom: 1px solid var(--tessera-border);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
header h1 {
|
|
57
|
+
margin: 0;
|
|
58
|
+
font-size: 1.125rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.progress {
|
|
62
|
+
flex: none;
|
|
63
|
+
padding: 0.15rem 0.6rem;
|
|
64
|
+
border-radius: 999px;
|
|
65
|
+
background: var(--tessera-bg-secondary);
|
|
66
|
+
color: var(--tessera-text-light);
|
|
67
|
+
font-size: 0.875rem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
main {
|
|
71
|
+
flex: 1;
|
|
72
|
+
width: 100%;
|
|
73
|
+
max-width: var(--tessera-content-max-width);
|
|
74
|
+
margin: 0 auto;
|
|
75
|
+
padding: var(--tessera-spacing-xl);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
footer {
|
|
79
|
+
display: flex;
|
|
80
|
+
justify-content: space-between;
|
|
81
|
+
gap: var(--tessera-spacing-md);
|
|
82
|
+
padding: var(--tessera-spacing-md) var(--tessera-spacing-xl);
|
|
83
|
+
border-top: 1px solid var(--tessera-border);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.nav-btn {
|
|
87
|
+
padding: 0.5rem 1rem;
|
|
88
|
+
border: 1px solid var(--tessera-border);
|
|
89
|
+
border-radius: 6px;
|
|
90
|
+
background: var(--tessera-bg);
|
|
91
|
+
color: var(--tessera-text);
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
transition:
|
|
94
|
+
background var(--tessera-transition-fast),
|
|
95
|
+
border-color var(--tessera-transition-fast);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.nav-btn:hover:not(:disabled) {
|
|
99
|
+
background: var(--tessera-bg-secondary);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.nav-btn--primary {
|
|
103
|
+
border-color: transparent;
|
|
104
|
+
background: var(--tessera-primary);
|
|
105
|
+
color: #fff;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.nav-btn--primary:hover:not(:disabled) {
|
|
109
|
+
background: var(--tessera-primary-dark);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.nav-btn:disabled {
|
|
113
|
+
opacity: 0.5;
|
|
114
|
+
cursor: not-allowed;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { title: 'Welcome', pages: ['welcome'] };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script module>
|
|
2
|
+
export const pageConfig = { title: 'Welcome' };
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
import Button from '$shared/Button.svelte';
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<h1>Welcome to __PROJECT_TITLE__</h1>
|
|
10
|
+
|
|
11
|
+
<p>This is a basic demo page of your Tessera course.</p>
|
|
12
|
+
|
|
13
|
+
<p>
|
|
14
|
+
Point your agent to <code>AGENTS.md</code> at the workspace root for the
|
|
15
|
+
authoring guide. The button below is imported from the workspace design system
|
|
16
|
+
via <code>$shared</code>.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<Button>A shared button</Button>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { title: 'Getting Started' };
|
package/dist/audit-BBJpQGqb.js
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { o as generateManifest, r as normalizeA11y, s as readCourseConfig } from "./validation-B-xTvM9B.js";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { existsSync, writeFileSync } from "node:fs";
|
|
4
|
-
//#region src/plugin/a11y/audit.ts
|
|
5
|
-
const IMPACT_RANK = {
|
|
6
|
-
minor: 1,
|
|
7
|
-
moderate: 2,
|
|
8
|
-
serious: 3,
|
|
9
|
-
critical: 4
|
|
10
|
-
};
|
|
11
|
-
const AUDIT_ENV_FLAG = "TESSERA_A11Y_AUDIT";
|
|
12
|
-
/** Map the `a11y.standard` enum to axe's cumulative `runOnly` tag list. */
|
|
13
|
-
function axeTags(standard) {
|
|
14
|
-
switch (standard) {
|
|
15
|
-
case "wcag2a": return ["wcag2a"];
|
|
16
|
-
case "wcag21aa": return [
|
|
17
|
-
"wcag2a",
|
|
18
|
-
"wcag2aa",
|
|
19
|
-
"wcag21a",
|
|
20
|
-
"wcag21aa"
|
|
21
|
-
];
|
|
22
|
-
default: return ["wcag2a", "wcag2aa"];
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
/** axe-applicable ignore entries: drop the Tier-1a/1b namespaces. */
|
|
26
|
-
function axeIgnoreRules(ignore) {
|
|
27
|
-
return ignore.filter((id) => !id.startsWith("tessera/") && !id.startsWith("a11y_"));
|
|
28
|
-
}
|
|
29
|
-
function isFailing(v, thresholdRank) {
|
|
30
|
-
return !v.impact || IMPACT_RANK[v.impact] >= thresholdRank;
|
|
31
|
-
}
|
|
32
|
-
async function tryImport(specifier) {
|
|
33
|
-
return import(specifier);
|
|
34
|
-
}
|
|
35
|
-
async function loadDeps() {
|
|
36
|
-
let chromium;
|
|
37
|
-
for (const spec of ["playwright", "@playwright/test"]) try {
|
|
38
|
-
const mod = await tryImport(spec);
|
|
39
|
-
if (mod.chromium) {
|
|
40
|
-
chromium = mod.chromium;
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
} catch {}
|
|
44
|
-
if (!chromium) return {
|
|
45
|
-
ok: false,
|
|
46
|
-
missing: "playwright"
|
|
47
|
-
};
|
|
48
|
-
try {
|
|
49
|
-
const mod = await tryImport("@axe-core/playwright");
|
|
50
|
-
if (!mod.default) return {
|
|
51
|
-
ok: false,
|
|
52
|
-
missing: "@axe-core/playwright"
|
|
53
|
-
};
|
|
54
|
-
return {
|
|
55
|
-
ok: true,
|
|
56
|
-
deps: {
|
|
57
|
-
chromium,
|
|
58
|
-
AxeBuilder: mod.default
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
} catch {
|
|
62
|
-
return {
|
|
63
|
-
ok: false,
|
|
64
|
-
missing: "@axe-core/playwright"
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Run the Tier-2 runtime accessibility audit against a built course. Builds (or
|
|
70
|
-
* reuses) dist/, serves it, drives Playwright + axe-core over each page, writes
|
|
71
|
-
* a11y-report.json, and returns a process exit code (0 pass, 1 fail/error).
|
|
72
|
-
*/
|
|
73
|
-
async function runAudit(projectRoot, options = {}) {
|
|
74
|
-
const threshold = options.threshold ?? "serious";
|
|
75
|
-
const deps = await loadDeps();
|
|
76
|
-
if (!deps.ok) {
|
|
77
|
-
console.error("\x1B[31m[tessera a11y]\x1B[0m Tier 2 needs Playwright + axe-core, which aren't installed.\n Install them to run the runtime audit:\n npm i -D playwright @axe-core/playwright\n npx playwright install chromium");
|
|
78
|
-
return 1;
|
|
79
|
-
}
|
|
80
|
-
const { chromium, AxeBuilder } = deps.deps;
|
|
81
|
-
const read = readCourseConfig(projectRoot);
|
|
82
|
-
const settings = normalizeA11y(read.ok ? read.config.a11y : void 0);
|
|
83
|
-
const tags = axeTags(settings.standard);
|
|
84
|
-
const disableRules = axeIgnoreRules(settings.ignore);
|
|
85
|
-
const manifest = generateManifest(resolve(projectRoot, "pages"));
|
|
86
|
-
const vite = await import("vite");
|
|
87
|
-
const auditDist = resolve(projectRoot, "node_modules", ".tessera-a11y");
|
|
88
|
-
const distHtml = resolve(auditDist, "index.html");
|
|
89
|
-
const prevEnv = process.env[AUDIT_ENV_FLAG];
|
|
90
|
-
process.env[AUDIT_ENV_FLAG] = "1";
|
|
91
|
-
let server;
|
|
92
|
-
try {
|
|
93
|
-
if (options.rebuild || !existsSync(distHtml)) {
|
|
94
|
-
console.log("[tessera a11y] Building course…");
|
|
95
|
-
await vite.build({
|
|
96
|
-
root: projectRoot,
|
|
97
|
-
build: {
|
|
98
|
-
outDir: auditDist,
|
|
99
|
-
emptyOutDir: true
|
|
100
|
-
},
|
|
101
|
-
logLevel: "warn"
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
server = await vite.preview({
|
|
105
|
-
root: projectRoot,
|
|
106
|
-
build: { outDir: auditDist },
|
|
107
|
-
preview: {
|
|
108
|
-
port: 0,
|
|
109
|
-
host: "127.0.0.1"
|
|
110
|
-
},
|
|
111
|
-
logLevel: "warn"
|
|
112
|
-
});
|
|
113
|
-
const baseUrl = server.resolvedUrls?.local?.[0];
|
|
114
|
-
if (!baseUrl) {
|
|
115
|
-
console.error("[tessera a11y] Could not determine preview server URL.");
|
|
116
|
-
return 1;
|
|
117
|
-
}
|
|
118
|
-
const browser = await chromium.launch();
|
|
119
|
-
const pages = [];
|
|
120
|
-
try {
|
|
121
|
-
const page = await (await browser.newContext()).newPage();
|
|
122
|
-
const auditUrl = new URL(baseUrl);
|
|
123
|
-
auditUrl.searchParams.set("__tessera_audit", "1");
|
|
124
|
-
await page.goto(auditUrl.href, { waitUntil: "networkidle" });
|
|
125
|
-
await page.waitForSelector("#tessera-app", { timeout: 2e4 });
|
|
126
|
-
const scan = async () => {
|
|
127
|
-
const builder = new AxeBuilder({ page }).withTags(tags);
|
|
128
|
-
if (disableRules.length > 0) builder.disableRules(disableRules);
|
|
129
|
-
return (await builder.analyze()).violations.map((v) => ({
|
|
130
|
-
id: v.id,
|
|
131
|
-
impact: v.impact ?? null,
|
|
132
|
-
help: v.help,
|
|
133
|
-
helpUrl: v.helpUrl,
|
|
134
|
-
nodes: v.nodes.length
|
|
135
|
-
}));
|
|
136
|
-
};
|
|
137
|
-
const navCount = await page.locator("button.tessera-nav-page").count();
|
|
138
|
-
if (navCount === 0) pages.push({
|
|
139
|
-
index: 0,
|
|
140
|
-
title: manifest.pages[0]?.title ?? "(entry)",
|
|
141
|
-
violations: await scan()
|
|
142
|
-
});
|
|
143
|
-
else for (let i = 0; i < navCount; i++) {
|
|
144
|
-
const btn = page.locator("button.tessera-nav-page").nth(i);
|
|
145
|
-
const title = (await btn.textContent())?.trim() || `Page ${i + 1}`;
|
|
146
|
-
await btn.click();
|
|
147
|
-
await page.waitForFunction((idx) => document.querySelectorAll("button.tessera-nav-page")[idx]?.getAttribute("aria-current") === "page", i, { timeout: 2e4 });
|
|
148
|
-
await page.waitForLoadState("networkidle");
|
|
149
|
-
pages.push({
|
|
150
|
-
index: i,
|
|
151
|
-
title,
|
|
152
|
-
violations: await scan()
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
} finally {
|
|
156
|
-
await browser.close();
|
|
157
|
-
}
|
|
158
|
-
const thresholdRank = IMPACT_RANK[threshold];
|
|
159
|
-
let totalViolations = 0;
|
|
160
|
-
let failingViolations = 0;
|
|
161
|
-
for (const p of pages) for (const v of p.violations) {
|
|
162
|
-
totalViolations++;
|
|
163
|
-
if (isFailing(v, thresholdRank)) failingViolations++;
|
|
164
|
-
}
|
|
165
|
-
const report = {
|
|
166
|
-
standard: settings.standard,
|
|
167
|
-
threshold,
|
|
168
|
-
pages,
|
|
169
|
-
totalViolations,
|
|
170
|
-
failingViolations,
|
|
171
|
-
passed: failingViolations === 0
|
|
172
|
-
};
|
|
173
|
-
const reportPath = resolve(projectRoot, "a11y-report.json");
|
|
174
|
-
writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
175
|
-
printSummary(report, reportPath);
|
|
176
|
-
return report.passed ? 0 : 1;
|
|
177
|
-
} catch (err) {
|
|
178
|
-
console.error(`\x1b[31m[tessera a11y]\x1b[0m Audit could not complete: ${err instanceof Error ? err.message : String(err)}`);
|
|
179
|
-
return 1;
|
|
180
|
-
} finally {
|
|
181
|
-
server?.httpServer?.close?.();
|
|
182
|
-
if (prevEnv === void 0) delete process.env[AUDIT_ENV_FLAG];
|
|
183
|
-
else process.env[AUDIT_ENV_FLAG] = prevEnv;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
function printSummary(report, reportPath) {
|
|
187
|
-
const thresholdRank = IMPACT_RANK[report.threshold];
|
|
188
|
-
for (const p of report.pages) {
|
|
189
|
-
if (p.violations.length === 0) {
|
|
190
|
-
console.log(`\x1b[32m ✓\x1b[0m ${p.title}`);
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
const mark = p.violations.some((v) => isFailing(v, thresholdRank)) ? "\x1B[31m ✗\x1B[0m" : "\x1B[33m ⚠\x1B[0m";
|
|
194
|
-
console.log(`${mark} ${p.title}`);
|
|
195
|
-
for (const v of p.violations) console.log(` [${v.impact ?? "n/a"}] ${v.id} — ${v.help} (${v.nodes} node${v.nodes === 1 ? "" : "s"})`);
|
|
196
|
-
}
|
|
197
|
-
console.log(`\n[tessera a11y] Report written to ${reportPath}`);
|
|
198
|
-
if (report.passed) console.log(`\x1b[32m[tessera a11y] Passed\x1b[0m — ${report.totalViolations} total finding(s), none at/above "${report.threshold}".`);
|
|
199
|
-
else console.log(`\x1b[31m[tessera a11y] Failed\x1b[0m — ${report.failingViolations} finding(s) at/above "${report.threshold}" (of ${report.totalViolations} total).`);
|
|
200
|
-
}
|
|
201
|
-
//#endregion
|
|
202
|
-
export { runAudit as n, AUDIT_ENV_FLAG as t };
|
|
203
|
-
|
|
204
|
-
//# sourceMappingURL=audit-BBJpQGqb.js.map
|