Skip to content

Commit a8aa8a7

Browse files
committed
graph experiment
1 parent 48691ed commit a8aa8a7

13 files changed

+221
-12
lines changed

app/cmd/graph.ts

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { buildResolver } from 'esm-resolve';
2+
import { LoadResult, loadAndMaybeTransform, parse } from '../lib/load.ts';
3+
import { aggregateImports } from '../../lib/internal/analyze/module.ts';
4+
import * as path from 'node:path';
5+
import { isLocalImport, relativize } from '../../lib/helper.ts';
6+
7+
export type ValidArgs = {
8+
paths: string[];
9+
};
10+
11+
type FoundInfo = {
12+
importer: string[];
13+
tags?: { local?: string[]; nested?: string[] };
14+
found?: boolean;
15+
};
16+
17+
const matchTag = /@kuto-(\w+)/g;
18+
19+
export default async function cmdGraph(args: ValidArgs) {
20+
const pending = new Set<string>();
21+
const mod: Record<string, FoundInfo> = {};
22+
23+
for (const raw of args.paths) {
24+
const entrypoint = relativize(raw);
25+
pending.add(entrypoint);
26+
mod[entrypoint] = { importer: [] };
27+
}
28+
29+
for (const p of pending) {
30+
pending.delete(p);
31+
if (!isLocalImport(p)) {
32+
continue; // rn we ignore "foo"
33+
}
34+
35+
const info = mod[p]!;
36+
37+
let x: LoadResult;
38+
try {
39+
x = await loadAndMaybeTransform(p);
40+
} catch (e) {
41+
info.found = false;
42+
continue;
43+
}
44+
info.found = true;
45+
const prog = parse(x.source, (comment) => {
46+
comment.replaceAll(matchTag, (_, tag) => {
47+
info.tags ??= {};
48+
info.tags.local ??= [];
49+
if (!info.tags.local.includes(tag)) {
50+
info.tags.local.push(tag);
51+
}
52+
return '';
53+
});
54+
});
55+
56+
// -- resolve additional imports
57+
58+
const resolver = buildResolver(p, {
59+
allowImportingExtraExtensions: true,
60+
resolveToAbsolute: true,
61+
});
62+
63+
const imports = aggregateImports(prog);
64+
65+
const resolved: string[] = [];
66+
for (const source of imports.mod.importSources()) {
67+
let key = source.name;
68+
69+
if (isLocalImport(source.name)) {
70+
const r = resolver(source.name);
71+
if (!r) {
72+
continue;
73+
}
74+
key = relativize(path.relative(process.cwd(), r));
75+
resolved.push(key);
76+
}
77+
78+
// create graph to thingo
79+
const prev = mod[key];
80+
if (prev !== undefined) {
81+
prev.importer.push(p);
82+
} else {
83+
mod[key] = { importer: [p] };
84+
pending.add(key);
85+
}
86+
}
87+
}
88+
89+
// descend tags
90+
91+
const expandTree = (key: string) => {
92+
const all = new Set<string>();
93+
const pending = [key];
94+
95+
while (pending.length) {
96+
const next = pending.pop()!;
97+
all.add(next);
98+
99+
for (const importer of mod[next].importer) {
100+
if (all.has(importer)) {
101+
continue;
102+
}
103+
pending.push(importer);
104+
}
105+
}
106+
107+
all.delete(key);
108+
return [...all];
109+
};
110+
111+
for (const key of Object.keys(mod)) {
112+
const o = mod[key];
113+
const tree = expandTree(key);
114+
115+
for (const localTag of o.tags?.local ?? []) {
116+
for (const okey of tree) {
117+
const o = mod[okey];
118+
o.tags ??= {};
119+
o.tags.nested ??= [];
120+
if (!o.tags.nested.includes(localTag)) {
121+
o.tags.nested.push(localTag);
122+
}
123+
}
124+
}
125+
}
126+
127+
const out = {
128+
mod,
129+
};
130+
console.info(JSON.stringify(out, undefined, 2));
131+
}

app/cmd/info.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import { aggregateImports } from '../../lib/internal/analyze/module.ts';
22
import { VarInfo, analyzeBlock } from '../../lib/internal/analyze/block.ts';
33
import { findVars, resolveConst } from '../../lib/interpret.ts';
44
import { createBlock } from '../../lib/internal/analyze/helper.ts';
5-
import { loadAndMaybeTransform } from '../lib/load.ts';
5+
import { loadAndMaybeTransform, parse } from '../lib/load.ts';
66
import { relativize } from '../../lib/helper.ts';
77

88
export type InfoArgs = {
99
path: string;
1010
};
1111

1212
export default async function cmdInfo(args: InfoArgs) {
13-
const { p } = await loadAndMaybeTransform(args.path);
13+
const { source } = await loadAndMaybeTransform(args.path);
14+
const p = parse(source);
1415

1516
const agg = aggregateImports(p);
1617
const block = createBlock(...agg.rest);

app/cmd/split.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
22
import * as path from 'node:path';
33
import { StaticExtractor } from '../../lib/extractor.ts';
44
import { liftDefault } from '../../lib/lift.ts';
5-
import { loadAndMaybeTransform } from '../lib/load.ts';
5+
import { loadAndMaybeTransform, parse } from '../lib/load.ts';
66
import { loadExisting } from '../lib/load.ts';
77
import { relativize } from '../../lib/helper.ts';
88
import { buildCorpusName } from '../../lib/name.ts';
@@ -31,10 +31,11 @@ export default async function cmdSplit(args: SpiltArgs) {
3131
keep: args.keep,
3232
});
3333

