@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.
- package/bin/astro-post-audit +0 -0
- package/bin/install.cjs +80 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/integration.d.ts +131 -6
- package/dist/integration.d.ts.map +1 -1
- package/dist/integration.js +91 -13
- package/dist/integration.test.d.ts +2 -0
- package/dist/integration.test.d.ts.map +1 -0
- package/dist/integration.test.js +160 -0
- package/package.json +6 -9
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,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';
|
package/dist/integration.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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;
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"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"}
|
package/dist/integration.js
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
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
|
-
//
|
|
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 @@
|
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
|
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": "^
|
|
60
|
+
"astro": "^5.0.0",
|
|
64
61
|
"typescript": "^5.9.3"
|
|
65
62
|
}
|
|
66
63
|
}
|