@casoon/astro-post-audit 0.1.3 → 0.2.1

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
1
  export { default } from './integration.js';
2
- export type { PostAuditOptions } 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,MAAM,kBAAkB,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
@@ -1,27 +1,234 @@
1
1
  import type { AstroIntegration } from 'astro';
2
+ /**
3
+ * Inline rules config that mirrors the Rust config structure.
4
+ * All sections and fields are optional — only set what you want to override.
5
+ */
6
+ export interface RulesConfig {
7
+ /** Site-level settings. */
8
+ site?: {
9
+ /** Base URL for canonical/sitemap checks. Also settable via `site` option or Astro's `site` config. */
10
+ base_url?: string;
11
+ };
12
+ /** File filters — glob patterns to include or exclude pages from all checks. */
13
+ filters?: {
14
+ /** Only check files matching these glob patterns. Merged with the top-level `include` option. */
15
+ include?: string[];
16
+ /** Skip files matching these glob patterns (e.g. `["404.html", "drafts/**"]`). Merged with the top-level `exclude` option. */
17
+ exclude?: string[];
18
+ };
19
+ /** URL normalization rules for internal link and canonical consistency. */
20
+ url_normalization?: {
21
+ /** Trailing slash policy. `"always"`: require trailing slash, `"never"`: forbid, `"ignore"`: no check. @default "always" */
22
+ trailing_slash?: 'always' | 'never' | 'ignore';
23
+ /** Whether `index.html` in URLs is allowed. `"forbid"`: warn on `/page/index.html` links, `"allow"`: permit them. @default "forbid" */
24
+ index_html?: 'forbid' | 'allow';
25
+ };
26
+ /** Canonical `<link rel="canonical">` tag checks. */
27
+ canonical?: {
28
+ /** Every page must have a canonical tag. @default true */
29
+ require?: boolean;
30
+ /** Canonical URL must be absolute (not relative). @default true */
31
+ absolute?: boolean;
32
+ /** Canonical must point to the same origin as `site`. @default true */
33
+ same_origin?: boolean;
34
+ /** Canonical must be a self-reference (point to the page itself). @default false */
35
+ self_reference?: boolean;
36
+ };
37
+ /** Robots meta tag checks. */
38
+ robots_meta?: {
39
+ /** Don't warn on pages with `noindex`. @default true */
40
+ allow_noindex?: boolean;
41
+ /** Treat any `noindex` page as an error. @default false */
42
+ fail_if_noindex?: boolean;
43
+ };
44
+ /** Internal link consistency checks. */
45
+ links?: {
46
+ /** Check that internal links resolve to existing pages. @default true */
47
+ check_internal?: boolean;
48
+ /** Broken internal links are errors (not just warnings). @default true */
49
+ fail_on_broken?: boolean;
50
+ /** Warn on query parameters (`?foo=bar`) in internal links. @default true */
51
+ forbid_query_params_internal?: boolean;
52
+ /** Validate that `#fragment` targets exist in the linked page. @default false */
53
+ check_fragments?: boolean;
54
+ /** Warn about pages with no incoming internal links (orphan pages). @default false */
55
+ detect_orphan_pages?: boolean;
56
+ /** Warn on `http://` in internal links (mixed content). @default true */
57
+ check_mixed_content?: boolean;
58
+ };
59
+ /** Sitemap cross-reference checks. */
60
+ sitemap?: {
61
+ /** `sitemap.xml` must exist in `dist/`. @default false */
62
+ require?: boolean;
63
+ /** Canonical URLs should appear in the sitemap. @default true */
64
+ canonical_must_be_in_sitemap?: boolean;
65
+ /** Sitemap must not contain non-canonical URLs. @default false */
66
+ forbid_noncanonical_in_sitemap?: boolean;
67
+ /** Every sitemap URL must correspond to a page in `dist/`. @default true */
68
+ entries_must_exist_in_dist?: boolean;
69
+ };
70
+ /** `robots.txt` file checks. */
71
+ robots_txt?: {
72
+ /** `robots.txt` must exist. @default false */
73
+ require?: boolean;
74
+ /** `robots.txt` must contain a link to the sitemap. @default false */
75
+ require_sitemap_link?: boolean;
76
+ };
77
+ /** Basic HTML structure checks. */
78
+ html_basics?: {
79
+ /** `<html lang="...">` attribute is required. @default true */
80
+ lang_attr_required?: boolean;
81
+ /** `<title>` tag is required and must be non-empty. @default true */
82
+ title_required?: boolean;
83
+ /** `<meta name="description">` is required. @default false */
84
+ meta_description_required?: boolean;
85
+ /** `<meta name="viewport">` is required. @default true */
86
+ viewport_required?: boolean;
87
+ /** Warn if `<title>` exceeds this character length. @default 60 */
88
+ title_max_length?: number;
89
+ /** Warn if meta description exceeds this character length. @default 160 */
90
+ meta_description_max_length?: number;
91
+ };
92
+ /** Heading hierarchy checks. */
93
+ headings?: {
94
+ /** Page must have at least one `<h1>`. @default true */
95
+ require_h1?: boolean;
96
+ /** Only one `<h1>` per page. @default true */
97
+ single_h1?: boolean;
98
+ /** No heading level gaps (e.g. `<h2>` followed by `<h4>`). @default false */
99
+ no_skip?: boolean;
100
+ };
101
+ /** Accessibility (a11y) heuristics — static checks, no layout computation. */
102
+ a11y?: {
103
+ /** `<img>` elements must have an `alt` attribute. @default true */
104
+ img_alt_required?: boolean;
105
+ /** Allow images with `role="presentation"` or `aria-hidden="true"` to skip `alt`. @default true */
106
+ allow_decorative_images?: boolean;
107
+ /** `<a>` elements must have an accessible name (text, `aria-label`, or `aria-labelledby`). @default true */
108
+ a_accessible_name_required?: boolean;
109
+ /** `<button>` elements must have an accessible name. @default true */
110
+ button_name_required?: boolean;
111
+ /** Form controls must have an associated `<label>`. @default true */
112
+ label_for_required?: boolean;
113
+ /** Warn on generic link text like "click here", "mehr", "weiter". @default true */
114
+ warn_generic_link_text?: boolean;
115
+ /** Warn if `aria-hidden="true"` is set on a focusable element. @default true */
116
+ aria_hidden_focusable_check?: boolean;
117
+ /** Require a skip navigation link (e.g. `<a href="#main-content">`). @default false */
118
+ require_skip_link?: boolean;
119
+ };
120
+ /** Asset reference and size checks. Enable via `checkAssets` option or set fields here. */
121
+ assets?: {
122
+ /** Check that `<img>`, `<script>`, `<link>` references resolve to files in `dist/`. @default false */
123
+ check_broken_assets?: boolean;
124
+ /** Warn if `<img>` is missing `width`/`height` attributes (CLS prevention). @default false */
125
+ check_image_dimensions?: boolean;
126
+ /** Warn if any image file exceeds this size in KB. Off by default. */
127
+ max_image_size_kb?: number;
128
+ /** Warn if any JS file exceeds this size in KB. Off by default. */
129
+ max_js_size_kb?: number;
130
+ /** Warn if any CSS file exceeds this size in KB. Off by default. */
131
+ max_css_size_kb?: number;
132
+ /** Warn if asset filenames lack a cache-busting hash. @default false */
133
+ require_hashed_filenames?: boolean;
134
+ };
135
+ /** Open Graph and Twitter Card meta tag checks. */
136
+ opengraph?: {
137
+ /** Require `og:title` meta tag. @default false */
138
+ require_og_title?: boolean;
139
+ /** Require `og:description` meta tag. @default false */
140
+ require_og_description?: boolean;
141
+ /** Require `og:image` meta tag. @default false */
142
+ require_og_image?: boolean;
143
+ /** Require `twitter:card` meta tag. @default false */
144
+ require_twitter_card?: boolean;
145
+ };
146
+ /** Structured data (JSON-LD) validation. Enable via `checkStructuredData` option or set fields here. */
147
+ structured_data?: {
148
+ /** Validate JSON-LD syntax and semantics (`@context`, `@type`, required properties). @default false */
149
+ check_json_ld?: boolean;
150
+ /** Every page must contain at least one JSON-LD block. @default false */
151
+ require_json_ld?: boolean;
152
+ };
153
+ /** Hreflang checks for multilingual sites. */
154
+ hreflang?: {
155
+ /** Enable hreflang link checks. @default false */
156
+ check_hreflang?: boolean;
157
+ /** Require an `x-default` hreflang entry. @default false */
158
+ require_x_default?: boolean;
159
+ /** Hreflang must include a self-referencing entry. @default false */
160
+ require_self_reference?: boolean;
161
+ /** Hreflang links must be reciprocal (A→B and B→A). @default false */
162
+ require_reciprocal?: boolean;
163
+ };
164
+ /** Security heuristic checks. Enable via `checkSecurity` option or set fields here. */
165
+ security?: {
166
+ /** Warn on `target="_blank"` without `rel="noopener"`. @default true */
167
+ check_target_blank?: boolean;
168
+ /** Warn on `http://` resource URLs (mixed content). @default true */
169
+ check_mixed_content?: boolean;
170
+ /** Warn on inline `<script>` tags. @default false */
171
+ warn_inline_scripts?: boolean;
172
+ };
173
+ /** Duplicate content detection. Enable via `checkDuplicates` option or set fields here. */
174
+ content_quality?: {
175
+ /** Warn if multiple pages share the same `<title>`. @default false */
176
+ detect_duplicate_titles?: boolean;
177
+ /** Warn if multiple pages share the same meta description. @default false */
178
+ detect_duplicate_descriptions?: boolean;
179
+ /** Warn if multiple pages share the same `<h1>`. @default false */
180
+ detect_duplicate_h1?: boolean;
181
+ /** Warn if pages have identical content (by hash). @default false */
182
+ detect_duplicate_pages?: boolean;
183
+ };
184
+ /**
185
+ * Override severity per rule ID.
186
+ * @example `{ "html/title-too-long": "off", "a11y/img-alt-missing": "error" }`
187
+ */
188
+ severity?: Record<string, 'error' | 'warning' | 'info' | 'off'>;
189
+ /** @deprecated Not yet implemented — will be ignored. */
190
+ external_links?: {
191
+ enabled?: boolean;
192
+ timeout_ms?: number;
193
+ max_concurrent?: number;
194
+ fail_on_broken?: boolean;
195
+ allow_domains?: string[];
196
+ block_domains?: string[];
197
+ };
198
+ }
2
199
  export interface PostAuditOptions {
3
- /** Path to rules.toml config file */
200
+ /** Path to a TOML config file. Mutually exclusive with `rules`. */
4
201
  config?: string;
5
- /** Base URL (auto-detected from Astro site config if not set) */
202
+ /** Inline rules config. Mutually exclusive with `config`. */
203
+ rules?: RulesConfig;
204
+ /** Base URL (auto-detected from Astro's `site` config if not set) */
6
205
  site?: string;
7
206
  /** Treat warnings as errors */
8
207
  strict?: boolean;
9
- /** Output format: 'text' or 'json' */
208
+ /** Output format */
10
209
  format?: 'text' | 'json';
210
+ /** Maximum number of errors before aborting */
211
+ maxErrors?: number;
212
+ /** Glob patterns to include */
213
+ include?: string[];
11
214
  /** Glob patterns to exclude */
12
215
  exclude?: string[];
13
- /** Skip sitemap checks */
216
+ /** Skip sitemap.xml checks */
14
217
  noSitemapCheck?: boolean;
15
218
  /** Enable asset reference checking */
16
219
  checkAssets?: boolean;
17
- /** Enable structured data validation */
220
+ /** Enable structured data (JSON-LD) validation */
18
221
  checkStructuredData?: boolean;
19
222
  /** Enable security heuristic checks */
20
223
  checkSecurity?: boolean;
21
224
  /** Enable duplicate content detection */
22
225
  checkDuplicates?: boolean;
23
- /** Disable the integration (useful for dev) */
226
+ /** Show page properties overview instead of running checks */
227
+ pageOverview?: boolean;
228
+ /** Disable the integration (useful for dev mode) */
24
229
  disable?: boolean;
230
+ /** Throw an AstroError when the audit finds errors (fails the build). Default: false */
231
+ throwOnError?: boolean;
25
232
  }
