@casoon/astro-post-audit 0.2.4 → 0.2.6

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.
Binary file
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=e2e.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e2e.integration.test.d.ts","sourceRoot":"","sources":["../src/e2e.integration.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,138 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { execFileSync } from "node:child_process";
4
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, symlinkSync, writeFileSync, } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+ const here = dirname(fileURLToPath(import.meta.url));
9
+ const packageRoot = dirname(here);
10
+ const packageNodeModules = join(packageRoot, "node_modules");
11
+ const astroCli = join(packageNodeModules, "astro", "astro.js");
12
+ const integrationUrl = pathToFileURL(join(here, "integration.js")).href;
13
+ function writeProject(root, pageContent, optionsLiteral) {
14
+ mkdirSync(join(root, "src", "pages"), { recursive: true });
15
+ symlinkSync(packageNodeModules, join(root, "node_modules"), "junction");
16
+ writeFileSync(join(root, "astro.config.mjs"), `import { defineConfig } from "astro/config";
17
+ import postAudit from "${integrationUrl}";
18
+
19
+ export default defineConfig({
20
+ site: "https://example.com",
21
+ integrations: [postAudit(${optionsLiteral})],
22
+ });`);
23
+ writeFileSync(join(root, "src", "pages", "index.astro"), pageContent);
24
+ }
25
+ function runAstroBuild(cwd, envOverrides = {}) {
26
+ try {
27
+ const out = execFileSync(process.execPath, [astroCli, "build"], {
28
+ cwd,
29
+ stdio: ["ignore", "pipe", "pipe"],
30
+ encoding: "utf-8",
31
+ env: { ...process.env, NO_COLOR: "1", ...envOverrides },
32
+ });
33
+ return { ok: true, output: out };
34
+ }
35
+ catch (err) {
36
+ const stdout = err && typeof err === "object" && "stdout" in err
37
+ ? String(err.stdout ?? "")
38
+ : "";
39
+ const stderr = err && typeof err === "object" && "stderr" in err
40
+ ? String(err.stderr ?? "")
41
+ : "";
42
+ return { ok: false, output: `${stdout}\n${stderr}` };
43
+ }
44
+ }
45
+ describe("postAudit e2e", () => {
46
+ it("passes on a valid Astro build fixture", () => {
47
+ const root = mkdtempSync(join(tmpdir(), "astro-post-audit-good-"));
48
+ writeProject(root, `---
49
+ const title = "Home";
50
+ ---
51
+ <html lang="en">
52
+ <head>
53
+ <meta charset="utf-8" />
54
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
55
+ <title>{title}</title>
56
+ <link rel="canonical" href="https://example.com/" />
57
+ </head>
58
+ <body>
59
+ <h1>Home</h1>
60
+ <a href="/">Home</a>
61
+ </body>
62
+ </html>`, "{ throwOnError: true }");
63
+ const result = runAstroBuild(root);
64
+ assert.equal(result.ok, true, result.output);
65
+ });
66
+ it("fails build on invalid fixture when throwOnError is enabled", () => {
67
+ const root = mkdtempSync(join(tmpdir(), "astro-post-audit-bad-"));
68
+ writeProject(root, `---
69
+ const title = "";
70
+ ---
71
+ <html>
72
+ <head>
73
+ <meta charset="utf-8" />
74
+ <title>{title}</title>
75
+ </head>
76
+ <body>
77
+ <h1></h1>
78
+ <img src="/x.png" />
79
+ </body>
80
+ </html>`, "{ throwOnError: true }");
81
+ const result = runAstroBuild(root);
82
+ assert.equal(result.ok, false, "build should fail for invalid fixture");
83
+ assert.match(result.output, /astro-post-audit found issues/i);
84
+ });
85
+ it("writes JSON report when output option is set", () => {
86
+ const root = mkdtempSync(join(tmpdir(), "astro-post-audit-output-"));
87
+ writeProject(root, `---
88
+ const title = "";
89
+ ---
90
+ <html>
91
+ <head>
92
+ <meta charset="utf-8" />
93
+ <title>{title}</title>
94
+ </head>
95
+ <body><h1>Home</h1></body>
96
+ </html>`, "{ throwOnError: false, output: 'audit-report.json' }");
97
+ const result = runAstroBuild(root);
98
+ assert.equal(result.ok, true, result.output);
99
+ const reportPath = join(root, "audit-report.json");
100
+ assert.equal(existsSync(reportPath), true, "report should exist");
101
+ const report = JSON.parse(readFileSync(reportPath, "utf-8"));
102
+ assert.ok(report.summary);
103
+ assert.ok(Array.isArray(report.findings));
104
+ });
105
+ it("fails on warnings when strict is enabled", () => {
106
+ const root = mkdtempSync(join(tmpdir(), "astro-post-audit-strict-"));
107
+ writeProject(root, `---
108
+ const title = "This title is intentionally much longer than sixty characters to trigger warning";
109
+ ---
110
+ <html lang="en">
111
+ <head>
112
+ <meta charset="utf-8" />
113
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
114
+ <title>{title}</title>
115
+ <link rel="canonical" href="https://example.com/" />
116
+ </head>
117
+ <body><h1>Home</h1></body>
118
+ </html>`, "{ throwOnError: true, strict: true }");
119
+ const result = runAstroBuild(root);
120
+ assert.equal(result.ok, false, "strict mode should fail on warning");
121
+ assert.match(result.output, /found issues/i);
122
+ });
123
+ it("skips audit when SKIP_AUDIT=1", () => {
124
+ const root = mkdtempSync(join(tmpdir(), "astro-post-audit-skip-"));
125
+ writeProject(root, `---
126
+ const title = "";
127
+ ---
128
+ <html>
129
+ <head>
130
+ <title>{title}</title>
131
+ </head>
132
+ <body><img src="/x.png" /></body>
133
+ </html>`, "{ throwOnError: true }");
134
+ const result = runAstroBuild(root, { SKIP_AUDIT: "1" });
135
+ assert.equal(result.ok, true, result.output);
136
+ assert.match(result.output, /Audit skipped/i);
137
+ });
138
+ });
@@ -1,4 +1,6 @@
1
- import type { AstroIntegration } from 'astro';
1
+ import type { AstroIntegration } from "astro";
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync, writeFileSync } from "node:fs";
2
4
  /**
3
5
  * Inline rules config that mirrors the Rust config structure.
4
6
  * All sections and fields are optional — only set what you want to override.
@@ -19,9 +21,9 @@ export interface RulesConfig {
19
21
  /** URL normalization rules for internal link and canonical consistency. */
