@pyreon/vite-plugin 0.23.0 → 0.24.1
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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +365 -18
- package/lib/types/index.d.ts +128 -2
- package/package.json +2 -2
- package/src/index.ts +545 -14
- package/src/tests/dev-server.test.ts +5 -1
- package/src/tests/lpih-auto-bridge.test.ts +408 -0
- package/src/tests/lpih-injection.test.ts +559 -0
- package/src/tests/vite-plugin.test.ts +5 -2
|
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
|
|
|
5386
5386
|
</script>
|
|
5387
5387
|
<script>
|
|
5388
5388
|
/*<!--*/
|
|
5389
|
-
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/index.ts","uid":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/index.ts","uid":"e04d9c6c-1"}]},{"name":"rocketstyle-collapse-C4eMAnwR.js","children":[{"name":"src/rocketstyle-collapse.ts","uid":"e04d9c6c-3"}]}],"isRoot":true},"nodeParts":{"e04d9c6c-1":{"renderedLength":45559,"gzipLength":14773,"brotliLength":0,"metaUid":"e04d9c6c-0"},"e04d9c6c-3":{"renderedLength":3424,"gzipLength":1530,"brotliLength":0,"metaUid":"e04d9c6c-2"}},"nodeMetas":{"e04d9c6c-0":{"id":"/src/index.ts","moduleParts":{"index.js":"e04d9c6c-1"},"imported":[{"uid":"e04d9c6c-4"},{"uid":"e04d9c6c-5"},{"uid":"e04d9c6c-6"},{"uid":"e04d9c6c-2","dynamic":true},{"uid":"e04d9c6c-7","dynamic":true}],"importedBy":[],"isEntry":true},"e04d9c6c-2":{"id":"/src/rocketstyle-collapse.ts","moduleParts":{"rocketstyle-collapse-C4eMAnwR.js":"e04d9c6c-3"},"imported":[{"uid":"e04d9c6c-8","dynamic":true}],"importedBy":[{"uid":"e04d9c6c-0"}]},"e04d9c6c-4":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"e04d9c6c-0"}]},"e04d9c6c-5":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"e04d9c6c-0"}]},"e04d9c6c-6":{"id":"@pyreon/compiler","moduleParts":{},"imported":[],"importedBy":[{"uid":"e04d9c6c-0"}]},"e04d9c6c-7":{"id":"node:fs/promises","moduleParts":{},"imported":[],"importedBy":[{"uid":"e04d9c6c-0"}]},"e04d9c6c-8":{"id":"vite","moduleParts":{},"imported":[],"importedBy":[{"uid":"e04d9c6c-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
|
|
5390
5390
|
|
|
5391
5391
|
const run = () => {
|
|
5392
5392
|
const width = window.innerWidth;
|
package/lib/index.js
CHANGED
|
@@ -179,6 +179,10 @@ function pyreonPlugin(options) {
|
|
|
179
179
|
const ssrConfig = options?.ssr;
|
|
180
180
|
const compat = options?.compat;
|
|
181
181
|
const islandsEnabled = options?.islands !== false;
|
|
182
|
+
const lpihOpt = options?.lpih;
|
|
183
|
+
const lpihEnabled = lpihOpt !== false;
|
|
184
|
+
const lpihUserCfg = lpihOpt && lpihOpt !== true ? lpihOpt : {};
|
|
185
|
+
const lpihIntervalMs = lpihUserCfg.intervalMs ?? 250;
|
|
182
186
|
const collapseOpt = options?.collapse;
|
|
183
187
|
const collapseEnabled = collapseOpt === true || collapseOpt != null && collapseOpt !== false;
|
|
184
188
|
const collapseUserCfg = collapseOpt && collapseOpt !== true ? collapseOpt : {};
|
|
@@ -355,7 +359,7 @@ function pyreonPlugin(options) {
|
|
|
355
359
|
let output = result.code;
|
|
356
360
|
if (!isBuild) {
|
|
357
361
|
output = injectHmr(output, id);
|
|
358
|
-
output = injectSignalNames(output);
|
|
362
|
+
output = injectSignalNames(output, id);
|
|
359
363
|
}
|
|
360
364
|
return {
|
|
361
365
|
code: output,
|
|
@@ -371,6 +375,7 @@ function pyreonPlugin(options) {
|
|
|
371
375
|
contextTimer = setTimeout(() => generateProjectContext(projectRoot), 500);
|
|
372
376
|
}
|
|
373
377
|
});
|
|
378
|
+
if (lpihEnabled) registerLpihMiddleware(server, projectRoot, lpihUserCfg);
|
|
374
379
|
if (!ssrConfig) return;
|
|
375
380
|
return () => {
|
|
376
381
|
server.middlewares.use(async (req, res, next) => {
|
|
@@ -385,6 +390,11 @@ function pyreonPlugin(options) {
|
|
|
385
390
|
}
|
|
386
391
|
});
|
|
387
392
|
};
|
|
393
|
+
},
|
|
394
|
+
transformIndexHtml(html) {
|
|
395
|
+
if (isBuild || !lpihEnabled) return void 0;
|
|
396
|
+
const script = buildLpihClientScript(lpihIntervalMs);
|
|
397
|
+
return html.replace("</head>", `${script}\n</head>`);
|
|
388
398
|
}
|
|
389
399
|
};
|
|
390
400
|
}
|
|
@@ -425,6 +435,130 @@ function generateProjectContext(root) {
|
|
|
425
435
|
} catch {}
|
|
426
436
|
}
|
|
427
437
|
/**
|
|
438
|
+
* Resolve the LPIH cache-file path for a given project root. Matches the
|
|
439
|
+
* convention `@pyreon/reactivity/lpih`'s `getDefaultLpihCachePath()` uses
|
|
440
|
+
* AND the LSP auto-discovers (R2, #777): `<projectRoot>/.pyreon-lpih.json`.
|
|
441
|
+
*
|
|
442
|
+
* @internal — exported for tests.
|
|
443
|
+
*/
|
|
444
|
+
function resolveLpihCachePath(projectRoot) {
|
|
445
|
+
return join(projectRoot, ".pyreon-lpih.json");
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Register the LPIH dev-server middleware on a Vite server. Extracted from
|
|
449
|
+
* `configureServer` so the `cachePath` option reference lives at module
|
|
450
|
+
* scope (top-level helper) rather than inside the plugin's inline body —
|
|
451
|
+
* keeps `scripts/audit-types.ts` happy regardless of how its comment-
|
|
452
|
+
* stripping handles the long inline `configureServer` block.
|
|
453
|
+
*
|
|
454
|
+
* @internal — exported for tests.
|
|
455
|
+
*/
|
|
456
|
+
function registerLpihMiddleware(server, projectRoot, userCfg) {
|
|
457
|
+
const cachePath = userCfg.cachePath ?? resolveLpihCachePath(projectRoot);
|
|
458
|
+
server.middlewares.use("/__pyreon_lpih__", (req, res) => {
|
|
459
|
+
if (req.method !== "POST") {
|
|
460
|
+
res.statusCode = 405;
|
|
461
|
+
res.end("Method Not Allowed");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
let body = "";
|
|
465
|
+
req.on("data", (chunk) => {
|
|
466
|
+
body += chunk.toString();
|
|
467
|
+
if (body.length > 1024 * 1024) {
|
|
468
|
+
res.statusCode = 413;
|
|
469
|
+
res.end("Payload Too Large");
|
|
470
|
+
req.destroy();
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
req.on("end", () => {
|
|
474
|
+
writeLpihCacheFile(cachePath, body).then(() => {
|
|
475
|
+
res.statusCode = 204;
|
|
476
|
+
res.end();
|
|
477
|
+
}).catch((err) => {
|
|
478
|
+
console.warn("[pyreon] LPIH cache write failed:", err instanceof Error ? err.message : err);
|
|
479
|
+
res.statusCode = 500;
|
|
480
|
+
res.end("LPIH cache write failed");
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
let _lpihSeq = 0;
|
|
486
|
+
/**
|
|
487
|
+
* Atomically write a LPIH cache file (tmp + rename), mirroring the
|
|
488
|
+
* `@pyreon/reactivity/lpih:writeLpihCache` implementation. The payload
|
|
489
|
+
* comes pre-serialized from the browser-side bridge — we validate the
|
|
490
|
+
* outer shape (`{ fires: [...] }`) and reject malformed bodies to stop a
|
|
491
|
+
* buggy client from corrupting the file the LSP reads.
|
|
492
|
+
*
|
|
493
|
+
* @internal — exported for tests.
|
|
494
|
+
*/
|
|
495
|
+
async function writeLpihCacheFile(path, body) {
|
|
496
|
+
let parsed;
|
|
497
|
+
try {
|
|
498
|
+
parsed = JSON.parse(body);
|
|
499
|
+
} catch {
|
|
500
|
+
throw new Error("LPIH bridge: payload is not valid JSON");
|
|
501
|
+
}
|
|
502
|
+
if (parsed === null || typeof parsed !== "object" || !Array.isArray(parsed.fires)) throw new Error("LPIH bridge: payload is missing `fires` array");
|
|
503
|
+
const fs = await import("node:fs/promises");
|
|
504
|
+
const tmp = `${path}.tmp.${typeof process !== "undefined" && "pid" in process ? process.pid : 0}.${++_lpihSeq}`;
|
|
505
|
+
try {
|
|
506
|
+
await fs.writeFile(tmp, JSON.stringify(parsed), "utf8");
|
|
507
|
+
await fs.rename(tmp, path);
|
|
508
|
+
} catch (err) {
|
|
509
|
+
try {
|
|
510
|
+
await fs.unlink(tmp);
|
|
511
|
+
} catch {}
|
|
512
|
+
throw err;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Build the `<script type="module">` body injected into the HTML head.
|
|
517
|
+
* The script imports devtools activation + `getFireSummaries` from
|
|
518
|
+
* `@pyreon/reactivity`, sets up a `setInterval` that POSTs every
|
|
519
|
+
* `intervalMs` ms, and registers a `beforeunload` cleanup so the timer
|
|
520
|
+
* doesn't outlive the page.
|
|
521
|
+
*
|
|
522
|
+
* Browser bundlers serve `@pyreon/reactivity` from the workspace via
|
|
523
|
+
* Vite's normal module resolution — no virtual module needed.
|
|
524
|
+
*
|
|
525
|
+
* @internal — exported for tests.
|
|
526
|
+
*/
|
|
527
|
+
function buildLpihClientScript(intervalMs) {
|
|
528
|
+
return `<script type="module">
|
|
529
|
+
// Pyreon LPIH auto-bridge — POSTs fire summaries to /__pyreon_lpih__
|
|
530
|
+
// so the LSP (pyreon-lint --lsp) sees live fire data. Dev-only.
|
|
531
|
+
const __px = await import('@pyreon/reactivity').catch(() => null)
|
|
532
|
+
if (__px) {
|
|
533
|
+
__px.activateReactiveDevtools()
|
|
534
|
+
const __pxGet = __px.getFireSummaries
|
|
535
|
+
const __pxInterval = ${JSON.stringify(intervalMs)}
|
|
536
|
+
const __pxPost = () => {
|
|
537
|
+
const summaries = __pxGet()
|
|
538
|
+
const payload = JSON.stringify({
|
|
539
|
+
fires: summaries.map((s) => ({
|
|
540
|
+
file: s.loc.file,
|
|
541
|
+
line: s.loc.line,
|
|
542
|
+
count: s.count,
|
|
543
|
+
kind: s.kind,
|
|
544
|
+
lastFire: s.lastFire,
|
|
545
|
+
rate1s: s.rate1s,
|
|
546
|
+
})),
|
|
547
|
+
})
|
|
548
|
+
fetch('/__pyreon_lpih__', { method: 'POST', body: payload, headers: { 'content-type': 'application/json' } }).catch(() => {
|
|
549
|
+
// Dev-server might be restarting; swallow + retry next interval.
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
const __pxId = setInterval(__pxPost, __pxInterval)
|
|
553
|
+
window.addEventListener('beforeunload', () => clearInterval(__pxId))
|
|
554
|
+
}
|
|
555
|
+
// If __px is null, @pyreon/reactivity isn't in the dep graph — stay silent,
|
|
556
|
+
// LPIH is opt-in via the runtime API too. The dynamic-import catch returns
|
|
557
|
+
// null instead of letting the rejection bubble so consumers without the
|
|
558
|
+
// package don't see a console error.
|
|
559
|
+
<\/script>`;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
428
562
|
* Regex that detects signal declarations (prefix + variable name).
|
|
429
563
|
* The arguments are extracted via balanced-paren matching in `injectHmr`.
|
|
430
564
|
* A brace-depth check filters out matches inside functions/blocks — only
|
|
@@ -523,37 +657,250 @@ function hasMultipleArgs(args) {
|
|
|
523
657
|
return false;
|
|
524
658
|
}
|
|
525
659
|
/**
|
|
526
|
-
* Inject `{ name:
|
|
660
|
+
* Inject `{ name?, __sourceLocation: { file, line, col } }` into
|
|
661
|
+
* `signal()` / `computed()` / `effect()` calls that don't already have
|
|
527
662
|
* an options argument. Only runs in dev mode for debugging/devtools.
|
|
528
663
|
*
|
|
529
|
-
*
|
|
664
|
+
* Three forms covered:
|
|
665
|
+
*
|
|
666
|
+
* `const count = signal(0)` →
|
|
667
|
+
* `const count = signal(0, { name: "count", __sourceLocation: {...} })`
|
|
668
|
+
*
|
|
669
|
+
* `const doubled = computed(() => count() * 2)` →
|
|
670
|
+
* `const doubled = computed(() => count() * 2, { name: "doubled", __sourceLocation: {...} })`
|
|
671
|
+
*
|
|
672
|
+
* `effect(() => console.log(count()))` →
|
|
673
|
+
* `effect(() => console.log(count()), { __sourceLocation: {...} })`
|
|
674
|
+
* (no `name` — anonymous effects have no binding to derive from)
|
|
530
675
|
*
|
|
531
676
|
* Module-scope signals rewritten to __hmr_signal() are naturally skipped
|
|
532
677
|
* because the regex matches `signal(` not `__hmr_signal(`.
|
|
678
|
+
*
|
|
679
|
+
* **LPIH integration**: `__sourceLocation` is consumed by
|
|
680
|
+
* `@pyreon/reactivity`'s `signal()` / `computed()` / `effect()` to skip
|
|
681
|
+
* the `new Error().stack` capture in `_rdRegister` — saves ~2.2µs per
|
|
682
|
+
* creation when devtools is active. The injected literal is byte-for-byte
|
|
683
|
+
* the same info the runtime would have parsed from the stack, so behavior
|
|
684
|
+
* is identical except no stack-parse cost.
|
|
685
|
+
*
|
|
686
|
+
* **Anonymous-effect detection**: `effect(` can also appear as a property
|
|
687
|
+
* access (`obj.effect(...)`), a longer identifier (`sideEffect(...)`), or
|
|
688
|
+
* a previously-injected call (`effect(fn, { ... })`). The unbound-effect
|
|
689
|
+
* pass guards against all three:
|
|
690
|
+
* - preceded by NOT `[A-Za-z0-9_$.]` (so `.effect`/`sideEffect` skip)
|
|
691
|
+
* - args do NOT already contain a 2nd arg (`hasMultipleArgs` check)
|
|
692
|
+
*
|
|
693
|
+
* @param code - source text
|
|
694
|
+
* @param moduleId - the file path to embed in the injected `__sourceLocation`.
|
|
695
|
+
* Vite passes the resolved module ID (absolute path).
|
|
533
696
|
*/
|
|
534
|
-
function injectSignalNames(code) {
|
|
535
|
-
const
|
|
697
|
+
function injectSignalNames(code, moduleId) {
|
|
698
|
+
const masked = _maskStringsAndComments(code);
|
|
699
|
+
const reBound = /(?:const|let)\s+(\w+)\s*=\s*(signal|computed|effect)\(/gm;
|
|
700
|
+
const reUnboundEffect = /(?<![\w$.])effect\(/gm;
|
|
536
701
|
const matches = [];
|
|
537
|
-
|
|
702
|
+
const covered = /* @__PURE__ */ new Set();
|
|
703
|
+
let m = reBound.exec(masked);
|
|
538
704
|
while (m !== null) {
|
|
539
705
|
const argsStart = m.index + m[0].length;
|
|
540
706
|
const args = extractBalancedArgs(code, argsStart);
|
|
541
|
-
if (args !== null && !hasMultipleArgs(args))
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
707
|
+
if (args !== null && !hasMultipleArgs(args)) {
|
|
708
|
+
matches.push({
|
|
709
|
+
start: argsStart,
|
|
710
|
+
end: argsStart + args.length,
|
|
711
|
+
name: m[1] ?? "",
|
|
712
|
+
args,
|
|
713
|
+
matchIdx: m.index
|
|
714
|
+
});
|
|
715
|
+
const tokStart = m.index + m[0].length - (m[2]?.length ?? 0) - 1;
|
|
716
|
+
covered.add(tokStart);
|
|
717
|
+
}
|
|
718
|
+
m = reBound.exec(masked);
|
|
548
719
|
}
|
|
549
|
-
|
|
720
|
+
reBound.lastIndex = 0;
|
|
721
|
+
m = reUnboundEffect.exec(masked);
|
|
722
|
+
while (m !== null) {
|
|
723
|
+
if (!covered.has(m.index)) {
|
|
724
|
+
const argsStart = m.index + m[0].length;
|
|
725
|
+
const args = extractBalancedArgs(code, argsStart);
|
|
726
|
+
if (args !== null && !hasMultipleArgs(args)) matches.push({
|
|
727
|
+
start: argsStart,
|
|
728
|
+
end: argsStart + args.length,
|
|
729
|
+
name: null,
|
|
730
|
+
args,
|
|
731
|
+
matchIdx: m.index
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
m = reUnboundEffect.exec(masked);
|
|
735
|
+
}
|
|
736
|
+
reUnboundEffect.lastIndex = 0;
|
|
737
|
+
if (matches.length === 0) return code;
|
|
738
|
+
matches.sort((a, b) => b.start - a.start);
|
|
739
|
+
const lineStarts = _computeLineStarts(code);
|
|
550
740
|
let output = code;
|
|
551
|
-
for (let i =
|
|
552
|
-
const { start, end, name, args } = matches[i];
|
|
553
|
-
|
|
741
|
+
for (let i = 0; i < matches.length; i++) {
|
|
742
|
+
const { start, end, name, args, matchIdx } = matches[i];
|
|
743
|
+
const { line, col } = _offsetToLineCol(matchIdx, lineStarts);
|
|
744
|
+
const locLiteral = `__sourceLocation: { file: ${JSON.stringify(moduleId)}, line: ${line}, col: ${col} }`;
|
|
745
|
+
const inner = name !== null ? `name: ${JSON.stringify(name)}, ${locLiteral}` : locLiteral;
|
|
746
|
+
output = `${output.slice(0, start)}${args}, { ${inner} }${output.slice(end)}`;
|
|
554
747
|
}
|
|
555
748
|
return output;
|
|
556
749
|
}
|
|
750
|
+
/**
|
|
751
|
+
* Mask string-literal / template-literal / comment regions in `code` by
|
|
752
|
+
* replacing their content with spaces. Returns a SAME-LENGTH string so
|
|
753
|
+
* regex match positions in the masked version line up with the original.
|
|
754
|
+
*
|
|
755
|
+
* Used by `injectSignalNames` to skip false-positive matches against
|
|
756
|
+
* reactive-primitive names that appear inside strings or comments. Without
|
|
757
|
+
* masking, a user's `const docs = \`effect(() => x)\`` template literal
|
|
758
|
+
* would get `, { __sourceLocation: ... }` injected INSIDE the string,
|
|
759
|
+
* corrupting runtime values.
|
|
760
|
+
*
|
|
761
|
+
* Handles:
|
|
762
|
+
* - `"..."` / `'...'` strings (escape-aware)
|
|
763
|
+
* - `` `...` `` template literals; interpolations `${...}` are KEPT as
|
|
764
|
+
* code (their content can contain real `signal()` calls worth catching)
|
|
765
|
+
* - `// ...` line comments
|
|
766
|
+
* - `/* ... *\/` block comments
|
|
767
|
+
*
|
|
768
|
+
* Regex literals (`/foo/g`) are NOT special-cased — they're rare and the
|
|
769
|
+
* downstream extractBalancedArgs handles unmatched parens by returning null.
|
|
770
|
+
*
|
|
771
|
+
* @internal — exported for tests.
|
|
772
|
+
*/
|
|
773
|
+
function _maskStringsAndComments(code) {
|
|
774
|
+
const out = [];
|
|
775
|
+
let i = 0;
|
|
776
|
+
const n = code.length;
|
|
777
|
+
while (i < n) {
|
|
778
|
+
const c = code[i];
|
|
779
|
+
const c1 = code[i + 1];
|
|
780
|
+
if (c === "/" && c1 === "/") {
|
|
781
|
+
while (i < n && code[i] !== "\n") {
|
|
782
|
+
out.push(" ");
|
|
783
|
+
i++;
|
|
784
|
+
}
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (c === "/" && c1 === "*") {
|
|
788
|
+
out.push(" ", " ");
|
|
789
|
+
i += 2;
|
|
790
|
+
while (i < n) {
|
|
791
|
+
if (code[i] === "*" && code[i + 1] === "/") {
|
|
792
|
+
out.push(" ", " ");
|
|
793
|
+
i += 2;
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
out.push(code[i] === "\n" ? "\n" : " ");
|
|
797
|
+
i++;
|
|
798
|
+
}
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (c === "\"" || c === "'") {
|
|
802
|
+
const quote = c;
|
|
803
|
+
out.push(" ");
|
|
804
|
+
i++;
|
|
805
|
+
while (i < n && code[i] !== quote) {
|
|
806
|
+
if (code[i] === "\\" && i + 1 < n) {
|
|
807
|
+
out.push(" ", code[i + 1] === "\n" ? "\n" : " ");
|
|
808
|
+
i += 2;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (code[i] === "\n") break;
|
|
812
|
+
out.push(" ");
|
|
813
|
+
i++;
|
|
814
|
+
}
|
|
815
|
+
if (i < n && code[i] === quote) {
|
|
816
|
+
out.push(" ");
|
|
817
|
+
i++;
|
|
818
|
+
}
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (c === "`") {
|
|
822
|
+
out.push(" ");
|
|
823
|
+
i++;
|
|
824
|
+
while (i < n && code[i] !== "`") {
|
|
825
|
+
if (code[i] === "\\" && i + 1 < n) {
|
|
826
|
+
out.push(" ", code[i + 1] === "\n" ? "\n" : " ");
|
|
827
|
+
i += 2;
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if (code[i] === "$" && code[i + 1] === "{") {
|
|
831
|
+
out.push(" ", " ");
|
|
832
|
+
i += 2;
|
|
833
|
+
let depth = 1;
|
|
834
|
+
while (i < n && depth > 0) {
|
|
835
|
+
if (code[i] === "{") {
|
|
836
|
+
depth++;
|
|
837
|
+
out.push(code[i] ?? " ");
|
|
838
|
+
i++;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (code[i] === "}") {
|
|
842
|
+
depth--;
|
|
843
|
+
if (depth === 0) {
|
|
844
|
+
out.push(" ");
|
|
845
|
+
i++;
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
out.push(code[i] ?? " ");
|
|
849
|
+
i++;
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
out.push(code[i] ?? " ");
|
|
853
|
+
i++;
|
|
854
|
+
}
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
out.push(code[i] === "\n" ? "\n" : " ");
|
|
858
|
+
i++;
|
|
859
|
+
}
|
|
860
|
+
if (i < n && code[i] === "`") {
|
|
861
|
+
out.push(" ");
|
|
862
|
+
i++;
|
|
863
|
+
}
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
out.push(c ?? "");
|
|
867
|
+
i++;
|
|
868
|
+
}
|
|
869
|
+
return out.join("");
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Compute the 0-indexed character offset for the start of each line.
|
|
873
|
+
* `lineStarts[i]` is the offset of the FIRST character on line i+1
|
|
874
|
+
* (1-based, so `lineStarts[0]` = offset 0 = line 1).
|
|
875
|
+
*
|
|
876
|
+
* @internal — exported for tests.
|
|
877
|
+
*/
|
|
878
|
+
function _computeLineStarts(code) {
|
|
879
|
+
const starts = [0];
|
|
880
|
+
for (let i = 0; i < code.length; i++) if (code.charCodeAt(i) === 10) starts.push(i + 1);
|
|
881
|
+
return starts;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Convert a 0-indexed offset to `{ line: 1-based, col: 1-based }` using a
|
|
885
|
+
* pre-computed line-starts array. Binary search → O(log N) per lookup.
|
|
886
|
+
*
|
|
887
|
+
* @internal — exported for tests.
|
|
888
|
+
*/
|
|
889
|
+
function _offsetToLineCol(offset, lineStarts) {
|
|
890
|
+
let lo = 0;
|
|
891
|
+
let hi = lineStarts.length - 1;
|
|
892
|
+
while (lo < hi) {
|
|
893
|
+
const mid = lo + hi + 1 >>> 1;
|
|
894
|
+
const v = lineStarts[mid];
|
|
895
|
+
if (v !== void 0 && v <= offset) lo = mid;
|
|
896
|
+
else hi = mid - 1;
|
|
897
|
+
}
|
|
898
|
+
const lineStart = lineStarts[lo] ?? 0;
|
|
899
|
+
return {
|
|
900
|
+
line: lo + 1,
|
|
901
|
+
col: offset - lineStart + 1
|
|
902
|
+
};
|
|
903
|
+
}
|
|
557
904
|
function injectHmr(code, moduleId) {
|
|
558
905
|
const hasSignals = SIGNAL_PREFIX_RE.test(code);
|
|
559
906
|
SIGNAL_PREFIX_RE.lastIndex = 0;
|
|
@@ -895,5 +1242,5 @@ export function __hmr_dispose(moduleId) {
|
|
|
895
1242
|
`;
|
|
896
1243
|
|
|
897
1244
|
//#endregion
|
|
898
|
-
export { pyreonPlugin as default };
|
|
1245
|
+
export { _computeLineStarts, _maskStringsAndComments, _offsetToLineCol, buildLpihClientScript, pyreonPlugin as default, registerLpihMiddleware, resolveLpihCachePath, writeLpihCacheFile };
|
|
899
1246
|
//# sourceMappingURL=index.js.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Plugin } from "vite";
|
|
1
|
+
import { Plugin, ViteDevServer } from "vite";
|
|
2
2
|
|
|
3
3
|
//#region src/index.d.ts
|
|
4
4
|
type CompatFramework = 'react' | 'preact' | 'vue' | 'solid' | 'svelte';
|
|
@@ -56,6 +56,30 @@ interface PyreonPluginOptions {
|
|
|
56
56
|
* hydrateIslandsAuto()
|
|
57
57
|
*/
|
|
58
58
|
islands?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* **LPIH auto-bridge** — zero-config Live Program Inlay Hints in dev.
|
|
61
|
+
*
|
|
62
|
+
* When `true` (the default in dev), the plugin auto-wires the LPIH
|
|
63
|
+
* cache file: the browser-side activates devtools + polls fire data
|
|
64
|
+
* every `intervalMs` (250ms default), and the dev-server middleware
|
|
65
|
+
* receives the POST + writes `<project-root>/.pyreon-lpih.json` using
|
|
66
|
+
* the atomic-rename pattern from `@pyreon/reactivity/lpih`. The LSP
|
|
67
|
+
* (`pyreon-lint --lsp`) auto-discovers that file, so the end-to-end
|
|
68
|
+
* "save file → see fire counts" loop needs ZERO user wiring.
|
|
69
|
+
*
|
|
70
|
+
* Set to `false` to opt out (e.g. if you're wiring `startLpihPolling()`
|
|
71
|
+
* yourself from a non-browser runtime, or you want LPIH off entirely).
|
|
72
|
+
* Pass an object to override the interval or the cache-file path.
|
|
73
|
+
*
|
|
74
|
+
* Build-only consumer: production builds skip injection entirely.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* pyreon({ lpih: true }) // default in dev
|
|
78
|
+
* pyreon({ lpih: false }) // opt out
|
|
79
|
+
* pyreon({ lpih: { intervalMs: 500 } }) // slower poll
|
|
80
|
+
* pyreon({ lpih: { cachePath: '/tmp/x.json' } }) // custom path
|
|
81
|
+
*/
|
|
82
|
+
lpih?: boolean | PyreonLpihOptions;
|
|
59
83
|
/**
|
|
60
84
|
* P0 — opt-in compile-time rocketstyle wrapper collapse. `true` uses
|
|
61
85
|
* the default provider/theme/mode wiring (PyreonUI + theme +
|
|
@@ -103,7 +127,109 @@ interface PyreonCollapseOptions {
|
|
|
103
127
|
source: string;
|
|
104
128
|
};
|
|
105
129
|
}
|
|
130
|
+
interface PyreonLpihOptions {
|
|
131
|
+
/**
|
|
132
|
+
* Poll interval in milliseconds. The browser-side bridge reads
|
|
133
|
+
* `getFireSummaries()` and POSTs every `intervalMs` to the dev-server
|
|
134
|
+
* middleware. Default 250ms — matches the LSP-debounce window so
|
|
135
|
+
* editor hints settle within one frame of the typical save→hint cycle.
|
|
136
|
+
*
|
|
137
|
+
* Lower values (e.g. 100ms) trade dev-server CPU for snappier hints;
|
|
138
|
+
* higher values (1000ms) reduce overhead for slow machines.
|
|
139
|
+
*/
|
|
140
|
+
intervalMs?: number;
|
|
141
|
+
/**
|
|
142
|
+
* Cache-file path override. Defaults to
|
|
143
|
+
* `<projectRoot>/.pyreon-lpih.json` — the convention the LSP auto-
|
|
144
|
+
* discovers (R2, #777). Override only if you need a non-default
|
|
145
|
+
* location (shared mount, custom workspace layout).
|
|
146
|
+
*/
|
|
147
|
+
cachePath?: string;
|
|
148
|
+
}
|
|
106
149
|
declare function pyreonPlugin(options?: PyreonPluginOptions): Plugin;
|
|
150
|
+
/**
|
|
151
|
+
* Resolve the LPIH cache-file path for a given project root. Matches the
|
|
152
|
+
* convention `@pyreon/reactivity/lpih`'s `getDefaultLpihCachePath()` uses
|
|
153
|
+
* AND the LSP auto-discovers (R2, #777): `<projectRoot>/.pyreon-lpih.json`.
|
|
154
|
+
*
|
|
155
|
+
* @internal — exported for tests.
|
|
156
|
+
*/
|
|
157
|
+
declare function resolveLpihCachePath(projectRoot: string): string;
|
|
158
|
+
/**
|
|
159
|
+
* Register the LPIH dev-server middleware on a Vite server. Extracted from
|
|
160
|
+
* `configureServer` so the `cachePath` option reference lives at module
|
|
161
|
+
* scope (top-level helper) rather than inside the plugin's inline body —
|
|
162
|
+
* keeps `scripts/audit-types.ts` happy regardless of how its comment-
|
|
163
|
+
* stripping handles the long inline `configureServer` block.
|
|
164
|
+
*
|
|
165
|
+
* @internal — exported for tests.
|
|
166
|
+
*/
|
|
167
|
+
declare function registerLpihMiddleware(server: ViteDevServer, projectRoot: string, userCfg: PyreonLpihOptions): void;
|
|
168
|
+
/**
|
|
169
|
+
* Atomically write a LPIH cache file (tmp + rename), mirroring the
|
|
170
|
+
* `@pyreon/reactivity/lpih:writeLpihCache` implementation. The payload
|
|
171
|
+
* comes pre-serialized from the browser-side bridge — we validate the
|
|
172
|
+
* outer shape (`{ fires: [...] }`) and reject malformed bodies to stop a
|
|
173
|
+
* buggy client from corrupting the file the LSP reads.
|
|
174
|
+
*
|
|
175
|
+
* @internal — exported for tests.
|
|
176
|
+
*/
|
|
177
|
+
declare function writeLpihCacheFile(path: string, body: string): Promise<void>;
|
|
178
|
+
/**
|
|
179
|
+
* Build the `<script type="module">` body injected into the HTML head.
|
|
180
|
+
* The script imports devtools activation + `getFireSummaries` from
|
|
181
|
+
* `@pyreon/reactivity`, sets up a `setInterval` that POSTs every
|
|
182
|
+
* `intervalMs` ms, and registers a `beforeunload` cleanup so the timer
|
|
183
|
+
* doesn't outlive the page.
|
|
184
|
+
*
|
|
185
|
+
* Browser bundlers serve `@pyreon/reactivity` from the workspace via
|
|
186
|
+
* Vite's normal module resolution — no virtual module needed.
|
|
187
|
+
*
|
|
188
|
+
* @internal — exported for tests.
|
|
189
|
+
*/
|
|
190
|
+
declare function buildLpihClientScript(intervalMs: number): string;
|
|
191
|
+
/**
|
|
192
|
+
* Mask string-literal / template-literal / comment regions in `code` by
|
|
193
|
+
* replacing their content with spaces. Returns a SAME-LENGTH string so
|
|
194
|
+
* regex match positions in the masked version line up with the original.
|
|
195
|
+
*
|
|
196
|
+
* Used by `injectSignalNames` to skip false-positive matches against
|
|
197
|
+
* reactive-primitive names that appear inside strings or comments. Without
|
|
198
|
+
* masking, a user's `const docs = \`effect(() => x)\`` template literal
|
|
199
|
+
* would get `, { __sourceLocation: ... }` injected INSIDE the string,
|
|
200
|
+
* corrupting runtime values.
|
|
201
|
+
*
|
|
202
|
+
* Handles:
|
|
203
|
+
* - `"..."` / `'...'` strings (escape-aware)
|
|
204
|
+
* - `` `...` `` template literals; interpolations `${...}` are KEPT as
|
|
205
|
+
* code (their content can contain real `signal()` calls worth catching)
|
|
206
|
+
* - `// ...` line comments
|
|
207
|
+
* - `/* ... *\/` block comments
|
|
208
|
+
*
|
|
209
|
+
* Regex literals (`/foo/g`) are NOT special-cased — they're rare and the
|
|
210
|
+
* downstream extractBalancedArgs handles unmatched parens by returning null.
|
|
211
|
+
*
|
|
212
|
+
* @internal — exported for tests.
|
|
213
|
+
*/
|
|
214
|
+
declare function _maskStringsAndComments(code: string): string;
|
|
215
|
+
/**
|
|
216
|
+
* Compute the 0-indexed character offset for the start of each line.
|
|
217
|
+
* `lineStarts[i]` is the offset of the FIRST character on line i+1
|
|
218
|
+
* (1-based, so `lineStarts[0]` = offset 0 = line 1).
|
|
219
|
+
*
|
|
220
|
+
* @internal — exported for tests.
|
|
221
|
+
*/
|
|
222
|
+
declare function _computeLineStarts(code: string): number[];
|
|
223
|
+
/**
|
|
224
|
+
* Convert a 0-indexed offset to `{ line: 1-based, col: 1-based }` using a
|
|
225
|
+
* pre-computed line-starts array. Binary search → O(log N) per lookup.
|
|
226
|
+
*
|
|
227
|
+
* @internal — exported for tests.
|
|
228
|
+
*/
|
|
229
|
+
declare function _offsetToLineCol(offset: number, lineStarts: number[]): {
|
|
230
|
+
line: number;
|
|
231
|
+
col: number;
|
|
232
|
+
};
|
|
107
233
|
//#endregion
|
|
108
|
-
export { CompatFramework, PyreonCollapseOptions, PyreonPluginOptions, pyreonPlugin as default };
|
|
234
|
+
export { CompatFramework, PyreonCollapseOptions, PyreonLpihOptions, PyreonPluginOptions, _computeLineStarts, _maskStringsAndComments, _offsetToLineCol, buildLpihClientScript, pyreonPlugin as default, registerLpihMiddleware, resolveLpihCachePath, writeLpihCacheFile };
|
|
109
235
|
//# sourceMappingURL=index2.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/vite-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"description": "Vite plugin for Pyreon — .pyreon SFC support, HMR, compiler integration",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/vite-plugin#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"prepublishOnly": "bun run build"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@pyreon/compiler": "^0.
|
|
46
|
+
"@pyreon/compiler": "^0.24.1"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"vite": "^8.0.0"
|