@casoon/astro-post-audit 0.2.3 → 0.2.5

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.
@@ -11,17 +13,17 @@ export interface RulesConfig {
11
13
  };
12
14
  /** File filters — glob patterns to include or exclude pages from all checks. */
13
15
  filters?: {
14
- /** Only check files matching these glob patterns. Merged with the top-level `include` option. */
16
+ /** Only check files matching these glob patterns. */
15
17
  include?: string[];
16
- /** Skip files matching these glob patterns (e.g. `["404.html", "drafts/**"]`). Merged with the top-level `exclude` option. */
18
+ /** Skip files matching these glob patterns (e.g. `["404.html", "drafts/**"]`). */
17
19
  exclude?: string[];
18
20
  };
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?: {
@@ -33,6 +35,8 @@ export interface RulesConfig {
33
35
  same_origin?: boolean;
34
36
  /** Canonical must be a self-reference (point to the page itself). @default false */
35
37
  self_reference?: boolean;
38
+ /** Warn when multiple pages share the same canonical URL (cluster detection). @default true */
39
+ detect_clusters?: boolean;
36
40
  };
37
41
  /** Robots meta tag checks. */
38
42
  robots_meta?: {
@@ -117,7 +121,7 @@ export interface RulesConfig {
117
121
  /** Require a skip navigation link (e.g. `<a href="#main-content">`). @default false */
118
122
  require_skip_link?: boolean;
119
123
  };
120
- /** Asset reference and size checks. Enable via `checkAssets` option or set fields here. */
124
+ /** Asset reference and size checks. */
121
125
  assets?: {
122
126
  /** Check that `<img>`, `<script>`, `<link>` references resolve to files in `dist/`. @default false */
123
127
  check_broken_assets?: boolean;
@@ -143,7 +147,7 @@ export interface RulesConfig {
143
147
  /** Require `twitter:card` meta tag. @default false */
144
148
  require_twitter_card?: boolean;
145
149
  };
146
- /** Structured data (JSON-LD) validation. Enable via `checkStructuredData` option or set fields here. */
150
+ /** Structured data (JSON-LD) validation. */
147
151
  structured_data?: {
148
152
  /** Validate JSON-LD syntax and semantics (`@context`, `@type`, required properties). @default false */
149
153
  check_json_ld?: boolean;
@@ -163,7 +167,7 @@ export interface RulesConfig {
163
167
  /** Hreflang links must be reciprocal (A→B and B→A). @default false */
164
168
  require_reciprocal?: boolean;
165
169
  };
166
- /** Security heuristic checks. Enable via `checkSecurity` option or set fields here. */
170
+ /** Security heuristic checks. */
167
171
  security?: {
168
172
  /** Warn on `target="_blank"` without `rel="noopener"`. @default true */
169
173
  check_target_blank?: boolean;
@@ -172,7 +176,7 @@ export interface RulesConfig {
172
176
  /** Warn on inline `<script>` tags. @default false */
173
177
  warn_inline_scripts?: boolean;
174
178
  };
175
- /** Duplicate content detection. Enable via `checkDuplicates` option or set fields here. */
179
+ /** Duplicate content detection. */
176
180
  content_quality?: {
177
181
  /** Warn if multiple pages share the same `<title>`. @default false */
178
182
  detect_duplicate_titles?: boolean;
@@ -185,50 +189,52 @@ export interface RulesConfig {
185
189
  };
186
190
  /**
187
191
  * Override severity per rule ID.
188
- * @example `{ "html/title-too-long": "off", "a11y/img-alt-missing": "error" }`
192
+ * @example `{ "html/title-too-long": "off", "a11y/img-alt": "error" }`
189
193
  */
190
- severity?: Record<string, 'error' | 'warning' | 'info' | 'off'>;
191
- /** @deprecated Not yet implemented will be ignored. */
194
+ severity?: Record<string, "error" | "warning" | "info" | "off">;
195
+ /** External link checking (HEAD requests to verify URLs return 2xx). */
192
196
  external_links?: {
197
+ /** Enable external link checking. @default false */
193
198
  enabled?: boolean;
199
+ /** Timeout per request in milliseconds. @default 3000 */
194
200
  timeout_ms?: number;
201
+ /** Maximum concurrent requests. @default 10 */
195
202
  max_concurrent?: number;
203
+ /** Broken external links are errors (not just warnings). @default false */
196
204
  fail_on_broken?: boolean;
205
+ /** Only check links to these domains (empty = all). */
197
206
  allow_domains?: string[];
207
+ /** Skip links to these domains. */
198
208
  block_domains?: string[];
199
209
  };
200
210
  }
201
211
  export interface PostAuditOptions {
202
- /** Inline rules config. */
212
+ /** Inline rules config — all check settings go here. */
203
213
  rules?: RulesConfig;
204
- /** Base URL (auto-detected from Astro's `site` config if not set) */
214
+ /** Preset to apply before user overrides. `"strict"` enables all checks, `"relaxed"` is lenient. */
215
+ preset?: "strict" | "relaxed";
216
+ /** Base URL (auto-detected from Astro's `site` config if not set). */
205
217
  site?: string;
206
- /** Treat warnings as errors */
218
+ /** Treat warnings as errors. */
207
219
  strict?: boolean;
208
- /** Output format */
209
- format?: 'text' | 'json';
210
- /** Maximum number of errors before aborting */
220
+ /** Maximum number of errors before aborting. */
211
221
  maxErrors?: number;
212
- /** Glob patterns to include */
213
- include?: string[];
214
- /** Glob patterns to exclude */
215
- exclude?: string[];
216
- /** Skip sitemap.xml checks */
217
- noSitemapCheck?: boolean;
218
- /** Enable asset reference checking */
219
- checkAssets?: boolean;
220
- /** Enable structured data (JSON-LD) validation */
221
- checkStructuredData?: boolean;
222
- /** Enable security heuristic checks */
223
- checkSecurity?: boolean;
224
- /** Enable duplicate content detection */
225
- checkDuplicates?: boolean;
226
- /** Show page properties overview instead of running checks */
222
+ /** Show page properties overview instead of running checks. */
227
223
  pageOverview?: boolean;
228
- /** Disable the integration (useful for dev mode) */
224
+ /** Write the JSON report to this file path (relative to project root). */
225
+ output?: string;
226
+ /** Print per-check timing benchmarks in the output. */
227
+ benchmark?: boolean;
228
+ /** Disable the integration (useful for dev mode). */
229
229
  disable?: boolean;
230
- /** Throw an AstroError when the audit finds errors (fails the build). Default: false */
230
+ /** Throw an error when the audit finds issues (fails the build). Default: false */
231
231
  throwOnError?: boolean;
232
232
  }
233
- export default function postAudit(options?: PostAuditOptions): AstroIntegration;
233
+ interface RuntimeDeps {
234
+ execFileSync: typeof execFileSync;
235
+ existsSync: typeof existsSync;
236
+ writeFileSync: typeof writeFileSync;
237
+ }
238
+ export default function postAudit(options?: PostAuditOptions, deps?: RuntimeDeps): AstroIntegration;
239
+ export {};
234
240
  //# 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,96 +1,136 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { existsSync } 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)
41
+ "astro:build:done": ({ dir, logger }) => {
42
+ if (options.disable ||
43
+ process.env.SKIP_AUDIT === "1" ||
44
+ process.env.SKIP_AUDIT === "true") {
45
+ if (process.env.SKIP_AUDIT) {
46
+ logger.info("Audit skipped (SKIP_AUDIT is set).");
47
+ }
21
48
  return;
49
+ }
22
50
  // Validate that rules is a non-empty object if provided
23
- 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) {
24
54
  logger.warn('astro-post-audit: "rules" is an empty object — using default config.');
25
55
  }
26
- const binaryPath = resolveBinaryPath();
56
+ const binaryPath = resolveBinaryPath(deps.existsSync);
27
57
  if (!binaryPath) {
28
58
  logger.warn('astro-post-audit binary not found. Run "npm rebuild @casoon/astro-post-audit".');
29
59
  return;
30
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
+ }
31
65
  const distPath = fileURLToPath(dir);
32
- const args = [distPath];
33
- // --site: explicit option > Astro config auto-detect
66
+ const args = [distPath, "--config-stdin"];
67
+ // Build the full JSON config for the Rust binary
34
68
  const site = options.site ?? siteUrl;
69
+ const stdinConfig = {
70
+ ...options.rules,
71
+ };
35
72
  if (site)
36
- args.push('--site', site);
37
- // Boolean flags
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
+ }
38
85
  if (options.strict)
39
- args.push('--strict');
40
- if (options.noSitemapCheck)
41
- args.push('--no-sitemap-check');
42
- if (options.checkAssets)
43
- args.push('--check-assets');
44
- if (options.checkStructuredData)
45
- args.push('--check-structured-data');
46
- if (options.checkSecurity)
47
- args.push('--check-security');
48
- if (options.checkDuplicates)
49
- args.push('--check-duplicates');
50
- if (options.pageOverview)
51
- args.push('--page-overview');
52
- // Value flags
53
- if (options.format)
54
- args.push('--format', options.format);
86
+ stdinConfig.strict = true;
87
+ if (options.benchmark)
88
+ stdinConfig.benchmark = true;
55
89
  if (options.maxErrors != null)
56
- args.push('--max-errors', String(options.maxErrors));
57
- // Array flags
58
- if (options.include) {
59
- for (const pattern of options.include) {
60
- args.push('--include', pattern);
61
- }
62
- }
63
- if (options.exclude) {
64
- for (const pattern of options.exclude) {
65
- args.push('--exclude', pattern);
66
- }
67
- }
68
- // Pipe inline rules config via stdin as JSON
69
- let stdinInput;
70
- if (options.rules) {
71
- args.push('--config-stdin');
72
- stdinInput = JSON.stringify(options.rules);
73
- }
74
- logger.info('Running post-build audit...');
90
+ stdinConfig.max_errors = options.maxErrors;
91
+ if (options.pageOverview)
92
+ stdinConfig.page_overview = true;
93
+ if (options.output)
94
+ stdinConfig.format = "json";
95
+ const stdinInput = JSON.stringify(stdinConfig);
96
+ logger.info("Running post-build audit...");
97
+ const captureOutput = !!options.output;
75
98
  try {
76
- execFileSync(binaryPath, args, {
77
- stdio: stdinInput ? ['pipe', 'inherit', 'inherit'] : 'inherit',
99
+ const result = deps.execFileSync(binaryPath, args, {
100
+ stdio: ["pipe", captureOutput ? "pipe" : "inherit", "inherit"],
78
101
  input: stdinInput,
102
+ encoding: captureOutput ? "utf-8" : undefined,
79
103
  });
80
- logger.info('All checks passed!');
104
+ if (captureOutput && result) {
105
+ deps.writeFileSync(options.output, result);
106
+ logger.info(`Report written to ${options.output}`);
107
+ }
108
+ logger.info("All checks passed!");
81
109
  }
82
110
  catch (err) {
83
- const exitCode = err && typeof err === 'object' && 'status' in err
111
+ const exitCode = err && typeof err === "object" && "status" in err
84
112
  ? err.status
85
113
  : undefined;
114
+ // Write output file even on exit code 1 (findings exist but run succeeded)
115
+ if (captureOutput &&
116
+ exitCode === 1 &&
117
+ err &&
118
+ typeof err === "object" &&
119
+ "stdout" in err) {
120
+ const stdout = err.stdout;
121
+ if (stdout) {
122
+ deps.writeFileSync(options.output, stdout);
123
+ logger.info(`Report written to ${options.output}`);
124
+ }
125
+ }
86
126
  if (exitCode === 1) {
87
127
  if (options.throwOnError) {
88
- throw new Error('astro-post-audit found issues. See output above.');
128
+ throw new Error("astro-post-audit found issues. See output above.");
89
129
  }
90
- logger.warn('Audit found issues. See output above.');
130
+ logger.warn("Audit found issues. See output above.");
91
131
  }
92
132
  else {
93
- logger.error(`Audit failed with exit code ${exitCode ?? 'unknown'}`);
133
+ logger.error(`Audit failed with exit code ${exitCode ?? "unknown"}`);
94
134
  }
95
135
  }
96
136
  },
@@ -1,58 +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
- format: 'json',
21
41
  maxErrors: 5,
22
- include: ['**/*.html'],
23
- exclude: ['drafts/**'],
24
- noSitemapCheck: true,
25
- checkAssets: true,
26
- checkStructuredData: true,
27
- checkSecurity: true,
28
- checkDuplicates: true,
29
42
  pageOverview: false,
43
+ output: "audit-report.json",
30
44
  disable: false,
31
45
  throwOnError: true,
46
+ rules: { canonical: { require: true } },
32
47
  };
33
48
  const integration = postAudit(options);
34
- assert.equal(integration.name, 'astro-post-audit');
49
+ assert.equal(integration.name, "astro-post-audit");
35
50
  });
36
- 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
+ };
37
63
  const integration = postAudit({
38
64
  rules: { canonical: { require: true } },
39
- });
40
- const hook = integration.hooks['astro:build:done'];
65
+ }, deps);
66
+ const hook = integration.hooks["astro:build:done"];
67
+ const { logger, error } = makeLogger();
41
68
  assert.doesNotThrow(() => hook({
42
- dir: new URL('file:///tmp/dist/'),
43
- logger: {
44
- info: () => { },
45
- warn: () => { },
46
- error: () => { },
47
- },
69
+ dir: new URL("file:///tmp/dist/"),
70
+ logger,
48
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")));
49
75
  });
50
- it('skips execution when disabled', () => {
76
+ it("skips execution when disabled", () => {
51
77
  const integration = postAudit({ disable: true });
52
- const hook = integration.hooks['astro:build:done'];
78
+ const hook = integration.hooks["astro:build:done"];
53
79
  // Should return immediately without doing anything
54
80
  assert.doesNotThrow(() => hook({
55
- dir: new URL('file:///tmp/dist/'),
81
+ dir: new URL("file:///tmp/dist/"),
56
82
  logger: {
57
83
  info: () => { },
58
84
  warn: () => { },
@@ -60,4 +86,28 @@ describe('postAudit', () => {
60
86
  },
61
87
  }));
62
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
+ });
63
113
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casoon/astro-post-audit",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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
+