20
22
  url_normalization?: {
21
23
  /** Trailing slash policy. `"always"`: require trailing slash, `"never"`: forbid, `"ignore"`: no check. @default "always" */
22
- trailing_slash?: 'always' | 'never' | 'ignore';
24
+ trailing_slash?: "always" | "never" | "ignore";
23
25
  /** Whether `index.html` in URLs is allowed. `"forbid"`: warn on `/page/index.html` links, `"allow"`: permit them. @default "forbid" */
24
- index_html?: 'forbid' | 'allow';
26
+ index_html?: "forbid" | "allow";
25
27
  };
26
28
  /** Canonical `<link rel="canonical">` tag checks. */
27
29
  canonical?: {
@@ -187,9 +189,9 @@ export interface RulesConfig {
187
189
  };
188
190
  /**
189
191
  * Override severity per rule ID.
190
- * @example `{ "html/title-too-long": "off", "a11y/img-alt-missing": "error" }`
192
+ * @example `{ "html/title-too-long": "off", "a11y/img-alt": "error" }`
191
193
  */
192
- severity?: Record<string, 'error' | 'warning' | 'info' | 'off'>;
194
+ severity?: Record<string, "error" | "warning" | "info" | "off">;
193
195
  /** External link checking (HEAD requests to verify URLs return 2xx). */
194
196
  external_links?: {
195
197
  /** Enable external link checking. @default false */
@@ -205,10 +207,37 @@ export interface RulesConfig {
205
207
  /** Skip links to these domains. */
206
208
  block_domains?: string[];
207
209
  };
210
+ /** I18n consistency audit across route locale, lang, hreflang, and canonical signals. */
211
+ i18n_audit?: {
212
+ /** Enable i18n consistency checks in dist output. @default false */
213
+ enabled?: boolean;
214
+ };
215
+ /** Crawl budget audit for URL variants, indexability mismatches, and duplicate clusters. */
216
+ crawl_budget?: {
217
+ /** Enable crawl budget checks in dist output. @default false */
218
+ enabled?: boolean;
219
+ };
220
+ /** Static render-blocking audit for critical resources and connection hints. */
221
+ render_blocking?: {
222
+ /** Enable render-blocking checks in dist output. @default false */
223
+ enabled?: boolean;
224
+ };
225
+ /** Static privacy and security posture audit (third-party, SRI, CSP readiness, consent indicators). */
226
+ privacy_security?: {
227
+ /** Enable privacy/security checks in dist output. @default false */
228
+ enabled?: boolean;
229
+ };
230
+ /** Cross-page structured-data graph consistency checks. */
231
+ structured_data_graph?: {
232
+ /** Enable cross-page JSON-LD consistency checks in dist output. @default false */
233
+ enabled?: boolean;
234
+ };
208
235
  }
209
236
  export interface PostAuditOptions {
210
237
  /** Inline rules config — all check settings go here. */
211
238
  rules?: RulesConfig;
239
+ /** Preset to apply before user overrides. `"strict"` enables all checks, `"relaxed"` is lenient. */
240
+ preset?: "strict" | "relaxed";
212
241
  /** Base URL (auto-detected from Astro's `site` config if not set). */
213
242
  site?: string;
214
243
  /** Treat warnings as errors. */
@@ -219,10 +248,18 @@ export interface PostAuditOptions {
219
248
  pageOverview?: boolean;
220
249
  /** Write the JSON report to this file path (relative to project root). */
221
250
  output?: string;
251
+ /** Print per-check timing benchmarks in the output. */
252
+ benchmark?: boolean;
222
253
  /** Disable the integration (useful for dev mode). */
223
254
  disable?: boolean;
224
255
  /** Throw an error when the audit finds issues (fails the build). Default: false */
225
256
  throwOnError?: boolean;
226
257
  }
227
- export default function postAudit(options?: PostAuditOptions): AstroIntegration;
258
+ interface RuntimeDeps {
259
+ execFileSync: typeof execFileSync;
260
+ existsSync: typeof existsSync;
261
+ writeFileSync: typeof writeFileSync;
262
+ }
263
+ export default function postAudit(options?: PostAuditOptions, deps?: RuntimeDeps): AstroIntegration;
264
+ export {};
228
265
  //# sourceMappingURL=integration.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"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"}
@@ -1,39 +1,69 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { existsSync, writeFileSync } from 'node:fs';
3
- import { dirname, join } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- function resolveBinaryPath() {
6
- const binDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin');
7
- const binaryName = process.platform === 'win32' ? 'astro-post-audit.exe' : 'astro-post-audit';
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const defaultDeps = {
6
+ execFileSync,
7
+ existsSync,
8
+ writeFileSync,
9
+ };
10
+ function resolveBinaryPath(exists) {
11
+ const binDir = join(dirname(fileURLToPath(import.meta.url)), "..", "bin");
12
+ const binaryName = process.platform === "win32" ? "astro-post-audit.exe" : "astro-post-audit";
8
13
  const binaryPath = join(binDir, binaryName);
9
- return existsSync(binaryPath) ? binaryPath : null;
14
+ return exists(binaryPath) ? binaryPath : null;
10
15
  }
11
- export default function postAudit(options = {}) {
16
+ function supportsConfigStdin(binaryPath, run) {
17
+ try {
18
+ const help = run(binaryPath, ["--help"], {
19
+ stdio: ["ignore", "pipe", "ignore"],
20
+ encoding: "utf-8",
21
+ });
22
+ return typeof help === "string" && help.includes("--config-stdin");
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ export default function postAudit(options = {}, deps = defaultDeps) {
12
29
  let siteUrl;
30
+ let astroTrailingSlash;
13
31
  return {
14
- name: 'astro-post-audit',
32
+ name: "astro-post-audit",
15
33
  hooks: {
16
- 'astro:config:done': ({ config }) => {
34
+ "astro:config:done": ({ config }) => {
17
35
  siteUrl = config.site?.toString();
36
+ // Bridge Astro's trailingSlash config automatically
37
+ if (config.trailingSlash) {
38
+ astroTrailingSlash = config.trailingSlash;
39
+ }
18
40
  },
19
- 'astro:build:done': ({ dir, logger }) => {
20
- if (options.disable || process.env.SKIP_AUDIT === '1' || process.env.SKIP_AUDIT === 'true') {
41
+ "astro:build:done": ({ dir, logger }) => {
42
+ if (options.disable ||
43
+ process.env.SKIP_AUDIT === "1" ||
44
+ process.env.SKIP_AUDIT === "true") {
21
45
  if (process.env.SKIP_AUDIT) {
22
- logger.info('Audit skipped (SKIP_AUDIT is set).');
46
+ logger.info("Audit skipped (SKIP_AUDIT is set).");
23
47
  }
24
48
  return;
25
49
  }
26
50
  // Validate that rules is a non-empty object if provided
27
- if (options.rules && typeof options.rules === 'object' && Object.keys(options.rules).length === 0) {
51
+ if (options.rules &&
52
+ typeof options.rules === "object" &&
53
+ Object.keys(options.rules).length === 0) {
28
54
  logger.warn('astro-post-audit: "rules" is an empty object — using default config.');
29
55
  }
30
- const binaryPath = resolveBinaryPath();
56
+ const binaryPath = resolveBinaryPath(deps.existsSync);
31
57
  if (!binaryPath) {
32
58
  logger.warn('astro-post-audit binary not found. Run "npm rebuild @casoon/astro-post-audit".');
33
59
  return;
34
60
  }
61
+ if (!supportsConfigStdin(binaryPath, deps.execFileSync)) {
62
+ logger.error('astro-post-audit binary is outdated and does not support --config-stdin. Run "npm rebuild @casoon/astro-post-audit".');
63
+ return;
64
+ }
35
65
  const distPath = fileURLToPath(dir);
36
- const args = [distPath, '--config-stdin'];
66
+ const args = [distPath, "--config-stdin"];
37
67
  // Build the full JSON config for the Rust binary
38
68
  const site = options.site ?? siteUrl;
39
69
  const stdinConfig = {
@@ -41,49 +71,66 @@ export default function postAudit(options = {}) {
41
71
  };
42
72
  if (site)
43
73
  stdinConfig.site = { base_url: site };
74
+ if (options.preset)
75
+ stdinConfig.preset = options.preset;
76
+ // Auto-bridge trailingSlash from Astro config if not explicitly set in rules
77
+ if (astroTrailingSlash &&
78
+ !options.rules?.url_normalization?.trailing_slash) {
79
+ stdinConfig.url_normalization = {
80
+ ...(stdinConfig.url_normalization ??
81
+ {}),
82
+ trailing_slash: astroTrailingSlash,
83
+ };
84
+ }
44
85
  if (options.strict)
45
86
  stdinConfig.strict = true;
87
+ if (options.benchmark)
88
+ stdinConfig.benchmark = true;
46
89
  if (options.maxErrors != null)
47
90
  stdinConfig.max_errors = options.maxErrors;
48
91
  if (options.pageOverview)
49
92
  stdinConfig.page_overview = true;
50
93
  if (options.output)
51
- stdinConfig.format = 'json';
94
+ stdinConfig.format = "json";
52
95
  const stdinInput = JSON.stringify(stdinConfig);
53
- logger.info('Running post-build audit...');
96
+ logger.info("Running post-build audit...");
54
97
  const captureOutput = !!options.output;
55
98
  try {
56
- const result = execFileSync(binaryPath, args, {
57
- stdio: ['pipe', captureOutput ? 'pipe' : 'inherit', 'inherit'],
99
+ const result = deps.execFileSync(binaryPath, args, {
100
+ stdio: ["pipe", captureOutput ? "pipe" : "inherit", "inherit"],
58
101
  input: stdinInput,
59
- encoding: captureOutput ? 'utf-8' : undefined,
102
+ encoding: captureOutput ? "utf-8" : undefined,
60
103
  });
61
104
  if (captureOutput && result) {
62
- writeFileSync(options.output, result);
105
+ deps.writeFileSync(options.output, result);
63
106
  logger.info(`Report written to ${options.output}`);
64
107
  }
65
- logger.info('All checks passed!');
108
+ logger.info("All checks passed!");
66
109
  }
67
110
  catch (err) {
68
- const exitCode = err && typeof err === 'object' && 'status' in err
111
+ const exitCode = err && typeof err === "object" && "status" in err
69
112
  ? err.status
70
113
  : undefined;
71
114
  // Write output file even on exit code 1 (findings exist but run succeeded)
72
- if (captureOutput && exitCode === 1 && err && typeof err === 'object' && 'stdout' in err) {
115
+ if (captureOutput &&
116
+ exitCode === 1 &&
117
+ err &&
118
+ typeof err === "object" &&
119
+ "stdout" in err) {
73
120
  const stdout = err.stdout;
74
121
  if (stdout) {
75
- writeFileSync(options.output, stdout);
122
+ deps.writeFileSync(options.output, stdout);
76
123
  logger.info(`Report written to ${options.output}`);
77
124
  }
78
125
  }
79
126
  if (exitCode === 1) {
80
127
  if (options.throwOnError) {
81
- throw new Error('astro-post-audit found issues. See output above.');
128
+ throw new Error("astro-post-audit found issues. See output above.");
82
129
  }
83
- logger.warn('Audit found issues. See output above.');
130
+ logger.warn("Audit found issues. See output above.");
84
131
  }
85
132
  else {
86
- logger.error(`Audit failed with exit code ${exitCode ?? 'unknown'}`);
133
+ logger.error(`Audit failed with exit code ${exitCode ?? "unknown"}`);
87
134
  }
88
135
  }
89
136
  },
@@ -1,52 +1,84 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import postAudit from './integration.js';
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import postAudit from "./integration.js";
4
+ function makeLogger() {
5
+ const info = [];
6
+ const warn = [];
7
+ const error = [];
8
+ return {
9
+ logger: {
10
+ info: (msg) => info.push(msg),
11
+ warn: (msg) => warn.push(msg),
12
+ error: (msg) => error.push(msg),
13
+ },
14
+ info,
15
+ warn,
16
+ error,
17
+ };
18
+ }
19
+ function makeExecMock(impl) {
20
+ return ((file, argsOrOptions) => {
21
+ const args = Array.isArray(argsOrOptions) ? argsOrOptions : [];
22
+ return impl(file, args);
23
+ });
24
+ }
4
25
  // ==========================================================================
5
26
  // postAudit integration factory
6
27
  // ==========================================================================
7
- describe('postAudit', () => {
8
- it('returns an AstroIntegration with correct name', () => {
28
+ describe("postAudit", () => {
29
+ it("returns an AstroIntegration with correct name", () => {
9
30
  const integration = postAudit();
10
- assert.equal(integration.name, 'astro-post-audit');
31
+ assert.equal(integration.name, "astro-post-audit");
11
32
  assert.ok(integration.hooks);
12
33
  });
13
- it('accepts empty options', () => {
34
+ it("accepts empty options", () => {
14
35
  const integration = postAudit({});
15
- assert.equal(integration.name, 'astro-post-audit');
36
+ assert.equal(integration.name, "astro-post-audit");
16
37
  });
17
- it('accepts all option types', () => {
38
+ it("accepts all option types", () => {
18
39
  const options = {
19
40
  strict: true,
20
41
  maxErrors: 5,
21
42
  pageOverview: false,
22
- output: 'audit-report.json',
43
+ output: "audit-report.json",
23
44
  disable: false,
24
45
  throwOnError: true,
25
46
  rules: { canonical: { require: true } },
26
47
  };
27
48
  const integration = postAudit(options);
28
- assert.equal(integration.name, 'astro-post-audit');
49
+ assert.equal(integration.name, "astro-post-audit");
29
50
  });
30
- it('does not throw when only rules is set', () => {
51
+ it("does not throw when only rules is set", () => {
52
+ const execCalls = [];
53
+ const deps = {
54
+ existsSync: () => true,
55
+ writeFileSync: () => { },
56
+ execFileSync: makeExecMock((_file, args) => {
57
+ execCalls.push({ args });
58
+ if (args[0] === "--help")
59
+ return "Usage: ... --config-stdin ...";
60
+ return "";
61
+ }),
62
+ };
31
63
  const integration = postAudit({
32
64
  rules: { canonical: { require: true } },
33
- });
34
- const hook = integration.hooks['astro:build:done'];
65
+ }, deps);
66
+ const hook = integration.hooks["astro:build:done"];
67
+ const { logger, error } = makeLogger();
35
68
  assert.doesNotThrow(() => hook({
36
- dir: new URL('file:///tmp/dist/'),
37
- logger: {
38
- info: () => { },
39
- warn: () => { },
40
- error: () => { },
41
- },
69
+ dir: new URL("file:///tmp/dist/"),
70
+ logger,
42
71
  }));
72
+ assert.equal(error.length, 0);
73
+ assert.ok(execCalls.some((c) => c.args[0] === "--help"));
74
+ assert.ok(execCalls.some((c) => c.args.includes("--config-stdin")));
43
75
  });
44
- it('skips execution when disabled', () => {
76
+ it("skips execution when disabled", () => {
45
77
  const integration = postAudit({ disable: true });
46
- const hook = integration.hooks['astro:build:done'];
78
+ const hook = integration.hooks["astro:build:done"];
47
79
  // Should return immediately without doing anything
48
80
  assert.doesNotThrow(() => hook({
49
- dir: new URL('file:///tmp/dist/'),
81
+ dir: new URL("file:///tmp/dist/"),
50
82
  logger: {
51
83
  info: () => { },
52
84
  warn: () => { },
@@ -54,4 +86,28 @@ describe('postAudit', () => {
54
86
  },
55
87
  }));
56
88
  });
89
+ it("logs an error and skips when binary is outdated", () => {
90
+ const execCalls = [];
91
+ const deps = {
92
+ existsSync: () => true,
93
+ writeFileSync: () => { },
94
+ execFileSync: makeExecMock((_file, args) => {
95
+ execCalls.push({ args });
96
+ if (args[0] === "--help")
97
+ return "Usage: ... --config <CONFIG> ...";
98
+ return "";
99
+ }),
100
+ };
101
+ const integration = postAudit({}, deps);
102
+ const hook = integration.hooks["astro:build:done"];
103
+ const { logger, error } = makeLogger();
104
+ hook({
105
+ dir: new URL("file:///tmp/dist/"),
106
+ logger,
107
+ });
108
+ assert.equal(execCalls.filter((c) => c.args[0] === "--help").length, 1);
109
+ assert.equal(execCalls.filter((c) => c.args.includes("--config-stdin")).length, 0);
110
+ assert.equal(error.length, 1);
111
+ assert.match(error[0], /outdated/i);
112
+ });
57
113
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casoon/astro-post-audit",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Astro integration for post-build auditing: SEO, links, and lightweight WCAG checks",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,20 +10,24 @@
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js"
13
- }
13
+ },
14
+ "./schema": "./schema.json"
14
15
  },
15
16
  "bin": {
16
17
  "astro-post-audit": "bin/run.cjs"
17
18
  },
18
19
  "scripts": {
19
- "build": "tsc",
20
- "test": "tsc && node --test dist/integration.test.js",
20
+ "build": "tsc && npm run generate-schema",
21
+ "generate-schema": "typescript-json-schema tsconfig.json PostAuditOptions --noExtraProps -o schema.json",
22
+ "verify:binary": "node scripts/verify-binary.cjs",
23
+ "test": "tsc && node --test dist/*.test.js",
21
24
  "prepublishOnly": "tsc",
22
25
  "postinstall": "node bin/install.cjs"
23
26
  },
24
27
  "files": [
25
28
  "bin/",
26
- "dist/"
29
+ "dist/",
30
+ "schema.json"
27
31
  ],
28
32
  "keywords": [
29
33
  "astro",
@@ -58,6 +62,7 @@
58
62
  "devDependencies": {
59
63
  "@types/node": "^22.10.1",
60
64
  "astro": "^5.0.0",
61
- "typescript": "^5.9.3"
65
+ "typescript": "^5.9.3",
66
+ "typescript-json-schema": "^0.67.1"
62
67
  }
63
68
  }
package/schema.json ADDED
@@ -0,0 +1,567 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "additionalProperties": false,
4
+ "definitions": {
5
+ "SeverityOverrides": {
6
+ "additionalProperties": {
7
+ "enum": ["error", "warning", "info", "off"],
8
+ "type": "string"
9
+ },
10
+ "description": "Map of rule IDs to severity levels.",
11
+ "type": "object"
12
+ },
13
+ "RulesConfig": {
14
+ "additionalProperties": false,
15
+ "description": "Inline rules config that mirrors the Rust config structure.\nAll sections and fields are optional — only set what you want to override.",
16
+ "properties": {
17
+ "a11y": {
18
+ "additionalProperties": false,
19
+ "description": "Accessibility (a11y) heuristics — static checks, no layout computation.",
20
+ "properties": {
21
+ "a_accessible_name_required": {
22
+ "default": true,
23
+ "description": "`<a>` elements must have an accessible name (text, `aria-label`, or `aria-labelledby`).",
24
+ "type": "boolean"
25
+ },
26
+ "allow_decorative_images": {
27
+ "default": true,
28
+ "description": "Allow images with `role=\"presentation\"` or `aria-hidden=\"true\"` to skip `alt`.",
29
+ "type": "boolean"
30
+ },
31
+ "aria_hidden_focusable_check": {
32
+ "default": true,
33
+ "description": "Warn if `aria-hidden=\"true\"` is set on a focusable element.",
34
+ "type": "boolean"
35
+ },
36
+ "button_name_required": {
37
+ "default": true,
38
+ "description": "`<button>` elements must have an accessible name.",
39
+ "type": "boolean"
40
+ },
41
+ "img_alt_required": {
42
+ "default": true,
43
+ "description": "`<img>` elements must have an `alt` attribute.",
44
+ "type": "boolean"
45
+ },
46
+ "label_for_required": {
47
+ "default": true,
48
+ "description": "Form controls must have an associated `<label>`.",
49
+ "type": "boolean"
50
+ },
51
+ "require_skip_link": {
52
+ "default": false,
53
+ "description": "Require a skip navigation link (e.g. `<a href=\"#main-content\">`).",
54
+ "type": "boolean"
55
+ },
56
+ "warn_generic_link_text": {
57
+ "default": true,
58
+ "description": "Warn on generic link text like \"click here\", \"mehr\", \"weiter\".",
59
+ "type": "boolean"
60
+ }
61
+ },
62
+ "type": "object"
63
+ },
64
+ "assets": {
65
+ "additionalProperties": false,
66
+ "description": "Asset reference and size checks.",
67
+ "properties": {
68
+ "check_broken_assets": {
69
+ "default": false,
70
+ "description": "Check that `<img>`, `<script>`, `<link>` references resolve to files in `dist/`.",
71
+ "type": "boolean"
72
+ },
73
+ "check_image_dimensions": {
74
+ "default": false,
75
+ "description": "Warn if `<img>` is missing `width`/`height` attributes (CLS prevention).",
76
+ "type": "boolean"
77
+ },
78
+ "max_css_size_kb": {
79
+ "description": "Warn if any CSS file exceeds this size in KB. Off by default.",
80
+ "type": "number"
81
+ },
82
+ "max_image_size_kb": {
83
+ "description": "Warn if any image file exceeds this size in KB. Off by default.",
84
+ "type": "number"
85
+ },
86
+ "max_js_size_kb": {
87
+ "description": "Warn if any JS file exceeds this size in KB. Off by default.",
88
+ "type": "number"
89
+ },
90
+ "require_hashed_filenames": {
91
+ "default": false,
92
+ "description": "Warn if asset filenames lack a cache-busting hash.",
93
+ "type": "boolean"
94
+ }
95
+ },
96
+ "type": "object"
97
+ },
98
+ "canonical": {
99
+ "additionalProperties": false,
100
+ "description": "Canonical `<link rel=\"canonical\">` tag checks.",
101
+ "properties": {
102
+ "absolute": {
103
+ "default": true,
104
+ "description": "Canonical URL must be absolute (not relative).",
105
+ "type": "boolean"
106
+ },
107
+ "detect_clusters": {
108
+ "default": true,
109
+ "description": "Warn when multiple pages share the same canonical URL (cluster detection).",
110
+ "type": "boolean"
111
+ },
112
+ "require": {
113
+ "default": true,
114
+ "description": "Every page must have a canonical tag.",
115
+ "type": "boolean"
116
+ },
117
+ "same_origin": {
118
+ "default": true,
119
+ "description": "Canonical must point to the same origin as `site`.",
120
+ "type": "boolean"
121
+ },
122
+ "self_reference": {
123
+ "default": false,
124
+ "description": "Canonical must be a self-reference (point to the page itself).",
125
+ "type": "boolean"
126
+ }
127
+ },
128
+ "type": "object"
129
+ },
130
+ "content_quality": {
131
+ "additionalProperties": false,
132
+ "description": "Duplicate content detection.",
133
+ "properties": {
134
+ "detect_duplicate_descriptions": {
135
+ "default": false,
136
+ "description": "Warn if multiple pages share the same meta description.",
137
+ "type": "boolean"
138
+ },
139
+ "detect_duplicate_h1": {
140
+ "default": false,
141
+ "description": "Warn if multiple pages share the same `<h1>`.",
142
+ "type": "boolean"
143
+ },
144
+ "detect_duplicate_pages": {
145
+ "default": false,
146
+ "description": "Warn if pages have identical content (by hash).",
147
+ "type": "boolean"
148
+ },
149
+ "detect_duplicate_titles": {
150
+ "default": false,
151
+ "description": "Warn if multiple pages share the same `<title>`.",
152
+ "type": "boolean"
153
+ }
154
+ },
155
+ "type": "object"
156
+ },
157
+ "external_links": {
158
+ "additionalProperties": false,
159
+ "description": "External link checking (HEAD requests to verify URLs return 2xx).",
160
+ "properties": {
161
+ "allow_domains": {
162
+ "description": "Only check links to these domains (empty = all).",
163
+ "items": {
164
+ "type": "string"
165
+ },
166
+ "type": "array"
167
+ },
168
+ "block_domains": {
169
+ "description": "Skip links to these domains.",
170
+ "items": {
171
+ "type": "string"
172
+ },
173
+ "type": "array"
174
+ },
175
+ "enabled": {
176
+ "default": false,
177
+ "description": "Enable external link checking.",
178
+ "type": "boolean"
179
+ },
180
+ "fail_on_broken": {
181
+ "default": false,
182
+ "description": "Broken external links are errors (not just warnings).",
183
+ "type": "boolean"
184
+ },
185
+ "max_concurrent": {
186
+ "default": 10,
187
+ "description": "Maximum concurrent requests.",
188
+ "type": "number"
189
+ },
190
+ "timeout_ms": {
191
+ "default": 3000,
192
+ "description": "Timeout per request in milliseconds.",
193
+ "type": "number"
194
+ }
195
+ },
196
+ "type": "object"
197
+ },
198
+ "filters": {
199
+ "additionalProperties": false,
200
+ "description": "File filters — glob patterns to include or exclude pages from all checks.",
201
+ "properties": {
202
+ "exclude": {
203
+ "description": "Skip files matching these glob patterns (e.g. `[\"404.html\", \"drafts/**\"]`).",
204
+ "items": {
205
+ "type": "string"
206
+ },
207
+ "type": "array"
208
+ },
209
+ "include": {
210
+ "description": "Only check files matching these glob patterns.",
211
+ "items": {
212
+ "type": "string"
213
+ },
214
+ "type": "array"
215
+ }
216
+ },
217
+ "type": "object"
218
+ },
219
+ "headings": {
220
+ "additionalProperties": false,
221
+ "description": "Heading hierarchy checks.",
222
+ "properties": {
223
+ "no_skip": {
224
+ "default": false,
225
+ "description": "No heading level gaps (e.g. `<h2>` followed by `<h4>`).",
226
+ "type": "boolean"
227
+ },
228
+ "require_h1": {
229
+ "default": true,
230
+ "description": "Page must have at least one `<h1>`.",
231
+ "type": "boolean"
232
+ },
233
+ "single_h1": {
234
+ "default": true,
235
+ "description": "Only one `<h1>` per page.",
236
+ "type": "boolean"
237
+ }
238
+ },
239
+ "type": "object"
240
+ },
241
+ "hreflang": {
242
+ "additionalProperties": false,
243
+ "description": "Hreflang checks for multilingual sites.",
244
+ "properties": {
245
+ "check_hreflang": {
246
+ "default": false,
247
+ "description": "Enable hreflang link checks.",
248
+ "type": "boolean"
249
+ },
250
+ "require_reciprocal": {
251
+ "default": false,
252
+ "description": "Hreflang links must be reciprocal (A→B and B→A).",
253
+ "type": "boolean"
254
+ },
255
+ "require_self_reference": {
256
+ "default": false,
257
+ "description": "Hreflang must include a self-referencing entry.",
258
+ "type": "boolean"
259
+ },
260
+ "require_x_default": {
261
+ "default": false,
262
+ "description": "Require an `x-default` hreflang entry.",
263
+ "type": "boolean"
264
+ }
265
+ },
266
+ "type": "object"
267
+ },
268
+ "html_basics": {
269
+ "additionalProperties": false,
270
+ "description": "Basic HTML structure checks.",
271
+ "properties": {
272
+ "lang_attr_required": {
273
+ "default": true,
274
+ "description": "`<html lang=\"...\">` attribute is required.",
275
+ "type": "boolean"
276
+ },
277
+ "meta_description_max_length": {
278
+ "default": 160,
279
+ "description": "Warn if meta description exceeds this character length.",
280
+ "type": "number"
281
+ },
282
+ "meta_description_required": {
283
+ "default": false,
284
+ "description": "`<meta name=\"description\">` is required.",
285
+ "type": "boolean"
286
+ },
287
+ "title_max_length": {
288
+ "default": 60,
289
+ "description": "Warn if `<title>` exceeds this character length.",
290
+ "type": "number"
291
+ },
292
+ "title_required": {
293
+ "default": true,
294
+ "description": "`<title>` tag is required and must be non-empty.",
295
+ "type": "boolean"
296
+ },
297
+ "viewport_required": {
298
+ "default": true,
299
+ "description": "`<meta name=\"viewport\">` is required.",
300
+ "type": "boolean"
301
+ }
302
+ },
303
+ "type": "object"
304
+ },
305
+ "links": {
306
+ "additionalProperties": false,
307
+ "description": "Internal link consistency checks.",
308
+ "properties": {
309
+ "check_fragments": {
310
+ "default": false,
311
+ "description": "Validate that `#fragment` targets exist in the linked page.",
312
+ "type": "boolean"
313
+ },
314
+ "check_internal": {
315
+ "default": true,
316
+ "description": "Check that internal links resolve to existing pages.",
317
+ "type": "boolean"
318
+ },
319
+ "check_mixed_content": {
320
+ "default": true,
321
+ "description": "Warn on `http://` in internal links (mixed content).",
322
+ "type": "boolean"
323
+ },
324
+ "detect_orphan_pages": {
325
+ "default": false,
326
+ "description": "Warn about pages with no incoming internal links (orphan pages).",
327
+ "type": "boolean"
328
+ },
329
+ "fail_on_broken": {
330
+ "default": true,
331
+ "description": "Broken internal links are errors (not just warnings).",
332
+ "type": "boolean"
333
+ },
334
+ "forbid_query_params_internal": {
335
+ "default": true,
336
+ "description": "Warn on query parameters (`?foo=bar`) in internal links.",
337
+ "type": "boolean"
338
+ }
339
+ },
340
+ "type": "object"
341
+ },
342
+ "opengraph": {
343
+ "additionalProperties": false,
344
+ "description": "Open Graph and Twitter Card meta tag checks.",
345
+ "properties": {
346
+ "require_og_description": {
347
+ "default": false,
348
+ "description": "Require `og:description` meta tag.",
349
+ "type": "boolean"
350
+ },
351
+ "require_og_image": {
352
+ "default": false,
353
+ "description": "Require `og:image` meta tag.",
354
+ "type": "boolean"
355
+ },
356
+ "require_og_title": {
357
+ "default": false,
358
+ "description": "Require `og:title` meta tag.",
359
+ "type": "boolean"
360
+ },
361
+ "require_twitter_card": {
362
+ "default": false,
363
+ "description": "Require `twitter:card` meta tag.",
364
+ "type": "boolean"
365
+ }
366
+ },
367
+ "type": "object"
368
+ },
369
+ "robots_meta": {
370
+ "additionalProperties": false,
371
+ "description": "Robots meta tag checks.",
372
+ "properties": {
373
+ "allow_noindex": {
374
+ "default": true,
375
+ "description": "Don't warn on pages with `noindex`.",
376
+ "type": "boolean"
377
+ },
378
+ "fail_if_noindex": {
379
+ "default": false,
380
+ "description": "Treat any `noindex` page as an error.",
381
+ "type": "boolean"
382
+ }
383
+ },
384
+ "type": "object"
385
+ },
386
+ "robots_txt": {
387
+ "additionalProperties": false,
388
+ "description": "`robots.txt` file checks.",
389
+ "properties": {
390
+ "require": {
391
+ "default": false,
392
+ "description": "`robots.txt` must exist.",
393
+ "type": "boolean"
394
+ },
395
+ "require_sitemap_link": {
396
+ "default": false,
397
+ "description": "`robots.txt` must contain a link to the sitemap.",
398
+ "type": "boolean"
399
+ }
400
+ },
401
+ "type": "object"
402
+ },
403
+ "security": {
404
+ "additionalProperties": false,
405
+ "description": "Security heuristic checks.",
406
+ "properties": {
407
+ "check_mixed_content": {
408
+ "default": true,
409
+ "description": "Warn on `http://` resource URLs (mixed content).",
410
+ "type": "boolean"
411
+ },
412
+ "check_target_blank": {
413
+ "default": true,
414
+ "description": "Warn on `target=\"_blank\"` without `rel=\"noopener\"`.",
415
+ "type": "boolean"
416
+ },
417
+ "warn_inline_scripts": {
418
+ "default": false,
419
+ "description": "Warn on inline `<script>` tags.",
420
+ "type": "boolean"
421
+ }
422
+ },
423
+ "type": "object"
424
+ },
425
+ "severity": {
426
+ "$ref": "#/definitions/SeverityOverrides",
427
+ "description": "Override severity per rule ID."
428
+ },
429
+ "site": {
430
+ "additionalProperties": false,
431
+ "description": "Site-level settings.",
432
+ "properties": {
433
+ "base_url": {
434
+ "description": "Base URL for canonical/sitemap checks. Also settable via `site` option or Astro's `site` config.",
435
+ "type": "string"
436
+ }
437
+ },
438
+ "type": "object"
439
+ },
440
+ "sitemap": {
441
+ "additionalProperties": false,
442
+ "description": "Sitemap cross-reference checks.",
443
+ "properties": {
444
+ "canonical_must_be_in_sitemap": {
445
+ "default": true,
446
+ "description": "Canonical URLs should appear in the sitemap.",
447
+ "type": "boolean"
448
+ },
449
+ "entries_must_exist_in_dist": {
450
+ "default": true,
451
+ "description": "Every sitemap URL must correspond to a page in `dist/`.",
452
+ "type": "boolean"
453
+ },
454
+ "forbid_noncanonical_in_sitemap": {
455
+ "default": false,
456
+ "description": "Sitemap must not contain non-canonical URLs.",
457
+ "type": "boolean"
458
+ },
459
+ "require": {
460
+ "default": false,
461
+ "description": "`sitemap.xml` must exist in `dist/`.",
462
+ "type": "boolean"
463
+ }
464
+ },
465
+ "type": "object"
466
+ },
467
+ "structured_data": {
468
+ "additionalProperties": false,
469
+ "description": "Structured data (JSON-LD) validation.",
470
+ "properties": {
471
+ "check_json_ld": {
472
+ "default": false,
473
+ "description": "Validate JSON-LD syntax and semantics (`@context`, `@type`, required properties).",
474
+ "type": "boolean"
475
+ },
476
+ "detect_duplicate_types": {
477
+ "default": false,
478
+ "description": "Warn if a page has multiple JSON-LD blocks with the same `@type`.",
479
+ "type": "boolean"
480
+ },
481
+ "require_json_ld": {
482
+ "default": false,
483
+ "description": "Every page must contain at least one JSON-LD block.",
484
+ "type": "boolean"
485
+ }
486
+ },
487
+ "type": "object"
488
+ },
489
+ "url_normalization": {
490
+ "additionalProperties": false,
491
+ "description": "URL normalization rules for internal link and canonical consistency.",
492
+ "properties": {
493
+ "index_html": {
494
+ "default": "forbid",
495
+ "description": "Whether `index.html` in URLs is allowed. `\"forbid\"`: warn on `/page/index.html` links, `\"allow\"`: permit them.",
496
+ "enum": [
497
+ "allow",
498
+ "forbid"
499
+ ],
500
+ "type": "string"
501
+ },
502
+ "trailing_slash": {
503
+ "default": "always",
504
+ "description": "Trailing slash policy. `\"always\"`: require trailing slash, `\"never\"`: forbid, `\"ignore\"`: no check.",
505
+ "enum": [
506
+ "always",
507
+ "ignore",
508
+ "never"
509
+ ],
510
+ "type": "string"
511
+ }
512
+ },
513
+ "type": "object"
514
+ }
515
+ },
516
+ "type": "object"
517
+ }
518
+ },
519
+ "properties": {
520
+ "benchmark": {
521
+ "description": "Print per-check timing benchmarks in the output.",
522
+ "type": "boolean"
523
+ },
524
+ "disable": {
525
+ "description": "Disable the integration (useful for dev mode).",
526
+ "type": "boolean"
527
+ },
528
+ "maxErrors": {
529
+ "description": "Maximum number of errors before aborting.",
530
+ "type": "number"
531
+ },
532
+ "output": {
533
+ "description": "Write the JSON report to this file path (relative to project root).",
534
+ "type": "string"
535
+ },
536
+ "pageOverview": {
537
+ "description": "Show page properties overview instead of running checks.",
538
+ "type": "boolean"
539
+ },
540
+ "preset": {
541
+ "description": "Preset to apply before user overrides. `\"strict\"` enables all checks, `\"relaxed\"` is lenient.",
542
+ "enum": [
543
+ "relaxed",
544
+ "strict"
545
+ ],
546
+ "type": "string"
547
+ },
548
+ "rules": {
549
+ "$ref": "#/definitions/RulesConfig",
550
+ "description": "Inline rules config — all check settings go here."
551
+ },
552
+ "site": {
553
+ "description": "Base URL (auto-detected from Astro's `site` config if not set).",
554
+ "type": "string"
555
+ },
556
+ "strict": {
557
+ "description": "Treat warnings as errors.",
558
+ "type": "boolean"
559
+ },
560
+ "throwOnError": {
561
+ "description": "Throw an error when the audit finds issues (fails the build). Default: false",
562
+ "type": "boolean"
563
+ }
564
+ },
565
+ "type": "object"
566
+ }
567
+