eaa-kit 0.1.0 → 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/README.md +13 -4
- package/dist/astro/index.d.ts +0 -9
- package/dist/astro/index.js +1 -1
- package/dist/audit-CqrR9pIO.js +2 -0
- package/dist/{audit-6gbV0Zjd.js → audit-D4gju2BT.js} +507 -140
- package/dist/{baseline-Itspu3-Y.js → baseline-CV_3lbER.js} +1 -1
- package/dist/{baseline-DQTnNlc4.js → baseline-CgBmzFTr.js} +16 -16
- package/dist/cli/index.js +54 -34
- package/dist/crawl-CtJbMNNb.js +254 -0
- package/dist/index.d.ts +155 -65
- package/dist/index.js +2 -1
- package/dist/init-DRKIdpK1.js +110 -0
- package/dist/{json-1ESNIiHY.js → json-C9xS1PNC.js} +3 -1
- package/dist/load-sCkKsvGQ.js +192 -0
- package/dist/{playwright-BfWuTG_u.js → playwright-DWux49V3.js} +4 -4
- package/dist/project-CiyzKQud.js +258 -0
- package/dist/{render-K9KxDDSA.js → render-BO0nVrrZ.js} +21 -208
- package/dist/routes-BxbSZKXC.js +123 -0
- package/dist/schema-CMZ8ItGk.js +192 -0
- package/package.json +4 -5
- package/dist/audit-VtuUOuyX.js +0 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
import { t as ImpactLevel } from "./impact-EEB9ZXmC.js";
|
|
2
|
-
|
|
2
|
+
//#region src/schema.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The small amount of schema validation this package actually needs.
|
|
5
|
+
*
|
|
6
|
+
* It replaced zod, which is 6.4 MB of a 37 MB install for three closed schemas
|
|
7
|
+
* — the config file, the JSON report and the baseline — each already wrapped in
|
|
8
|
+
* hand-written error messages. Paying that on every install of an accessibility
|
|
9
|
+
* linter was not a good trade.
|
|
10
|
+
*
|
|
11
|
+
* The shape mirrors what it replaced closely enough that the call sites did not
|
|
12
|
+
* change: `safeParse` returns either the parsed value or a list of issues with
|
|
13
|
+
* a path, and the callers turn those into their own messages.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately not a general-purpose validator. It does what these three
|
|
16
|
+
* schemas need and no more; anything else should be added here when a schema
|
|
17
|
+
* needs it, rather than guessed at now.
|
|
18
|
+
*/
|
|
19
|
+
interface Issue {
|
|
20
|
+
/** Where in the document, e.g. ['provider', 'email'] or ['pages', 0, 'path']. */
|
|
21
|
+
path: Array<string | number>;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A validator that reads `value` and either returns a value of type T or
|
|
26
|
+
* records why it could not.
|
|
27
|
+
*
|
|
28
|
+
* `read` returns undefined when it recorded an issue. Callers must check
|
|
29
|
+
* `issues.length` rather than the return value, since undefined is also a legal
|
|
30
|
+
* parsed value for an optional field.
|
|
31
|
+
*/
|
|
32
|
+
interface Schema<T> {
|
|
33
|
+
read(value: unknown, path: Array<string | number>, issues: Issue[]): T | undefined;
|
|
34
|
+
/** Present when the field may be absent, so objects know not to require it. */
|
|
35
|
+
readonly isOptional?: boolean;
|
|
36
|
+
/** Supplies a value when the field is absent. */
|
|
37
|
+
readonly fallback?: () => T;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Marks a field that may be absent from the output object, not merely undefined
|
|
41
|
+
* in it. `withDefault` is absent from the *input* but always present in the
|
|
42
|
+
* output, so only `optional` carries this.
|
|
43
|
+
*/
|
|
44
|
+
declare const OPTIONAL: unique symbol;
|
|
45
|
+
type OptionalSchema<T> = Schema<T | undefined> & {
|
|
46
|
+
readonly [OPTIONAL]: true;
|
|
47
|
+
};
|
|
48
|
+
type Infer<S> = S extends OptionalSchema<infer T> ? T | undefined : S extends Schema<infer T> ? T : never;
|
|
49
|
+
type OptionalKeys<S> = { [K in keyof S]: S[K] extends OptionalSchema<unknown> ? K : never; }[keyof S];
|
|
50
|
+
/** Optional fields become optional keys, so `exactOptionalPropertyTypes` holds. */
|
|
51
|
+
type ObjectOf<S> = { [K in Exclude<keyof S, OptionalKeys<S>>]: Infer<S[K]>; } & { [K in OptionalKeys<S>]?: Infer<S[K]>; };
|
|
52
|
+
/**
|
|
53
|
+
* A field with a default: absent from what an author writes, always present in
|
|
54
|
+
* what comes out.
|
|
55
|
+
*/
|
|
56
|
+
declare const DEFAULTED: unique symbol;
|
|
57
|
+
type DefaultedSchema<T> = Schema<T> & {
|
|
58
|
+
readonly [DEFAULTED]: true;
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
3
61
|
//#region src/config/define.d.ts
|
|
4
62
|
/** Countries with their own supervisory body and statute text. */
|
|
5
63
|
declare const COUNTRIES: readonly ['AT', 'DE', 'CH'];
|
|
@@ -23,70 +81,102 @@ type AssessmentMethod = (typeof ASSESSMENT_METHODS)[number];
|
|
|
23
81
|
*/
|
|
24
82
|
declare const ISSUE_REASONS: readonly ['disproportionate-burden', 'out-of-scope', 'fix-planned'];
|
|
25
83
|
type IssueReason = (typeof ISSUE_REASONS)[number];
|
|
26
|
-
declare const configSchema:
|
|
27
|
-
site:
|
|
28
|
-
name:
|
|
29
|
-
url:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
declare const configSchema: Schema<ObjectOf<{
|
|
85
|
+
site: Schema<ObjectOf<{
|
|
86
|
+
name: Schema<string>;
|
|
87
|
+
url: Schema<string>;
|
|
88
|
+
/** BCP 47 tag of the site itself, e.g. 'de-AT'. */
|
|
89
|
+
locale: Schema<string>;
|
|
90
|
+
}>>;
|
|
91
|
+
provider: Schema<ObjectOf<{
|
|
92
|
+
/** The legal entity answerable for the service. */
|
|
93
|
+
legalName: Schema<string>;
|
|
94
|
+
/**
|
|
95
|
+
* The feedback address. Required: the EAA obliges providers to offer a way
|
|
96
|
+
* to report accessibility barriers, and a statement without one is not
|
|
97
|
+
* usable for its purpose.
|
|
98
|
+
*/
|
|
99
|
+
email: Schema<string>;
|
|
100
|
+
phone: OptionalSchema<string>;
|
|
101
|
+
address: OptionalSchema<string>;
|
|
102
|
+
/**
|
|
103
|
+
* A contact or feedback form, offered alongside the address rather than
|
|
104
|
+
* instead of it: the EAA requires a way to report barriers, and a form is
|
|
105
|
+
* the one channel a visitor who cannot use email may still be able to use.
|
|
106
|
+
*/
|
|
107
|
+
feedbackUrl: OptionalSchema<string>;
|
|
108
|
+
}>>;
|
|
109
|
+
compliance: Schema<ObjectOf<{
|
|
110
|
+
status: Schema<"compliant" | "non-compliant" | "partially-compliant">;
|
|
111
|
+
standard: DefaultedSchema<string>;
|
|
112
|
+
knownIssues: DefaultedSchema<ObjectOf<{
|
|
113
|
+
/** What is not accessible, in the statement's language. */
|
|
114
|
+
description: Schema<string>;
|
|
115
|
+
/** WCAG success criteria, e.g. ['1.4.3']. */
|
|
116
|
+
successCriteria: DefaultedSchema<string[]>;
|
|
117
|
+
/** EN 301 549 clauses, e.g. ['9.1.4.3']. */
|
|
118
|
+
en301549: DefaultedSchema<string[]>;
|
|
119
|
+
reason: OptionalSchema<"disproportionate-burden" | "fix-planned" | "out-of-scope">;
|
|
120
|
+
/** ISO date by which the barrier is expected to be removed. */
|
|
121
|
+
remedyBy: OptionalSchema<string>;
|
|
122
|
+
}>[]>;
|
|
123
|
+
/** When the assessment was carried out. */
|
|
124
|
+
assessedOn: Schema<string>;
|
|
125
|
+
assessmentMethod: DefaultedSchema<"external-audit" | "self-assessment">;
|
|
126
|
+
/**
|
|
127
|
+
* Reason attached to barriers taken from an audit report, which carries no
|
|
128
|
+
* reason of its own. 'fix-planned' is the honest default for a barrier an
|
|
129
|
+
* automated run just found; the other two are claims only a human can make.
|
|
130
|
+
*/
|
|
131
|
+
auditReason: DefaultedSchema<"disproportionate-burden" | "fix-planned" | "out-of-scope">;
|
|
132
|
+
}>>;
|
|
133
|
+
enforcement: Schema<ObjectOf<{
|
|
134
|
+
/** Drives which supervisory body and statute the template names. */
|
|
135
|
+
country: Schema<"AT" | "CH" | "DE">;
|
|
136
|
+
}>>;
|
|
137
|
+
}>>;
|
|
138
|
+
/**
|
|
139
|
+
* What an author writes in `eaa.config.ts`.
|
|
140
|
+
*
|
|
141
|
+
* Written out rather than inferred from the schema. It is the type people see
|
|
142
|
+
* in their editor while filling the file in, so it is worth being readable, and
|
|
143
|
+
* it differs from the parsed type in two ways inference makes awkward: fields
|
|
144
|
+
* with defaults may be left out, and a known issue may be a bare string.
|
|
145
|
+
*/
|
|
146
|
+
interface EaaConfigInput {
|
|
147
|
+
site: {
|
|
148
|
+
name: string;
|
|
149
|
+
url: string;
|
|
150
|
+
locale: string;
|
|
151
|
+
};
|
|
152
|
+
provider: {
|
|
153
|
+
legalName: string;
|
|
154
|
+
email: string;
|
|
155
|
+
phone?: string;
|
|
156
|
+
address?: string;
|
|
157
|
+
feedbackUrl?: string;
|
|
158
|
+
};
|
|
159
|
+
compliance: {
|
|
160
|
+
status: ComplianceStatus;
|
|
161
|
+
standard?: string;
|
|
162
|
+
knownIssues?: Array<string | KnownIssueInput>;
|
|
163
|
+
assessedOn: string;
|
|
164
|
+
assessmentMethod?: AssessmentMethod;
|
|
165
|
+
auditReason?: IssueReason;
|
|
166
|
+
};
|
|
167
|
+
enforcement: {
|
|
168
|
+
country: Country;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** One barrier, as written in a config file. */
|
|
172
|
+
interface KnownIssueInput {
|
|
173
|
+
description: string;
|
|
174
|
+
successCriteria?: string[];
|
|
175
|
+
en301549?: string[];
|
|
176
|
+
reason?: IssueReason;
|
|
177
|
+
remedyBy?: string;
|
|
178
|
+
}
|
|
179
|
+
type EaaConfig = Infer<typeof configSchema>;
|
|
90
180
|
type KnownIssue = EaaConfig['compliance']['knownIssues'][number];
|
|
91
181
|
/**
|
|
92
182
|
* Identity function that gives `eaa.config.ts` its types. Deliberately does not
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as COMPLIANCE_STATUSES, c as ISSUE_REASONS, d as defineConfig, f as parseConfig, i as ASSESSMENT_METHODS, l as STATEMENT_LOCALES, n as findConfigFile, o as COUNTRIES, r as loadConfig, s as ConfigError, t as CONFIG_FILENAMES, u as configSchema } from "./load-sCkKsvGQ.js";
|
|
2
|
+
import { a as summariseAuditReport, i as readAuditReport, n as toHtmlBody, o as StatementError, r as toHtmlDocument, t as renderStatement } from "./render-BO0nVrrZ.js";
|
|
2
3
|
export { ASSESSMENT_METHODS, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, ConfigError, ISSUE_REASONS, STATEMENT_LOCALES, StatementError, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { o as COUNTRIES, t as CONFIG_FILENAMES } from "./load-sCkKsvGQ.js";
|
|
2
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
//#region src/cli/init.ts
|
|
7
|
+
const COUNTRY_LOCALES = {
|
|
8
|
+
AT: "de-AT",
|
|
9
|
+
DE: "de-DE",
|
|
10
|
+
CH: "de-CH"
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Everything the project already says about itself.
|
|
14
|
+
*
|
|
15
|
+
* A guess that is wrong is worse than an empty field somebody has to fill, so
|
|
16
|
+
* this only reads what a project states outright.
|
|
17
|
+
*/
|
|
18
|
+
async function detectDefaults(cwd) {
|
|
19
|
+
const detected = {};
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(await readFile(path.join(cwd, "package.json"), "utf8"));
|
|
22
|
+
if (typeof pkg.name === "string" && pkg.name !== "") detected.name = pkg.name.replace(/^@[^/]+\//, "");
|
|
23
|
+
if (typeof pkg.homepage === "string" && /^https?:\/\//.test(pkg.homepage)) detected.url = pkg.homepage;
|
|
24
|
+
} catch {}
|
|
25
|
+
return detected;
|
|
26
|
+
}
|
|
27
|
+
/** Whether a config is already there, so init never overwrites one silently. */
|
|
28
|
+
async function existingConfig(cwd) {
|
|
29
|
+
for (const name of CONFIG_FILENAMES) try {
|
|
30
|
+
await stat(path.join(cwd, name));
|
|
31
|
+
return name;
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
/** Reads answers, showing what each will be if the reader just hits enter. */
|
|
35
|
+
function terminalPrompt() {
|
|
36
|
+
const rl = createInterface({
|
|
37
|
+
input: process.stdin,
|
|
38
|
+
output: process.stderr
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
ask: async (question, fallback) => {
|
|
42
|
+
const shown = fallback === "" ? "" : pc.dim(` (${fallback})`);
|
|
43
|
+
const answer = (await rl.question(`${question}${shown}: `)).trim();
|
|
44
|
+
return answer === "" ? fallback : answer;
|
|
45
|
+
},
|
|
46
|
+
close: () => rl.close()
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async function runInitCommand(options = {}) {
|
|
50
|
+
const cwd = options.cwd ?? process.cwd();
|
|
51
|
+
const target = path.resolve(cwd, options.output ?? "eaa.config.json");
|
|
52
|
+
const already = await existingConfig(cwd);
|
|
53
|
+
if (already !== void 0 && !options.force) {
|
|
54
|
+
process.stderr.write(`${pc.yellow("warning")} ${already} already exists. Pass --force to overwrite it.\n`);
|
|
55
|
+
return { exitCode: 1 };
|
|
56
|
+
}
|
|
57
|
+
const detected = await detectDefaults(cwd);
|
|
58
|
+
const terminal = options.ask === void 0 && !options.yes && process.stdin.isTTY === true ? terminalPrompt() : void 0;
|
|
59
|
+
const rl = options.ask ?? terminal?.ask;
|
|
60
|
+
const ask = async (question, fallback) => rl === void 0 ? fallback : rl(question, fallback);
|
|
61
|
+
if (rl !== void 0) process.stderr.write(`${pc.bold("eaa-kit init")}\n${pc.dim("Everything here is a claim you are making. Press enter to take a default.\n\n")}`);
|
|
62
|
+
const name = await ask("Site name", detected.name ?? "");
|
|
63
|
+
const url = await ask("Site URL", detected.url ?? "https://example.com");
|
|
64
|
+
const country = normaliseCountry(await ask(`Country whose law applies (${COUNTRIES.join("/")})`, "AT"));
|
|
65
|
+
const locale = await ask("Language of the site", COUNTRY_LOCALES[country]);
|
|
66
|
+
const legalName = await ask("Legal entity answerable for the site", name);
|
|
67
|
+
const email = await ask("Feedback email", "");
|
|
68
|
+
const feedbackUrl = await ask("Feedback or contact form URL (optional)", "");
|
|
69
|
+
terminal?.close();
|
|
70
|
+
const config = {
|
|
71
|
+
site: {
|
|
72
|
+
name,
|
|
73
|
+
url,
|
|
74
|
+
locale
|
|
75
|
+
},
|
|
76
|
+
provider: {
|
|
77
|
+
legalName,
|
|
78
|
+
email,
|
|
79
|
+
...feedbackUrl === "" ? {} : { feedbackUrl }
|
|
80
|
+
},
|
|
81
|
+
compliance: {
|
|
82
|
+
status: "partially-compliant",
|
|
83
|
+
assessedOn: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
84
|
+
assessmentMethod: "self-assessment",
|
|
85
|
+
knownIssues: []
|
|
86
|
+
},
|
|
87
|
+
enforcement: { country }
|
|
88
|
+
};
|
|
89
|
+
try {
|
|
90
|
+
await writeFile(target, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
91
|
+
} catch (cause) {
|
|
92
|
+
process.stderr.write(`${pc.red("error")} Could not write ${path.basename(target)}: ${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
93
|
+
return { exitCode: 2 };
|
|
94
|
+
}
|
|
95
|
+
process.stderr.write(`Wrote ${path.relative(cwd, target) || path.basename(target)}\n`);
|
|
96
|
+
const missing = [name === "" ? "site.name" : void 0, email === "" ? "provider.email" : void 0].filter((field) => field !== void 0);
|
|
97
|
+
if (missing.length > 0) process.stderr.write(`${pc.yellow("warning")} ${missing.join(" and ")} ${missing.length === 1 ? "is" : "are"} empty and required. Fill ${missing.length === 1 ? "it" : "them"} in before generating a statement.\n`);
|
|
98
|
+
process.stderr.write(pc.dim("Read it before publishing anything from it: status is partially-compliant,\nwhich is the honest default before an audit has run.\n\nNext: eaa-kit audit · eaa-kit statement\n"));
|
|
99
|
+
return {
|
|
100
|
+
file: target,
|
|
101
|
+
exitCode: 0
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Falls back rather than failing: a typo should not throw away the answers. */
|
|
105
|
+
function normaliseCountry(value) {
|
|
106
|
+
const upper = value.trim().toUpperCase();
|
|
107
|
+
return COUNTRIES.includes(upper) ? upper : "AT";
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
export { runInitCommand };
|
|
@@ -23,7 +23,9 @@ function buildJsonReport(audits, options) {
|
|
|
23
23
|
generatedAt,
|
|
24
24
|
engine: audits[0]?.engine ?? "jsdom",
|
|
25
25
|
target: {
|
|
26
|
-
|
|
26
|
+
source: options.directory,
|
|
27
|
+
kind: options.sourceKind ?? "directory",
|
|
28
|
+
directory: (options.sourceKind ?? "directory") === "directory" ? options.directory : null,
|
|
27
29
|
baseUrl: options.baseUrl ?? null
|
|
28
30
|
},
|
|
29
31
|
summary: buildSummary(audits, options.failOn),
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { _ as withDefault, c as object, f as safeParse, g as url, h as union, i as isoDate, l as optional, m as transform, n as email, p as string, r as enumeration, t as array, u as pipe } from "./schema-CMZ8ItGk.js";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
//#region src/config/define.ts
|
|
6
|
+
/** Countries with their own supervisory body and statute text. */
|
|
7
|
+
const COUNTRIES = [
|
|
8
|
+
"AT",
|
|
9
|
+
"DE",
|
|
10
|
+
"CH"
|
|
11
|
+
];
|
|
12
|
+
/** Languages a statement can be rendered in. */
|
|
13
|
+
const STATEMENT_LOCALES = ["de", "en"];
|
|
14
|
+
/**
|
|
15
|
+
* Wording follows the EU model statement: fully, partially, or not conformant
|
|
16
|
+
* with the standard. "partially-compliant" is the honest answer for most sites
|
|
17
|
+
* and the one that carries obligations to list what is missing.
|
|
18
|
+
*/
|
|
19
|
+
const COMPLIANCE_STATUSES = [
|
|
20
|
+
"compliant",
|
|
21
|
+
"partially-compliant",
|
|
22
|
+
"non-compliant"
|
|
23
|
+
];
|
|
24
|
+
const ASSESSMENT_METHODS = ["self-assessment", "external-audit"];
|
|
25
|
+
/**
|
|
26
|
+
* Why a known barrier still exists. The first two are the grounds the EU regime
|
|
27
|
+
* recognises for leaving something inaccessible; the third is a plain promise to
|
|
28
|
+
* fix it, which is what most small sites actually mean.
|
|
29
|
+
*/
|
|
30
|
+
const ISSUE_REASONS = [
|
|
31
|
+
"disproportionate-burden",
|
|
32
|
+
"out-of-scope",
|
|
33
|
+
"fix-planned"
|
|
34
|
+
];
|
|
35
|
+
const knownIssueObject = object({
|
|
36
|
+
/** What is not accessible, in the statement's language. */
|
|
37
|
+
description: string({ min: 1 }),
|
|
38
|
+
/** WCAG success criteria, e.g. ['1.4.3']. */
|
|
39
|
+
successCriteria: withDefault(array(string()), () => []),
|
|
40
|
+
/** EN 301 549 clauses, e.g. ['9.1.4.3']. */
|
|
41
|
+
en301549: withDefault(array(string()), () => []),
|
|
42
|
+
reason: optional(enumeration(ISSUE_REASONS)),
|
|
43
|
+
/** ISO date by which the barrier is expected to be removed. */
|
|
44
|
+
remedyBy: optional(isoDate())
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* A bare string is accepted as shorthand for `{ description }`. It is put
|
|
48
|
+
* through the object schema so both branches produce the same output type,
|
|
49
|
+
* rather than a union that callers have to narrow before reading `remedyBy`.
|
|
50
|
+
*/
|
|
51
|
+
const knownIssueSchema = union([pipe(transform(string({ min: 1 }), (description) => ({ description })), knownIssueObject), knownIssueObject], "expected a description, or an object with one");
|
|
52
|
+
const configSchema = object({
|
|
53
|
+
site: object({
|
|
54
|
+
name: string({ min: 1 }),
|
|
55
|
+
url: url(),
|
|
56
|
+
/** BCP 47 tag of the site itself, e.g. 'de-AT'. */
|
|
57
|
+
locale: string({ min: 2 })
|
|
58
|
+
}),
|
|
59
|
+
provider: object({
|
|
60
|
+
/** The legal entity answerable for the service. */
|
|
61
|
+
legalName: string({ min: 1 }),
|
|
62
|
+
/**
|
|
63
|
+
* The feedback address. Required: the EAA obliges providers to offer a way
|
|
64
|
+
* to report accessibility barriers, and a statement without one is not
|
|
65
|
+
* usable for its purpose.
|
|
66
|
+
*/
|
|
67
|
+
email: email(),
|
|
68
|
+
phone: optional(string({ min: 1 })),
|
|
69
|
+
address: optional(string({ min: 1 })),
|
|
70
|
+
/**
|
|
71
|
+
* A contact or feedback form, offered alongside the address rather than
|
|
72
|
+
* instead of it: the EAA requires a way to report barriers, and a form is
|
|
73
|
+
* the one channel a visitor who cannot use email may still be able to use.
|
|
74
|
+
*/
|
|
75
|
+
feedbackUrl: optional(url())
|
|
76
|
+
}),
|
|
77
|
+
compliance: object({
|
|
78
|
+
status: enumeration(COMPLIANCE_STATUSES),
|
|
79
|
+
standard: withDefault(string({ min: 1 }), () => "EN 301 549 V3.2.1 (WCAG 2.2 AA)"),
|
|
80
|
+
knownIssues: withDefault(array(knownIssueSchema), () => []),
|
|
81
|
+
/** When the assessment was carried out. */
|
|
82
|
+
assessedOn: isoDate(),
|
|
83
|
+
assessmentMethod: withDefault(enumeration(ASSESSMENT_METHODS), () => "self-assessment"),
|
|
84
|
+
/**
|
|
85
|
+
* Reason attached to barriers taken from an audit report, which carries no
|
|
86
|
+
* reason of its own. 'fix-planned' is the honest default for a barrier an
|
|
87
|
+
* automated run just found; the other two are claims only a human can make.
|
|
88
|
+
*/
|
|
89
|
+
auditReason: withDefault(enumeration(ISSUE_REASONS), () => "fix-planned")
|
|
90
|
+
}),
|
|
91
|
+
enforcement: object({
|
|
92
|
+
/** Drives which supervisory body and statute the template names. */
|
|
93
|
+
country: enumeration(COUNTRIES) })
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* Identity function that gives `eaa.config.ts` its types. Deliberately does not
|
|
97
|
+
* validate: a config file is loaded and checked in one place, so that an error
|
|
98
|
+
* points at the file rather than at wherever the module happened to be
|
|
99
|
+
* imported.
|
|
100
|
+
*/
|
|
101
|
+
function defineConfig(config) {
|
|
102
|
+
return config;
|
|
103
|
+
}
|
|
104
|
+
var ConfigError = class extends Error {
|
|
105
|
+
issues;
|
|
106
|
+
name = "ConfigError";
|
|
107
|
+
constructor(message, issues = []) {
|
|
108
|
+
super(message);
|
|
109
|
+
this.issues = issues;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
/** Validate an already-loaded config object. */
|
|
113
|
+
function parseConfig(value, source = "config") {
|
|
114
|
+
const result = safeParse(configSchema, value);
|
|
115
|
+
if (result.success) return result.data;
|
|
116
|
+
const issues = result.error.issues.map((issue) => {
|
|
117
|
+
const path = issue.path.join(".");
|
|
118
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
119
|
+
});
|
|
120
|
+
throw new ConfigError(`${source} is not valid`, issues);
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/config/load.ts
|
|
124
|
+
/** Checked in this order, first match wins. */
|
|
125
|
+
const CONFIG_FILENAMES = [
|
|
126
|
+
"eaa.config.ts",
|
|
127
|
+
"eaa.config.mts",
|
|
128
|
+
"eaa.config.js",
|
|
129
|
+
"eaa.config.mjs",
|
|
130
|
+
"eaa.config.json"
|
|
131
|
+
];
|
|
132
|
+
/**
|
|
133
|
+
* Find and load `eaa.config.{ts,mts,js,mjs,json}`.
|
|
134
|
+
*
|
|
135
|
+
* TypeScript configs are imported directly: Node strips types natively from
|
|
136
|
+
* 22.18 onwards, which is below this package's floor, so no bundler or loader
|
|
137
|
+
* dependency is needed. The failure mode that remains is a project with no
|
|
138
|
+
* package.json at all, where Node cannot tell ESM from CommonJS; the error says
|
|
139
|
+
* so rather than surfacing "Unexpected token 'export'".
|
|
140
|
+
*/
|
|
141
|
+
async function loadConfig(options = {}) {
|
|
142
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
143
|
+
const file = options.path ? path.resolve(cwd, options.path) : await findConfigFile(cwd);
|
|
144
|
+
if (!file) throw new ConfigError(`No config file found in ${cwd} or its parent directories`, CONFIG_FILENAMES.map((name) => `looked for ${name}`));
|
|
145
|
+
if (!await isFile(file)) throw new ConfigError(`Config file not found: ${file}`);
|
|
146
|
+
return {
|
|
147
|
+
config: parseConfig(file.endsWith(".json") ? await importJson(file) : await importModule(file), path.basename(file)),
|
|
148
|
+
path: file
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** Walks up from `cwd`, so the CLI works from a subdirectory of the project. */
|
|
152
|
+
async function findConfigFile(cwd) {
|
|
153
|
+
let directory = path.resolve(cwd);
|
|
154
|
+
while (true) {
|
|
155
|
+
for (const name of CONFIG_FILENAMES) {
|
|
156
|
+
const candidate = path.join(directory, name);
|
|
157
|
+
if (await isFile(candidate)) return candidate;
|
|
158
|
+
}
|
|
159
|
+
const parent = path.dirname(directory);
|
|
160
|
+
if (parent === directory) return void 0;
|
|
161
|
+
directory = parent;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function importJson(file) {
|
|
165
|
+
const raw = await readFile(file, "utf8");
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(raw);
|
|
168
|
+
} catch (cause) {
|
|
169
|
+
throw new ConfigError(`${path.basename(file)} is not valid JSON`, [cause instanceof Error ? cause.message : String(cause)]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function importModule(file) {
|
|
173
|
+
let module;
|
|
174
|
+
try {
|
|
175
|
+
module = await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
|
|
176
|
+
} catch (cause) {
|
|
177
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
178
|
+
const hint = message.includes("Unexpected token") ? "If the project has no package.json, Node cannot tell ESM from CommonJS. Add one with \"type\": \"module\", or use eaa.config.json." : message;
|
|
179
|
+
throw new ConfigError(`Could not load ${path.basename(file)}`, [hint]);
|
|
180
|
+
}
|
|
181
|
+
if (module.default === void 0) throw new ConfigError(`${path.basename(file)} has no default export`, ["Expected: export default defineConfig({ … })"]);
|
|
182
|
+
return module.default;
|
|
183
|
+
}
|
|
184
|
+
async function isFile(candidate) {
|
|
185
|
+
try {
|
|
186
|
+
return (await stat(candidate)).isFile();
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
export { COMPLIANCE_STATUSES as a, ISSUE_REASONS as c, defineConfig as d, parseConfig as f, ASSESSMENT_METHODS as i, STATEMENT_LOCALES as l, findConfigFile as n, COUNTRIES as o, loadConfig as r, ConfigError as s, CONFIG_FILENAMES as t, configSchema as u };
|
|
@@ -144,7 +144,7 @@ async function runBrowserAudit(directory, pages, options = {}) {
|
|
|
144
144
|
const tags = options.tags ?? DEFAULT_TAGS;
|
|
145
145
|
const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
146
146
|
const viewport = options.viewport ?? DEFAULT_VIEWPORT;
|
|
147
|
-
const server = await serveDirectory(directory);
|
|
147
|
+
const server = directory === void 0 ? void 0 : await serveDirectory(directory);
|
|
148
148
|
const browser = await chromium.launch({ headless: true });
|
|
149
149
|
try {
|
|
150
150
|
const context = await browser.newContext({
|
|
@@ -152,7 +152,7 @@ async function runBrowserAudit(directory, pages, options = {}) {
|
|
|
152
152
|
bypassCSP: true
|
|
153
153
|
});
|
|
154
154
|
const audits = [];
|
|
155
|
-
for (const page of pages) audits.push(await auditOne(context, server
|
|
155
|
+
for (const page of pages) audits.push(await auditOne(context, server?.origin, page, {
|
|
156
156
|
tags,
|
|
157
157
|
timeout,
|
|
158
158
|
...options
|
|
@@ -161,7 +161,7 @@ async function runBrowserAudit(directory, pages, options = {}) {
|
|
|
161
161
|
return audits;
|
|
162
162
|
} finally {
|
|
163
163
|
await browser.close();
|
|
164
|
-
await server
|
|
164
|
+
await server?.close();
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
async function auditOne(context, origin, page, options) {
|
|
@@ -175,7 +175,7 @@ async function auditOne(context, origin, page, options) {
|
|
|
175
175
|
const tab = await context.newPage();
|
|
176
176
|
try {
|
|
177
177
|
tab.setDefaultTimeout(options.timeout);
|
|
178
|
-
const target = servedUrl(origin, page.relativePath);
|
|
178
|
+
const target = origin === void 0 ? page.absolutePath : servedUrl(origin, page.relativePath);
|
|
179
179
|
const response = await tab.goto(target, {
|
|
180
180
|
waitUntil: "load",
|
|
181
181
|
timeout: options.timeout
|