26
233
  export default function postAudit(options?: PostAuditOptions): AstroIntegration;
27
234
  //# 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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"}
@@ -2,6 +2,12 @@ import { execFileSync } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
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';
8
+ const binaryPath = join(binDir, binaryName);
9
+ return existsSync(binaryPath) ? binaryPath : null;
10
+ }
5
11
  export default function postAudit(options = {}) {
6
12
  let siteUrl;
7
13
  return {
@@ -13,27 +19,29 @@ export default function postAudit(options = {}) {
13
19
  'astro:build:done': ({ dir, logger }) => {
14
20
  if (options.disable)
15
21
  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)) {
22
+ // Validate mutual exclusion: config and rules cannot both be set
23
+ if (options.config && options.rules) {
24
+ throw new Error('astro-post-audit: "config" and "rules" are mutually exclusive. ' +
25
+ 'Use "config" to point to a config file, OR use "rules" to provide inline config — not both.');
26
+ }
27
+ // Validate that rules is a non-empty object if provided
28
+ if (options.rules && typeof options.rules === 'object' && Object.keys(options.rules).length === 0) {
29
+ logger.warn('astro-post-audit: "rules" is an empty object — using default config.');
30
+ }
31
+ const binaryPath = resolveBinaryPath();
32
+ if (!binaryPath) {
23
33
  logger.warn('astro-post-audit binary not found. Run "npm rebuild @casoon/astro-post-audit".');
24
34
  return;
25
35
  }
36
+ const distPath = fileURLToPath(dir);
26
37
  const args = [distPath];
27
- // Use site from options, or auto-detect from Astro config
38
+ // --site: explicit option > Astro config auto-detect
28
39
  const site = options.site ?? siteUrl;
29
40
  if (site)
30
41
  args.push('--site', site);
42
+ // Boolean flags
31
43
  if (options.strict)
32
44
  args.push('--strict');
33
- if (options.format)
34
- args.push('--format', options.format);
35
- if (options.config)
36
- args.push('--config', options.config);
37
45
  if (options.noSitemapCheck)
38
46
  args.push('--no-sitemap-check');
39
47
  if (options.checkAssets)
@@ -44,14 +52,39 @@ export default function postAudit(options = {}) {
44
52
  args.push('--check-security');
45
53
  if (options.checkDuplicates)
46
54
  args.push('--check-duplicates');
55
+ if (options.pageOverview)
56
+ args.push('--page-overview');
57
+ // Value flags
58
+ if (options.format)
59
+ args.push('--format', options.format);
60
+ if (options.maxErrors != null)
61
+ args.push('--max-errors', String(options.maxErrors));
62
+ // Array flags
63
+ if (options.include) {
64
+ for (const pattern of options.include) {
65
+ args.push('--include', pattern);
66
+ }
67
+ }
47
68
  if (options.exclude) {
48
69
  for (const pattern of options.exclude) {
49
70
  args.push('--exclude', pattern);
50
71
  }
51
72
  }
73
+ // Config: explicit file path OR pipe inline rules via stdin
74
+ let stdinInput;
75
+ if (options.config) {
76
+ args.push('--config', options.config);
77
+ }
78
+ else if (options.rules) {
79
+ args.push('--config-stdin');
80
+ stdinInput = JSON.stringify(options.rules);
81
+ }
52
82
  logger.info('Running post-build audit...');
53
83
  try {
54
- execFileSync(binaryPath, args, { stdio: 'inherit' });
84
+ execFileSync(binaryPath, args, {
85
+ stdio: stdinInput ? ['pipe', 'inherit', 'inherit'] : 'inherit',
86
+ input: stdinInput,
87
+ });
55
88
  logger.info('All checks passed!');
56
89
  }
57
90
  catch (err) {
@@ -59,6 +92,9 @@ export default function postAudit(options = {}) {
59
92
  ? err.status
60
93
  : undefined;
61
94
  if (exitCode === 1) {
95
+ if (options.throwOnError) {
96
+ throw new Error('astro-post-audit found issues. See output above.');
97
+ }
62
98
  logger.warn('Audit found issues. See output above.');
63
99
  }
64
100
  else {
@@ -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,94 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import postAudit from './integration.js';
4
+ // ==========================================================================
5
+ // postAudit integration factory
6
+ // ==========================================================================
7
+ describe('postAudit', () => {
8
+ it('returns an AstroIntegration with correct name', () => {
9
+ const integration = postAudit();
10
+ assert.equal(integration.name, 'astro-post-audit');
11
+ assert.ok(integration.hooks);
12
+ });
13
+ it('accepts empty options', () => {
14
+ const integration = postAudit({});
15
+ assert.equal(integration.name, 'astro-post-audit');
16
+ });
17
+ it('accepts all option types', () => {
18
+ const options = {
19
+ strict: true,
20
+ format: 'json',
21
+ maxErrors: 5,
22
+ include: ['**/*.html'],
23
+ exclude: ['drafts/**'],
24
+ noSitemapCheck: true,
25
+ checkAssets: true,
26
+ checkStructuredData: true,
27
+ checkSecurity: true,
28
+ checkDuplicates: true,
29
+ pageOverview: false,
30
+ disable: false,
31
+ throwOnError: true,
32
+ };
33
+ const integration = postAudit(options);
34
+ assert.equal(integration.name, 'astro-post-audit');
35
+ });
36
+ it('throws when both config and rules are set', () => {
37
+ const integration = postAudit({
38
+ config: '/path/to/rules.toml',
39
+ rules: { canonical: { require: true } },
40
+ });
41
+ // Simulate astro:build:done hook
42
+ const hook = integration.hooks['astro:build:done'];
43
+ assert.throws(() => hook({
44
+ dir: new URL('file:///tmp/dist/'),
45
+ logger: {
46
+ info: () => { },
47
+ warn: () => { },
48
+ error: () => { },
49
+ },
50
+ }), {
51
+ message: /mutually exclusive/,
52
+ });
53
+ });
54
+ it('does not throw when only config is set', () => {
55
+ const integration = postAudit({ config: '/path/to/rules.toml' });
56
+ const hook = integration.hooks['astro:build:done'];
57
+ // Should not throw (will just warn about missing binary)
58
+ assert.doesNotThrow(() => hook({
59
+ dir: new URL('file:///tmp/dist/'),
60
+ logger: {
61
+ info: () => { },
62
+ warn: () => { },
63
+ error: () => { },
64
+ },
65
+ }));
66
+ });
67
+ it('does not throw when only rules is set', () => {
68
+ const integration = postAudit({
69
+ rules: { canonical: { require: true } },
70
+ });
71
+ const hook = integration.hooks['astro:build:done'];
72
+ assert.doesNotThrow(() => hook({
73
+ dir: new URL('file:///tmp/dist/'),
74
+ logger: {
75
+ info: () => { },
76
+ warn: () => { },
77
+ error: () => { },
78
+ },
79
+ }));
80
+ });
81
+ it('skips execution when disabled', () => {
82
+ const integration = postAudit({ disable: true });
83
+ const hook = integration.hooks['astro:build:done'];
84
+ // Should return immediately without doing anything
85
+ assert.doesNotThrow(() => hook({
86
+ dir: new URL('file:///tmp/dist/'),
87
+ logger: {
88
+ info: () => { },
89
+ warn: () => { },
90
+ error: () => { },
91
+ },
92
+ }));
93
+ });
94
+ });
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.1",
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
  }