yuku-analyzer 0.5.32
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 +164 -0
- package/analyzer.js +290 -0
- package/binding.js +63 -0
- package/decode.js +1462 -0
- package/engine.js +198 -0
- package/index.d.ts +578 -0
- package/index.js +2 -0
- package/module.js +611 -0
- package/package.json +54 -0
- package/walk.js +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# yuku-analyzer
|
|
2
|
+
|
|
3
|
+
Full semantic analysis for JavaScript and TypeScript: scopes, symbols, resolved references, closures, and cross-file module linking. Powered by [Yuku](https://github.com/yuku-toolchain/yuku), written in Zig.
|
|
4
|
+
|
|
5
|
+
The usual options are a hand-rolled scope tracker (fragile, per-file, re-bugged in every tool) or the TypeScript compiler (correct, but hundreds of milliseconds per file and a compiler-sized dependency). This is the fast path between them. One native call per file, then every query is plain JavaScript with zero per-query FFI cost, sub-millisecond on a typical file.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install yuku-analyzer yuku-parser
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`yuku-parser` is a peer dependency.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { Analyzer, SymbolFlags } from "yuku-analyzer";
|
|
17
|
+
|
|
18
|
+
const analyzer = new Analyzer();
|
|
19
|
+
|
|
20
|
+
analyzer.addFile("lib.ts", `export const helper = (x: number) => x * 2;`);
|
|
21
|
+
const main = analyzer.addFile(
|
|
22
|
+
"main.ts",
|
|
23
|
+
`import { helper } from "./lib.ts";
|
|
24
|
+
export const out = helper(21);`,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// per-file semantics
|
|
28
|
+
const helperSym = main.rootScope.find("helper");
|
|
29
|
+
console.log(helperSym.has(SymbolFlags.Import)); // true
|
|
30
|
+
console.log(helperSym.references.length); // 1, the call site
|
|
31
|
+
|
|
32
|
+
// cross-file: follow the import to where helper is actually defined
|
|
33
|
+
const def = helperSym.definition();
|
|
34
|
+
console.log(def.module.path); // "lib.ts"
|
|
35
|
+
console.log(def.symbol.has(SymbolFlags.Const)); // true
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Why this exists
|
|
39
|
+
|
|
40
|
+
JavaScript tooling that needs real semantics has had two options: track scopes by hand during a walk (fragile, incomplete, and re-implemented in every tool), or embed a full language service (heavy). `yuku-analyzer` is the missing middle: the exact scope and binding model of a production compiler, exposed to JavaScript as a small, fast object graph.
|
|
41
|
+
|
|
42
|
+
Nothing semantic is reimplemented in JavaScript. The binder, scope tree, reference resolution, and module records are computed by the same well-tested native analyzer that powers the rest of Yuku, then shipped to JavaScript as one compact buffer that the JS side only decodes, through lazy zero-copy views. That handoff is usually where native tooling stalls: either every query pays an FFI round trip, or the semantics get a hand-written JS twin that slowly drifts. Here there is one implementation and one crossing, so it cannot drift, and the corner cases arrive already correct: catch-clause scope sharing, named function expression scopes, `var` hoist targets, TS declaration merging, value space versus type space, and write detection through destructuring patterns.
|
|
43
|
+
|
|
44
|
+
## What one `addFile` gives you
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
const module = analyzer.addFile("app.tsx", source);
|
|
48
|
+
|
|
49
|
+
module.ast // ESTree / TS-ESTree program
|
|
50
|
+
module.scopes // every lexical scope, as a tree
|
|
51
|
+
module.symbols // every declared binding
|
|
52
|
+
module.references // every identifier use, resolved to its symbol
|
|
53
|
+
module.unresolvedReferences // free names and globals
|
|
54
|
+
module.imports // spec-true import records
|
|
55
|
+
module.exports // spec-true export records
|
|
56
|
+
module.diagnostics // syntax + semantic errors
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
And node-level queries that work directly on AST nodes:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
module.symbolOf(node) // the symbol a node declares or references
|
|
63
|
+
module.referenceOf(node) // the reference recorded for an identifier
|
|
64
|
+
module.scopeOf(node) // the innermost scope containing the node
|
|
65
|
+
module.resolve("name") // scope-chain lookup, like the engine does at runtime
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Node identity is exact: the node you reach by walking `module.ast` and the node a semantic query returns are the same JavaScript object, so `===` always works.
|
|
69
|
+
|
|
70
|
+
## Walking with semantic context
|
|
71
|
+
|
|
72
|
+
`module.walk` is a typed visitor walk where every handler also receives the current scope, symbol, and reference. No manual scope tracking, ever:
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
module.walk({
|
|
76
|
+
Identifier(node, ctx) {
|
|
77
|
+
if (ctx.reference?.isWrite && ctx.symbol?.has(SymbolFlags.Import)) {
|
|
78
|
+
console.log(`${node.name} assigns to an import`);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
FunctionDeclaration: {
|
|
82
|
+
enter(node, ctx) {
|
|
83
|
+
console.log(node.id.name, "declared in a", ctx.scope.kind, "scope");
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The walk mutates in place: `ctx.replace(node)`, `ctx.remove()`, `ctx.insertBefore(node)`, `ctx.insertAfter(node)`, plus `ctx.skip()` and `ctx.stop()`. Transform the AST during the walk and print it with [`yuku-codegen`](https://www.npmjs.com/package/yuku-codegen).
|
|
90
|
+
|
|
91
|
+
## Scanning without building the AST
|
|
92
|
+
|
|
93
|
+
`module.scan` visits the parsed node records directly in the binary buffer. Nothing is materialized unless you ask for it, and symbols and references resolve in index space:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
const writes = [];
|
|
97
|
+
module.scan({
|
|
98
|
+
Identifier(cursor) {
|
|
99
|
+
if (cursor.reference?.isWrite) writes.push(cursor.reference.name);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This answers questions like "every write to an imported binding across 10,000 files" without constructing a single AST object. For semantic queries like this, scanning runs about 4x faster than the equivalent walk, since it never builds the AST. Scan to find, walk to change.
|
|
105
|
+
|
|
106
|
+
## Closure analysis
|
|
107
|
+
|
|
108
|
+
`capturesOf` reports the free variables of any function: every outer binding it closes over, with the capturing reference sites and whether the function writes to the binding. Shadowing and aliasing are handled by the resolved reference table, not by name matching:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
const [fn] = main.findAll("FunctionDeclaration");
|
|
112
|
+
for (const capture of main.capturesOf(fn)) {
|
|
113
|
+
console.log(capture.symbol.name, capture.isWritten ? "(written)" : "");
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Cross-file analysis
|
|
118
|
+
|
|
119
|
+
The analyzer joins imports to exports across every added file with the spec's ResolveExport semantics: re-export and `export *` chains are followed per name, `default` never travels through `export *`, and a name supplied by multiple `export *` declarations through different bindings is reported as ambiguous.
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
// where is this binding actually defined?
|
|
123
|
+
analyzer.definitionOf(symbol); // { module, symbol } or null for external modules
|
|
124
|
+
|
|
125
|
+
// every use across the whole graph, imports followed back
|
|
126
|
+
analyzer.referencesOf(symbol); // [{ module, reference }, ...]
|
|
127
|
+
|
|
128
|
+
module.exportedNames(); // every exported name, `export *` chains included
|
|
129
|
+
module.dependencies; // modules this file imports from
|
|
130
|
+
module.dependents; // modules that import this file
|
|
131
|
+
analyzer.diagnostics; // e.g. "Module './lib.ts' has no export 'helpr'"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Linking is automatic: every cross-file surface relinks on demand after files change. Call `analyzer.link()` explicitly if you want to control when the work happens.
|
|
135
|
+
|
|
136
|
+
Module resolution is pluggable. The default resolves relative specifiers among added files with standard extension and index probing. Pass your own resolver for anything else:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
const analyzer = new Analyzer({
|
|
140
|
+
resolve: (specifier, importerPath) => myResolver(specifier, importerPath),
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Performance
|
|
145
|
+
|
|
146
|
+
Analysis runs in the native parser pass, so full semantics cost roughly half of parsing time on top of the parse itself. Validated against 55,000+ real-world files.
|
|
147
|
+
|
|
148
|
+
Concretely, on an Apple M-series machine: parsing plus complete semantic analysis of a typical source file lands well under a millisecond, walking sustains tens of millions of nodes per second, and linking a 2,000-module graph takes about a millisecond.
|
|
149
|
+
|
|
150
|
+
## TypeScript
|
|
151
|
+
|
|
152
|
+
Everything is fully typed. Visitor handlers receive exact node types, AST types come from `yuku-parser`, and the semantic surface (`Module`, `Scope`, `Symbol`, `Reference`, `Import`, `Export`, `Capture`) is exported:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import type { Module, Symbol, Capture } from "yuku-analyzer";
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Documentation
|
|
159
|
+
|
|
160
|
+
The full documentation, including the architecture, every type, and the design decisions: [yuku.fyi/analyzer](https://yuku.fyi/analyzer).
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT
|
package/analyzer.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { Module, SymbolFlags } from "./module.js";
|
|
2
|
+
|
|
3
|
+
const RESOLVE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mts", ".mjs", ".cts", ".cjs"];
|
|
4
|
+
|
|
5
|
+
// the third resolution outcome of ResolveExport (16.2.1.7.2.2): more
|
|
6
|
+
// than one `export *` declaration supplies the name through different
|
|
7
|
+
// bindings. distinct from null (not found / circular).
|
|
8
|
+
const AMBIGUOUS = Symbol("ambiguous");
|
|
9
|
+
|
|
10
|
+
export class Analyzer {
|
|
11
|
+
#modules = new Map();
|
|
12
|
+
#resolve;
|
|
13
|
+
#diagnostics = [];
|
|
14
|
+
#dirty = false;
|
|
15
|
+
#linking = false;
|
|
16
|
+
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.#resolve = options.resolve ?? defaultResolve(this.#modules);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
addFile(path, source, options) {
|
|
22
|
+
const module = new Module(this, path, source, options);
|
|
23
|
+
this.#modules.set(path, module);
|
|
24
|
+
this.#dirty = true;
|
|
25
|
+
return module;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
removeFile(path) {
|
|
29
|
+
const removed = this.#modules.delete(path);
|
|
30
|
+
if (removed) this.#dirty = true;
|
|
31
|
+
return removed;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module(path) {
|
|
35
|
+
return this.#modules.get(path);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get modules() {
|
|
39
|
+
return this.#modules;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get diagnostics() {
|
|
43
|
+
this._ensureLinked();
|
|
44
|
+
return this.#diagnostics;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// the static half of spec linking. resolves every module request to an
|
|
48
|
+
// added module, wires the dependency graph, and reports binding
|
|
49
|
+
// validation failures as diagnostics rather than throwing. unresolvable
|
|
50
|
+
// specifiers are skipped so a partial graph still links.
|
|
51
|
+
link() {
|
|
52
|
+
this.#dirty = false;
|
|
53
|
+
this.#linking = true;
|
|
54
|
+
this.#diagnostics = [];
|
|
55
|
+
for (const module of this.#modules.values()) {
|
|
56
|
+
module._deps = [];
|
|
57
|
+
module._dependents = [];
|
|
58
|
+
// drop earlier resolutions so removed modules cannot leak through
|
|
59
|
+
for (const record of module.imports) record._resolved = null;
|
|
60
|
+
for (const record of module.exports) record._resolved = null;
|
|
61
|
+
}
|
|
62
|
+
for (const module of this.#modules.values()) {
|
|
63
|
+
for (const record of module.imports) {
|
|
64
|
+
record._resolved = this.#resolveModule(record.specifier, module, false);
|
|
65
|
+
if (record._resolved === null) continue;
|
|
66
|
+
this.#wire(module, record._resolved);
|
|
67
|
+
// namespace and side-effect imports name nothing to validate
|
|
68
|
+
if (record.name !== null) {
|
|
69
|
+
this.#validate(module, record, record.name, "Import");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const record of module.exports) {
|
|
73
|
+
if (record.specifier === null) continue;
|
|
74
|
+
record._resolved = this.#resolveModule(record.specifier, module, false);
|
|
75
|
+
if (record._resolved === null) continue;
|
|
76
|
+
this.#wire(module, record._resolved);
|
|
77
|
+
// `export * as ns` (fromName null) trivially resolves and bare
|
|
78
|
+
// `export *` conflicts are deferred to use sites, per spec
|
|
79
|
+
if (record.fromName !== null) {
|
|
80
|
+
this.#validate(module, record, record.fromName, "Re-export");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.#linking = false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// reports a record whose name does not resolve, or resolves
|
|
88
|
+
// ambiguously, against its (already resolved) source module
|
|
89
|
+
#validate(module, record, name, what) {
|
|
90
|
+
const resolution = this.#resolveExport(record._resolved, name, []);
|
|
91
|
+
if (resolution !== null && resolution !== AMBIGUOUS) return;
|
|
92
|
+
const message =
|
|
93
|
+
resolution === null
|
|
94
|
+
? `Module '${record.specifier}' has no export '${name}'`
|
|
95
|
+
: `${what} '${name}' of module '${record.specifier}' is ambiguous: multiple 'export *' declarations supply it`;
|
|
96
|
+
this.#diagnostics.push({
|
|
97
|
+
severity: "error",
|
|
98
|
+
message,
|
|
99
|
+
module: module.path,
|
|
100
|
+
start: record.node.start,
|
|
101
|
+
end: record.node.end,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// follows an import binding to its defining module and symbol via
|
|
106
|
+
// ResolveExport (InitializeEnvironment step 7.c). namespace
|
|
107
|
+
// resolutions yield { module, symbol: null }; unresolved or ambiguous
|
|
108
|
+
// chains yield null.
|
|
109
|
+
definitionOf(symbol) {
|
|
110
|
+
this._ensureLinked();
|
|
111
|
+
let module = symbol.module;
|
|
112
|
+
let current = symbol;
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
while ((current.flags & SymbolFlags.Import) !== 0) {
|
|
115
|
+
const key = `${module.path}:${current.id}`;
|
|
116
|
+
if (seen.has(key)) return null;
|
|
117
|
+
seen.add(key);
|
|
118
|
+
const record = module._importOfSymbol(current.id);
|
|
119
|
+
if (record === undefined || record._resolved === null) return null;
|
|
120
|
+
if (record.isNamespace || record.name === null) {
|
|
121
|
+
return { module: record._resolved, symbol: null };
|
|
122
|
+
}
|
|
123
|
+
const resolution = this.#resolveExport(record._resolved, record.name, []);
|
|
124
|
+
if (resolution === null || resolution === AMBIGUOUS) return null;
|
|
125
|
+
if (resolution.namespace) return { module: resolution.module, symbol: null };
|
|
126
|
+
if (resolution.symbol === null) return null;
|
|
127
|
+
module = resolution.module;
|
|
128
|
+
current = resolution.symbol;
|
|
129
|
+
}
|
|
130
|
+
return { module, symbol: current };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
referencesOf(symbol) {
|
|
134
|
+
this._ensureLinked();
|
|
135
|
+
const origin = this.definitionOf(symbol) ?? { module: symbol.module, symbol };
|
|
136
|
+
if (origin.symbol === null) return [];
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const reference of origin.symbol.references) {
|
|
139
|
+
out.push({ module: origin.module, reference });
|
|
140
|
+
}
|
|
141
|
+
for (const module of this.#modules.values()) {
|
|
142
|
+
if (module === origin.module) continue;
|
|
143
|
+
for (const record of module.imports) {
|
|
144
|
+
if (record.local === null) continue;
|
|
145
|
+
const definition = this.definitionOf(record.local);
|
|
146
|
+
if (
|
|
147
|
+
definition !== null &&
|
|
148
|
+
definition.symbol === origin.symbol &&
|
|
149
|
+
definition.module === origin.module
|
|
150
|
+
) {
|
|
151
|
+
for (const reference of record.local.references) {
|
|
152
|
+
out.push({ module, reference });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_ensureLinked() {
|
|
161
|
+
if (this.#dirty && !this.#linking) this.link();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#resolveModule(specifier, importer, quiet) {
|
|
165
|
+
const resolved = this.#resolve(specifier, importer.path);
|
|
166
|
+
if (resolved === null || resolved === undefined) return null;
|
|
167
|
+
const module = this.#modules.get(resolved);
|
|
168
|
+
if (module === undefined) {
|
|
169
|
+
if (!quiet) {
|
|
170
|
+
this.#diagnostics.push({
|
|
171
|
+
severity: "warning",
|
|
172
|
+
message: `Resolver returned '${resolved}' for '${specifier}' but no such file was added`,
|
|
173
|
+
module: importer.path,
|
|
174
|
+
start: 0,
|
|
175
|
+
end: 0,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return module;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#wire(from, to) {
|
|
184
|
+
if (!from._deps.includes(to)) from._deps.push(to);
|
|
185
|
+
if (!to._dependents.includes(from)) to._dependents.push(from);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ResolveExport(exportName, resolveSet), 16.2.1.7.2.2: resolves a
|
|
189
|
+
// name to the binding that defines it, following named re-export and
|
|
190
|
+
// `export *` chains. returns { module, symbol, namespace } (the
|
|
191
|
+
// spec's ResolvedBinding, with namespace: true for its namespace
|
|
192
|
+
// [[BindingName]]), null when unresolvable or circular, or AMBIGUOUS.
|
|
193
|
+
//
|
|
194
|
+
// local and indirect entries live in one name-keyed map (_exportMap);
|
|
195
|
+
// duplicate export names are an early error, so the spec's step 5/6
|
|
196
|
+
// partition cannot change the outcome. ParseModule step 10.a.ii
|
|
197
|
+
// (rewriting an export of an imported binding through its original
|
|
198
|
+
// module) is applied at resolution time instead of collection time.
|
|
199
|
+
#resolveExport(module, exportName, resolveSet) {
|
|
200
|
+
// step 3: a repeated (module, exportName) pair is a circular import
|
|
201
|
+
// request. the pair key still permits re-entering a module for a
|
|
202
|
+
// different name, which renaming re-export chains legitimately do.
|
|
203
|
+
for (const entry of resolveSet) {
|
|
204
|
+
if (entry.module === module && entry.exportName === exportName) return null;
|
|
205
|
+
}
|
|
206
|
+
resolveSet.push({ module, exportName });
|
|
207
|
+
|
|
208
|
+
const direct = module._exportMap().get(exportName);
|
|
209
|
+
if (direct !== undefined) {
|
|
210
|
+
if (direct.specifier === null) {
|
|
211
|
+
const local = direct.local;
|
|
212
|
+
if (local !== null && (local.flags & SymbolFlags.Import) !== 0) {
|
|
213
|
+
// export of an imported binding: resolve through the original
|
|
214
|
+
// module (the ParseModule rewrite)
|
|
215
|
+
const record = module._importOfSymbol(local.id);
|
|
216
|
+
if (record !== undefined) {
|
|
217
|
+
const imported =
|
|
218
|
+
record._resolved ?? this.#resolveModule(record.specifier, module, true);
|
|
219
|
+
if (imported === null) return null;
|
|
220
|
+
if (record.isNamespace) return { module: imported, symbol: null, namespace: true };
|
|
221
|
+
return this.#resolveExport(imported, record.name, resolveSet);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// step 5: the module provides the direct binding
|
|
225
|
+
return { module, symbol: local, namespace: false };
|
|
226
|
+
}
|
|
227
|
+
const next = direct._resolved ?? this.#resolveModule(direct.specifier, module, true);
|
|
228
|
+
if (next === null) return null;
|
|
229
|
+
// step 6: `export * as ns` binds the namespace, named re-exports
|
|
230
|
+
// follow the chain under the source name
|
|
231
|
+
if (direct.isNamespaceReexport) return { module: next, symbol: null, namespace: true };
|
|
232
|
+
return this.#resolveExport(next, direct.fromName, resolveSet);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// step 7: "default" is never satisfied through `export *`
|
|
236
|
+
if (exportName === "default") return null;
|
|
237
|
+
|
|
238
|
+
// steps 8-9: identical star bindings collapse, different ones are
|
|
239
|
+
// ambiguous
|
|
240
|
+
let starResolution = null;
|
|
241
|
+
for (const star of module._starExports()) {
|
|
242
|
+
const next = star._resolved ?? this.#resolveModule(star.specifier, module, true);
|
|
243
|
+
if (next === null) continue;
|
|
244
|
+
const resolution = this.#resolveExport(next, exportName, resolveSet);
|
|
245
|
+
if (resolution === AMBIGUOUS) return AMBIGUOUS;
|
|
246
|
+
if (resolution === null) continue;
|
|
247
|
+
if (starResolution === null) {
|
|
248
|
+
starResolution = resolution;
|
|
249
|
+
} else if (
|
|
250
|
+
resolution.module !== starResolution.module ||
|
|
251
|
+
resolution.namespace !== starResolution.namespace ||
|
|
252
|
+
resolution.symbol !== starResolution.symbol
|
|
253
|
+
) {
|
|
254
|
+
return AMBIGUOUS;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return starResolution;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function defaultResolve(modules) {
|
|
262
|
+
return (specifier, importer) => {
|
|
263
|
+
if (!specifier.startsWith(".")) return null;
|
|
264
|
+
const base = joinPath(dirnameOf(importer), specifier);
|
|
265
|
+
if (modules.has(base)) return base;
|
|
266
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
267
|
+
if (modules.has(base + ext)) return base + ext;
|
|
268
|
+
}
|
|
269
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
270
|
+
const index = `${base}/index${ext}`;
|
|
271
|
+
if (modules.has(index)) return index;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function dirnameOf(path) {
|
|
278
|
+
const i = path.lastIndexOf("/");
|
|
279
|
+
return i === -1 ? "" : path.slice(0, i);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function joinPath(dir, relative) {
|
|
283
|
+
const parts = dir === "" ? [] : dir.split("/");
|
|
284
|
+
for (const part of relative.split("/")) {
|
|
285
|
+
if (part === "" || part === ".") continue;
|
|
286
|
+
if (part === "..") parts.pop();
|
|
287
|
+
else parts.push(part);
|
|
288
|
+
}
|
|
289
|
+
return parts.join("/");
|
|
290
|
+
}
|
package/binding.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const { platform, arch } = process;
|
|
10
|
+
|
|
11
|
+
const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-');
|
|
12
|
+
|
|
13
|
+
function isMusl() {
|
|
14
|
+
if (platform !== 'linux') return false;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
if (readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')) return true;
|
|
18
|
+
} catch {}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const report = typeof process.report?.getReport === 'function'
|
|
22
|
+
? process.report.getReport()
|
|
23
|
+
: null;
|
|
24
|
+
if (report) {
|
|
25
|
+
const header = typeof report === 'string' ? JSON.parse(report).header : report.header;
|
|
26
|
+
if (header?.glibcVersionRuntime) return false;
|
|
27
|
+
if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isFileMusl)) return true;
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
return execSync('ldd --version', { encoding: 'utf8' }).includes('musl');
|
|
33
|
+
} catch {}
|
|
34
|
+
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadBinding() {
|
|
39
|
+
const errors = [];
|
|
40
|
+
const libc = platform === 'linux' ? (isMusl() ? '-musl' : '-gnu') : '';
|
|
41
|
+
const suffix = `${platform}-${arch}${libc}`;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
return require(join(__dirname, '@yuku-analyzer', 'binding-' + suffix, 'yuku-analyzer.node'));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
errors.push(e);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
return require('@yuku-analyzer/binding-' + suffix + '/yuku-analyzer.node');
|
|
51
|
+
} catch (e) {
|
|
52
|
+
errors.push(e);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Failed to load native binding for ${platform}-${arch}.\n` +
|
|
57
|
+
`If this persists, try removing node_modules and reinstalling.\n` +
|
|
58
|
+
errors.map(e => ` - ${e.message}`).join('\n'),
|
|
59
|
+
{ cause: errors[errors.length - 1] }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default loadBinding();
|