@geml/geml 1.1.1 → 1.4.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/LICENSE +21 -21
- package/README.md +155 -79
- package/codemap/adapters/crg.mjs +120 -109
- package/codemap/adapters/joern.mjs +131 -131
- package/codemap/adapters/scip.mjs +658 -229
- package/codemap/browser-stub.mjs +29 -24
- package/codemap/build.mjs +609 -354
- package/codemap/cross-stack.mjs +303 -0
- package/codemap/detect.mjs +399 -185
- package/codemap/emit.mjs +480 -361
- package/codemap/entries.mjs +129 -0
- package/codemap/exclude.mjs +52 -52
- package/codemap/find.mjs +63 -0
- package/codemap/foldings.mjs +110 -0
- package/codemap/joern-export.sc +83 -83
- package/codemap/mcp-server.mjs +172 -143
- package/codemap/normalize.mjs +0 -0
- package/codemap/recipe-trust.mjs +103 -0
- package/codemap/refresh.mjs +310 -160
- package/codemap/render-all.mjs +64 -64
- package/codemap/serve.mjs +578 -378
- package/codemap/sfc-virtualize.mjs +367 -0
- package/codemap/verify.mjs +148 -126
- package/dist/block-edit.d.ts +1 -0
- package/dist/block-edit.js +112 -0
- package/dist/from-md.js +66 -7
- package/dist/geml.d.ts +2 -1
- package/dist/geml.js +1103 -233
- package/dist/history.js +102 -62
- package/dist/inline.d.ts +2 -1
- package/dist/inline.js +61 -8
- package/dist/render-html.d.ts +3 -0
- package/dist/render-html.js +95 -0
- package/dist/render.d.ts +33 -3
- package/dist/render.js +558 -283
- package/dist/serialize.js +22 -2
- package/dist/table.js +40 -5
- package/dist/to-md.js +4 -0
- package/package.json +62 -58
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// geml-code-graph SFC virtualizer — Vue/Svelte single-file components become
|
|
3
|
+
// indexable TypeScript. scip-typescript cannot read .vue/.svelte; this script
|
|
4
|
+
// projects each SFC to a shadow TS file (script AND template — an
|
|
5
|
+
// @click="save" in a Vue template becomes a real reference to save), plus a
|
|
6
|
+
// line-mapping sidecar the scip adapter uses to attribute every symbol back
|
|
7
|
+
// to the original .vue/.svelte source. Env-driven like joern-export.sc:
|
|
8
|
+
//
|
|
9
|
+
// GEML_SRC project (sub)root to walk for .vue/.svelte and real TS/JS
|
|
10
|
+
// GEML_OUT output dir (the "virtual dir"): shadows + sidecars + tsconfig
|
|
11
|
+
//
|
|
12
|
+
// The build invokes it hermetically — @geml/geml stays zero-dependency:
|
|
13
|
+
// npx -y -p @vue/language-core -p svelte2tsx -p svelte -p typescript@5 \
|
|
14
|
+
// node <abs path to this script>
|
|
15
|
+
// npx does NOT put the -p packages on NODE_PATH; it prepends
|
|
16
|
+
// <npm-cache>/_npx/<hash>/node_modules/.bin to PATH. We derive the
|
|
17
|
+
// node_modules dir from that PATH entry and createRequire() out of it
|
|
18
|
+
// (proven on Windows); the target project's own node_modules and this
|
|
19
|
+
// script's context are fallbacks, in that order. typescript is pinned @5:
|
|
20
|
+
// typescript@latest is 7.x, which @vue/language-core does not accept.
|
|
21
|
+
//
|
|
22
|
+
// Vue — @vue/language-core (Volar): createVueLanguagePlugin().createVirtualCode()
|
|
23
|
+
// and the `script_ts|js|tsx|jsx` embedded code, whose text contains the
|
|
24
|
+
// <script> verbatim plus the template projection; its offset mappings
|
|
25
|
+
// [sourceOffsets, generatedOffsets, lengths] become the line map.
|
|
26
|
+
// Svelte — svelte2tsx {code, map}: everything lands inside a generated
|
|
27
|
+
// $$render(); the source-map v3 `mappings` VLQ string is decoded here
|
|
28
|
+
// (no dependency) into the same line-map shape.
|
|
29
|
+
//
|
|
30
|
+
// Per SFC <rel>.vue|.svelte the virtual dir receives:
|
|
31
|
+
// <rel>.<vue|svelte>.<ts|js|tsx|jsx> the shadow (".vue.ts" naming makes
|
|
32
|
+
// `import x from './App.vue'` resolve
|
|
33
|
+
// to the shadow by TS extension probing)
|
|
34
|
+
// <rel>.….map.json the mapping sidecar:
|
|
35
|
+
// { "version": 1,
|
|
36
|
+
// "original": "src/App.vue", // posix, relative to GEML_SRC
|
|
37
|
+
// "framework": "vue" | "svelte",
|
|
38
|
+
// "component": "App", // basename minus extension
|
|
39
|
+
// "lines": [[generatedLine, originalLine], …], // 1-based, sorted by
|
|
40
|
+
// // generatedLine, first mapping wins;
|
|
41
|
+
// // a generated line absent here is
|
|
42
|
+
// // PURE GENERATED (never attribute it)
|
|
43
|
+
// "regions": [{ "name": "template", "start": 8, "end": 11 }] }
|
|
44
|
+
// // 1-based ORIGINAL line spans of
|
|
45
|
+
// // template/markup — a reference whose
|
|
46
|
+
// // mapped line falls here but whose
|
|
47
|
+
// // caller is generated-only belongs to
|
|
48
|
+
// // the synthetic <Component>.template
|
|
49
|
+
// plus one sfc-manifest.json ({ src, files:[{shadow, original, map}] }) the
|
|
50
|
+
// scip adapter loads, and one synthetic tsconfig.json.
|
|
51
|
+
//
|
|
52
|
+
// tsconfig include strategy: explicit "files" only (shadows + the project's
|
|
53
|
+
// real .ts/.tsx/.js/.jsx via relative paths) — no include globs, so sibling
|
|
54
|
+
// virtual dirs, node_modules and build output can never leak in.
|
|
55
|
+
// "rootDirs": [".", <rel GEML_SRC>] merges the two trees for RELATIVE import
|
|
56
|
+
// resolution: a shadow's `./helper` finds the real <src>/helper.ts, a real
|
|
57
|
+
// main.ts's `./App.vue` finds the shadow App.vue.ts. Bare imports (vue,
|
|
58
|
+
// svelte) resolve through "paths" → every node_modules dir walking up from
|
|
59
|
+
// GEML_SRC, so hoisted monorepo layouts work.
|
|
60
|
+
import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
|
|
61
|
+
import { join, dirname, relative, resolve, basename, delimiter } from "node:path";
|
|
62
|
+
import { createRequire } from "node:module";
|
|
63
|
+
import { SKIP_DIRS } from "./detect.mjs";
|
|
64
|
+
|
|
65
|
+
const posix = (p) => p.replace(/\\/g, "/");
|
|
66
|
+
|
|
67
|
+
// ---- library resolution (npx -p → project → this script) -------------------
|
|
68
|
+
function makeResolver(srcAbs) {
|
|
69
|
+
const requires = [];
|
|
70
|
+
const npxBin = (process.env.PATH ?? "")
|
|
71
|
+
.split(delimiter)
|
|
72
|
+
.find((p) => /[\\/]_npx[\\/]/.test(p) && /[\\/]\.bin[\\/]?$/.test(p));
|
|
73
|
+
if (npxBin) {
|
|
74
|
+
try { requires.push(createRequire(join(npxBin.replace(/[\\/]\.bin[\\/]?$/, ""), "x.js"))); } catch { /* malformed PATH entry */ }
|
|
75
|
+
}
|
|
76
|
+
for (let d = srcAbs; ; ) {
|
|
77
|
+
if (existsSync(join(d, "node_modules"))) {
|
|
78
|
+
try { requires.push(createRequire(join(d, "node_modules", "x.js"))); } catch { /* keep walking */ }
|
|
79
|
+
}
|
|
80
|
+
const up = dirname(d);
|
|
81
|
+
if (up === d) break;
|
|
82
|
+
d = up;
|
|
83
|
+
}
|
|
84
|
+
try { requires.push(createRequire(import.meta.url)); } catch { /* no local context */ }
|
|
85
|
+
const lib = (name) => {
|
|
86
|
+
for (const r of requires) { try { return r(name); } catch { /* next origin */ } }
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
lib.path = (name) => {
|
|
90
|
+
for (const r of requires) { try { return r.resolve(name); } catch { /* next origin */ } }
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
return lib;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---- shared line helpers ----------------------------------------------------
|
|
97
|
+
// offset -> 0-based line, O(log n) over precomputed line starts.
|
|
98
|
+
function lineIndex(text) {
|
|
99
|
+
const starts = [0];
|
|
100
|
+
for (let i = 0; i < text.length; i++) if (text[i] === "\n") starts.push(i + 1);
|
|
101
|
+
return (off) => {
|
|
102
|
+
let lo = 0, hi = starts.length - 1;
|
|
103
|
+
while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (starts[mid] <= off) lo = mid; else hi = mid - 1; }
|
|
104
|
+
return lo;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Volar offset mappings -> [[genLine, origLine], …] (1-based). A multi-line
|
|
109
|
+
// mapping segment is a verbatim copy, so lines advance in lockstep.
|
|
110
|
+
function linePairsFromVolar(srcText, genText, mappings) {
|
|
111
|
+
const srcLine = lineIndex(srcText), genLine = lineIndex(genText);
|
|
112
|
+
const triples = [];
|
|
113
|
+
for (const m of mappings) {
|
|
114
|
+
for (let i = 0; i < m.sourceOffsets.length; i++) {
|
|
115
|
+
triples.push([m.sourceOffsets[i], m.generatedOffsets[i], m.lengths?.[i] ?? 0]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
triples.sort((a, b) => a[1] - b[1]);
|
|
119
|
+
const pairs = new Map(); // genLine0 -> origLine0, first mapping wins
|
|
120
|
+
for (const [s, g, l] of triples) {
|
|
121
|
+
const g0 = genLine(g), s0 = srcLine(s);
|
|
122
|
+
const span = genLine(g + Math.max(l - 1, 0)) - g0;
|
|
123
|
+
for (let k = 0; k <= span; k++) if (!pairs.has(g0 + k)) pairs.set(g0 + k, s0 + k);
|
|
124
|
+
}
|
|
125
|
+
return [...pairs.entries()].sort((a, b) => a[0] - b[0]).map(([g, s]) => [g + 1, s + 1]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Source-map v3 `mappings` VLQ -> [[genLine, origLine], …] (1-based). Tiny
|
|
129
|
+
// hand-rolled base64-VLQ decoder — per generated line the FIRST segment that
|
|
130
|
+
// names a source line wins. Fields per segment: [genCol, srcIdx, srcLine,
|
|
131
|
+
// srcCol, name]; all deltas except genCol carry across lines.
|
|
132
|
+
function linePairsFromV3(mappings) {
|
|
133
|
+
const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
134
|
+
const val = new Map([...B64].map((c, i) => [c, i]));
|
|
135
|
+
const pairs = [];
|
|
136
|
+
let srcIdx = 0, srcLine = 0, srcCol = 0, name = 0;
|
|
137
|
+
const rows = String(mappings).split(";");
|
|
138
|
+
for (let g = 0; g < rows.length; g++) {
|
|
139
|
+
let taken = false;
|
|
140
|
+
for (const seg of rows[g].split(",")) {
|
|
141
|
+
if (!seg) continue;
|
|
142
|
+
const fields = [];
|
|
143
|
+
let shift = 0, cur = 0;
|
|
144
|
+
for (const ch of seg) {
|
|
145
|
+
const d = val.get(ch);
|
|
146
|
+
cur |= (d & 31) << shift;
|
|
147
|
+
if (d & 32) { shift += 5; continue; }
|
|
148
|
+
fields.push(cur & 1 ? -(cur >>> 1) : cur >>> 1);
|
|
149
|
+
shift = 0; cur = 0;
|
|
150
|
+
}
|
|
151
|
+
if (fields.length >= 4) {
|
|
152
|
+
srcIdx += fields[1]; srcLine += fields[2]; srcCol += fields[3];
|
|
153
|
+
if (fields.length >= 5) name += fields[4];
|
|
154
|
+
if (!taken) { pairs.push([g + 1, srcLine + 1]); taken = true; }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return pairs;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---- walk --------------------------------------------------------------------
|
|
162
|
+
function walk(rootAbs) {
|
|
163
|
+
const sfc = [], real = [];
|
|
164
|
+
const rec = (dir) => {
|
|
165
|
+
let ents;
|
|
166
|
+
try { ents = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
167
|
+
for (const e of ents) {
|
|
168
|
+
if (e.isDirectory()) {
|
|
169
|
+
if (!SKIP_DIRS.has(e.name) && !e.name.startsWith(".")) rec(join(dir, e.name));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!e.isFile()) continue;
|
|
173
|
+
const rel = posix(relative(rootAbs, join(dir, e.name)));
|
|
174
|
+
if (/\.(vue|svelte)$/i.test(e.name)) sfc.push(rel);
|
|
175
|
+
else if (/\.(ts|tsx|js|jsx)$/i.test(e.name) && !e.name.endsWith(".d.ts")) real.push(rel);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
rec(rootAbs);
|
|
179
|
+
return { sfc: sfc.sort(), real: real.sort() };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---- vue ---------------------------------------------------------------------
|
|
183
|
+
function makeVue(lib) {
|
|
184
|
+
const ts = lib("typescript");
|
|
185
|
+
const core = lib("@vue/language-core");
|
|
186
|
+
if (!ts || !core) return null;
|
|
187
|
+
const vueOptions = core.resolveVueCompilerOptions
|
|
188
|
+
? core.resolveVueCompilerOptions({}) // language-core 2.x
|
|
189
|
+
: core.getDefaultCompilerOptions(); // language-core 3.x
|
|
190
|
+
const plugin = core.createVueLanguagePlugin(ts, { allowJs: true }, vueOptions, (id) => id);
|
|
191
|
+
return (absPosix, text) => {
|
|
192
|
+
const code = plugin.createVirtualCode(absPosix, "vue", ts.ScriptSnapshot.fromString(text));
|
|
193
|
+
if (!code) throw new Error("not a valid vue file for this plugin");
|
|
194
|
+
let embedded;
|
|
195
|
+
for (const ec of core.forEachEmbeddedCode(code)) {
|
|
196
|
+
if (/^script_(ts|tsx|js|jsx)$/.test(ec.id)) { embedded = ec; break; }
|
|
197
|
+
}
|
|
198
|
+
if (!embedded) throw new Error("no script embedded code produced");
|
|
199
|
+
const gen = embedded.snapshot.getText(0, embedded.snapshot.getLength());
|
|
200
|
+
const regions = [];
|
|
201
|
+
const tpl = code.vueSfc?.descriptor?.template?.loc;
|
|
202
|
+
if (tpl) regions.push({ name: "template", start: tpl.start.line, end: tpl.end.line });
|
|
203
|
+
else {
|
|
204
|
+
// parse-less fallback: the top-level <template> block by text position
|
|
205
|
+
const idx = lineIndex(text);
|
|
206
|
+
const open = text.search(/<template[\s>]/);
|
|
207
|
+
const close = text.lastIndexOf("</template>");
|
|
208
|
+
if (open >= 0 && close > open) regions.push({ name: "template", start: idx(open) + 1, end: idx(close) + 1 });
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
ext: embedded.id.slice("script_".length),
|
|
212
|
+
gen,
|
|
213
|
+
lines: linePairsFromVolar(text, gen, embedded.mappings),
|
|
214
|
+
regions,
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---- svelte ------------------------------------------------------------------
|
|
220
|
+
function makeSvelte(lib) {
|
|
221
|
+
const mod = lib("svelte2tsx");
|
|
222
|
+
if (!mod) return null;
|
|
223
|
+
const svelte2tsx = mod.svelte2tsx ?? mod;
|
|
224
|
+
return (absPosix, text) => {
|
|
225
|
+
const isTsFile = /<script[^>]*\blang\s*=\s*["']?ts/.test(text);
|
|
226
|
+
const out = svelte2tsx(text, { filename: absPosix, isTsFile, mode: "ts" });
|
|
227
|
+
// markup = every line outside top-level <script>/<style> blocks, trimmed
|
|
228
|
+
// to its non-blank extent; a component may hold several disjoint spans.
|
|
229
|
+
const idx = lineIndex(text);
|
|
230
|
+
const total = idx(text.length) + 1;
|
|
231
|
+
const blocked = new Array(total).fill(false);
|
|
232
|
+
for (const m of text.matchAll(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/g)) {
|
|
233
|
+
const a = idx(m.index), b = idx(m.index + m[0].length - 1);
|
|
234
|
+
for (let i = a; i <= b; i++) blocked[i] = true;
|
|
235
|
+
}
|
|
236
|
+
const srcLines = text.split("\n");
|
|
237
|
+
const regions = [];
|
|
238
|
+
for (let i = 0; i < total; i++) {
|
|
239
|
+
if (blocked[i] || !srcLines[i]?.trim()) continue;
|
|
240
|
+
let j = i;
|
|
241
|
+
while (j + 1 < total && !blocked[j + 1]) j++;
|
|
242
|
+
while (j > i && !srcLines[j]?.trim()) j--;
|
|
243
|
+
regions.push({ name: "template", start: i + 1, end: j + 1 });
|
|
244
|
+
i = j;
|
|
245
|
+
}
|
|
246
|
+
return { ext: "ts", gen: out.code, lines: linePairsFromV3(out.map.mappings), regions };
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---- main --------------------------------------------------------------------
|
|
251
|
+
const srcAbs = resolve(process.env.GEML_SRC ?? ".");
|
|
252
|
+
const outAbs = resolve(process.env.GEML_OUT ?? "");
|
|
253
|
+
if (!process.env.GEML_OUT) {
|
|
254
|
+
console.error("sfc-virtualize: GEML_OUT (output dir) is required; GEML_SRC defaults to cwd");
|
|
255
|
+
process.exit(2);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const lib = makeResolver(srcAbs);
|
|
259
|
+
const { sfc, real } = walk(srcAbs);
|
|
260
|
+
if (!sfc.length) {
|
|
261
|
+
console.error(`sfc-virtualize: no .vue/.svelte files under ${srcAbs} — nothing to do`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let vue = null, svelte = null;
|
|
266
|
+
const missing = [];
|
|
267
|
+
if (sfc.some((f) => /\.vue$/i.test(f))) {
|
|
268
|
+
vue = makeVue(lib);
|
|
269
|
+
if (!vue) missing.push("@vue/language-core (+ typescript@5)");
|
|
270
|
+
}
|
|
271
|
+
if (sfc.some((f) => /\.svelte$/i.test(f))) {
|
|
272
|
+
svelte = makeSvelte(lib);
|
|
273
|
+
if (!svelte) missing.push("svelte2tsx (+ svelte)");
|
|
274
|
+
}
|
|
275
|
+
if (missing.length) {
|
|
276
|
+
console.error(
|
|
277
|
+
`sfc-virtualize: cannot resolve ${missing.join(" and ")} — run via\n`
|
|
278
|
+
+ " npx -y -p @vue/language-core -p svelte2tsx -p svelte -p typescript@5 node <this script>\n"
|
|
279
|
+
+ "or install them in the target project.",
|
|
280
|
+
);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
mkdirSync(outAbs, { recursive: true });
|
|
285
|
+
const manifest = [];
|
|
286
|
+
let nVue = 0, nSvelte = 0, failed = 0;
|
|
287
|
+
for (const rel of sfc) {
|
|
288
|
+
const absPosix = posix(join(srcAbs, rel));
|
|
289
|
+
try {
|
|
290
|
+
const text = readFileSync(join(srcAbs, rel), "utf8");
|
|
291
|
+
const isVue = /\.vue$/i.test(rel);
|
|
292
|
+
const r = (isVue ? vue : svelte)(absPosix, text);
|
|
293
|
+
const shadowRel = `${rel}.${r.ext}`;
|
|
294
|
+
const shadowAbs = join(outAbs, shadowRel);
|
|
295
|
+
mkdirSync(dirname(shadowAbs), { recursive: true });
|
|
296
|
+
writeFileSync(shadowAbs, r.gen);
|
|
297
|
+
const mapRel = `${shadowRel}.map.json`;
|
|
298
|
+
writeFileSync(join(outAbs, mapRel), JSON.stringify({
|
|
299
|
+
version: 1,
|
|
300
|
+
original: rel,
|
|
301
|
+
framework: isVue ? "vue" : "svelte",
|
|
302
|
+
component: basename(rel).replace(/\.(vue|svelte)$/i, ""),
|
|
303
|
+
lines: r.lines,
|
|
304
|
+
regions: r.regions,
|
|
305
|
+
}));
|
|
306
|
+
manifest.push({ shadow: shadowRel, original: rel, map: mapRel });
|
|
307
|
+
isVue ? nVue++ : nSvelte++;
|
|
308
|
+
} catch (e) {
|
|
309
|
+
failed++;
|
|
310
|
+
console.error(`sfc-virtualize: FAILED ${rel}: ${e.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!manifest.length) {
|
|
315
|
+
console.error("sfc-virtualize: every SFC failed to virtualize — nothing to index");
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// svelte2tsx global shims (svelteHTML etc) — cosmetic for indexing (user
|
|
320
|
+
// symbols resolve without them) but cheap to include when findable.
|
|
321
|
+
const extraFiles = [];
|
|
322
|
+
if (nSvelte) {
|
|
323
|
+
for (const shim of ["svelte2tsx/svelte-shims-v4.d.ts", "svelte2tsx/svelte-shims.d.ts"]) {
|
|
324
|
+
const p = lib.path(shim);
|
|
325
|
+
if (p) {
|
|
326
|
+
try {
|
|
327
|
+
copyFileSync(p, join(outAbs, "svelte-shims.d.ts"));
|
|
328
|
+
extraFiles.push("svelte-shims.d.ts");
|
|
329
|
+
} catch { /* optional */ }
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const relSrc = posix(relative(outAbs, srcAbs)) || ".";
|
|
336
|
+
const nmPaths = [];
|
|
337
|
+
for (let d = srcAbs; ; ) {
|
|
338
|
+
if (existsSync(join(d, "node_modules"))) nmPaths.push(`${posix(relative(outAbs, join(d, "node_modules")))}/*`);
|
|
339
|
+
const up = dirname(d);
|
|
340
|
+
if (up === d) break;
|
|
341
|
+
d = up;
|
|
342
|
+
}
|
|
343
|
+
writeFileSync(join(outAbs, "tsconfig.json"), JSON.stringify({
|
|
344
|
+
compilerOptions: {
|
|
345
|
+
allowJs: true, checkJs: false, noEmit: true, skipLibCheck: true,
|
|
346
|
+
module: "esnext", target: "esnext", moduleResolution: "node",
|
|
347
|
+
jsx: "preserve", baseUrl: ".",
|
|
348
|
+
rootDirs: [".", relSrc],
|
|
349
|
+
...(nmPaths.length ? { paths: { "*": nmPaths } } : {}),
|
|
350
|
+
},
|
|
351
|
+
files: [
|
|
352
|
+
...manifest.map((m) => m.shadow),
|
|
353
|
+
...extraFiles,
|
|
354
|
+
...real.map((f) => `${relSrc}/${f}`),
|
|
355
|
+
],
|
|
356
|
+
}, null, 2));
|
|
357
|
+
|
|
358
|
+
writeFileSync(join(outAbs, "sfc-manifest.json"), JSON.stringify({
|
|
359
|
+
version: 1,
|
|
360
|
+
src: posix(srcAbs),
|
|
361
|
+
files: manifest,
|
|
362
|
+
}, null, 2));
|
|
363
|
+
|
|
364
|
+
console.error(
|
|
365
|
+
`sfc-virtualize: ${manifest.length} shadow(s) (${nVue} vue, ${nSvelte} svelte)`
|
|
366
|
+
+ `${failed ? `, ${failed} FAILED` : ""}, ${real.length} real source file(s) -> ${outAbs}`,
|
|
367
|
+
);
|
package/codemap/verify.mjs
CHANGED
|
@@ -1,126 +1,148 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// geml-code-graph verify — the codemap's correctness oracle, two passes:
|
|
3
|
-
//
|
|
4
|
-
// 1. `geml check` over every .geml (document structure, id uniqueness,
|
|
5
|
-
// native references).
|
|
6
|
-
// 2. The codemap-profile pass (docs/codemap-profile.md): CSV cells and meta
|
|
7
|
-
// values are opaque to the GEML standard BY DESIGN (the standard stays
|
|
8
|
-
// untouched), so edge integrity is checked here — the from/to columns of
|
|
9
|
-
// #calls / #called-by / #ref-by tables and every meta `entry` value must
|
|
10
|
-
// resolve (`#id` in the same document, `doc.geml#id` in a sibling).
|
|
11
|
-
// A renamed or deleted method therefore fails the build, not the reader.
|
|
12
|
-
//
|
|
13
|
-
// geml codemap verify [dir] [--geml <path-to-geml.js|geml>]
|
|
14
|
-
import { readdirSync, existsSync, readFileSync } from "node:fs";
|
|
15
|
-
import { join, resolve, dirname, relative } from "node:path";
|
|
16
|
-
import { posix } from "node:path";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { spawnSync } from "node:child_process";
|
|
19
|
-
|
|
20
|
-
const args = process.argv.slice(2);
|
|
21
|
-
const flagI = args.indexOf("--geml");
|
|
22
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
23
|
-
console.error("usage: geml codemap verify [dir] [--geml <path>] (dir defaults to ./.geml-code-graph)");
|
|
24
|
-
process.exit(2);
|
|
25
|
-
}
|
|
26
|
-
const dir = args.find((a, i) => !a.startsWith("-") && (flagI < 0 || i !== flagI + 1)) || ".geml-code-graph";
|
|
27
|
-
const rootDir = resolve(dir);
|
|
28
|
-
|
|
29
|
-
// Resolve the geml CLI (pass 1) and the parser API (pass 2).
|
|
30
|
-
const localParser = resolve(dirname(fileURLToPath(import.meta.url)), "../dist/geml.js");
|
|
31
|
-
let cli = flagI >= 0 ? args[flagI + 1] : undefined;
|
|
32
|
-
if (!cli) cli = existsSync(localParser) ? localParser : "geml";
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
126
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// geml-code-graph verify — the codemap's correctness oracle, two passes:
|
|
3
|
+
//
|
|
4
|
+
// 1. `geml check` over every .geml (document structure, id uniqueness,
|
|
5
|
+
// native references).
|
|
6
|
+
// 2. The codemap-profile pass (docs/codemap-profile.md): CSV cells and meta
|
|
7
|
+
// values are opaque to the GEML standard BY DESIGN (the standard stays
|
|
8
|
+
// untouched), so edge integrity is checked here — the from/to columns of
|
|
9
|
+
// #calls / #called-by / #ref-by tables and every meta `entry` value must
|
|
10
|
+
// resolve (`#id` in the same document, `doc.geml#id` in a sibling).
|
|
11
|
+
// A renamed or deleted method therefore fails the build, not the reader.
|
|
12
|
+
//
|
|
13
|
+
// geml codemap verify [dir] [--geml <path-to-geml.js|geml>]
|
|
14
|
+
import { readdirSync, existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join, resolve, dirname, relative } from "node:path";
|
|
16
|
+
import { posix } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const flagI = args.indexOf("--geml");
|
|
22
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
23
|
+
console.error("usage: geml codemap verify [dir] [--geml <path>] (dir defaults to ./.geml-code-graph)");
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
const dir = args.find((a, i) => !a.startsWith("-") && (flagI < 0 || i !== flagI + 1)) || ".geml-code-graph";
|
|
27
|
+
const rootDir = resolve(dir);
|
|
28
|
+
|
|
29
|
+
// Resolve the geml CLI (pass 1) and the parser API (pass 2).
|
|
30
|
+
const localParser = resolve(dirname(fileURLToPath(import.meta.url)), "../dist/geml.js");
|
|
31
|
+
let cli = flagI >= 0 ? args[flagI + 1] : undefined;
|
|
32
|
+
if (!cli) cli = existsSync(localParser) ? localParser : "geml";
|
|
33
|
+
// win32 cmd.exe quoting (same rationale as build.mjs). q: space-aware quote for
|
|
34
|
+
// the PROGRAM token — a bare launcher name (geml resolved via PATH) whose
|
|
35
|
+
// .cmd/.bat shim uses %~dp0 breaks if the name is blanket-quoted. shq: ALWAYS
|
|
36
|
+
// double-quote ARGUMENTS — Node does not escape args under shell:true, so a
|
|
37
|
+
// `.geml` filename containing & | ( ) would otherwise break out and inject.
|
|
38
|
+
// cmd.exe treats those metacharacters and whitespace as literal inside quotes;
|
|
39
|
+
// CRT rules for embedded " / trailing \.
|
|
40
|
+
const q = (s) => (/[\s"]/.test(String(s)) ? `"${String(s).replace(/"/g, '\\"')}"` : String(s));
|
|
41
|
+
const shq = (s) => `"${String(s).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
42
|
+
const runCheck = (file) => {
|
|
43
|
+
// The built-parser path: run node on geml.js directly (array args, no shell).
|
|
44
|
+
if (cli.endsWith(".js")) return spawnSync(process.execPath, [cli, "check", file], { encoding: "utf8" });
|
|
45
|
+
// A non-.js cli may be a .cmd/.bat launcher (e.g. geml.cmd on PATH), which
|
|
46
|
+
// Node can only spawn through the shell. Hand cmd.exe ONE pre-escaped command
|
|
47
|
+
// string (never an args array — that is the unescaped, injection-prone path).
|
|
48
|
+
if (process.platform === "win32") {
|
|
49
|
+
return spawnSync([q(cli), ...["check", file].map(shq)].join(" "), { encoding: "utf8", shell: true });
|
|
50
|
+
}
|
|
51
|
+
return spawnSync(cli, ["check", file], { encoding: "utf8" });
|
|
52
|
+
};
|
|
53
|
+
if (!existsSync(localParser)) {
|
|
54
|
+
console.error("verify: the profile pass needs the built parser (cd geml-parser && npm install && npm run build)");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const { parse } = await import(`file://${localParser.replace(/\\/g, "/")}`);
|
|
58
|
+
|
|
59
|
+
const files = [];
|
|
60
|
+
const walk = (d) => {
|
|
61
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
62
|
+
const p = join(d, e.name);
|
|
63
|
+
if (e.isDirectory()) walk(p);
|
|
64
|
+
else if (e.name.endsWith(".geml")) files.push(p);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
walk(rootDir);
|
|
68
|
+
files.sort();
|
|
69
|
+
|
|
70
|
+
// ---- pass 1: geml check ----
|
|
71
|
+
let failed = 0;
|
|
72
|
+
for (const f of files) {
|
|
73
|
+
const r = runCheck(f);
|
|
74
|
+
if (r.status !== 0) {
|
|
75
|
+
failed++;
|
|
76
|
+
console.error(`FAIL ${f}`);
|
|
77
|
+
console.error((r.stderr || r.stdout || "").split("\n").slice(0, 4).map((l) => ` ${l}`).join("\n"));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- pass 2: codemap profile references ----
|
|
82
|
+
const REF_TABLES = new Set(["calls", "called-by", "ref-by"]);
|
|
83
|
+
// Cross-stack link tables: `from`/`to` may be a #ref (resolved cross-tree
|
|
84
|
+
// link — checked) OR plain `file:line` text (a call/route outside any indexed
|
|
85
|
+
// function — tolerated, nothing to resolve).
|
|
86
|
+
const LINK_TABLES = new Set(["api-calls", "api-served-by"]);
|
|
87
|
+
const relDoc = (f) => relative(rootDir, f).replace(/\\/g, "/");
|
|
88
|
+
const docs = new Map(); // relPath -> { ids:Set, blocks }
|
|
89
|
+
const collectIds = (blocks, ids) => {
|
|
90
|
+
for (const b of blocks) {
|
|
91
|
+
if (b.id) ids.add(b.id);
|
|
92
|
+
if (b.children) collectIds(b.children, ids);
|
|
93
|
+
if (b.items) for (const it of b.items) if (it.children) collectIds(it.children, ids);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
for (const f of files) {
|
|
97
|
+
const doc = parse(readFileSync(f, "utf8"));
|
|
98
|
+
const ids = new Set();
|
|
99
|
+
collectIds(doc.children, ids);
|
|
100
|
+
docs.set(relDoc(f), { ids, blocks: doc.children });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let refErrors = 0;
|
|
104
|
+
const err = (doc, where, msg) => {
|
|
105
|
+
refErrors++;
|
|
106
|
+
console.error(`REF ${doc} ${where}: ${msg}`);
|
|
107
|
+
};
|
|
108
|
+
const checkRef = (fromDoc, where, ref, lenient = false) => {
|
|
109
|
+
ref = String(ref).trim();
|
|
110
|
+
if (!ref) return lenient ? undefined : err(fromDoc, where, "empty reference cell");
|
|
111
|
+
const h = ref.indexOf("#");
|
|
112
|
+
if (h < 0) return lenient ? undefined : err(fromDoc, where, `not a reference: \`${ref}\``);
|
|
113
|
+
let targetDoc = fromDoc;
|
|
114
|
+
if (h > 0) targetDoc = posix.normalize(posix.join(posix.dirname(fromDoc), ref.slice(0, h)));
|
|
115
|
+
const id = ref.slice(h + 1);
|
|
116
|
+
const target = docs.get(targetDoc);
|
|
117
|
+
if (!target) return err(fromDoc, where, `cannot resolve document \`${ref.slice(0, h)}\``);
|
|
118
|
+
// reads/writes values may carry a plain-text `.member` suffix (ids never contain '.')
|
|
119
|
+
const bare = id.split(".")[0];
|
|
120
|
+
if (!target.ids.has(bare)) return err(fromDoc, where, `unresolved reference \`${ref}\``);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
for (const [docPath, { blocks }] of docs) {
|
|
124
|
+
for (const b of blocks) {
|
|
125
|
+
if (b.kind !== "block") continue;
|
|
126
|
+
if (b.type === "table" && (REF_TABLES.has(b.id) || LINK_TABLES.has(b.id)) && b.table) {
|
|
127
|
+
const lenient = LINK_TABLES.has(b.id);
|
|
128
|
+
const fromCol = b.table.columns.indexOf("from");
|
|
129
|
+
const toCol = b.table.columns.indexOf("to");
|
|
130
|
+
if (fromCol < 0 || toCol < 0) { err(docPath, `#${b.id}`, "missing from/to columns"); continue; }
|
|
131
|
+
b.table.rows.forEach((row, i) => {
|
|
132
|
+
checkRef(docPath, `#${b.id} row ${i + 1} from`, row[fromCol]?.text ?? "", lenient);
|
|
133
|
+
checkRef(docPath, `#${b.id} row ${i + 1} to`, row[toCol]?.text ?? "", lenient);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (b.type === "meta" && b.data?.entry) {
|
|
137
|
+
for (const ref of String(b.data.entry).split(/\s+/).filter(Boolean)) {
|
|
138
|
+
checkRef(docPath, "meta entry", ref);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.error(
|
|
145
|
+
`verify: ${files.length - failed}/${files.length} documents pass geml check; `
|
|
146
|
+
+ `profile references: ${refErrors === 0 ? "all resolve" : `${refErrors} dangling`}`,
|
|
147
|
+
);
|
|
148
|
+
process.exit(failed || refErrors ? 1 : 0);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function normalizeBlockId(blockSrc: string, newId: string): string;
|