@nitpicker/core 0.24.0 → 0.25.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/README.md +165 -35
- package/lib/assert-u32-id.d.ts +15 -0
- package/lib/assert-u32-id.js +22 -0
- package/lib/check-native-executor-config.d.ts +21 -0
- package/lib/check-native-executor-config.js +24 -0
- package/lib/compute-alias-keys.d.ts +28 -0
- package/lib/compute-alias-keys.js +30 -0
- package/lib/compute-body-hash.d.ts +22 -0
- package/lib/compute-body-hash.js +25 -0
- package/lib/compute-url-sort-ranks.d.ts +28 -0
- package/lib/compute-url-sort-ranks.js +49 -0
- package/lib/create-isolated-cluster-run.d.ts +35 -0
- package/lib/create-isolated-cluster-run.js +52 -0
- package/lib/create-template-classification-run.d.ts +49 -0
- package/lib/create-template-classification-run.js +386 -0
- package/lib/group-alias-candidates.d.ts +40 -0
- package/lib/group-alias-candidates.js +104 -0
- package/lib/load-native-binding.d.ts +26 -0
- package/lib/load-native-binding.js +152 -0
- package/lib/match-js-license-comment-signal.d.ts +19 -0
- package/lib/match-js-license-comment-signal.js +21 -0
- package/lib/match-selector-html.d.ts +41 -0
- package/lib/match-selector-html.js +42 -0
- package/lib/match-selector-snapshots.d.ts +40 -0
- package/lib/match-selector-snapshots.js +57 -0
- package/lib/match-structural-technology-signals.d.ts +23 -0
- package/lib/match-structural-technology-signals.js +25 -0
- package/lib/max-decoded-snapshot-bytes.d.ts +22 -0
- package/lib/max-decoded-snapshot-bytes.js +35 -0
- package/lib/native-binding.d.ts +9 -0
- package/lib/native-binding.js +9 -0
- package/lib/pack-stored-snapshots.d.ts +23 -0
- package/lib/pack-stored-snapshots.js +53 -0
- package/lib/run-with-signal.d.ts +27 -0
- package/lib/run-with-signal.js +54 -0
- package/lib/search-html-document.d.ts +32 -0
- package/lib/search-html-document.js +31 -0
- package/lib/search-html-snapshots.d.ts +46 -0
- package/lib/search-html-snapshots.js +54 -0
- package/lib/template-block-sample-size.d.ts +13 -0
- package/lib/template-block-sample-size.js +14 -0
- package/lib/template-exact-page-limit.d.ts +18 -0
- package/lib/template-exact-page-limit.js +19 -0
- package/lib/template-memory-budget.d.ts +16 -0
- package/lib/template-memory-budget.js +17 -0
- package/lib/to-native-selector-plan.d.ts +16 -0
- package/lib/to-native-selector-plan.js +40 -0
- package/lib/to-u32-ids.d.ts +14 -0
- package/lib/to-u32-ids.js +23 -0
- package/lib/types.d.ts +736 -248
- package/lib/validate-search-pattern.d.ts +25 -0
- package/lib/validate-search-pattern.js +27 -0
- package/package.json +83 -16
- package/lib/core.d.ts +0 -4
- package/lib/core.js +0 -4
- package/lib/discover-analyze-plugins.d.ts +0 -14
- package/lib/discover-analyze-plugins.js +0 -32
- package/lib/get-table-cache-root.d.ts +0 -18
- package/lib/get-table-cache-root.js +0 -22
- package/lib/hooks/define-plugin.d.ts +0 -68
- package/lib/hooks/define-plugin.js +0 -69
- package/lib/import-modules.d.ts +0 -24
- package/lib/import-modules.js +0 -38
- package/lib/load-plugin-settings.d.ts +0 -44
- package/lib/load-plugin-settings.js +0 -125
- package/lib/nitpicker.d.ts +0 -150
- package/lib/nitpicker.js +0 -437
- package/lib/page-analysis-worker.d.ts +0 -58
- package/lib/page-analysis-worker.js +0 -68
- package/lib/read-plugin-labels.d.ts +0 -15
- package/lib/read-plugin-labels.js +0 -30
- package/lib/table.d.ts +0 -75
- package/lib/table.js +0 -132
- package/lib/url-event-bus.d.ts +0 -33
- package/lib/url-event-bus.js +0 -21
- package/lib/worker/runner.d.ts +0 -25
- package/lib/worker/runner.js +0 -31
- package/lib/worker/types.d.ts +0 -23
- package/lib/worker/types.js +0 -1
- package/lib/worker/worker-pool.d.ts +0 -82
- package/lib/worker/worker-pool.js +0 -278
- package/lib/worker/worker.d.ts +0 -34
- package/lib/worker/worker.js +0 -108
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
/**
|
|
6
|
+
* Oldest glibc the Linux addon is built against (the `manylinux_2_28`
|
|
7
|
+
* release container). Matches the floor of Node.js 24's own Linux binaries,
|
|
8
|
+
* so any Linux that can run a supported Node can load the addon.
|
|
9
|
+
*/
|
|
10
|
+
const MIN_GLIBC = [2, 28];
|
|
11
|
+
const SUPPORTED_PLATFORMS_MESSAGE = 'Supported platforms: darwin-arm64 (Apple silicon macOS), linux-x64 with glibc >= 2.28 (including WSL2).';
|
|
12
|
+
/**
|
|
13
|
+
* Prebuilt addon for each supported host: the platform package that ships
|
|
14
|
+
* it, and its file name (identical inside that package and at the
|
|
15
|
+
* `@nitpicker/core` root, where a local `yarn build` puts it).
|
|
16
|
+
*/
|
|
17
|
+
const PLATFORM_ADDONS = {
|
|
18
|
+
'darwin-arm64': {
|
|
19
|
+
packageName: '@nitpicker/core-darwin-arm64',
|
|
20
|
+
fileName: 'core.darwin-arm64.node',
|
|
21
|
+
},
|
|
22
|
+
'linux-x64': {
|
|
23
|
+
packageName: '@nitpicker/core-linux-x64-gnu',
|
|
24
|
+
fileName: 'core.linux-x64-gnu.node',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Loads the native addon for the running platform.
|
|
29
|
+
*
|
|
30
|
+
* Looks for a locally built `core.<platform>.node` at the package root
|
|
31
|
+
* first (a monorepo checkout after `yarn build`), then for the prebuilt
|
|
32
|
+
* platform package installed through `optionalDependencies`. Prebuilt
|
|
33
|
+
* binaries exist only for darwin-arm64 and linux-x64 (glibc), and there is
|
|
34
|
+
* no JavaScript fallback, so any other platform fails here with the list of
|
|
35
|
+
* supported ones instead of a bare `MODULE_NOT_FOUND`.
|
|
36
|
+
*
|
|
37
|
+
* Load errors of an addon that does exist (a corrupt or ABI-incompatible
|
|
38
|
+
* file) are rethrown as-is rather than retried against the other location:
|
|
39
|
+
* silently picking a different binary would hide the broken one.
|
|
40
|
+
* @param options - Overrides for the host facts; omitted fields use the
|
|
41
|
+
* running process.
|
|
42
|
+
* @returns The addon's exported functions.
|
|
43
|
+
* @throws {Error} When the platform is unsupported or the platform package is
|
|
44
|
+
* not installed.
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const binding = loadNativeBinding();
|
|
48
|
+
* binding.computeBodyHash(Buffer.from('<body>x</body>'));
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function loadNativeBinding(options) {
|
|
52
|
+
const platform = options?.platform ?? process.platform;
|
|
53
|
+
const arch = options?.arch ?? process.arch;
|
|
54
|
+
const packageDir = options?.packageDir ??
|
|
55
|
+
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
56
|
+
const fileExists = options?.fileExists ?? existsSync;
|
|
57
|
+
const requireModule = options?.requireModule ?? createRequire(import.meta.url);
|
|
58
|
+
const host = `${platform}-${arch}`;
|
|
59
|
+
const addon = host in PLATFORM_ADDONS
|
|
60
|
+
? PLATFORM_ADDONS[host]
|
|
61
|
+
: null;
|
|
62
|
+
if (!addon) {
|
|
63
|
+
throw new Error(`@nitpicker/core has no prebuilt native addon for ${host}. ${SUPPORTED_PLATFORMS_MESSAGE}`);
|
|
64
|
+
}
|
|
65
|
+
if (platform === 'linux') {
|
|
66
|
+
// Detected only here: building the diagnostic report costs startup time
|
|
67
|
+
// that macOS (no libc choice) has no reason to pay.
|
|
68
|
+
const glibcVersion = options?.glibcVersion === undefined ? detectGlibcVersion() : options.glibcVersion;
|
|
69
|
+
if (glibcVersion === null) {
|
|
70
|
+
throw new Error(`@nitpicker/core requires glibc on Linux, but this system uses another C library (e.g. musl). ${SUPPORTED_PLATFORMS_MESSAGE}`);
|
|
71
|
+
}
|
|
72
|
+
if (!isAtLeast(glibcVersion, MIN_GLIBC)) {
|
|
73
|
+
throw new Error(`@nitpicker/core requires glibc >= ${MIN_GLIBC.join('.')}, but this system has glibc ${glibcVersion}. ${SUPPORTED_PLATFORMS_MESSAGE}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const localBuild = path.join(packageDir, addon.fileName);
|
|
77
|
+
if (fileExists(localBuild)) {
|
|
78
|
+
return requireModule(localBuild);
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return requireModule(addon.packageName);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
// The same error covers a package that is absent and one that is present
|
|
85
|
+
// without its binary (a source checkout before `yarn build`), so the
|
|
86
|
+
// message names both fixes instead of guessing.
|
|
87
|
+
if (isModuleNotFound(error, addon.packageName)) {
|
|
88
|
+
throw new Error(`@nitpicker/core could not load its native addon from ${addon.packageName}: the package is not installed, or is installed without ${addon.fileName}. ` +
|
|
89
|
+
'In a source checkout, run `yarn build` (it builds the addon locally). ' +
|
|
90
|
+
'Otherwise reinstall without --no-optional / --omit=optional, which skip the platform package.', { cause: error });
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The glibc version the process runs against, read from Node's diagnostic
|
|
97
|
+
* report header (absent under musl).
|
|
98
|
+
*
|
|
99
|
+
* Network interfaces are excluded from the report for the call — they are
|
|
100
|
+
* the slow part of building it and irrelevant here — and the previous
|
|
101
|
+
* setting is restored so the process's own diagnostic reports are unchanged.
|
|
102
|
+
* @returns The glibc version string, or `null` when the C library is not glibc.
|
|
103
|
+
*/
|
|
104
|
+
function detectGlibcVersion() {
|
|
105
|
+
// `excludeNetwork` exists at runtime on every supported Node (>= 24) but is
|
|
106
|
+
// missing from the installed `@types/node`'s `ProcessReport`.
|
|
107
|
+
const processReport = process.report;
|
|
108
|
+
const previous = processReport.excludeNetwork;
|
|
109
|
+
processReport.excludeNetwork = true;
|
|
110
|
+
try {
|
|
111
|
+
const report = processReport.getReport();
|
|
112
|
+
return report.header?.glibcVersionRuntime ?? null;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
processReport.excludeNetwork = previous;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Compares a `major.minor` version numerically, so `2.9` is older than
|
|
120
|
+
* `2.28` (a string comparison would say the opposite).
|
|
121
|
+
* @param version - Version string such as `"2.35"`.
|
|
122
|
+
* @param minimum - Required `[major, minor]`.
|
|
123
|
+
* @returns Whether `version` is at least `minimum`.
|
|
124
|
+
*/
|
|
125
|
+
function isAtLeast(version, minimum) {
|
|
126
|
+
const [minMajor, minMinor] = minimum;
|
|
127
|
+
const [major = 0, minor = 0] = version.split('.').map(Number);
|
|
128
|
+
return major > minMajor || (major === minMajor && minor >= minMinor);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Whether `error` is Node's `MODULE_NOT_FOUND` for `specifier` itself, as
|
|
132
|
+
* opposed to a module that `specifier` failed to load in turn.
|
|
133
|
+
*
|
|
134
|
+
* Only the first line is compared — the rest of the message is the require
|
|
135
|
+
* stack, which names the platform package's own files when something inside
|
|
136
|
+
* it is missing. That line names either the package itself (`Cannot find
|
|
137
|
+
* module '<specifier>'`, not installed) or a path inside it (`Cannot find
|
|
138
|
+
* module '…/<specifier>/core.….node'`, installed without its binary).
|
|
139
|
+
* @param error - The value thrown by `require`.
|
|
140
|
+
* @param specifier - The specifier that was required.
|
|
141
|
+
* @returns `true` only for a not-found error whose first line names the
|
|
142
|
+
* package or its own `main` file.
|
|
143
|
+
*/
|
|
144
|
+
function isModuleNotFound(error, specifier) {
|
|
145
|
+
if (!(error instanceof Error) ||
|
|
146
|
+
!('code' in error) ||
|
|
147
|
+
error.code !== 'MODULE_NOT_FOUND') {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const [firstLine = ''] = error.message.split('\n');
|
|
151
|
+
return firstLine.includes(`'${specifier}'`) || firstLine.includes(`/${specifier}/`);
|
|
152
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StructuralTechnologySignal } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Matches the license-banner fingerprints (`js-license-comment`, such as
|
|
4
|
+
* Vue's leading `/*! * Vue.js v3…` comment) against the leading bytes of a
|
|
5
|
+
* JS resource, returning the first that occurs. The same definitions
|
|
6
|
+
* {@link import('./match-structural-technology-signals.js').matchStructuralTechnologySignals}
|
|
7
|
+
* applies to HTML; the other fingerprints describe HTML and are not tested.
|
|
8
|
+
* @param script - The resource's leading bytes as fetched. Bytes that are
|
|
9
|
+
* not valid UTF-8 (a prefix cut inside a character) read as U+FFFD in the
|
|
10
|
+
* evidence, as `Buffer.prototype.toString('utf8')` decodes them.
|
|
11
|
+
* @returns The first matching signal, or `null` when none matches.
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* matchJsLicenseCommentSignal(Buffer.from('/*!\n * Vue.js v3.4.0\n *\/'));
|
|
15
|
+
* // { technology: 'Vue', signalType: 'js-license-comment',
|
|
16
|
+
* // evidence: '/*!\n * Vue.js v3', weight: 55, category: 'JavaScript frameworks' }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function matchJsLicenseCommentSignal(script: Uint8Array): StructuralTechnologySignal | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { nativeBinding } from './native-binding.js';
|
|
2
|
+
/**
|
|
3
|
+
* Matches the license-banner fingerprints (`js-license-comment`, such as
|
|
4
|
+
* Vue's leading `/*! * Vue.js v3…` comment) against the leading bytes of a
|
|
5
|
+
* JS resource, returning the first that occurs. The same definitions
|
|
6
|
+
* {@link import('./match-structural-technology-signals.js').matchStructuralTechnologySignals}
|
|
7
|
+
* applies to HTML; the other fingerprints describe HTML and are not tested.
|
|
8
|
+
* @param script - The resource's leading bytes as fetched. Bytes that are
|
|
9
|
+
* not valid UTF-8 (a prefix cut inside a character) read as U+FFFD in the
|
|
10
|
+
* evidence, as `Buffer.prototype.toString('utf8')` decodes them.
|
|
11
|
+
* @returns The first matching signal, or `null` when none matches.
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* matchJsLicenseCommentSignal(Buffer.from('/*!\n * Vue.js v3.4.0\n *\/'));
|
|
15
|
+
* // { technology: 'Vue', signalType: 'js-license-comment',
|
|
16
|
+
* // evidence: '/*!\n * Vue.js v3', weight: 55, category: 'JavaScript frameworks' }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function matchJsLicenseCommentSignal(script) {
|
|
20
|
+
return nativeBinding.matchJsLicenseCommentSignal(script);
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { HtmlMatchOutcome, SelectorPlan } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decides whether any element of one stored HTML document matches a planned
|
|
4
|
+
* selector list, using the cheapest stage that is exact: an ordered-literal
|
|
5
|
+
* prefilter, then the start tags alone for selectors one tag decides, then
|
|
6
|
+
* an open-element stack.
|
|
7
|
+
*
|
|
8
|
+
* The result is a judgement about the stored string read as markup with
|
|
9
|
+
* balanced tags (the interpretation rules are in `@nitpicker/cli`'s
|
|
10
|
+
* `docs/query.md`), not a DOM query: serialization is not injective, so it
|
|
11
|
+
* is not guaranteed to equal the verdict on the DOM the string came from.
|
|
12
|
+
* Tag and attribute names ignore ASCII case only.
|
|
13
|
+
* @param options - The plan and the document.
|
|
14
|
+
* @param options.plan - The validated selector list.
|
|
15
|
+
* @param options.html - The stored markup, as a string or as UTF-8 bytes.
|
|
16
|
+
* @returns The verdict, which stages ran, and how many elements the stack tested.
|
|
17
|
+
* @throws {Error} With code `InvalidArg` if the plan is malformed.
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const { matched } = matchSelectorHtml({
|
|
21
|
+
* plan: {
|
|
22
|
+
* alternatives: [
|
|
23
|
+
* {
|
|
24
|
+
* compounds: [{ tag: 'img', attributes: [], nth: [], negations: [] }],
|
|
25
|
+
* combinators: [],
|
|
26
|
+
* prefilterLiterals: ['<img'],
|
|
27
|
+
* direct: true,
|
|
28
|
+
* },
|
|
29
|
+
* ],
|
|
30
|
+
* needsChildIndex: false,
|
|
31
|
+
* needsTypeIndex: false,
|
|
32
|
+
* },
|
|
33
|
+
* html: '<p><img src="a.png"></p>',
|
|
34
|
+
* });
|
|
35
|
+
* matched; // true
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function matchSelectorHtml(options: {
|
|
39
|
+
readonly plan: SelectorPlan;
|
|
40
|
+
readonly html: string | Uint8Array;
|
|
41
|
+
}): HtmlMatchOutcome;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { nativeBinding } from './native-binding.js';
|
|
2
|
+
import { toNativeSelectorPlan } from './to-native-selector-plan.js';
|
|
3
|
+
/**
|
|
4
|
+
* Decides whether any element of one stored HTML document matches a planned
|
|
5
|
+
* selector list, using the cheapest stage that is exact: an ordered-literal
|
|
6
|
+
* prefilter, then the start tags alone for selectors one tag decides, then
|
|
7
|
+
* an open-element stack.
|
|
8
|
+
*
|
|
9
|
+
* The result is a judgement about the stored string read as markup with
|
|
10
|
+
* balanced tags (the interpretation rules are in `@nitpicker/cli`'s
|
|
11
|
+
* `docs/query.md`), not a DOM query: serialization is not injective, so it
|
|
12
|
+
* is not guaranteed to equal the verdict on the DOM the string came from.
|
|
13
|
+
* Tag and attribute names ignore ASCII case only.
|
|
14
|
+
* @param options - The plan and the document.
|
|
15
|
+
* @param options.plan - The validated selector list.
|
|
16
|
+
* @param options.html - The stored markup, as a string or as UTF-8 bytes.
|
|
17
|
+
* @returns The verdict, which stages ran, and how many elements the stack tested.
|
|
18
|
+
* @throws {Error} With code `InvalidArg` if the plan is malformed.
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const { matched } = matchSelectorHtml({
|
|
22
|
+
* plan: {
|
|
23
|
+
* alternatives: [
|
|
24
|
+
* {
|
|
25
|
+
* compounds: [{ tag: 'img', attributes: [], nth: [], negations: [] }],
|
|
26
|
+
* combinators: [],
|
|
27
|
+
* prefilterLiterals: ['<img'],
|
|
28
|
+
* direct: true,
|
|
29
|
+
* },
|
|
30
|
+
* ],
|
|
31
|
+
* needsChildIndex: false,
|
|
32
|
+
* needsTypeIndex: false,
|
|
33
|
+
* },
|
|
34
|
+
* html: '<p><img src="a.png"></p>',
|
|
35
|
+
* });
|
|
36
|
+
* matched; // true
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function matchSelectorHtml(options) {
|
|
40
|
+
const { plan, html } = options;
|
|
41
|
+
return nativeBinding.matchSelectorHtml(toNativeSelectorPlan(plan), typeof html === 'string' ? Buffer.from(html, 'utf8') : html);
|
|
42
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SelectorPlan, SnapshotMatchOutcome, StoredHtmlSnapshot } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Judges a batch of stored HTML snapshots against a planned selector list.
|
|
4
|
+
*
|
|
5
|
+
* Each snapshot is decompressed, read as UTF-8 (an invalid sequence becomes
|
|
6
|
+
* U+FFFD, as `Buffer#toString('utf8')` reads it) and judged by
|
|
7
|
+
* {@link import('./match-selector-html.js').matchSelectorHtml}'s rules, in
|
|
8
|
+
* parallel on the native thread pool. No more decompressed documents are
|
|
9
|
+
* alive at once than there are threads. The call waits its turn behind any
|
|
10
|
+
* other asynchronous native call when `NITPICKER_NATIVE_CONCURRENCY` calls are
|
|
11
|
+
* already running (see `checkNativeExecutorConfig`). Only the batch's stored bytes cross
|
|
12
|
+
* the boundary, as one buffer and one offsets array.
|
|
13
|
+
*
|
|
14
|
+
* A snapshot that cannot be decompressed — its zstd data is corrupt, or it
|
|
15
|
+
* decompresses past `MAX_DECODED_SNAPSHOT_BYTES` — fails the whole call; it is
|
|
16
|
+
* never judged "no match".
|
|
17
|
+
* @param options - The plan and the batch.
|
|
18
|
+
* @param options.plan - The validated selector list.
|
|
19
|
+
* @param options.snapshots - The stored snapshots, in the order the verdicts are returned.
|
|
20
|
+
* @param options.signal - Stops the call at its next snapshot, or within the next mebibyte of a decompression under way; a call still waiting for its turn stops when the turn comes.
|
|
21
|
+
* @returns One verdict per snapshot, in input order.
|
|
22
|
+
* @throws {Error} If a codec is not `zstd` or `none`, or the batch takes more than 4 GiB.
|
|
23
|
+
* The returned promise rejects with a plain error naming the snapshot
|
|
24
|
+
* (`snapshot <n> could not be decoded`) if its zstd data is corrupt or
|
|
25
|
+
* decompresses past the limit, and with the signal's reason if `signal`
|
|
26
|
+
* aborted before the call finished.
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const verdicts = await matchSelectorSnapshots({
|
|
30
|
+
* plan,
|
|
31
|
+
* snapshots: rows.map((row) => ({ body: row.body, codec: row.codec })),
|
|
32
|
+
* });
|
|
33
|
+
* const matchedCount = verdicts.filter((verdict) => verdict.matched).length;
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function matchSelectorSnapshots(options: {
|
|
37
|
+
readonly plan: SelectorPlan;
|
|
38
|
+
readonly snapshots: readonly StoredHtmlSnapshot[];
|
|
39
|
+
readonly signal?: AbortSignal;
|
|
40
|
+
}): Promise<SnapshotMatchOutcome[]>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { MAX_DECODED_SNAPSHOT_BYTES } from './max-decoded-snapshot-bytes.js';
|
|
2
|
+
import { nativeBinding } from './native-binding.js';
|
|
3
|
+
import { packStoredSnapshots } from './pack-stored-snapshots.js';
|
|
4
|
+
import { runWithSignal } from './run-with-signal.js';
|
|
5
|
+
import { toNativeSelectorPlan } from './to-native-selector-plan.js';
|
|
6
|
+
/** Bit 0 of a verdict byte the native call returns: an element matched. */
|
|
7
|
+
const MATCHED = 1;
|
|
8
|
+
/** Bit 1: the ordered-literal prefilter rejected the document alone. */
|
|
9
|
+
const PREFILTERED = 2;
|
|
10
|
+
/** Bit 2: the open-element stack judged the document. */
|
|
11
|
+
const TOKENIZED = 4;
|
|
12
|
+
/**
|
|
13
|
+
* Judges a batch of stored HTML snapshots against a planned selector list.
|
|
14
|
+
*
|
|
15
|
+
* Each snapshot is decompressed, read as UTF-8 (an invalid sequence becomes
|
|
16
|
+
* U+FFFD, as `Buffer#toString('utf8')` reads it) and judged by
|
|
17
|
+
* {@link import('./match-selector-html.js').matchSelectorHtml}'s rules, in
|
|
18
|
+
* parallel on the native thread pool. No more decompressed documents are
|
|
19
|
+
* alive at once than there are threads. The call waits its turn behind any
|
|
20
|
+
* other asynchronous native call when `NITPICKER_NATIVE_CONCURRENCY` calls are
|
|
21
|
+
* already running (see `checkNativeExecutorConfig`). Only the batch's stored bytes cross
|
|
22
|
+
* the boundary, as one buffer and one offsets array.
|
|
23
|
+
*
|
|
24
|
+
* A snapshot that cannot be decompressed — its zstd data is corrupt, or it
|
|
25
|
+
* decompresses past `MAX_DECODED_SNAPSHOT_BYTES` — fails the whole call; it is
|
|
26
|
+
* never judged "no match".
|
|
27
|
+
* @param options - The plan and the batch.
|
|
28
|
+
* @param options.plan - The validated selector list.
|
|
29
|
+
* @param options.snapshots - The stored snapshots, in the order the verdicts are returned.
|
|
30
|
+
* @param options.signal - Stops the call at its next snapshot, or within the next mebibyte of a decompression under way; a call still waiting for its turn stops when the turn comes.
|
|
31
|
+
* @returns One verdict per snapshot, in input order.
|
|
32
|
+
* @throws {Error} If a codec is not `zstd` or `none`, or the batch takes more than 4 GiB.
|
|
33
|
+
* The returned promise rejects with a plain error naming the snapshot
|
|
34
|
+
* (`snapshot <n> could not be decoded`) if its zstd data is corrupt or
|
|
35
|
+
* decompresses past the limit, and with the signal's reason if `signal`
|
|
36
|
+
* aborted before the call finished.
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const verdicts = await matchSelectorSnapshots({
|
|
40
|
+
* plan,
|
|
41
|
+
* snapshots: rows.map((row) => ({ body: row.body, codec: row.codec })),
|
|
42
|
+
* });
|
|
43
|
+
* const matchedCount = verdicts.filter((verdict) => verdict.matched).length;
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export async function matchSelectorSnapshots(options) {
|
|
47
|
+
const { plan, snapshots, signal } = options;
|
|
48
|
+
// Before packing: a request that is already cancelled copies nothing.
|
|
49
|
+
signal?.throwIfAborted();
|
|
50
|
+
const { bodies, offsets, codecs } = packStoredSnapshots(snapshots);
|
|
51
|
+
const flags = await runWithSignal(signal, (cancelToken) => nativeBinding.matchSelectorSnapshots(toNativeSelectorPlan(plan), bodies, offsets, codecs, MAX_DECODED_SNAPSHOT_BYTES, cancelToken));
|
|
52
|
+
return [...flags].map((flag) => ({
|
|
53
|
+
matched: (flag & MATCHED) !== 0,
|
|
54
|
+
prefiltered: (flag & PREFILTERED) !== 0,
|
|
55
|
+
tokenized: (flag & TOKENIZED) !== 0,
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { StructuralTechnologySignal } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Matches the fixed list of frontend-framework fingerprints (Next.js,
|
|
4
|
+
* Astro, Vue, Nuxt, Svelte, SvelteKit, Remix, Gatsby, Angular — HTML
|
|
5
|
+
* markers, URL path fragments, scoped attributes, license banners) against
|
|
6
|
+
* a page's HTML, returning one signal per fingerprint found, in the list's
|
|
7
|
+
* order, with the evidence cut from its first match.
|
|
8
|
+
*
|
|
9
|
+
* The list lives in `crates/nitpicker_tech_detect`. Its output equals what
|
|
10
|
+
* the JavaScript regular expressions it was ported from stored into
|
|
11
|
+
* existing archives' `technology_signals`, so old and new crawls stay
|
|
12
|
+
* comparable. Takes UTF-8 bytes so the native side never builds a UTF-16
|
|
13
|
+
* copy, and scans the page once for all fingerprints.
|
|
14
|
+
* @param html - The page's HTML as UTF-8 bytes — `Buffer.from(html, 'utf8')`.
|
|
15
|
+
* @returns The signals found; empty when the page shows no fingerprint.
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* matchStructuralTechnologySignals(Buffer.from('<div data-v-1a2b3c4d></div>'));
|
|
19
|
+
* // [{ technology: 'Vue', signalType: 'scoped-attr', evidence: ' data-v-1a2b3c4d',
|
|
20
|
+
* // weight: 40, category: 'JavaScript frameworks' }]
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function matchStructuralTechnologySignals(html: Uint8Array): StructuralTechnologySignal[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { nativeBinding } from './native-binding.js';
|
|
2
|
+
/**
|
|
3
|
+
* Matches the fixed list of frontend-framework fingerprints (Next.js,
|
|
4
|
+
* Astro, Vue, Nuxt, Svelte, SvelteKit, Remix, Gatsby, Angular — HTML
|
|
5
|
+
* markers, URL path fragments, scoped attributes, license banners) against
|
|
6
|
+
* a page's HTML, returning one signal per fingerprint found, in the list's
|
|
7
|
+
* order, with the evidence cut from its first match.
|
|
8
|
+
*
|
|
9
|
+
* The list lives in `crates/nitpicker_tech_detect`. Its output equals what
|
|
10
|
+
* the JavaScript regular expressions it was ported from stored into
|
|
11
|
+
* existing archives' `technology_signals`, so old and new crawls stay
|
|
12
|
+
* comparable. Takes UTF-8 bytes so the native side never builds a UTF-16
|
|
13
|
+
* copy, and scans the page once for all fingerprints.
|
|
14
|
+
* @param html - The page's HTML as UTF-8 bytes — `Buffer.from(html, 'utf8')`.
|
|
15
|
+
* @returns The signals found; empty when the page shows no fingerprint.
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* matchStructuralTechnologySignals(Buffer.from('<div data-v-1a2b3c4d></div>'));
|
|
19
|
+
* // [{ technology: 'Vue', signalType: 'scoped-attr', evidence: ' data-v-1a2b3c4d',
|
|
20
|
+
* // weight: 40, category: 'JavaScript frameworks' }]
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function matchStructuralTechnologySignals(html) {
|
|
24
|
+
return nativeBinding.matchStructuralTechnologySignals(html);
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bytes one zstd-compressed HTML snapshot may decompress to (3 × (2^29 − 24),
|
|
3
|
+
* about 1.5 GiB). A snapshot that decompresses past it is reported as
|
|
4
|
+
* undecodable instead of being held whole: the size a zstd frame header
|
|
5
|
+
* states can be absent or false, so a stored blob of a few kilobytes can
|
|
6
|
+
* otherwise expand to any size, on every thread of a batch at once (RFC 8878,
|
|
7
|
+
* §8). A snapshot stored uncompressed is not limited — it is already in
|
|
8
|
+
* memory at its size.
|
|
9
|
+
*
|
|
10
|
+
* The value is the most the crawler can store, so no snapshot it stored is
|
|
11
|
+
* ever refused: a snapshot is the string `page.content()` returns, saved as
|
|
12
|
+
* UTF-8, a V8 string holds at most 2^29 − 24 UTF-16 code units, and one code
|
|
13
|
+
* unit takes at most 3 bytes of UTF-8. Read from the addon, which owns the value; the
|
|
14
|
+
* batch wrappers pass this module's export back to it, so a spec can shrink
|
|
15
|
+
* it. An addon that lacks it fails the import rather than leaving
|
|
16
|
+
* `maxOutputLength` undefined, which would decompress without a limit.
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* zstdDecompressSync(body, { maxOutputLength: MAX_DECODED_SNAPSHOT_BYTES });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare const MAX_DECODED_SNAPSHOT_BYTES: number;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { nativeBinding } from './native-binding.js';
|
|
2
|
+
/**
|
|
3
|
+
* The addon's limit, checked to be a positive integer.
|
|
4
|
+
* @returns The limit in bytes.
|
|
5
|
+
* @throws {TypeError} If the addon does not export it.
|
|
6
|
+
*/
|
|
7
|
+
function readLimit() {
|
|
8
|
+
const limit = nativeBinding.MAX_DECODED_SNAPSHOT_BYTES;
|
|
9
|
+
if (typeof limit !== 'number' || !Number.isSafeInteger(limit) || limit <= 0) {
|
|
10
|
+
throw new TypeError(`The native addon exports no valid MAX_DECODED_SNAPSHOT_BYTES (got ${String(limit)}); rebuild or reinstall @nitpicker/core.`);
|
|
11
|
+
}
|
|
12
|
+
return limit;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Bytes one zstd-compressed HTML snapshot may decompress to (3 × (2^29 − 24),
|
|
16
|
+
* about 1.5 GiB). A snapshot that decompresses past it is reported as
|
|
17
|
+
* undecodable instead of being held whole: the size a zstd frame header
|
|
18
|
+
* states can be absent or false, so a stored blob of a few kilobytes can
|
|
19
|
+
* otherwise expand to any size, on every thread of a batch at once (RFC 8878,
|
|
20
|
+
* §8). A snapshot stored uncompressed is not limited — it is already in
|
|
21
|
+
* memory at its size.
|
|
22
|
+
*
|
|
23
|
+
* The value is the most the crawler can store, so no snapshot it stored is
|
|
24
|
+
* ever refused: a snapshot is the string `page.content()` returns, saved as
|
|
25
|
+
* UTF-8, a V8 string holds at most 2^29 − 24 UTF-16 code units, and one code
|
|
26
|
+
* unit takes at most 3 bytes of UTF-8. Read from the addon, which owns the value; the
|
|
27
|
+
* batch wrappers pass this module's export back to it, so a spec can shrink
|
|
28
|
+
* it. An addon that lacks it fails the import rather than leaving
|
|
29
|
+
* `maxOutputLength` undefined, which would decompress without a limit.
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* zstdDecompressSync(body, { maxOutputLength: MAX_DECODED_SNAPSHOT_BYTES });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const MAX_DECODED_SNAPSHOT_BYTES = readLimit();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NativeBinding } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The native addon, loaded once when this module is first imported.
|
|
4
|
+
*
|
|
5
|
+
* Loaded eagerly rather than on first call so an unsupported platform fails
|
|
6
|
+
* at CLI startup with the supported-platform list, not halfway through a
|
|
7
|
+
* crawl when the first page is hashed.
|
|
8
|
+
*/
|
|
9
|
+
export declare const nativeBinding: NativeBinding;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { loadNativeBinding } from './load-native-binding.js';
|
|
2
|
+
/**
|
|
3
|
+
* The native addon, loaded once when this module is first imported.
|
|
4
|
+
*
|
|
5
|
+
* Loaded eagerly rather than on first call so an unsupported platform fails
|
|
6
|
+
* at CLI startup with the supported-platform list, not halfway through a
|
|
7
|
+
* crawl when the first page is hashed.
|
|
8
|
+
*/
|
|
9
|
+
export const nativeBinding = loadNativeBinding();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { StoredHtmlSnapshot } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lays a batch of stored snapshots out the way the native batch calls take
|
|
4
|
+
* it: the stored bytes back to back, the `n + 1` offsets that cut them into
|
|
5
|
+
* `n` snapshots, and one codec byte per snapshot. A batch crosses the
|
|
6
|
+
* boundary as three buffers, whatever its size.
|
|
7
|
+
* @param snapshots - The stored snapshots, in the order the results come back.
|
|
8
|
+
* @returns The three buffers.
|
|
9
|
+
* @throws {Error} If a codec is not `zstd` or `none`.
|
|
10
|
+
* @throws {RangeError} If the batch takes more than 4 GiB.
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const { bodies, offsets, codecs } = packStoredSnapshots([
|
|
14
|
+
* { body: Buffer.from('<p>a</p>'), codec: 'none' },
|
|
15
|
+
* ]);
|
|
16
|
+
* offsets; // Uint32Array [0, 8]
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function packStoredSnapshots(snapshots: readonly StoredHtmlSnapshot[]): {
|
|
20
|
+
readonly bodies: Uint8Array;
|
|
21
|
+
readonly offsets: Uint32Array;
|
|
22
|
+
readonly codecs: Uint8Array;
|
|
23
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** The native calls address a batch with `u32` offsets. */
|
|
2
|
+
const MAX_BATCH_BYTES = 4_294_967_295;
|
|
3
|
+
/** The codec byte the native calls read for a snapshot stored as it is. */
|
|
4
|
+
const CODEC_NONE = 0;
|
|
5
|
+
/** The codec byte the native calls read for a zstd snapshot. */
|
|
6
|
+
const CODEC_ZSTD = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Lays a batch of stored snapshots out the way the native batch calls take
|
|
9
|
+
* it: the stored bytes back to back, the `n + 1` offsets that cut them into
|
|
10
|
+
* `n` snapshots, and one codec byte per snapshot. A batch crosses the
|
|
11
|
+
* boundary as three buffers, whatever its size.
|
|
12
|
+
* @param snapshots - The stored snapshots, in the order the results come back.
|
|
13
|
+
* @returns The three buffers.
|
|
14
|
+
* @throws {Error} If a codec is not `zstd` or `none`.
|
|
15
|
+
* @throws {RangeError} If the batch takes more than 4 GiB.
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const { bodies, offsets, codecs } = packStoredSnapshots([
|
|
19
|
+
* { body: Buffer.from('<p>a</p>'), codec: 'none' },
|
|
20
|
+
* ]);
|
|
21
|
+
* offsets; // Uint32Array [0, 8]
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function packStoredSnapshots(snapshots) {
|
|
25
|
+
const offsets = new Uint32Array(snapshots.length + 1);
|
|
26
|
+
const codecs = new Uint8Array(snapshots.length);
|
|
27
|
+
let total = 0;
|
|
28
|
+
for (const [index, snapshot] of snapshots.entries()) {
|
|
29
|
+
switch (snapshot.codec) {
|
|
30
|
+
case 'none': {
|
|
31
|
+
codecs[index] = CODEC_NONE;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case 'zstd': {
|
|
35
|
+
codecs[index] = CODEC_ZSTD;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
default: {
|
|
39
|
+
throw new Error(`Unknown page_html_blobs.codec: ${snapshot.codec}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
total += snapshot.body.byteLength;
|
|
43
|
+
if (total > MAX_BATCH_BYTES) {
|
|
44
|
+
throw new RangeError(`The batch takes more than ${MAX_BATCH_BYTES} bytes (4 GiB), which the native call cannot address.`);
|
|
45
|
+
}
|
|
46
|
+
offsets[index + 1] = total;
|
|
47
|
+
}
|
|
48
|
+
const bodies = new Uint8Array(total);
|
|
49
|
+
for (const [index, snapshot] of snapshots.entries()) {
|
|
50
|
+
bodies.set(snapshot.body, offsets[index]);
|
|
51
|
+
}
|
|
52
|
+
return { bodies, offsets, codecs };
|
|
53
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { NativeCancelToken } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runs one asynchronous native call so an `AbortSignal` can stop it.
|
|
4
|
+
*
|
|
5
|
+
* With no signal, `run` gets no token and nothing is allocated. With one, a
|
|
6
|
+
* fresh native token is raised when the signal fires, and the call stops at
|
|
7
|
+
* its next check (between snapshots, between mebibytes of a snapshot's
|
|
8
|
+
* decompression, or when it leaves the queue) and rejects
|
|
9
|
+
* with code `Cancelled`; that rejection, and only that one, is replaced by
|
|
10
|
+
* the signal's reason, so an abort reads as the caller's own abort. The
|
|
11
|
+
* addon reports a cancellation over a decode failure of the same batch (the
|
|
12
|
+
* caller asked to stop); a batch whose failure came before any snapshot saw
|
|
13
|
+
* the abort still rejects with that failure, which is passed on unchanged.
|
|
14
|
+
* A signal that is already aborted rejects with its reason without calling
|
|
15
|
+
* the addon.
|
|
16
|
+
* @param signal - The caller's signal, if any.
|
|
17
|
+
* @param run - Starts the native call, passing the token as its last argument.
|
|
18
|
+
* @returns What `run` resolved with.
|
|
19
|
+
* @throws {unknown} The signal's reason when the signal aborted before or during the call.
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const flags = await runWithSignal(signal, (token) =>
|
|
23
|
+
* nativeBinding.matchSelectorSnapshots(plan, bodies, offsets, codecs, maxDecodedBytes, token),
|
|
24
|
+
* );
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function runWithSignal<T>(signal: AbortSignal | undefined, run: (token?: NativeCancelToken) => Promise<T>): Promise<T>;
|