@eightmedia/flightcheck 1.1.0 → 1.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/CHANGELOG.md CHANGED
@@ -5,6 +5,7 @@
5
5
  ### Added
6
6
 
7
7
  - Findings include an optional `fix` suggestion in `*-report.json` and Markdown (`- Fix: …`)
8
+ - Crawlee storage uses `.flightcheckStorage/<run-hash>/` and is cleaned up after each crawl
8
9
 
9
10
  ### Changed
10
11
 
@@ -0,0 +1,18 @@
1
+ /** Root directory for ephemeral Crawlee request-queue storage (cwd-relative). */
2
+ export declare const FLIGHTCHECK_STORAGE_ROOT = ".flightcheckStorage";
3
+ /**
4
+ * Create a unique per-run storage path: `.flightcheckStorage/<hash>`.
5
+ * Concurrent runs each get their own subdirectory so cleanup of one does not
6
+ * wipe another run's queue files.
7
+ */
8
+ export declare function createRunStoragePath(options?: {
9
+ cwd?: string;
10
+ seed?: string;
11
+ }): {
12
+ rootDir: string;
13
+ runDir: string;
14
+ runId: string;
15
+ };
16
+ /** Remove a single run subdirectory. Ignores missing paths. */
17
+ export declare function cleanupRunStorageDir(runDir: string): Promise<void>;
18
+ //# sourceMappingURL=crawleeStorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crawleeStorage.d.ts","sourceRoot":"","sources":["../../src/crawler/crawleeStorage.ts"],"names":[],"mappings":"AAIA,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,wBAAwB,CAAC;AAE9D;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CASrD;AAED,+DAA+D;AAC/D,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAExE"}
@@ -0,0 +1,24 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ /** Root directory for ephemeral Crawlee request-queue storage (cwd-relative). */
5
+ export const FLIGHTCHECK_STORAGE_ROOT = ".flightcheckStorage";
6
+ /**
7
+ * Create a unique per-run storage path: `.flightcheckStorage/<hash>`.
8
+ * Concurrent runs each get their own subdirectory so cleanup of one does not
9
+ * wipe another run's queue files.
10
+ */
11
+ export function createRunStoragePath(options) {
12
+ const cwd = options?.cwd ?? process.cwd();
13
+ const runId = createHash("sha256")
14
+ .update(`${options?.seed ?? ""}:${Date.now()}:${process.pid}:${randomBytes(8).toString("hex")}`)
15
+ .digest("hex")
16
+ .slice(0, 16);
17
+ const rootDir = path.join(cwd, FLIGHTCHECK_STORAGE_ROOT);
18
+ const runDir = path.join(rootDir, runId);
19
+ return { rootDir, runDir, runId };
20
+ }
21
+ /** Remove a single run subdirectory. Ignores missing paths. */
22
+ export async function cleanupRunStorageDir(runDir) {
23
+ await rm(runDir, { recursive: true, force: true });
24
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"runCrawler.d.ts","sourceRoot":"","sources":["../../src/crawler/runCrawler.ts"],"names":[],"mappings":"AAUA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAOjE,OAAO,KAAK,EAAiB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAMrE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,YAAY,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,CA4K7H"}
1
+ {"version":3,"file":"runCrawler.d.ts","sourceRoot":"","sources":["../../src/crawler/runCrawler.ts"],"names":[],"mappings":"AAUA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAQjE,OAAO,KAAK,EAAiB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAMrE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,YAAY,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,CAkM7H"}
@@ -1,7 +1,8 @@
1
- import { CheerioCrawler, RequestQueue, log, LogLevel } from "crawlee";
1
+ import { CheerioCrawler, Configuration, MemoryStorage, RequestQueue, log, LogLevel } from "crawlee";
2
2
  import pino from "pino";
3
3
  import { DEFAULT_BURST_DELAY_MS, DEFAULT_CONCURRENCY, DEFAULT_DELAY_MS, DEFAULT_MAX_PAGES, DEFAULT_TIMEOUT_MS, DEFAULT_USER_AGENT, } from "./defaults.js";
4
4
  import { CHECK_GROUPS } from "./checkGroups.js";
