vouchington-tooling 0.37.1 → 0.37.2
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 +29 -0
- package/dist/shared-context/source-graph-owner.d.mts +4 -0
- package/dist/shared-context/source-graph-owner.mjs +67 -0
- package/dist/shared-context/source-graph-resolve.d.mts +8 -0
- package/dist/shared-context/source-graph-resolve.mjs +60 -0
- package/dist/shared-context/source-graph.d.mts +24 -0
- package/dist/shared-context/source-graph.mjs +86 -0
- package/dist/source-module-graph/index.d.mts +2 -0
- package/dist/source-module-graph/index.mjs +1 -0
- package/package.json +11 -1
- package/scripts/allocate-browser-safe-ports.py +3 -0
package/README.md
CHANGED
|
@@ -89,6 +89,22 @@ vouchington post-review
|
|
|
89
89
|
vouchington stage-review-payload optional|required <source> <destination>
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
### Running `allocate-browser-safe-ports` without an install
|
|
93
|
+
|
|
94
|
+
The allocator is a standalone Python script that needs only the Python 3 standard library: no
|
|
95
|
+
Node, no `pnpm install`. Run it from the extracted published tarball:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm pack vouchington-tooling@<version> # or download the registry .tgz
|
|
99
|
+
mkdir extracted && tar -xzf vouchington-tooling-<version>.tgz -C extracted
|
|
100
|
+
python3 extracted/package/scripts/allocate-browser-safe-ports.py <count> [--policy ...] [--forbidden-ports ...]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`pnpm dlx` and `npx` resolve the package's whole dependency tree, which can fail under
|
|
104
|
+
`minimumReleaseAge` (`ERR_PNPM_NO_MATURE_MATCHING_VERSION`) when a lockstep-published dependency
|
|
105
|
+
straddles the cutoff. CI jobs without an install should fetch the tarball instead. Tests enforce
|
|
106
|
+
that the script stays stdlib-only and reads only its two sibling JSON files.
|
|
107
|
+
|
|
92
108
|
For persistent `pnpm-install`, v5 metadata tracks structural inputs separately from the
|
|
93
109
|
`--install-scripts` policy. A warm scripts-enabled tree can therefore toggle
|
|
94
110
|
`true → false → true` without forced reconciliation; a tree first installed with scripts disabled
|
|
@@ -216,6 +232,7 @@ import {
|
|
|
216
232
|
installFakeGit,
|
|
217
233
|
runNamedChecks,
|
|
218
234
|
} from 'vouchington-tooling/shared-context'
|
|
235
|
+
import { createSourceModuleGraph } from 'vouchington-tooling/source-module-graph'
|
|
219
236
|
import {
|
|
220
237
|
decodeSelectedFiles,
|
|
221
238
|
writeSelectedFilesOutput,
|
|
@@ -657,6 +674,18 @@ TypeScript and route rows from Markdown. Callers configure declaration names and
|
|
|
657
674
|
runbook headings, then compare the returned data using their own route policy.
|
|
658
675
|
See the [API contract](docs/basic-auth-doc-sync.md).
|
|
659
676
|
|
|
677
|
+
## TypeScript source graph
|
|
678
|
+
|
|
679
|
+
`vouchington-tooling/source-module-graph` exports `createSourceModuleGraph`. It accepts the
|
|
680
|
+
`modules` report from released no-mistakes
|
|
681
|
+
`analyzeTypeScriptModules({ root, files })`, plus the caller's root, ordered source extensions,
|
|
682
|
+
aliases, and file inventory. It resolves local imports and reexport owners without reading or
|
|
683
|
+
parsing source again. `runtimeImports(file)` uses binding references rather than declaration
|
|
684
|
+
presence, and `reachableFrom(entrypoints, { runtimeOnly: true })` follows used runtime imports,
|
|
685
|
+
reexports, and literal dynamic/require loads. Missing or incomplete facts for a reached file
|
|
686
|
+
throw; external specifiers resolve to `null`. The default file inventory limit is 10,000
|
|
687
|
+
files and can be reduced with `maxFiles`.
|
|
688
|
+
|
|
660
689
|
## SQL view declarations
|
|
661
690
|
|
|
662
691
|
`vouchington-tooling/sql-ast` exports `extractViewDeclarations(sql)` and the
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TypeScriptModuleFacts } from 'no-mistakes';
|
|
2
|
+
import { type SourceGraphPaths } from './source-graph-resolve.mts';
|
|
3
|
+
import type { SourceExportOwner } from './source-graph.mts';
|
|
4
|
+
export declare function createExportOwnerResolver(paths: SourceGraphPaths, factsFor: (file: string) => TypeScriptModuleFacts, maxFiles: number): (file: string, exportName: string) => SourceExportOwner | null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { relativeFile, resolveSourcePath } from "./source-graph-resolve.mjs";
|
|
2
|
+
export function createExportOwnerResolver(paths, factsFor, maxFiles) {
|
|
3
|
+
return (file, exportName) => {
|
|
4
|
+
const start = relativeFile(paths.root, file);
|
|
5
|
+
if (!start || !paths.files.has(start))
|
|
6
|
+
return null;
|
|
7
|
+
const queue = [{ file: start, exportName }];
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
const owners = new Map();
|
|
10
|
+
let unresolved = false;
|
|
11
|
+
for (let index = 0; index < queue.length; index++) {
|
|
12
|
+
const current = queue[index];
|
|
13
|
+
const key = `${current.file}\0${current.exportName}`;
|
|
14
|
+
if (seen.has(key))
|
|
15
|
+
continue;
|
|
16
|
+
seen.add(key);
|
|
17
|
+
if (seen.size > maxFiles)
|
|
18
|
+
throw new Error('Source graph file limit exceeded');
|
|
19
|
+
const facts = factsFor(current.file);
|
|
20
|
+
const direct = facts.exports.filter((item) => !item.typeOnly && item.exported === current.exportName);
|
|
21
|
+
const matches = direct.length > 0
|
|
22
|
+
? direct
|
|
23
|
+
: current.exportName === 'default'
|
|
24
|
+
? [] // ESM export-star never reexports default.
|
|
25
|
+
: facts.exports.filter((item) => !item.typeOnly && item.local === '*' && item.exported === '*');
|
|
26
|
+
for (const item of matches) {
|
|
27
|
+
if (item.specifier) {
|
|
28
|
+
const target = resolveSourcePath(paths, current.file, item.specifier);
|
|
29
|
+
if (!target) {
|
|
30
|
+
unresolved = true;
|
|
31
|
+
}
|
|
32
|
+
else if (item.local === '*' && item.exported !== '*') {
|
|
33
|
+
factsFor(target);
|
|
34
|
+
owners.set(`${target}\0*`, { file: target, exportName: '*' });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
queue.push({
|
|
38
|
+
file: target,
|
|
39
|
+
exportName: item.local === '*' ? current.exportName : item.local,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const imported = facts.imports.flatMap((entry) => entry.bindings
|
|
45
|
+
.filter((binding) => !binding.typeOnly && binding.local === item.local)
|
|
46
|
+
.map((binding) => ({ entry, binding })))[0];
|
|
47
|
+
if (imported) {
|
|
48
|
+
const target = resolveSourcePath(paths, current.file, imported.entry.specifier);
|
|
49
|
+
if (!target) {
|
|
50
|
+
unresolved = true;
|
|
51
|
+
}
|
|
52
|
+
else if (imported.binding.kind === 'namespace') {
|
|
53
|
+
factsFor(target);
|
|
54
|
+
owners.set(`${target}\0*`, { file: target, exportName: '*' });
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
queue.push({ file: target, exportName: imported.binding.imported });
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const owner = { file: current.file, exportName: item.exported };
|
|
62
|
+
owners.set(`${owner.file}\0${owner.exportName}`, owner);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return !unresolved && owners.size === 1 ? [...owners.values()][0] : null;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type SourceGraphPaths = {
|
|
2
|
+
root: string;
|
|
3
|
+
extensions: readonly string[];
|
|
4
|
+
aliases: Readonly<Record<string, string>>;
|
|
5
|
+
files: ReadonlySet<string>;
|
|
6
|
+
};
|
|
7
|
+
export declare function relativeFile(root: string, file: string): string | null;
|
|
8
|
+
export declare function resolveSourcePath(paths: SourceGraphPaths, fromFile: string, specifier: string): string | null;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isAbsolute, posix, relative, win32 } from 'node:path';
|
|
2
|
+
export function relativeFile(root, file) {
|
|
3
|
+
const value = isAbsolute(file) ? relative(root, file) : file;
|
|
4
|
+
if (win32.isAbsolute(value) || /^[A-Za-z]:/u.test(value))
|
|
5
|
+
return null;
|
|
6
|
+
const normalized = posix.normalize(value.replaceAll('\\', '/'));
|
|
7
|
+
return normalized === '..' || normalized.startsWith('../') || posix.isAbsolute(normalized)
|
|
8
|
+
? null
|
|
9
|
+
: normalized;
|
|
10
|
+
}
|
|
11
|
+
export function resolveSourcePath(paths, fromFile, specifier) {
|
|
12
|
+
const normalizedFrom = relativeFile(paths.root, fromFile);
|
|
13
|
+
if (!normalizedFrom || !paths.files.has(normalizedFrom))
|
|
14
|
+
return null;
|
|
15
|
+
let target = null;
|
|
16
|
+
if (specifier.startsWith('.')) {
|
|
17
|
+
target = posix.join(posix.dirname(normalizedFrom), specifier);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const alias = Object.keys(paths.aliases)
|
|
21
|
+
.filter((prefix) => specifier.startsWith(prefix))
|
|
22
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
23
|
+
if (alias)
|
|
24
|
+
target = posix.join(paths.aliases[alias], specifier.slice(alias.length));
|
|
25
|
+
}
|
|
26
|
+
if (!target)
|
|
27
|
+
return null;
|
|
28
|
+
const base = relativeFile(paths.root, target);
|
|
29
|
+
if (!base)
|
|
30
|
+
return null;
|
|
31
|
+
const runtimeExtension = posix.extname(base);
|
|
32
|
+
const mappedExtensions = {
|
|
33
|
+
'.js': ['.ts', '.tsx'],
|
|
34
|
+
'.jsx': ['.tsx'],
|
|
35
|
+
'.mjs': ['.mts'],
|
|
36
|
+
'.cjs': ['.cts'],
|
|
37
|
+
};
|
|
38
|
+
const mapped = mappedExtensions[runtimeExtension];
|
|
39
|
+
if (mapped) {
|
|
40
|
+
const stem = base.slice(0, -runtimeExtension.length);
|
|
41
|
+
for (const suffix of paths.extensions) {
|
|
42
|
+
if (!mapped.includes(suffix))
|
|
43
|
+
continue;
|
|
44
|
+
const candidate = `${stem}${suffix}`;
|
|
45
|
+
if (paths.files.has(candidate))
|
|
46
|
+
return candidate;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const suffix of paths.extensions) {
|
|
50
|
+
const candidate = `${base}${suffix}`;
|
|
51
|
+
if (paths.files.has(candidate))
|
|
52
|
+
return candidate;
|
|
53
|
+
}
|
|
54
|
+
for (const suffix of paths.extensions) {
|
|
55
|
+
const candidate = posix.join(base, `index${suffix}`);
|
|
56
|
+
if (paths.files.has(candidate))
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
return paths.files.has(base) ? base : null;
|
|
60
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TypeScriptModuleImport, TypeScriptModulesReport } from 'no-mistakes';
|
|
2
|
+
export type SourceModuleGraphOptions = {
|
|
3
|
+
root: string;
|
|
4
|
+
extensions: readonly string[];
|
|
5
|
+
aliases?: Readonly<Record<string, string>>;
|
|
6
|
+
files: readonly string[];
|
|
7
|
+
facts: TypeScriptModulesReport;
|
|
8
|
+
/** Caps the supplied file inventory and every traversal. */
|
|
9
|
+
maxFiles?: number;
|
|
10
|
+
};
|
|
11
|
+
export type SourceExportOwner = {
|
|
12
|
+
file: string;
|
|
13
|
+
exportName: string;
|
|
14
|
+
};
|
|
15
|
+
export type SourceModuleGraph = {
|
|
16
|
+
resolveSource(fromFile: string, specifier: string): string | null;
|
|
17
|
+
runtimeImports(file: string): TypeScriptModuleImport[];
|
|
18
|
+
reachableFrom(entrypoints: readonly string[], options?: {
|
|
19
|
+
runtimeOnly?: boolean;
|
|
20
|
+
}): string[];
|
|
21
|
+
resolveExportOwner(file: string, exportName: string): SourceExportOwner | null;
|
|
22
|
+
};
|
|
23
|
+
/** Resolve only caller-supplied files; syntax and binding facts come from no-mistakes. */
|
|
24
|
+
export declare function createSourceModuleGraph(options: SourceModuleGraphOptions): SourceModuleGraph;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { createExportOwnerResolver } from "./source-graph-owner.mjs";
|
|
3
|
+
import { relativeFile, resolveSourcePath } from "./source-graph-resolve.mjs";
|
|
4
|
+
/** Resolve only caller-supplied files; syntax and binding facts come from no-mistakes. */
|
|
5
|
+
export function createSourceModuleGraph(options) {
|
|
6
|
+
const root = resolve(options.root);
|
|
7
|
+
const maxFiles = options.maxFiles ?? 10_000;
|
|
8
|
+
if (!Number.isSafeInteger(maxFiles) || maxFiles < 1 || options.files.length > maxFiles) {
|
|
9
|
+
throw new Error('Source graph file limit exceeded');
|
|
10
|
+
}
|
|
11
|
+
const fileSet = new Set();
|
|
12
|
+
for (const file of options.files) {
|
|
13
|
+
const normalized = relativeFile(root, file);
|
|
14
|
+
if (!normalized)
|
|
15
|
+
throw new Error(`Source graph file escapes root: ${file}`);
|
|
16
|
+
fileSet.add(normalized);
|
|
17
|
+
}
|
|
18
|
+
const paths = {
|
|
19
|
+
root,
|
|
20
|
+
extensions: options.extensions,
|
|
21
|
+
aliases: options.aliases ?? {},
|
|
22
|
+
files: fileSet,
|
|
23
|
+
};
|
|
24
|
+
const modules = new Map();
|
|
25
|
+
for (const module of options.facts.modules) {
|
|
26
|
+
const file = relativeFile(root, module.fileName);
|
|
27
|
+
if (!file || !fileSet.has(file) || modules.has(file)) {
|
|
28
|
+
throw new Error(`Unexpected source module facts: ${module.fileName}`);
|
|
29
|
+
}
|
|
30
|
+
modules.set(file, module);
|
|
31
|
+
}
|
|
32
|
+
function factsFor(file) {
|
|
33
|
+
const normalized = relativeFile(root, file);
|
|
34
|
+
const facts = normalized && fileSet.has(normalized) ? modules.get(normalized) : undefined;
|
|
35
|
+
if (!facts || !facts.complete)
|
|
36
|
+
throw new Error(`Incomplete source module facts: ${file}`);
|
|
37
|
+
return facts;
|
|
38
|
+
}
|
|
39
|
+
function runtimeImports(file) {
|
|
40
|
+
const facts = factsFor(file);
|
|
41
|
+
return facts.imports.flatMap((entry) => {
|
|
42
|
+
if (entry.typeOnly)
|
|
43
|
+
return [];
|
|
44
|
+
if (entry.bindings.length === 0)
|
|
45
|
+
return [entry]; // Side-effect import.
|
|
46
|
+
const bindings = entry.bindings.filter((imported) => {
|
|
47
|
+
if (imported.typeOnly)
|
|
48
|
+
return false;
|
|
49
|
+
const binding = facts.bindings.find((item) => item.id === imported.bindingId);
|
|
50
|
+
if (!binding)
|
|
51
|
+
throw new Error(`Missing import binding facts: ${file}`);
|
|
52
|
+
return binding.references.some((reference) => reference.runtime && !reference.typeOnly);
|
|
53
|
+
});
|
|
54
|
+
return bindings.length > 0 ? [{ ...entry, bindings }] : [];
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function reachableFrom(entrypoints, traversal = {}) {
|
|
58
|
+
const queue = entrypoints.map((file) => relativeFile(root, file));
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
for (let index = 0; index < queue.length; index++) {
|
|
61
|
+
const file = queue[index];
|
|
62
|
+
if (!file || !fileSet.has(file) || seen.has(file))
|
|
63
|
+
continue;
|
|
64
|
+
seen.add(file);
|
|
65
|
+
const facts = factsFor(file);
|
|
66
|
+
const imports = traversal.runtimeOnly ? runtimeImports(file) : facts.imports;
|
|
67
|
+
const specifiers = [
|
|
68
|
+
...imports.map((item) => item.specifier),
|
|
69
|
+
...facts.exports.flatMap((item) => item.specifier && (!traversal.runtimeOnly || !item.typeOnly) ? [item.specifier] : []),
|
|
70
|
+
...facts.loads.map((item) => item.specifier),
|
|
71
|
+
];
|
|
72
|
+
for (const specifier of specifiers) {
|
|
73
|
+
const resolved = resolveSourcePath(paths, file, specifier);
|
|
74
|
+
if (resolved && !seen.has(resolved))
|
|
75
|
+
queue.push(resolved);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return [...seen].sort();
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
resolveSource: (fromFile, specifier) => resolveSourcePath(paths, fromFile, specifier),
|
|
82
|
+
runtimeImports,
|
|
83
|
+
reachableFrom,
|
|
84
|
+
resolveExportOwner: createExportOwnerResolver(paths, factsFor, maxFiles),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createSourceModuleGraph } from "../shared-context/source-graph.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -136,6 +136,11 @@
|
|
|
136
136
|
"import": "./dist/shared-context/index.mjs",
|
|
137
137
|
"default": "./dist/shared-context/index.mjs"
|
|
138
138
|
},
|
|
139
|
+
"./source-module-graph": {
|
|
140
|
+
"types": "./dist/source-module-graph/index.d.mts",
|
|
141
|
+
"import": "./dist/source-module-graph/index.mjs",
|
|
142
|
+
"default": "./dist/source-module-graph/index.mjs"
|
|
143
|
+
},
|
|
139
144
|
"./gha-selected-files": {
|
|
140
145
|
"types": "./dist/gha-selected-files/index.d.mts",
|
|
141
146
|
"import": "./dist/gha-selected-files/index.mjs",
|
|
@@ -376,12 +381,14 @@
|
|
|
376
381
|
"@types/picomatch": "^4.0.3",
|
|
377
382
|
"@types/spdx-expression-parse": "4.0.0",
|
|
378
383
|
"agent-blackboard": "^0.6.0",
|
|
384
|
+
"no-mistakes": "0.78.0",
|
|
379
385
|
"smol-toml": "1.9.0",
|
|
380
386
|
"typescript5": "npm:typescript@5.9.3"
|
|
381
387
|
},
|
|
382
388
|
"peerDependencies": {
|
|
383
389
|
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
384
390
|
"agent-blackboard": "^0.6.0",
|
|
391
|
+
"no-mistakes": "^0.78.0",
|
|
385
392
|
"typescript": ">=5",
|
|
386
393
|
"vitest": ">=5"
|
|
387
394
|
},
|
|
@@ -392,6 +399,9 @@
|
|
|
392
399
|
"@modelcontextprotocol/sdk": {
|
|
393
400
|
"optional": true
|
|
394
401
|
},
|
|
402
|
+
"no-mistakes": {
|
|
403
|
+
"optional": true
|
|
404
|
+
},
|
|
395
405
|
"typescript": {
|
|
396
406
|
"optional": true
|
|
397
407
|
},
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# Must stay stdlib-only and self-contained (reads only the sibling fetch-forbidden-ports.json
|
|
3
|
+
# and runner-port-policy.json): CI runs it from the extracted published tarball with no install.
|
|
4
|
+
# Enforced by allocate-browser-safe-ports-no-install.test.mts.
|
|
2
5
|
from __future__ import annotations
|
|
3
6
|
|
|
4
7
|
import argparse
|