@shrkcrft/boundaries 0.1.0-alpha.28 → 0.1.0-alpha.30
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/dist/baseline/compute-baseline.d.ts.map +1 -1
- package/dist/baseline/compute-baseline.js +2 -1
- package/dist/extract/extract-tokens.d.ts +19 -1
- package/dist/extract/extract-tokens.d.ts.map +1 -1
- package/dist/extract/extract-tokens.js +53 -1
- package/dist/extract/import-edges.d.ts +49 -0
- package/dist/extract/import-edges.d.ts.map +1 -0
- package/dist/extract/import-edges.js +157 -0
- package/dist/extract/inspect-source.d.ts +6 -0
- package/dist/extract/inspect-source.d.ts.map +1 -1
- package/dist/extract/inspect-source.js +6 -1
- package/dist/extract/parse-imports.d.ts +58 -0
- package/dist/extract/parse-imports.d.ts.map +1 -0
- package/dist/extract/parse-imports.js +155 -0
- package/dist/generated/check-provenance.d.ts +20 -2
- package/dist/generated/check-provenance.d.ts.map +1 -1
- package/dist/generated/check-provenance.js +24 -5
- package/dist/generated/scan-generated.d.ts +49 -3
- package/dist/generated/scan-generated.d.ts.map +1 -1
- package/dist/generated/scan-generated.js +90 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/policy/evaluate-policy.d.ts +1 -1
- package/dist/policy/evaluate-policy.d.ts.map +1 -1
- package/dist/policy/evaluate-policy.js +2 -1
- package/dist/util/walk-files.d.ts +21 -2
- package/dist/util/walk-files.d.ts.map +1 -1
- package/dist/util/walk-files.js +85 -4
- package/dist/wiring/evaluate-wiring.d.ts +10 -3
- package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
- package/dist/wiring/evaluate-wiring.js +12 -7
- package/dist/wiring/explain-wiring.d.ts +7 -9
- package/dist/wiring/explain-wiring.d.ts.map +1 -1
- package/dist/wiring/explain-wiring.js +18 -0
- package/dist/wiring/plan-wiring-fix.d.ts +75 -0
- package/dist/wiring/plan-wiring-fix.d.ts.map +1 -0
- package/dist/wiring/plan-wiring-fix.js +302 -0
- package/dist/wiring/registration-graph.d.ts +1 -1
- package/dist/wiring/registration-graph.d.ts.map +1 -1
- package/dist/wiring/registration-graph.js +8 -3
- package/dist/wiring/registry-query.d.ts +1 -1
- package/dist/wiring/registry-query.d.ts.map +1 -1
- package/dist/wiring/registry-query.js +7 -3
- package/dist/wiring/scan-wiring-files.d.ts +1 -1
- package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
- package/dist/wiring/scan-wiring-files.js +5 -1
- package/dist/wiring/sink-imports.d.ts +84 -0
- package/dist/wiring/sink-imports.d.ts.map +1 -0
- package/dist/wiring/sink-imports.js +115 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compute-baseline.d.ts","sourceRoot":"","sources":["../../src/baseline/compute-baseline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"compute-baseline.d.ts","sourceRoot":"","sources":["../../src/baseline/compute-baseline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAMpD,gFAAgF;AAChF,MAAM,WAAW,iBAAiB;IAChC,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAC1C,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,EACrB,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,iBAAiB,CAenB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { matchesAny } from "../scan/glob.js";
|
|
2
2
|
import { readMatchingFiles } from "../util/walk-files.js";
|
|
3
|
+
import { loadTsconfigPaths } from "../scan/tsconfig-aliases.js";
|
|
3
4
|
import { extractTokens } from "../extract/extract-tokens.js";
|
|
4
5
|
/**
|
|
5
6
|
* Compute a baseline from the extraction DSL — the pure alternative to shelling
|
|
@@ -17,7 +18,7 @@ export function computeBaselineFromExtractor(projectRoot, source, excludeDirs =
|
|
|
17
18
|
const files = [...cache.entries()]
|
|
18
19
|
.filter(([path]) => matchesAny(path, source.files ?? []))
|
|
19
20
|
.map(([path, content]) => ({ path, content }));
|
|
20
|
-
const res = extractTokens(source, files);
|
|
21
|
+
const res = extractTokens(source, files, { tsconfigPaths: loadTsconfigPaths(projectRoot) });
|
|
21
22
|
if (res.error) {
|
|
22
23
|
return { text: '[]', ids: [], filesScanned: files.length, error: res.error };
|
|
23
24
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type IWiringSource } from '@shrkcrft/core';
|
|
2
|
+
import type { ITsconfigPathsMap } from '../scan/tsconfig-aliases.js';
|
|
2
3
|
/** A file made available to an extractor. */
|
|
3
4
|
export interface IExtractFileEntry {
|
|
4
5
|
/** Project-relative POSIX path. */
|
|
@@ -11,11 +12,28 @@ export interface IExtractedSite {
|
|
|
11
12
|
readonly file: string;
|
|
12
13
|
readonly line: number;
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Optional project context an extractor may need.
|
|
17
|
+
*
|
|
18
|
+
* Kept OUT of the source spec because it is not part of the rule — it is what
|
|
19
|
+
* the caller knows about the workspace. Only `import-edges` reads it today, and
|
|
20
|
+
* every other kind stays a pure function of (source, file contents).
|
|
21
|
+
*/
|
|
22
|
+
export interface IExtractContext {
|
|
23
|
+
/** tsconfig `paths` map, so alias specifiers resolve as the compiler would. */
|
|
24
|
+
readonly tsconfigPaths?: ITsconfigPathsMap;
|
|
25
|
+
}
|
|
14
26
|
/** Outcome of running one source: the sites, or a configuration error. */
|
|
15
27
|
export interface IExtractResult {
|
|
16
28
|
readonly sites: readonly IExtractedSite[];
|
|
17
29
|
/** Set when the SOURCE is misconfigured (never thrown — rules degrade). */
|
|
18
30
|
readonly error?: string;
|
|
31
|
+
/**
|
|
32
|
+
* A diagnosis for a zero-match that is CORRECT but almost certainly not what
|
|
33
|
+
* the author meant. Distinct from `error`: nothing is wrong with the source,
|
|
34
|
+
* it just cannot mean what it looks like it means.
|
|
35
|
+
*/
|
|
36
|
+
readonly hint?: string;
|
|
19
37
|
}
|
|
20
38
|
/**
|
|
21
39
|
* Run one source's extractor over the given files.
|
|
@@ -26,5 +44,5 @@ export interface IExtractResult {
|
|
|
26
44
|
* new extractor kind for free). Never throws — a misconfigured source returns
|
|
27
45
|
* an `error` and no sites.
|
|
28
46
|
*/
|
|
29
|
-
export declare function extractTokens(source: IWiringSource, files: readonly IExtractFileEntry[]): IExtractResult;
|
|
47
|
+
export declare function extractTokens(source: IWiringSource, files: readonly IExtractFileEntry[], context?: IExtractContext): IExtractResult;
|
|
30
48
|
//# sourceMappingURL=extract-tokens.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extract-tokens.d.ts","sourceRoot":"","sources":["../../src/extract/extract-tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"extract-tokens.d.ts","sourceRoot":"","sources":["../../src/extract/extract-tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AASrE,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAChC,mCAAmC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;CAC5C;AAED,0EAA0E;AAC1E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,SAAS,iBAAiB,EAAE,EACnC,OAAO,GAAE,eAAoB,GAC5B,cAAc,CAwEhB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveExtractorAnchor, resolveExtractorKind, validateWiringSource, } from '@shrkcrft/core';
|
|
2
2
|
import { safeCompile } from "../util/safe-regex.js";
|
|
3
|
+
import { extractImportEdges } from "./import-edges.js";
|
|
3
4
|
import { elementToken, elementValue, escapeRegex, lineOf, scanBalanced, } from "./scan-literals.js";
|
|
4
5
|
/**
|
|
5
6
|
* Run one source's extractor over the given files.
|
|
@@ -10,7 +11,7 @@ import { elementToken, elementValue, escapeRegex, lineOf, scanBalanced, } from "
|
|
|
10
11
|
* new extractor kind for free). Never throws — a misconfigured source returns
|
|
11
12
|
* an `error` and no sites.
|
|
12
13
|
*/
|
|
13
|
-
export function extractTokens(source, files) {
|
|
14
|
+
export function extractTokens(source, files, context = {}) {
|
|
14
15
|
const error = validateWiringSource(source);
|
|
15
16
|
if (error)
|
|
16
17
|
return { sites: [], error };
|
|
@@ -45,6 +46,20 @@ export function extractTokens(source, files) {
|
|
|
45
46
|
case 'json-path':
|
|
46
47
|
sites = byJsonPath(source.jsonPath, files);
|
|
47
48
|
break;
|
|
49
|
+
case 'filenames':
|
|
50
|
+
sites = byFilenames(source, files);
|
|
51
|
+
break;
|
|
52
|
+
case 'import-edges': {
|
|
53
|
+
const edges = extractImportEdges(source, files, {
|
|
54
|
+
...(context.tsconfigPaths ? { tsconfigPaths: context.tsconfigPaths } : {}),
|
|
55
|
+
});
|
|
56
|
+
if (edges.error)
|
|
57
|
+
return { sites: [], error: edges.error };
|
|
58
|
+
if (edges.hint)
|
|
59
|
+
return { sites: edges.sites, hint: edges.hint };
|
|
60
|
+
sites = edges.sites;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
48
63
|
default:
|
|
49
64
|
return { sites: [], error: `unknown extract kind "${String(kind)}"` };
|
|
50
65
|
}
|
|
@@ -68,6 +83,43 @@ export function extractTokens(source, files) {
|
|
|
68
83
|
}
|
|
69
84
|
return { sites };
|
|
70
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* One id per FILE, derived from its path.
|
|
88
|
+
*
|
|
89
|
+
* Every other extractor reads a file's CONTENTS, so "a declared thing must have
|
|
90
|
+
* its sibling FILE" — a `FOO_DESCRIPTOR` const paired with `FOO_DESCRIPTOR.ts` —
|
|
91
|
+
* had no expression at all. Paired with a `parity` wiring rule this asserts the
|
|
92
|
+
* correspondence in BOTH directions: a const with no file, and a file with no
|
|
93
|
+
* const.
|
|
94
|
+
*
|
|
95
|
+
* The site line is 1: the id comes from the path, not from any line in it, and
|
|
96
|
+
* claiming a more specific location would be inventing one.
|
|
97
|
+
*/
|
|
98
|
+
function byFilenames(source, files) {
|
|
99
|
+
const capture = source.capturePath ?? 'stem';
|
|
100
|
+
let re;
|
|
101
|
+
if (capture === 'regex') {
|
|
102
|
+
const compiled = safeCompile(source.pathPattern, source.pathPatternFlags);
|
|
103
|
+
if (!compiled.re)
|
|
104
|
+
return [];
|
|
105
|
+
re = compiled.re;
|
|
106
|
+
}
|
|
107
|
+
const sites = [];
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
if (capture === 'regex') {
|
|
110
|
+
re.lastIndex = 0;
|
|
111
|
+
const m = re.exec(file.path);
|
|
112
|
+
if (m?.[1])
|
|
113
|
+
sites.push({ token: m[1], file: file.path, line: 1 });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const basename = file.path.slice(file.path.lastIndexOf('/') + 1);
|
|
117
|
+
const dot = basename.indexOf('.');
|
|
118
|
+
const token = capture === 'basename' || dot <= 0 ? basename : basename.slice(0, dot);
|
|
119
|
+
sites.push({ token, file: file.path, line: 1 });
|
|
120
|
+
}
|
|
121
|
+
return sites;
|
|
122
|
+
}
|
|
71
123
|
/** Capture-group-1 of a pattern, per file. */
|
|
72
124
|
function byRegex(source, files) {
|
|
73
125
|
const { re } = safeCompile(source.pattern, source.flags);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { IWiringSource } from '@shrkcrft/core';
|
|
2
|
+
import { type ITsconfigPathsMap } from '../scan/tsconfig-aliases.js';
|
|
3
|
+
import type { IExtractFileEntry, IExtractedSite } from './extract-tokens.js';
|
|
4
|
+
/**
|
|
5
|
+
* The `import-edges` extractor — the dependency graph as a rule input.
|
|
6
|
+
*
|
|
7
|
+
* Every other extractor reads file CONTENTS: a regex, an array literal, an enum
|
|
8
|
+
* body, an export list. None of them can see which file imports what, so an
|
|
9
|
+
* entire class of invariant — alias-resolved dependency direction — was
|
|
10
|
+
* inexpressible as a rule, and repos hand-rolled scripts for it (adoption
|
|
11
|
+
* ledgers, orphan scans, deprecation ratchets).
|
|
12
|
+
*
|
|
13
|
+
* This emits that missing fact as an id set, so every existing plane gets it for
|
|
14
|
+
* free: a `baseline` over the set is an adoption ledger (a LOST edge is a silent
|
|
15
|
+
* de-adoption); `direction: 'no-shrink'` over the importer set is a deprecation
|
|
16
|
+
* ratchet; a `wiring` rule whose registered side is "symbols with ≥1 importer"
|
|
17
|
+
* finds dead generated code a byte-drift gate cannot see.
|
|
18
|
+
*
|
|
19
|
+
* **On freshness.** It reads the files the caller already walked, resolving
|
|
20
|
+
* specifiers on the spot — there is no persisted graph index behind it and
|
|
21
|
+
* therefore no staleness question to get wrong. That is deliberate: a rule
|
|
22
|
+
* answering from a stale graph would be a confident false green, the exact
|
|
23
|
+
* failure this engine exists to prevent.
|
|
24
|
+
*
|
|
25
|
+
* **Honest scope.** It resolves what the text says plus tsconfig path aliases —
|
|
26
|
+
* the same resolution `check boundaries` uses. It does not follow a re-export
|
|
27
|
+
* chain to attribute a symbol to its original module: a barrel is reported as
|
|
28
|
+
* the module the consumer actually names. That is a lexer's honest answer, and
|
|
29
|
+
* `to.files` matches the resolved target when you need the real path.
|
|
30
|
+
*/
|
|
31
|
+
/** What an emitted id represents. */
|
|
32
|
+
export type ImportEdgeEmit = 'edge' | 'symbol' | 'from';
|
|
33
|
+
/** Context a caller supplies so alias specifiers resolve. */
|
|
34
|
+
export interface IImportEdgeContext {
|
|
35
|
+
readonly tsconfigPaths?: ITsconfigPathsMap;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Extract the import edges a source selects.
|
|
39
|
+
*
|
|
40
|
+
* `files` is the CONSUMER glob (the `from` side): it reuses the field every
|
|
41
|
+
* other extractor uses, so `--changed-only` footprinting, the shared walk, and
|
|
42
|
+
* `$use` all work on it unchanged.
|
|
43
|
+
*/
|
|
44
|
+
export declare function extractImportEdges(source: IWiringSource, files: readonly IExtractFileEntry[], context?: IImportEdgeContext): {
|
|
45
|
+
sites: IExtractedSite[];
|
|
46
|
+
error?: string;
|
|
47
|
+
hint?: string;
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=import-edges.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-edges.d.ts","sourceRoot":"","sources":["../../src/extract/import-edges.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,OAAO,EAA0B,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAG7F,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,qCAAqC;AACrC,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAkFxD,6DAA6D;AAC7D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;CAC5C;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,SAAS,iBAAiB,EAAE,EACnC,OAAO,GAAE,kBAAuB,GAC/B;IAAE,KAAK,EAAE,cAAc,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAgF5D"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { matchesAny } from "../scan/glob.js";
|
|
2
|
+
import { resolveAliasCandidates } from "../scan/tsconfig-aliases.js";
|
|
3
|
+
import { safeCompile } from "../util/safe-regex.js";
|
|
4
|
+
import { parseImportStatements } from "./parse-imports.js";
|
|
5
|
+
/** Normalize a relative specifier against the importing file's directory. */
|
|
6
|
+
function resolveRelative(fromFile, specifier) {
|
|
7
|
+
const dir = fromFile.includes('/') ? fromFile.slice(0, fromFile.lastIndexOf('/')) : '';
|
|
8
|
+
const stack = dir.split('/').filter(Boolean);
|
|
9
|
+
for (const part of specifier.split('/')) {
|
|
10
|
+
if (part === '' || part === '.')
|
|
11
|
+
continue;
|
|
12
|
+
if (part === '..') {
|
|
13
|
+
stack.pop();
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
stack.push(part);
|
|
17
|
+
}
|
|
18
|
+
return stack.join('/');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a specifier the way the boundary engine does: relative paths against
|
|
22
|
+
* the importing file, bare specifiers through the tsconfig alias map, and
|
|
23
|
+
* anything left over kept as the literal package name.
|
|
24
|
+
*/
|
|
25
|
+
function resolveSpecifier(fromFile, specifier, aliases) {
|
|
26
|
+
if (specifier.startsWith('.')) {
|
|
27
|
+
return { specifier, path: resolveRelative(fromFile, specifier) };
|
|
28
|
+
}
|
|
29
|
+
if (aliases) {
|
|
30
|
+
const candidates = resolveAliasCandidates(specifier, aliases);
|
|
31
|
+
// The first candidate is the one the compiler would try first; keeping only
|
|
32
|
+
// it makes the emitted id deterministic rather than order-dependent.
|
|
33
|
+
if (candidates.length > 0)
|
|
34
|
+
return { specifier, path: candidates[0] };
|
|
35
|
+
}
|
|
36
|
+
return { specifier };
|
|
37
|
+
}
|
|
38
|
+
/** Whether a resolved specifier is one the rule's `to` selects. */
|
|
39
|
+
function matchesTarget(resolved, to, modulePattern) {
|
|
40
|
+
const hasSelector = to.module !== undefined || to.modulePattern !== undefined || (to.files?.length ?? 0) > 0;
|
|
41
|
+
// No target selector at all means "every import" — useful when the rule
|
|
42
|
+
// narrows by symbol alone.
|
|
43
|
+
if (!hasSelector)
|
|
44
|
+
return true;
|
|
45
|
+
if (to.module !== undefined) {
|
|
46
|
+
// Exact package, or a subpath of it: `@x/generated` selects
|
|
47
|
+
// `@x/generated` and `@x/generated/views` but never `@x/generated-legacy`.
|
|
48
|
+
if (resolved.specifier === to.module || resolved.specifier.startsWith(`${to.module}/`)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (modulePattern) {
|
|
53
|
+
modulePattern.lastIndex = 0;
|
|
54
|
+
if (modulePattern.test(resolved.specifier))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (to.files && to.files.length > 0 && resolved.path !== undefined) {
|
|
58
|
+
if (matchesAny(resolved.path, to.files))
|
|
59
|
+
return true;
|
|
60
|
+
// Specifiers routinely omit the extension, so probe the usual endings
|
|
61
|
+
// rather than forcing every config to spell `*.{ts,tsx,…}` itself.
|
|
62
|
+
for (const ext of ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx']) {
|
|
63
|
+
if (matchesAny(resolved.path + ext, to.files))
|
|
64
|
+
return true;
|
|
65
|
+
if (matchesAny(`${resolved.path}/index${ext}`, to.files))
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extract the import edges a source selects.
|
|
73
|
+
*
|
|
74
|
+
* `files` is the CONSUMER glob (the `from` side): it reuses the field every
|
|
75
|
+
* other extractor uses, so `--changed-only` footprinting, the shared walk, and
|
|
76
|
+
* `$use` all work on it unchanged.
|
|
77
|
+
*/
|
|
78
|
+
export function extractImportEdges(source, files, context = {}) {
|
|
79
|
+
const to = source.to ?? {};
|
|
80
|
+
const emit = source.emit ?? 'edge';
|
|
81
|
+
let modulePattern;
|
|
82
|
+
if (to.modulePattern !== undefined) {
|
|
83
|
+
const compiled = safeCompile(to.modulePattern, to.modulePatternFlags);
|
|
84
|
+
if (compiled.error || !compiled.re)
|
|
85
|
+
return { sites: [], error: `to.modulePattern ${compiled.error}` };
|
|
86
|
+
modulePattern = compiled.re;
|
|
87
|
+
}
|
|
88
|
+
let symbolPattern;
|
|
89
|
+
if (to.match !== undefined) {
|
|
90
|
+
const compiled = safeCompile(to.match, to.matchFlags);
|
|
91
|
+
if (compiled.error || !compiled.re)
|
|
92
|
+
return { sites: [], error: `to.match ${compiled.error}` };
|
|
93
|
+
symbolPattern = compiled.re;
|
|
94
|
+
}
|
|
95
|
+
const sites = [];
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
for (const statement of parseImportStatements(file.content)) {
|
|
98
|
+
const resolved = resolveSpecifier(file.path, statement.specifier, context.tsconfigPaths);
|
|
99
|
+
if (!matchesTarget(resolved, to, modulePattern))
|
|
100
|
+
continue;
|
|
101
|
+
const symbols = statement.bindings
|
|
102
|
+
.map((b) => b.imported ?? b.local)
|
|
103
|
+
.filter((name) => {
|
|
104
|
+
if (!symbolPattern)
|
|
105
|
+
return true;
|
|
106
|
+
symbolPattern.lastIndex = 0;
|
|
107
|
+
return symbolPattern.test(name);
|
|
108
|
+
});
|
|
109
|
+
// A symbol filter that matched nothing means this statement is not a hit,
|
|
110
|
+
// even though its module was: the rule asked about specific symbols.
|
|
111
|
+
if (symbolPattern && symbols.length === 0)
|
|
112
|
+
continue;
|
|
113
|
+
if (emit === 'from') {
|
|
114
|
+
sites.push({ token: file.path, file: file.path, line: statement.line });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (symbols.length === 0) {
|
|
118
|
+
// A side-effect or type-only import binds no name; the edge is still a
|
|
119
|
+
// real dependency, so it is reported against the module it names.
|
|
120
|
+
if (emit === 'symbol')
|
|
121
|
+
continue;
|
|
122
|
+
sites.push({
|
|
123
|
+
token: `${file.path} → ${resolved.specifier}`,
|
|
124
|
+
file: file.path,
|
|
125
|
+
line: statement.line,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
for (const symbol of symbols) {
|
|
130
|
+
sites.push({
|
|
131
|
+
token: emit === 'symbol' ? symbol : `${file.path} → ${symbol}`,
|
|
132
|
+
file: file.path,
|
|
133
|
+
line: statement.line,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// The one place a user's intuition reliably trips: `to.files` matches an
|
|
139
|
+
// import's DIRECTLY-resolved path, and a symbol re-exported through a barrel
|
|
140
|
+
// resolves to the package entry, not the deep file. "Edges to files in
|
|
141
|
+
// generated/**" then finds nothing, correctly but unhelpfully. Detecting the
|
|
142
|
+
// exact shape of that dead end and naming the fix costs one conditional.
|
|
143
|
+
//
|
|
144
|
+
// Narrow on purpose. A rule that ALREADY targets by module has been told to
|
|
145
|
+
// use the thing it is using, which is worse than saying nothing — so the
|
|
146
|
+
// hint fires only when `to.files` is the sole target selector.
|
|
147
|
+
const targetsByFilesOnly = (to.files?.length ?? 0) > 0 && to.module === undefined && to.modulePattern === undefined;
|
|
148
|
+
if (sites.length === 0 && files.length > 0 && targetsByFilesOnly) {
|
|
149
|
+
return {
|
|
150
|
+
sites,
|
|
151
|
+
hint: '0 edges via `to.files` — that matches an import\'s DIRECTLY-resolved path, so a symbol ' +
|
|
152
|
+
're-exported through a barrel/package resolves to the package entry, not the deep file. ' +
|
|
153
|
+
'Target by `to.module` + `to.match` instead.',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return { sites };
|
|
157
|
+
}
|
|
@@ -10,6 +10,12 @@ export interface ISourceInspection {
|
|
|
10
10
|
readonly sites: readonly IExtractedSite[];
|
|
11
11
|
/** Set when the source is misconfigured. */
|
|
12
12
|
readonly error?: string;
|
|
13
|
+
/**
|
|
14
|
+
* A diagnosis for a zero-match that is technically correct but almost
|
|
15
|
+
* certainly not what the author meant — surfaced alongside the loud skip so
|
|
16
|
+
* the dead end explains itself.
|
|
17
|
+
*/
|
|
18
|
+
readonly hint?: string;
|
|
13
19
|
}
|
|
14
20
|
/**
|
|
15
21
|
* Resolve ONE source against the tree and report what it matched.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inspect-source.d.ts","sourceRoot":"","sources":["../../src/extract/inspect-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"inspect-source.d.ts","sourceRoot":"","sources":["../../src/extract/inspect-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGzE,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,sCAAsC;IACtC,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,EACrB,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,iBAAiB,CAoBnB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { matchesAny } from "../scan/glob.js";
|
|
2
2
|
import { readMatchingFiles } from "../util/walk-files.js";
|
|
3
3
|
import { extractTokens } from "./extract-tokens.js";
|
|
4
|
+
import { loadTsconfigPaths } from "../scan/tsconfig-aliases.js";
|
|
4
5
|
/**
|
|
5
6
|
* Resolve ONE source against the tree and report what it matched.
|
|
6
7
|
*
|
|
@@ -16,12 +17,16 @@ export function inspectSource(projectRoot, source, excludeDirs = []) {
|
|
|
16
17
|
const files = [...cache.entries()]
|
|
17
18
|
.filter(([path]) => matchesAny(path, globs))
|
|
18
19
|
.map(([path, content]) => ({ path, content }));
|
|
19
|
-
|
|
20
|
+
// `import-edges` resolves alias specifiers the way the compiler would, so it
|
|
21
|
+
// needs the project's tsconfig paths. Loading it here (rather than inside the
|
|
22
|
+
// extractor) keeps every extractor a pure function of its inputs.
|
|
23
|
+
const res = extractTokens(source, files, { tsconfigPaths: loadTsconfigPaths(projectRoot) });
|
|
20
24
|
const sites = [...res.sites].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.token.localeCompare(b.token));
|
|
21
25
|
return {
|
|
22
26
|
filesScanned: files.length,
|
|
23
27
|
ids: [...new Set(sites.map((s) => s.token))].sort(),
|
|
24
28
|
sites,
|
|
25
29
|
...(res.error ? { error: res.error } : {}),
|
|
30
|
+
...(res.hint ? { hint: res.hint } : {}),
|
|
26
31
|
};
|
|
27
32
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE import-statement parser.
|
|
3
|
+
*
|
|
4
|
+
* Two features need to read import statements out of source text: the
|
|
5
|
+
* `--fix` planner (does this sink bind the token, and how?) and the
|
|
6
|
+
* `import-edges` extractor (which file imports what, from where?). A second
|
|
7
|
+
* parser would drift from the first, and the two would then disagree about
|
|
8
|
+
* what a file imports — which is precisely the class of silent disagreement
|
|
9
|
+
* this engine exists to eliminate. So both consume this.
|
|
10
|
+
*
|
|
11
|
+
* It is a lexer, not a compiler: it answers what the text says, which is the
|
|
12
|
+
* largest thing that stays honest across `.ts`, `.tsx`, `.mts` and plain JS
|
|
13
|
+
* with one code path.
|
|
14
|
+
*/
|
|
15
|
+
/** How a name entered scope through an import. */
|
|
16
|
+
export type ImportBindingKind = 'named' | 'default' | 'namespace';
|
|
17
|
+
/** One name bound by an import statement. */
|
|
18
|
+
export interface IImportBinding {
|
|
19
|
+
/** The name as used in the file (after any `as` alias). */
|
|
20
|
+
readonly local: string;
|
|
21
|
+
readonly kind: ImportBindingKind;
|
|
22
|
+
/** The exported name, for a named import (differs from `local` when aliased). */
|
|
23
|
+
readonly imported?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* How the dependency was expressed.
|
|
27
|
+
*
|
|
28
|
+
* `--fix` only ever extends the STATIC import block, so it filters on this;
|
|
29
|
+
* the dependency graph counts every kind, because a fence that missed
|
|
30
|
+
* `await import('../secret')` would have a hole exactly where someone
|
|
31
|
+
* motivated would put one.
|
|
32
|
+
*/
|
|
33
|
+
export type ImportStatementKind = 'import' | 'reexport' | 'side-effect' | 'dynamic' | 'require';
|
|
34
|
+
/** One parsed dependency statement. */
|
|
35
|
+
export interface IParsedImport {
|
|
36
|
+
readonly specifier: string;
|
|
37
|
+
readonly bindings: readonly IImportBinding[];
|
|
38
|
+
/** 1-based line of the statement's start. */
|
|
39
|
+
readonly line: number;
|
|
40
|
+
/** Character offset of the statement's start. */
|
|
41
|
+
readonly index: number;
|
|
42
|
+
/** The matched statement text, for style detection (quotes, semicolons). */
|
|
43
|
+
readonly raw: string;
|
|
44
|
+
/** True for `import type { … }` — binds no runtime value. */
|
|
45
|
+
readonly typeOnly: boolean;
|
|
46
|
+
readonly kind: ImportStatementKind;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Every import / re-export statement in `content`, with the names it binds.
|
|
50
|
+
*
|
|
51
|
+
* A `import type { … }` statement is returned with `typeOnly: true` rather than
|
|
52
|
+
* dropped: the `--fix` planner must ignore it (it binds no value), while the
|
|
53
|
+
* dependency graph legitimately counts it as an edge. Returning it and letting
|
|
54
|
+
* each caller decide keeps that judgement at the call site instead of baking
|
|
55
|
+
* one feature's answer into the shared parser.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseImportStatements(content: string): IParsedImport[];
|
|
58
|
+
//# sourceMappingURL=parse-imports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-imports.d.ts","sourceRoot":"","sources":["../../src/extract/parse-imports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,kDAAkD;AAClD,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;AAElE,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,GAAG,SAAS,CAAC;AAEhG,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IAC7C,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;CACpC;AA0FD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,EAAE,CAgDtE"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE import-statement parser.
|
|
3
|
+
*
|
|
4
|
+
* Two features need to read import statements out of source text: the
|
|
5
|
+
* `--fix` planner (does this sink bind the token, and how?) and the
|
|
6
|
+
* `import-edges` extractor (which file imports what, from where?). A second
|
|
7
|
+
* parser would drift from the first, and the two would then disagree about
|
|
8
|
+
* what a file imports — which is precisely the class of silent disagreement
|
|
9
|
+
* this engine exists to eliminate. So both consume this.
|
|
10
|
+
*
|
|
11
|
+
* It is a lexer, not a compiler: it answers what the text says, which is the
|
|
12
|
+
* largest thing that stays honest across `.ts`, `.tsx`, `.mts` and plain JS
|
|
13
|
+
* with one code path.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Matches one import/re-export statement up to its specifier.
|
|
17
|
+
*
|
|
18
|
+
* The clause may span lines (a multi-line `{ … }` block is normal) but never
|
|
19
|
+
* contains a `;` or a quote, which is what keeps the lazy match from running
|
|
20
|
+
* past the end of its own statement into the next one.
|
|
21
|
+
*
|
|
22
|
+
* `export … from '…'` is matched too: a re-export is an edge in the dependency
|
|
23
|
+
* graph exactly like an import, and a barrel that is invisible here would make
|
|
24
|
+
* every consumer behind it invisible as well.
|
|
25
|
+
*/
|
|
26
|
+
const IMPORT_STATEMENT = /\b(import|export)\s+([^;'"]*?)\s*from\s*['"]([^'"]+)['"]\s*;?/g;
|
|
27
|
+
/**
|
|
28
|
+
* The specifier-only forms, which bind no name: a side-effect import, a dynamic
|
|
29
|
+
* `import()`, and `require()`. Each is a real dependency edge — the boundary
|
|
30
|
+
* engine counts all three — even though none of them introduces a binding.
|
|
31
|
+
*/
|
|
32
|
+
const SIDE_EFFECT_IMPORT = /\bimport\s*['"]([^'"]+)['"]\s*;?/g;
|
|
33
|
+
const DYNAMIC_IMPORT = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
34
|
+
const REQUIRE_CALL = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
35
|
+
/** Split a clause on commas that are OUTSIDE a `{ … }` block. */
|
|
36
|
+
function splitTopLevel(clause) {
|
|
37
|
+
const out = [];
|
|
38
|
+
let depth = 0;
|
|
39
|
+
let current = '';
|
|
40
|
+
for (const ch of clause) {
|
|
41
|
+
if (ch === '{')
|
|
42
|
+
depth += 1;
|
|
43
|
+
if (ch === '}')
|
|
44
|
+
depth -= 1;
|
|
45
|
+
if (ch === ',' && depth === 0) {
|
|
46
|
+
out.push(current);
|
|
47
|
+
current = '';
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
current += ch;
|
|
51
|
+
}
|
|
52
|
+
if (current.trim() !== '')
|
|
53
|
+
out.push(current);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** Parse the clause between `import`/`export` and `from`. */
|
|
57
|
+
function parseClause(clause) {
|
|
58
|
+
const bindings = [];
|
|
59
|
+
for (const part of splitTopLevel(clause.trim())) {
|
|
60
|
+
const piece = part.trim();
|
|
61
|
+
if (piece === '')
|
|
62
|
+
continue;
|
|
63
|
+
if (piece.startsWith('*')) {
|
|
64
|
+
const m = /^\*\s*as\s+([A-Za-z_$][\w$]*)$/.exec(piece);
|
|
65
|
+
if (m)
|
|
66
|
+
bindings.push({ local: m[1], kind: 'namespace' });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (piece.startsWith('{')) {
|
|
70
|
+
const inner = piece.replace(/^\{/, '').replace(/\}$/, '');
|
|
71
|
+
for (const spec of inner.split(',')) {
|
|
72
|
+
const s = spec.trim();
|
|
73
|
+
// An inline `type` specifier binds no runtime value.
|
|
74
|
+
if (s === '' || /^type\b/.test(s))
|
|
75
|
+
continue;
|
|
76
|
+
const aliased = /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(s);
|
|
77
|
+
if (aliased) {
|
|
78
|
+
bindings.push({ local: aliased[2], kind: 'named', imported: aliased[1] });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (/^[A-Za-z_$][\w$]*$/.test(s)) {
|
|
82
|
+
bindings.push({ local: s, kind: 'named', imported: s });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (/^[A-Za-z_$][\w$]*$/.test(piece)) {
|
|
88
|
+
bindings.push({ local: piece, kind: 'default' });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return bindings;
|
|
92
|
+
}
|
|
93
|
+
/** 1-based line number of `index` within `content`. */
|
|
94
|
+
function lineAt(content, index) {
|
|
95
|
+
let line = 1;
|
|
96
|
+
for (let i = 0; i < index && i < content.length; i += 1) {
|
|
97
|
+
if (content[i] === '\n')
|
|
98
|
+
line += 1;
|
|
99
|
+
}
|
|
100
|
+
return line;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Every import / re-export statement in `content`, with the names it binds.
|
|
104
|
+
*
|
|
105
|
+
* A `import type { … }` statement is returned with `typeOnly: true` rather than
|
|
106
|
+
* dropped: the `--fix` planner must ignore it (it binds no value), while the
|
|
107
|
+
* dependency graph legitimately counts it as an edge. Returning it and letting
|
|
108
|
+
* each caller decide keeps that judgement at the call site instead of baking
|
|
109
|
+
* one feature's answer into the shared parser.
|
|
110
|
+
*/
|
|
111
|
+
export function parseImportStatements(content) {
|
|
112
|
+
const out = [];
|
|
113
|
+
const claimed = [];
|
|
114
|
+
let m;
|
|
115
|
+
IMPORT_STATEMENT.lastIndex = 0;
|
|
116
|
+
while ((m = IMPORT_STATEMENT.exec(content)) !== null) {
|
|
117
|
+
const clause = m[2];
|
|
118
|
+
const typeOnly = /^type\b/.test(clause.trim());
|
|
119
|
+
claimed.push({ start: m.index, end: m.index + m[0].length });
|
|
120
|
+
out.push({
|
|
121
|
+
specifier: m[3],
|
|
122
|
+
bindings: typeOnly ? [] : parseClause(clause),
|
|
123
|
+
line: lineAt(content, m.index),
|
|
124
|
+
index: m.index,
|
|
125
|
+
raw: m[0],
|
|
126
|
+
typeOnly,
|
|
127
|
+
kind: m[1] === 'export' ? 'reexport' : 'import',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// A `import x from 'y'` also contains the text `import … 'y'`, so the
|
|
131
|
+
// specifier-only patterns would re-report it. Skipping anything inside an
|
|
132
|
+
// already-claimed span keeps each dependency counted exactly once.
|
|
133
|
+
const overlapsClaimed = (start) => claimed.some((c) => start >= c.start && start < c.end);
|
|
134
|
+
for (const [re, kind] of [
|
|
135
|
+
[SIDE_EFFECT_IMPORT, 'side-effect'],
|
|
136
|
+
[DYNAMIC_IMPORT, 'dynamic'],
|
|
137
|
+
[REQUIRE_CALL, 'require'],
|
|
138
|
+
]) {
|
|
139
|
+
re.lastIndex = 0;
|
|
140
|
+
while ((m = re.exec(content)) !== null) {
|
|
141
|
+
if (overlapsClaimed(m.index))
|
|
142
|
+
continue;
|
|
143
|
+
out.push({
|
|
144
|
+
specifier: m[1],
|
|
145
|
+
bindings: [],
|
|
146
|
+
line: lineAt(content, m.index),
|
|
147
|
+
index: m.index,
|
|
148
|
+
raw: m[0],
|
|
149
|
+
typeOnly: false,
|
|
150
|
+
kind,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out.sort((a, b) => a.index - b.index);
|
|
155
|
+
}
|