34-
const { p, source } = await loadAndMaybeTransform(args.sourcePath);
34+
const { source } = await loadAndMaybeTransform(args.sourcePath);
35+
const prog = parse(source);
3536

3637
const e = new StaticExtractor({
37-
p,
38+
p: prog,
3839
source,
3940
sourceName,
4041
staticName,
@@ -69,7 +70,7 @@ export default async function cmdSplit(args: SpiltArgs) {
6970
for (const name of disused) {
7071
try {
7172
fs.rmSync(path.join(dist, name));
72-
} catch { }
73+
} catch {}
7374
}
7475
fs.writeFileSync(path.join(dist, sourceName), out.main);
7576
for (const [name, code] of out.static) {

app/index.ts

+14
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ cmd.register('info', {
1616
},
1717
});
1818

19+
cmd.register('graph', {
20+
description: 'Validate a module graph based on conditions',
21+
positional: true,
22+
usageSuffix: '<entrypoint> <entrypoints...>',
23+
async handler(res) {
24+
if (res.positionals.length < 1) {
25+
throw new cmd.CommandError();
26+
}
27+
28+
const { default: cmdGraph } = await import('./cmd/graph.ts');
29+
return cmdGraph({ paths: res.positionals });
30+
},
31+
});
32+
1933
cmd.register('split', {
2034
description: 'Split a JS module into runtime and static code',
2135
flags: {

app/lib/load.ts

+20-5
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,17 @@ import { buildJoin, urlAgnosticRelativeBasename } from '../lib/helper.ts';
44
import { aggregateImports } from '../../lib/internal/analyze/module.ts';
55
import * as acorn from 'acorn';
66

7-
export function parse(source: string) {
8-
return acorn.parse(source, { ecmaVersion: 'latest', sourceType: 'module' });
7+
export function parse(source: string, comment?: (text: string) => void) {
8+
const args: acorn.Options = {
9+
ecmaVersion: 'latest',
10+
sourceType: 'module',
11+
};
12+
if (comment) {
13+
args.onComment = (isBlock, text) => {
14+
comment(text);
15+
};
16+
}
17+
return acorn.parse(source, args);
918
}
1019

1120
function hasCorpusSuffix(s: string) {
@@ -90,7 +99,13 @@ export async function loadExisting(args: LoadExistingArgs) {
9099

91100
const needsBuildExt = (ext: string) => ['.ts', '.tsx', '.jsx'].includes(ext);
92101

93-
export async function loadAndMaybeTransform(name: string) {
102+
export type LoadResult = {
103+
name: string;
104+
source: string;
105+
stat: fs.Stats;
106+
};
107+
108+
export async function loadAndMaybeTransform(name: string): Promise<LoadResult> {
94109
const { ext } = path.parse(name);
95110
let source = fs.readFileSync(name, 'utf-8');
96111
const stat = fs.statSync(name);
@@ -102,10 +117,10 @@ export async function loadAndMaybeTransform(name: string) {
102117
loader: ext.endsWith('x') ? 'tsx' : 'ts',
103118
format: 'esm',
104119
platform: 'neutral',
120+
legalComments: 'inline',
105121
});
106122
source = t.code;
107123
}
108124

109-
const p = parse(source);
110-
return { p, name, source, stat };
125+
return { name, source, stat };
111126
}

lib/helper.ts

+7
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,10 @@ export function relativize(s: string) {
1717
} catch (e) {}
1818
return './' + s;
1919
}
20+
21+
export function isLocalImport(s: string) {
22+
if (/^\.{0,2}\//.test(s)) {
23+
return true;
24+
}
25+
return false;
26+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@types/node": "^20.11.28",
88
"acorn": "^8.11.3",
99
"esbuild": "^0.20.2",
10+
"esm-resolve": "^1.0.11",
1011
"tsx": "^4.7.1",
1112
"vite": "^5.2.6",
1213
"zx": "^7.2.3"

pnpm-lock.yaml

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

release.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
set -eu
44

5-
TARGET=node14
5+
TARGET=node16
66
OUTFILE=dist/raw/app.js
77

88
esbuild \

test-info.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//export let x = 123;
2+
3+
import * as Something from './somewhere-else';
4+
import { Foo } from 'bar';
5+
6+
Something();
7+
8+
export function foo() {}
9+
10+
function x() {
11+
Something();
12+
{
13+
Foo.bar++;
14+
}
15+
}

test/valid/a.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const x = 1;
2+
export { x as x };
3+
4+
import './b.ts';
5+
6+
/** top-level sth */
7+
// whatever
8+
9+
import 'we-ignore-this';

test/valid/b.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
//!@kuto-b
2+
//!@kuto-whatever

test/valid/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//!@kuto-index
2+
//!@kuto-whatever
3+
4+
import { x as y } from './a.ts';
5+
6+
console.info(y);

0 commit comments

Comments
 (0)