@geml/geml 1.4.2 → 1.4.4

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.
@@ -1,367 +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
- );
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
+ );