@casoon/astro-post-audit 0.1.3 → 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.
Binary file
package/bin/install.cjs CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const { execSync } = require("child_process");
9
+ const crypto = require("crypto");
9
10
  const fs = require("fs");
10
11
  const path = require("path");
11
12
  const https = require("https");
@@ -81,6 +82,68 @@ async function download(url, dest) {
81
82
  });
82
83
  }
83
84
 
85
+ /**
86
+ * Download a URL and return the contents as a Buffer.
87
+ */
88
+ async function downloadToBuffer(url) {
89
+ return new Promise((resolve, reject) => {
90
+ const follow = (url, redirects = 0) => {
91
+ if (redirects > 5) {
92
+ reject(new Error("Too many redirects"));
93
+ return;
94
+ }
95
+
96
+ https.get(url, (res) => {
97
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
98
+ follow(res.headers.location, redirects + 1);
99
+ return;
100
+ }
101
+
102
+ if (res.statusCode !== 200) {
103
+ reject(new Error(`Download failed: HTTP ${res.statusCode} from ${url}`));
104
+ return;
105
+ }
106
+
107
+ const chunks = [];
108
+ res.on("data", (chunk) => chunks.push(chunk));
109
+ res.on("end", () => resolve(Buffer.concat(chunks)));
110
+ }).on("error", reject);
111
+ };
112
+
113
+ follow(url);
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Verify SHA256 hash of a file against the expected hash from the checksum file.
119
+ * The checksum file format is: "<hex_hash> <filename>\n"
120
+ */
121
+ function verifySha256(filePath, checksumContent, archiveFilename) {
122
+ const fileBuffer = fs.readFileSync(filePath);
123
+ const actualHash = crypto.createHash("sha256").update(fileBuffer).digest("hex");
124
+
125
+ // Parse checksum file: each line is "<hash> <filename>" (two spaces)
126
+ const lines = checksumContent.toString("utf-8").trim().split("\n");
127
+ for (const line of lines) {
128
+ const match = line.match(/^([0-9a-f]{64})\s+(.+)$/);
129
+ if (match && match[2].trim() === archiveFilename) {
130
+ const expectedHash = match[1];
131
+ if (actualHash !== expectedHash) {
132
+ throw new Error(
133
+ `SHA256 mismatch for ${archiveFilename}!\n` +
134
+ ` Expected: ${expectedHash}\n` +
135
+ ` Actual: ${actualHash}\n` +
136
+ "The downloaded binary may have been tampered with. Aborting install."
137
+ );
138
+ }
139
+ console.log(` SHA256 verified: ${actualHash}`);
140
+ return;
141
+ }
142
+ }
143
+
144
+ console.warn(` Warning: SHA256 checksum not found for ${archiveFilename} — skipping verification.`);
145
+ }
146
+
84
147
  async function main() {
85
148
  const target = getPlatformTarget();
86
149
  const binaryName = getBinaryName();
@@ -95,6 +158,7 @@ async function main() {
95
158
 
96
159
  const url = getDownloadUrl(target);
97
160
  const archiveExt = process.platform === "win32" ? ".zip" : ".tar.gz";
161
+ const archiveFilename = `${PACKAGE}-v${VERSION}-${target}${archiveExt}`;
98
162
  const archivePath = path.join(binDir, `download${archiveExt}`);
99
163
 
100
164
  console.log(`Downloading ${PACKAGE} v${VERSION} for ${target}...`);
@@ -103,6 +167,22 @@ async function main() {
103
167
  try {
104
168
  await download(url, archivePath);
105
169
 
170
+ // Verify SHA256 checksum
171
+ const checksumUrl = `${url}.sha256`;
172
+ try {
173
+ const checksumData = await downloadToBuffer(checksumUrl);
174
+ verifySha256(archivePath, checksumData, archiveFilename);
175
+ } catch (checksumErr) {
176
+ if (checksumErr.message.includes("SHA256 mismatch")) {
177
+ // Hash mismatch is fatal — abort immediately
178
+ fs.unlinkSync(archivePath);
179
+ throw checksumErr;
180
+ }
181
+ // Checksum file not available (older release) — warn and continue
182
+ console.warn(` Warning: Could not download checksum file: ${checksumErr.message}`);
183
+ console.warn(" Skipping SHA256 verification.");
184
+ }
185
+
106
186
  // Extract
107
187
  if (process.platform === "win32") {
108
188
  // Use PowerShell to extract zip on Windows
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { default } from './integration.js';
2
- export type { PostAuditOptions } from './integration.js';
1
+ export { default, rulesToToml } from './integration.js';
2
+ export type { PostAuditOptions, RulesConfig } from './integration.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACxD,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { default } from './integration.js';
1
+ export { default, rulesToToml } from './integration.js';
@@ -1,27 +1,152 @@
1
1
  import type { AstroIntegration } from 'astro';
2
+ /**
3
+ * Inline rules config that mirrors the Rust rules.toml structure.
4
+ * All sections and fields are optional — only set what you want to override.
5
+ */
6
+ export interface RulesConfig {
7
+ site?: {
8
+ base_url?: string;
9
+ };
10
+ url_normalization?: {
11
+ trailing_slash?: 'always' | 'never' | 'ignore';
12
+ index_html?: 'forbid' | 'allow';
13
+ };
14
+ canonical?: {
15
+ require?: boolean;
16
+ absolute?: boolean;
17
+ same_origin?: boolean;
18
+ self_reference?: boolean;
19
+ };
20
+ robots_meta?: {
21
+ allow_noindex?: boolean;
22
+ fail_if_noindex?: boolean;
23
+ };
24
+ links?: {
25
+ check_internal?: boolean;
26
+ fail_on_broken?: boolean;
27
+ forbid_query_params_internal?: boolean;
28
+ check_fragments?: boolean;
29
+ detect_orphan_pages?: boolean;
30
+ check_mixed_content?: boolean;
31
+ };
32
+ sitemap?: {
33
+ require?: boolean;
34
+ canonical_must_be_in_sitemap?: boolean;
35
+ forbid_noncanonical_in_sitemap?: boolean;
36
+ entries_must_exist_in_dist?: boolean;
37
+ };
38
+ robots_txt?: {
39
+ require?: boolean;
40
+ require_sitemap_link?: boolean;
41
+ };
42
+ html_basics?: {
43
+ lang_attr_required?: boolean;
44
+ title_required?: boolean;
45
+ meta_description_required?: boolean;
46
+ viewport_required?: boolean;
47
+ title_max_length?: number;
48
+ meta_description_max_length?: number;
49
+ };
50
+ headings?: {
51
+ require_h1?: boolean;
52
+ single_h1?: boolean;
53
+ no_skip?: boolean;
54
+ };
55
+ a11y?: {
56
+ img_alt_required?: boolean;
57
+ allow_decorative_images?: boolean;
58
+ a_accessible_name_required?: boolean;
59
+ button_name_required?: boolean;
60
+ label_for_required?: boolean;
61
+ warn_generic_link_text?: boolean;
62
+ aria_hidden_focusable_check?: boolean;
63
+ require_skip_link?: boolean;
64
+ };
65
+ assets?: {
66
+ check_broken_assets?: boolean;
67
+ check_image_dimensions?: boolean;
68
+ max_image_size_kb?: number;
69
+ max_js_size_kb?: number;
70
+ max_css_size_kb?: number;
71
+ require_hashed_filenames?: boolean;
72
+ };
73
+ opengraph?: {
74
+ require_og_title?: boolean;
75
+ require_og_description?: boolean;
76
+ require_og_image?: boolean;
77
+ require_twitter_card?: boolean;
78
+ };
79
+ structured_data?: {
80
+ check_json_ld?: boolean;
81
+ require_json_ld?: boolean;
82
+ };
83
+ hreflang?: {
84
+ check_hreflang?: boolean;
85
+ require_x_default?: boolean;
86
+ require_self_reference?: boolean;
87
+ require_reciprocal?: boolean;
88
+ };
89
+ security?: {
90
+ check_target_blank?: boolean;
91
+ check_mixed_content?: boolean;
92
+ warn_inline_scripts?: boolean;
93
+ };
94
+ content_quality?: {
95
+ detect_duplicate_titles?: boolean;
96
+ detect_duplicate_descriptions?: boolean;
97
+ detect_duplicate_h1?: boolean;
98
+ detect_duplicate_pages?: boolean;
99
+ };
100
+ /** Custom severity overrides per rule ID. Maps rule IDs to 'error' | 'warning' | 'info' | 'off'. */
101
+ severity?: Record<string, 'error' | 'warning' | 'info' | 'off'>;
102
+ /** @deprecated Not yet implemented — will be ignored. */
103
+ external_links?: {
104
+ enabled?: boolean;
105
+ timeout_ms?: number;
106
+ max_concurrent?: number;
107
+ fail_on_broken?: boolean;
108
+ allow_domains?: string[];
109
+ block_domains?: string[];
110
+ };
111
+ }
2
112
  export interface PostAuditOptions {
3
- /** Path to rules.toml config file */
113
+ /** Path to rules.toml config file. Mutually exclusive with `rules`. */
4
114
  config?: string;
5
- /** Base URL (auto-detected from Astro site config if not set) */
115
+ /** Inline rules config (generates a temporary rules.toml). Mutually exclusive with `config`. */
116
+ rules?: RulesConfig;
117
+ /** Base URL (auto-detected from Astro's `site` config if not set) */
6
118
  site?: string;
7
119
  /** Treat warnings as errors */
8
120
  strict?: boolean;
9
- /** Output format: 'text' or 'json' */
121
+ /** Output format */
10
122
  format?: 'text' | 'json';
123
+ /** Maximum number of errors before aborting */
124
+ maxErrors?: number;
125
+ /** Glob patterns to include */
126
+ include?: string[];
11
127
  /** Glob patterns to exclude */
12
128
  exclude?: string[];
13
- /** Skip sitemap checks */
129
+ /** Skip sitemap.xml checks */
14
130
  noSitemapCheck?: boolean;
15
131
  /** Enable asset reference checking */
16
132
  checkAssets?: boolean;
17
- /** Enable structured data validation */
133
+ /** Enable structured data (JSON-LD) validation */
18
134
  checkStructuredData?: boolean;
19
135
  /** Enable security heuristic checks */
20
136
  checkSecurity?: boolean;
21
137
  /** Enable duplicate content detection */
22
138
  checkDuplicates?: boolean;
23
- /** Disable the integration (useful for dev) */
139
+ /** Show page properties overview instead of running checks */
140
+ pageOverview?: boolean;
141
+ /** Disable the integration (useful for dev mode) */
24
142
  disable?: boolean;
143
+ /** Throw an AstroError when the audit finds errors (fails the build). Default: false */
144
+ throwOnError?: boolean;
25
145
  }
146
+ /**
147
+ * Serialize a RulesConfig object to TOML format.
148
+ * @internal Exported for testing only.
149
+ */
150
+ export declare function rulesToToml(rules: RulesConfig): string;
26
151
  export default function postAudit(options?: PostAuditOptions): AstroIntegration;
27
152
  //# sourceMappingURL=integration.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAM9C,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+BAA+B;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,0BAA0B;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,sCAAsC;IACtC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wCAAwC;IACxC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,yCAAyC;IACzC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,+CAA+C;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,OAAO,GAAE,gBAAqB,GAC7B,gBAAgB,CAyElB"}
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,44 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, mkdtempSync, writeFileSync, unlinkSync, rmdirSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
4
5
  import { fileURLToPath } from 'node:url';
6
+ /**
7
+ * Serialize a RulesConfig object to TOML format.
8
+ * @internal Exported for testing only.
9
+ */
10
+ export function rulesToToml(rules) {
11
+ const lines = [];
12
+ for (const [section, values] of Object.entries(rules)) {
13
+ if (values == null || typeof values !== 'object')
14
+ continue;
15
+ lines.push(`[${section}]`);
16
+ for (const [key, val] of Object.entries(values)) {
17
+ if (val === undefined)
18
+ continue;
19
+ // Quote keys that contain special chars (e.g. rule IDs like "html/lang-missing")
20
+ const tomlKey = /^[a-zA-Z0-9_-]+$/.test(key) ? key : `"${key}"`;
21
+ if (typeof val === 'string') {
22
+ lines.push(`${tomlKey} = "${val}"`);
23
+ }
24
+ else if (Array.isArray(val)) {
25
+ const items = val.map((v) => `"${v}"`).join(', ');
26
+ lines.push(`${tomlKey} = [${items}]`);
27
+ }
28
+ else {
29
+ lines.push(`${tomlKey} = ${val}`);
30
+ }
31
+ }
32
+ lines.push('');
33
+ }
34
+ return lines.join('\n');
35
+ }
36
+ function resolveBinaryPath() {
37
+ const binDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin');
38
+ const binaryName = process.platform === 'win32' ? 'astro-post-audit.exe' : 'astro-post-audit';
39
+ const binaryPath = join(binDir, binaryName);
40
+ return existsSync(binaryPath) ? binaryPath : null;
41
+ }
5
42
  export default function postAudit(options = {}) {
6
43
  let siteUrl;
7
44
  return {
@@ -13,27 +50,29 @@ export default function postAudit(options = {}) {
13
50
  'astro:build:done': ({ dir, logger }) => {
14
51
  if (options.disable)
15
52
  return;
16
- const distPath = fileURLToPath(dir);
17
- const binDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin');
18
- const binaryName = process.platform === 'win32'
19
- ? 'astro-post-audit.exe'
20
- : 'astro-post-audit';
21
- const binaryPath = join(binDir, binaryName);
22
- if (!existsSync(binaryPath)) {
53
+ // Validate mutual exclusion: config and rules cannot both be set
54
+ if (options.config && options.rules) {
55
+ throw new Error('astro-post-audit: "config" and "rules" are mutually exclusive. ' +
56
+ 'Use "config" to point to a rules.toml file, OR use "rules" to provide inline config — not both.');
57
+ }
58
+ // Validate that rules is a non-empty object if provided
59
+ if (options.rules && typeof options.rules === 'object' && Object.keys(options.rules).length === 0) {
60
+ logger.warn('astro-post-audit: "rules" is an empty object — using default config.');
61
+ }
62
+ const binaryPath = resolveBinaryPath();
63
+ if (!binaryPath) {
23
64
  logger.warn('astro-post-audit binary not found. Run "npm rebuild @casoon/astro-post-audit".');
24
65
  return;
25
66
  }
67
+ const distPath = fileURLToPath(dir);
26
68
  const args = [distPath];
27
- // Use site from options, or auto-detect from Astro config
69
+ // --site: explicit option > Astro config auto-detect
28
70
  const site = options.site ?? siteUrl;
29
71
  if (site)
30
72
  args.push('--site', site);
73
+ // Boolean flags
31
74
  if (options.strict)
32
75
  args.push('--strict');
33
- if (options.format)
34
- args.push('--format', options.format);
35
- if (options.config)
36
- args.push('--config', options.config);
37
76
  if (options.noSitemapCheck)
38
77
  args.push('--no-sitemap-check');
39
78
  if (options.checkAssets)
@@ -44,11 +83,35 @@ export default function postAudit(options = {}) {
44
83
  args.push('--check-security');
45
84
  if (options.checkDuplicates)
46
85
  args.push('--check-duplicates');
86
+ if (options.pageOverview)
87
+ args.push('--page-overview');
88
+ // Value flags
89
+ if (options.format)
90
+ args.push('--format', options.format);
91
+ if (options.maxErrors != null)
92
+ args.push('--max-errors', String(options.maxErrors));
93
+ // Array flags
94
+ if (options.include) {
95
+ for (const pattern of options.include) {
96
+ args.push('--include', pattern);
97
+ }
98
+ }
47
99
  if (options.exclude) {
48
100
  for (const pattern of options.exclude) {
49
101
  args.push('--exclude', pattern);
50
102
  }
51
103
  }
104
+ // Config: explicit file path OR generate temp from inline rules
105
+ let tempConfigPath;
106
+ if (options.config) {
107
+ args.push('--config', options.config);
108
+ }
109
+ else if (options.rules) {
110
+ const tempDir = mkdtempSync(join(tmpdir(), 'astro-post-audit-'));
111
+ tempConfigPath = join(tempDir, 'rules.toml');
112
+ writeFileSync(tempConfigPath, rulesToToml(options.rules), 'utf-8');
113
+ args.push('--config', tempConfigPath);
114
+ }
52
115
  logger.info('Running post-build audit...');
53
116
  try {
54
117
  execFileSync(binaryPath, args, { stdio: 'inherit' });
@@ -59,12 +122,27 @@ export default function postAudit(options = {}) {
59
122
  ? err.status
60
123
  : undefined;
61
124
  if (exitCode === 1) {
125
+ if (options.throwOnError) {
126
+ throw new Error('astro-post-audit found issues. See output above.');
127
+ }
62
128
  logger.warn('Audit found issues. See output above.');
63
129
  }
64
130
  else {
65
131
  logger.error(`Audit failed with exit code ${exitCode ?? 'unknown'}`);
66
132
  }
67
133
  }
134
+ finally {
135
+ // Clean up temp config
136
+ if (tempConfigPath) {
137
+ try {
138
+ unlinkSync(tempConfigPath);
139
+ rmdirSync(dirname(tempConfigPath));
140
+ }
141
+ catch {
142
+ // ignore cleanup errors
143
+ }
144
+ }
145
+ }
68
146
  },
69
147
  },
70
148
  };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.test.d.ts","sourceRoot":"","sources":["../src/integration.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,160 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import postAudit, { rulesToToml } from './integration.js';
4
+ // ==========================================================================
5
+ // rulesToToml serialization
6
+ // ==========================================================================
7
+ describe('rulesToToml', () => {
8
+ it('serializes boolean values', () => {
9
+ const rules = {
10
+ canonical: { require: true, absolute: false },
11
+ };
12
+ const toml = rulesToToml(rules);
13
+ assert.ok(toml.includes('[canonical]'));
14
+ assert.ok(toml.includes('require = true'));
15
+ assert.ok(toml.includes('absolute = false'));
16
+ });
17
+ it('serializes string values with quotes', () => {
18
+ const rules = {
19
+ url_normalization: { trailing_slash: 'always', index_html: 'forbid' },
20
+ };
21
+ const toml = rulesToToml(rules);
22
+ assert.ok(toml.includes('[url_normalization]'));
23
+ assert.ok(toml.includes('trailing_slash = "always"'));
24
+ assert.ok(toml.includes('index_html = "forbid"'));
25
+ });
26
+ it('serializes numeric values', () => {
27
+ const rules = {
28
+ html_basics: { title_max_length: 60, meta_description_max_length: 160 },
29
+ };
30
+ const toml = rulesToToml(rules);
31
+ assert.ok(toml.includes('title_max_length = 60'));
32
+ assert.ok(toml.includes('meta_description_max_length = 160'));
33
+ });
34
+ it('serializes array values', () => {
35
+ const rules = {
36
+ external_links: {
37
+ allow_domains: ['example.com', 'test.org'],
38
+ },
39
+ };
40
+ const toml = rulesToToml(rules);
41
+ assert.ok(toml.includes('[external_links]'));
42
+ assert.ok(toml.includes('allow_domains = ["example.com", "test.org"]'));
43
+ });
44
+ it('skips undefined values', () => {
45
+ const rules = {
46
+ canonical: { require: true },
47
+ };
48
+ const toml = rulesToToml(rules);
49
+ // Only 'require' should be present, not 'absolute', 'same_origin', 'self_reference'
50
+ assert.ok(toml.includes('require = true'));
51
+ assert.ok(!toml.includes('absolute'));
52
+ assert.ok(!toml.includes('same_origin'));
53
+ });
54
+ it('handles empty rules', () => {
55
+ const toml = rulesToToml({});
56
+ assert.equal(toml, '');
57
+ });
58
+ it('handles multiple sections', () => {
59
+ const rules = {
60
+ canonical: { require: true },
61
+ headings: { require_h1: true, single_h1: false },
62
+ };
63
+ const toml = rulesToToml(rules);
64
+ assert.ok(toml.includes('[canonical]'));
65
+ assert.ok(toml.includes('[headings]'));
66
+ assert.ok(toml.includes('require_h1 = true'));
67
+ assert.ok(toml.includes('single_h1 = false'));
68
+ });
69
+ });
70
+ // ==========================================================================
71
+ // postAudit integration factory
72
+ // ==========================================================================
73
+ describe('postAudit', () => {
74
+ it('returns an AstroIntegration with correct name', () => {
75
+ const integration = postAudit();
76
+ assert.equal(integration.name, 'astro-post-audit');
77
+ assert.ok(integration.hooks);
78
+ });
79
+ it('accepts empty options', () => {
80
+ const integration = postAudit({});
81
+ assert.equal(integration.name, 'astro-post-audit');
82
+ });
83
+ it('accepts all option types', () => {
84
+ const options = {
85
+ strict: true,
86
+ format: 'json',
87
+ maxErrors: 5,
88
+ include: ['**/*.html'],
89
+ exclude: ['drafts/**'],
90
+ noSitemapCheck: true,
91
+ checkAssets: true,
92
+ checkStructuredData: true,
93
+ checkSecurity: true,
94
+ checkDuplicates: true,
95
+ pageOverview: false,
96
+ disable: false,
97
+ throwOnError: true,
98
+ };
99
+ const integration = postAudit(options);
100
+ assert.equal(integration.name, 'astro-post-audit');
101
+ });
102
+ it('throws when both config and rules are set', () => {
103
+ const integration = postAudit({
104
+ config: '/path/to/rules.toml',
105
+ rules: { canonical: { require: true } },
106
+ });
107
+ // Simulate astro:build:done hook
108
+ const hook = integration.hooks['astro:build:done'];
109
+ assert.throws(() => hook({
110
+ dir: new URL('file:///tmp/dist/'),
111
+ logger: {
112
+ info: () => { },
113
+ warn: () => { },
114
+ error: () => { },
115
+ },
116
+ }), {
117
+ message: /mutually exclusive/,
118
+ });
119
+ });
120
+ it('does not throw when only config is set', () => {
121
+ const integration = postAudit({ config: '/path/to/rules.toml' });
122
+ const hook = integration.hooks['astro:build:done'];
123
+ // Should not throw (will just warn about missing binary)
124
+ assert.doesNotThrow(() => hook({
125
+ dir: new URL('file:///tmp/dist/'),
126
+ logger: {
127
+ info: () => { },
128
+ warn: () => { },
129
+ error: () => { },
130
+ },
131
+ }));
132
+ });
133
+ it('does not throw when only rules is set', () => {
134
+ const integration = postAudit({
135
+ rules: { canonical: { require: true } },
136
+ });
137
+ const hook = integration.hooks['astro:build:done'];
138
+ assert.doesNotThrow(() => hook({
139
+ dir: new URL('file:///tmp/dist/'),
140
+ logger: {
141
+ info: () => { },
142
+ warn: () => { },
143
+ error: () => { },
144
+ },
145
+ }));
146
+ });
147
+ it('skips execution when disabled', () => {
148
+ const integration = postAudit({ disable: true });
149
+ const hook = integration.hooks['astro:build:done'];
150
+ // Should return immediately without doing anything
151
+ assert.doesNotThrow(() => hook({
152
+ dir: new URL('file:///tmp/dist/'),
153
+ logger: {
154
+ info: () => { },
155
+ warn: () => { },
156
+ error: () => { },
157
+ },
158
+ }));
159
+ });
160
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@casoon/astro-post-audit",
3
- "version": "0.1.3",
4
- "description": "Fast post-build auditor for Astro sites: SEO, links, and lightweight WCAG checks",
3
+ "version": "0.2.0",
4
+ "description": "Astro integration for post-build auditing: SEO, links, and lightweight WCAG checks",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -17,6 +17,8 @@
17
17
  },
18
18
  "scripts": {
19
19
  "build": "tsc",
20
+ "test": "tsc && node --test dist/integration.test.js",
21
+ "prepublishOnly": "tsc",
20
22
  "postinstall": "node bin/install.cjs"
21
23
  },
22
24
  "files": [
@@ -51,16 +53,11 @@
51
53
  "arm64"
52
54
  ],
53
55
  "peerDependencies": {
54
- "astro": "^5.0.0 || ^6.0.0-beta.0 || ^6.0.0"
55
- },
56
- "peerDependenciesMeta": {
57
- "astro": {
58
- "optional": true
59
- }
56
+ "astro": "^5.0.0 || ^6.0.0-beta.0"
60
57
  },
61
58
  "devDependencies": {
62
59
  "@types/node": "^22.10.1",
63
- "astro": "^6.0.0-beta.15",
60
+ "astro": "^5.0.0",
64
61
  "typescript": "^5.9.3"
65
62
  }
66
63
  }