5
+ import { cleanupRunStorageDir, createRunStoragePath } from "./crawleeStorage.js";
5
6
  import { extractInternalLinks, extractVerificationRecord } from "./extractors.js";
6
7
  import { detectStackFromHtml } from "./stack.js";
7
8
  import { extractSameOriginScripts } from "./jsAssets.js";
@@ -82,100 +83,118 @@ export async function runCrawler(options) {
82
83
  return { secrets, scannedAssets };
83
84
  };
84
85
  const robotsPolicy = respectRobotsTxt ? await loadRobotsPolicy(origin, timeoutMs) : null;
85
- const requestQueue = await RequestQueue.open();
86
- const records = [];
87
- const seen = new Set();
88
- const discovery = createDiscoveryGate(seedUrls, maxPages);
89
- // Seeds are already smart-ordered (home/root/list before details); FIFO preserves that.
90
- for (const seed of seedUrls) {
91
- if (!matchesExcludePattern(seed, excludePatterns)) {
92
- const normalized = normalizeUrl(seed);
93
- await requestQueue.addRequest({ url: seed, uniqueKey: normalized });
94
- seen.add(normalized);
95
- discovery.remember(normalized);
96
- }
97
- }
98
- let processedSinceBurst = 0;
99
- let interrupted = false;
100
- const persist = () => {
101
- writeReportToFile(records, options.outputPath);
102
- return records;
103
- };
104
- process.on("SIGINT", async () => {
105
- interrupted = true;
106
- logger.info("[crawl] SIGINT ontvangen, graceful stop gestart");
86
+ // Per-run Crawlee storage so concurrent crawls do not share/purge each other,
87
+ // then remove this run's subdirectory when finished.
88
+ const { runDir, runId } = createRunStoragePath({ seed: startUrl });
89
+ const configuration = new Configuration({
90
+ storageClient: new MemoryStorage({ localDataDirectory: runDir }),
91
+ defaultRequestQueueId: runId,
92
+ purgeOnStart: true,
107
93
  });
108
- logger.info(`[crawl] start url=${startUrl} output=${options.outputPath} maxPages=${maxPages} concurrency=${concurrency} delayMs=${delayMs} burstDelayMs=${burstDelayMs} timeoutMs=${timeoutMs} seeds=${seedUrls.length} respectRobotsTxt=${respectRobotsTxt}`);
109
- const crawler = new CheerioCrawler({
110
- requestQueue,
111
- maxRequestsPerCrawl: maxPages,
112
- maxConcurrency: concurrency,
113
- // +60 s headroom when security scanning is active: the handler does sequential JS fetches
114
- // per page; the failedAssets/jsScanCache ensures dead/cached assets aren't re-fetched.
115
- requestHandlerTimeoutSecs: Math.ceil(timeoutMs / 1000) + (activeGroups.has("security") ? 60 : 0),
116
- requestHandler: async ({ request, body, contentType, response }) => {
117
- if (interrupted)
118
- return;
119
- if (delayMs > 0) {
120
- await sleep(delayMs);
94
+ try {
95
+ const requestQueue = await RequestQueue.open(runId, { config: configuration });
96
+ const records = [];
97
+ const seen = new Set();
98
+ const discovery = createDiscoveryGate(seedUrls, maxPages);
99
+ // Seeds are already smart-ordered (home/root/list before details); FIFO preserves that.
100
+ for (const seed of seedUrls) {
101
+ if (!matchesExcludePattern(seed, excludePatterns)) {
102
+ const normalized = normalizeUrl(seed);
103
+ await requestQueue.addRequest({ url: seed, uniqueKey: normalized });
104
+ seen.add(normalized);
105
+ discovery.remember(normalized);
121
106
  }
122
- if (burstDelayMs > 0 && processedSinceBurst > 0 && processedSinceBurst % concurrency === 0) {
123
- await sleep(burstDelayMs);
124
- }
125
- const currentUrl = normalizeUrl(request.loadedUrl ?? request.url);
126
- if (robotsPolicy && !robotsPolicy.isAllowed(currentUrl)) {
127
- const skipped = extractVerificationRecord("", currentUrl, null, "");
128
- skipped.note = "Skipped by robots.txt policy";
129
- records.push(skipped);
107
+ }
108
+ let processedSinceBurst = 0;
109
+ let interrupted = false;
110
+ const persist = () => {
111
+ writeReportToFile(records, options.outputPath);
112
+ return records;
113
+ };
114
+ process.on("SIGINT", async () => {
115
+ interrupted = true;
116
+ logger.info("[crawl] SIGINT ontvangen, graceful stop gestart");
117
+ });
118
+ logger.info(`[crawl] start url=${startUrl} output=${options.outputPath} maxPages=${maxPages} concurrency=${concurrency} delayMs=${delayMs} burstDelayMs=${burstDelayMs} timeoutMs=${timeoutMs} seeds=${seedUrls.length} respectRobotsTxt=${respectRobotsTxt} storage=${runDir}`);
119
+ const crawler = new CheerioCrawler({
120
+ requestQueue,
121
+ maxRequestsPerCrawl: maxPages,
122
+ maxConcurrency: concurrency,
123
+ // +60 s headroom when security scanning is active: the handler does sequential JS fetches
124
+ // per page; the failedAssets/jsScanCache ensures dead/cached assets aren't re-fetched.
125
+ requestHandlerTimeoutSecs: Math.ceil(timeoutMs / 1000) + (activeGroups.has("security") ? 60 : 0),
126
+ requestHandler: async ({ request, body, contentType, response }) => {
127
+ if (interrupted)
128
+ return;
129
+ if (delayMs > 0) {
130
+ await sleep(delayMs);
131
+ }
132
+ if (burstDelayMs > 0 && processedSinceBurst > 0 && processedSinceBurst % concurrency === 0) {
133
+ await sleep(burstDelayMs);
134
+ }
135
+ const currentUrl = normalizeUrl(request.loadedUrl ?? request.url);
136
+ if (robotsPolicy && !robotsPolicy.isAllowed(currentUrl)) {
137
+ const skipped = extractVerificationRecord("", currentUrl, null, "");
138
+ skipped.note = "Skipped by robots.txt policy";
139
+ records.push(skipped);
140
+ const currentRecords = persist();
141
+ processedSinceBurst += 1;
142
+ logger.info(`[crawl] processed=${currentRecords.length} url=${currentUrl} status=robots-blocked`);
143
+ return;
144
+ }
145
+ const html = typeof body === "string" ? body : "";
146
+ const status = response?.statusCode ?? null;
147
+ const effectiveContentType = contentType?.type ?? "";
148
+ const record = extractVerificationRecord(html, currentUrl, status, effectiveContentType, activeGroups);
149
+ if (activeGroups.has("stack") && effectiveContentType.includes("text/html") && html) {
150
+ record.stack = { detections: detectStackFromHtml(html, currentUrl) };
151
+ }
152
+ if (activeGroups.has("security") && effectiveContentType.includes("text/html") && html) {
153
+ record.security = await scanSecurity(html, currentUrl);
154
+ }
155
+ records.push(record);
130
156
  const currentRecords = persist();
131
157
  processedSinceBurst += 1;
132
- logger.info(`[crawl] processed=${currentRecords.length} url=${currentUrl} status=robots-blocked`);
133
- return;
134
- }
135
- const html = typeof body === "string" ? body : "";
136
- const status = response?.statusCode ?? null;
137
- const effectiveContentType = contentType?.type ?? "";
138
- const record = extractVerificationRecord(html, currentUrl, status, effectiveContentType, activeGroups);
139
- if (activeGroups.has("stack") && effectiveContentType.includes("text/html") && html) {
140
- record.stack = { detections: detectStackFromHtml(html, currentUrl) };
141
- }
142
- if (activeGroups.has("security") && effectiveContentType.includes("text/html") && html) {
143
- record.security = await scanSecurity(html, currentUrl);
144
- }
145
- records.push(record);
146
- const currentRecords = persist();
147
- processedSinceBurst += 1;
148
- logger.info(`[crawl] processed=${currentRecords.length} url=${currentUrl} status=${status ?? "error"}`);
149
- if (effectiveContentType.includes("text/html")) {
150
- const links = extractInternalLinks(html, currentUrl, origin);
151
- for (const link of links) {
152
- const normalized = normalizeUrl(link);
153
- if (seen.has(normalized))
154
- continue;
155
- if (matchesExcludePattern(normalized, excludePatterns))
156
- continue;
157
- if (robotsPolicy && !robotsPolicy.isAllowed(normalized))
158
- continue;
159
- if (!discovery.allowEnqueue(normalized))
160
- continue;
161
- seen.add(normalized);
162
- discovery.remember(normalized);
163
- const forefront = discovery.isStructural(normalized);
164
- await requestQueue.addRequest({ url: normalized, uniqueKey: normalized }, { forefront });
158
+ logger.info(`[crawl] processed=${currentRecords.length} url=${currentUrl} status=${status ?? "error"}`);
159
+ if (effectiveContentType.includes("text/html")) {
160
+ const links = extractInternalLinks(html, currentUrl, origin);
161
+ for (const link of links) {
162
+ const normalized = normalizeUrl(link);
163
+ if (seen.has(normalized))
164
+ continue;
165
+ if (matchesExcludePattern(normalized, excludePatterns))
166
+ continue;
167
+ if (robotsPolicy && !robotsPolicy.isAllowed(normalized))
168
+ continue;
169
+ if (!discovery.allowEnqueue(normalized))
170
+ continue;
171
+ seen.add(normalized);
172
+ discovery.remember(normalized);
173
+ const forefront = discovery.isStructural(normalized);
174
+ await requestQueue.addRequest({ url: normalized, uniqueKey: normalized }, { forefront });
175
+ }
165
176
  }
166
- }
167
- },
168
- failedRequestHandler: async ({ request }, error) => {
169
- const currentUrl = normalizeUrl(request.loadedUrl ?? request.url);
170
- const record = extractVerificationRecord("", currentUrl, null, "");
171
- record.error = error instanceof Error ? error.message : "Unknown error";
172
- records.push(record);
173
- const currentRecords = persist();
174
- logger.info(`[crawl] processed=${currentRecords.length} url=${currentUrl} status=error`);
175
- },
176
- });
177
- await crawler.run();
178
- const finalRecords = persist();
179
- logger.info(`[crawl] klaar pages=${finalRecords.length} interrupted=${interrupted}`);
180
- return { records: finalRecords, interrupted };
177
+ },
178
+ failedRequestHandler: async ({ request }, error) => {
179
+ const currentUrl = normalizeUrl(request.loadedUrl ?? request.url);
180
+ const record = extractVerificationRecord("", currentUrl, null, "");
181
+ record.error = error instanceof Error ? error.message : "Unknown error";
182
+ records.push(record);
183
+ const currentRecords = persist();
184
+ logger.info(`[crawl] processed=${currentRecords.length} url=${currentUrl} status=error`);
185
+ },
186
+ }, configuration);
187
+ await crawler.run();
188
+ const finalRecords = persist();
189
+ logger.info(`[crawl] klaar pages=${finalRecords.length} interrupted=${interrupted}`);
190
+ return { records: finalRecords, interrupted };
191
+ }
192
+ finally {
193
+ try {
194
+ await cleanupRunStorageDir(runDir);
195
+ }
196
+ catch (error) {
197
+ logger.warn(`[crawl] kon Crawlee storage niet opruimen path=${runDir} error=${error instanceof Error ? error.message : String(error)}`);
198
+ }
199
+ }
181
200
  }
package/docs/crawler.md CHANGED
@@ -169,3 +169,6 @@ Selection lives in `src/crawler/smartPageSelection.ts`.
169
169
  - With `--respect-robots-txt`, URLs excluded by `robots.txt` are skipped.
170
170
  - After each URL, output is written atomically.
171
171
  - On `SIGINT`, the crawl stops gracefully while preserving partial output.
172
+ - Crawlee request-queue files go under `.flightcheckStorage/<run-hash>/` (not the generic
173
+ `storage/` folder). That subdirectory is deleted when the crawl finishes, so a second
174
+ concurrent run keeps its own files until it completes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eightmedia/flightcheck",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Pre-launch website audit: crawl SEO/a11y/security checks and emit structured JSON + Markdown reports.",
5
5
  "license": "GPL-3.0",
6
6
  "type": "module",