@pyreon/vite-plugin 0.24.6 → 0.25.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.
@@ -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":"4f3120a0-1"}]},{"name":"rocketstyle-collapse-C4eMAnwR.js","children":[{"name":"src/rocketstyle-collapse.ts","uid":"4f3120a0-3"}]}],"isRoot":true},"nodeParts":{"4f3120a0-1":{"renderedLength":45599,"gzipLength":14790,"brotliLength":0,"metaUid":"4f3120a0-0"},"4f3120a0-3":{"renderedLength":3424,"gzipLength":1530,"brotliLength":0,"metaUid":"4f3120a0-2"}},"nodeMetas":{"4f3120a0-0":{"id":"/src/index.ts","moduleParts":{"index.js":"4f3120a0-1"},"imported":[{"uid":"4f3120a0-4"},{"uid":"4f3120a0-5"},{"uid":"4f3120a0-6"},{"uid":"4f3120a0-2","dynamic":true},{"uid":"4f3120a0-7","dynamic":true}],"importedBy":[],"isEntry":true},"4f3120a0-2":{"id":"/src/rocketstyle-collapse.ts","moduleParts":{"rocketstyle-collapse-C4eMAnwR.js":"4f3120a0-3"},"imported":[{"uid":"4f3120a0-8","dynamic":true}],"importedBy":[{"uid":"4f3120a0-0"}]},"4f3120a0-4":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"4f3120a0-0"}]},"4f3120a0-5":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"4f3120a0-0"}]},"4f3120a0-6":{"id":"@pyreon/compiler","moduleParts":{},"imported":[],"importedBy":[{"uid":"4f3120a0-0"}]},"4f3120a0-7":{"id":"node:fs/promises","moduleParts":{},"imported":[],"importedBy":[{"uid":"4f3120a0-0"}]},"4f3120a0-8":{"id":"vite","moduleParts":{},"imported":[],"importedBy":[{"uid":"4f3120a0-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/index.ts","uid":"3bd95fb3-1"}]},{"name":"rocketstyle-collapse-DGnwgDhC.js","children":[{"name":"src/rocketstyle-collapse.ts","uid":"3bd95fb3-3"}]}],"isRoot":true},"nodeParts":{"3bd95fb3-1":{"renderedLength":48036,"gzipLength":15642,"brotliLength":0,"metaUid":"3bd95fb3-0"},"3bd95fb3-3":{"renderedLength":4657,"gzipLength":2132,"brotliLength":0,"metaUid":"3bd95fb3-2"}},"nodeMetas":{"3bd95fb3-0":{"id":"/src/index.ts","moduleParts":{"index.js":"3bd95fb3-1"},"imported":[{"uid":"3bd95fb3-4"},{"uid":"3bd95fb3-5"},{"uid":"3bd95fb3-6"},{"uid":"3bd95fb3-2","dynamic":true},{"uid":"3bd95fb3-7","dynamic":true}],"importedBy":[],"isEntry":true},"3bd95fb3-2":{"id":"/src/rocketstyle-collapse.ts","moduleParts":{"rocketstyle-collapse-DGnwgDhC.js":"3bd95fb3-3"},"imported":[{"uid":"3bd95fb3-8"},{"uid":"3bd95fb3-9","dynamic":true}],"importedBy":[{"uid":"3bd95fb3-0"}]},"3bd95fb3-4":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bd95fb3-0"}]},"3bd95fb3-5":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bd95fb3-0"}]},"3bd95fb3-6":{"id":"@pyreon/compiler","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bd95fb3-0"}]},"3bd95fb3-7":{"id":"node:fs/promises","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bd95fb3-0"}]},"3bd95fb3-8":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bd95fb3-2"}]},"3bd95fb3-9":{"id":"vite","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bd95fb3-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
@@ -40,7 +40,7 @@ const __DEV__ = typeof process !== "undefined" && process.env.NODE_ENV !== "prod
40
40
  const _countSink = globalThis;
41
41
  let _createCollapseResolver = null;
42
42
  async function loadCreateCollapseResolver() {
43
- if (!_createCollapseResolver) _createCollapseResolver = (await import("./rocketstyle-collapse-C4eMAnwR.js")).createCollapseResolver;
43
+ if (!_createCollapseResolver) _createCollapseResolver = (await import("./rocketstyle-collapse-DGnwgDhC.js")).createCollapseResolver;
44
44
  return _createCollapseResolver;
45
45
  }
46
46
  const HMR_RUNTIME_ID = "\0pyreon/hmr-runtime";
@@ -175,6 +175,56 @@ function scanPyreonDeps(root) {
175
175
  return [];
176
176
  }
177
177
  }
178
+ /**
179
+ * Walk up from `root` looking for the nearest `node_modules/@pyreon`
180
+ * directory, then return every subdirectory name as `@pyreon/<name>`.
181
+ *
182
+ * This is the TRANSITIVE @pyreon/* dependency list — direct + every
183
+ * indirect dep that any direct dep pulled in. Required for `resolve.dedupe`
184
+ * because the original `scanPyreonDeps()` reads `package.json` only and
185
+ * misses anything a direct dep transitively requires (a user with only
186
+ * `@pyreon/zero` declared transitively pulls @pyreon/core, @pyreon/router,
187
+ * @pyreon/runtime-dom, etc. — none of which appear in their package.json).
188
+ *
189
+ * Bun / npm / pnpm all create `node_modules/@pyreon/<name>` entries for
190
+ * every resolved version, so a filesystem walk gives the exact set the
191
+ * bundler will see. Returns an empty array if no `node_modules/@pyreon`
192
+ * directory is reachable (fresh project before `bun install`, etc.) —
193
+ * dedupe then has nothing to do, which is the correct degradation.
194
+ */
195
+ function scanPyreonDepsTransitive(root) {
196
+ let dir = root;
197
+ for (let i = 0; i < 10; i++) {
198
+ const candidate = join(dir, "node_modules", "@pyreon");
199
+ if (existsSync(candidate)) try {
200
+ return readdirSync(candidate, { withFileTypes: true }).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => `@pyreon/${e.name}`).sort();
201
+ } catch {
202
+ return [];
203
+ }
204
+ const parent = dirname(dir);
205
+ if (parent === dir) break;
206
+ dir = parent;
207
+ }
208
+ return [];
209
+ }
210
+ /**
211
+ * Truthy env-var parser. Accepts `1` / `true` / `yes` / `on`
212
+ * (case-insensitive). Returns `false` for `undefined`, empty string,
213
+ * `0`, `false`, `no`, `off`, or any unrecognized value.
214
+ *
215
+ * Exported via `_internal` for unit-testability — the previous strict
216
+ * `=== '1'` shape caught a user reporting `PYREON_DISABLE_DEDUPE=true`
217
+ * silently not working. Env-var hatches are usually reached for under
218
+ * stress; rejecting alternatives is exactly the wrong moment to be strict.
219
+ *
220
+ * @internal
221
+ */
222
+ function _isTruthyEnv(v) {
223
+ if (v === void 0) return false;
224
+ const lower = v.toLowerCase();
225
+ return lower === "1" || lower === "true" || lower === "yes" || lower === "on";
226
+ }
227
+ const isTruthyEnv = _isTruthyEnv;
178
228
  function pyreonPlugin(options) {
179
229
  const ssrConfig = options?.ssr;
180
230
  const compat = options?.compat;
@@ -234,13 +284,19 @@ function pyreonPlugin(options) {
234
284
  projectRoot = userConfig.root ?? process.cwd();
235
285
  const compatExclude = compat ? Object.keys(COMPAT_ALIASES[compat]) : [];
236
286
  const pyreonExclude = scanPyreonDeps(projectRoot);
287
+ const optimizeDepsExclude = Array.from(new Set([...compatExclude, ...pyreonExclude]));
288
+ const dedupeList = isTruthyEnv((typeof process !== "undefined" && process.env ? process.env : void 0)?.PYREON_DISABLE_DEDUPE) ? [] : scanPyreonDepsTransitive(projectRoot);
289
+ const jsxSource = "@pyreon/core";
237
290
  return {
238
- resolve: { conditions: ["bun"] },
291
+ resolve: {
292
+ conditions: ["bun"],
293
+ ...dedupeList.length > 0 ? { dedupe: dedupeList } : {}
294
+ },
239
295
  ssr: { noExternal: [/@pyreon\//] },
240
- optimizeDeps: { exclude: Array.from(new Set([...compatExclude, ...pyreonExclude])) },
296
+ optimizeDeps: { exclude: optimizeDepsExclude },
241
297
  oxc: { jsx: {
242
298
  runtime: "automatic",
243
- importSource: "@pyreon/core"
299
+ importSource: jsxSource
244
300
  } },
245
301
  ...env.isSsrBuild && ssrConfig ? { build: {
246
302
  ssr: true,
@@ -1243,5 +1299,5 @@ export function __hmr_dispose(moduleId) {
1243
1299
  `;
1244
1300
 
1245
1301
  //#endregion
1246
- export { _computeLineStarts, _maskStringsAndComments, _offsetToLineCol, buildLpihClientScript, pyreonPlugin as default, registerLpihMiddleware, resolveLpihCachePath, writeLpihCacheFile };
1302
+ export { _computeLineStarts, _isTruthyEnv, _maskStringsAndComments, _offsetToLineCol, buildLpihClientScript, pyreonPlugin as default, registerLpihMiddleware, resolveLpihCachePath, writeLpihCacheFile };
1247
1303
  //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["pathJoin"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @pyreon/vite-plugin — Vite integration for Pyreon framework.\n *\n * Applies Pyreon's JSX reactive transform to .tsx, .jsx, and .pyreon files,\n * and configures Vite to use Pyreon's JSX runtime.\n *\n * ## Basic usage (SPA)\n *\n * import pyreon from \"@pyreon/vite-plugin\"\n * export default { plugins: [pyreon()] }\n *\n * ## Drop-in compat mode (zero code changes)\n *\n * import pyreon from \"@pyreon/vite-plugin\"\n * export default { plugins: [pyreon({ compat: \"react\" })] }\n *\n * Aliases `react`, `react-dom`, `vue`, `solid-js`, or `preact` imports to\n * Pyreon's compat packages — existing code works without changing imports.\n *\n * ## SSR mode\n *\n * import pyreon from \"@pyreon/vite-plugin\"\n * export default { plugins: [pyreon({ ssr: { entry: \"./src/entry-server.ts\" } })] }\n *\n * In SSR mode, the plugin adds dev server middleware that:\n * 1. Loads your server entry via Vite's `ssrLoadModule`\n * 2. Calls the exported `handler` or default export (Request → Response)\n * 3. Returns the SSR'd HTML for every non-asset request\n *\n * For production, build separately:\n * vite build # client bundle\n * vite build --ssr src/entry-server.ts --outDir dist/server # server bundle\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'\nimport { dirname, join as pathJoin } from 'node:path'\nimport {\n type CollapsibleSite,\n generateContext,\n scanCollapsibleSites,\n transformDeferInline,\n transformJSX,\n} from '@pyreon/compiler'\nimport type { CollapseResolver } from './rocketstyle-collapse'\nimport type { Plugin, ViteDevServer } from 'vite'\n\n// Dev-mode counter sink — see packages/internals/perf-harness for contract.\nconst __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'\nconst _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }\n\n// Lazy — the resolver module (and its `vite` SSR machinery) must NOT be\n// on the static import path of this cheap entry. It loads ONLY when\n// `pyreon({ collapse })` is enabled AND a collapsible site is scanned;\n// collapse-off consumers never pull it (bundle-budget + cold-load).\nlet _createCollapseResolver:\n | ((root: string) => Promise<CollapseResolver>)\n | null = null\nasync function loadCreateCollapseResolver(): Promise<\n (root: string) => Promise<CollapseResolver>\n> {\n if (!_createCollapseResolver) {\n _createCollapseResolver = (await import('./rocketstyle-collapse')).createCollapseResolver\n }\n return _createCollapseResolver\n}\n\n// Virtual module ID for the HMR runtime\nconst HMR_RUNTIME_ID = '\\0pyreon/hmr-runtime'\nconst HMR_RUNTIME_IMPORT = 'virtual:pyreon/hmr-runtime'\n\n// Virtual module ID for the auto-generated islands registry. See\n// `prescanIslandDeclarations` + the `load` hook for emit shape. Consumed by\n// `hydrateIslandsAuto()` in `@pyreon/server/client`.\nconst ISLANDS_REGISTRY_ID = '\\0pyreon/islands-registry'\nconst ISLANDS_REGISTRY_IMPORT = 'virtual:pyreon/islands-registry'\n\nexport type CompatFramework = 'react' | 'preact' | 'vue' | 'solid' | 'svelte'\n\nexport interface PyreonPluginOptions {\n /**\n * Alias imports from an existing framework to Pyreon's compat layer.\n *\n * This lets you drop Pyreon into an existing project with zero code changes —\n * `import { useState } from \"react\"` will resolve to `@pyreon/react-compat`.\n *\n * @example\n * pyreon({ compat: \"react\" }) // react + react-dom → @pyreon/react-compat\n * pyreon({ compat: \"vue\" }) // vue → @pyreon/vue-compat\n * pyreon({ compat: \"solid\" }) // solid-js → @pyreon/solid-compat\n * pyreon({ compat: \"svelte\" }) // svelte + svelte/store → @pyreon/svelte-compat\n * pyreon({ compat: \"preact\" }) // preact + hooks + signals → @pyreon/preact-compat\n */\n compat?: CompatFramework\n\n /**\n * Enable SSR dev middleware.\n *\n * Pass an object with `entry` pointing to your server entry file.\n * The entry must export a `handler` function: `(req: Request) => Promise<Response>`\n * or a default export of the same type.\n *\n * @example\n * pyreonPlugin({ ssr: { entry: \"./src/entry-server.ts\" } })\n */\n ssr?: {\n /** Server entry file path (e.g. \"./src/entry-server.ts\") */\n entry: string\n }\n\n /**\n * Auto-discover `island()` declarations and expose them as\n * `virtual:pyreon/islands-registry` for `hydrateIslandsAuto()` in\n * `@pyreon/server/client`.\n *\n * Eliminates the manual sync between `island()` declarations and the\n * client-side `hydrateIslands({ ... })` registry — typo / forgotten entry /\n * registry drift is the #1 author foot-gun for islands.\n *\n * Defaults to `true`. The prescan is cheap (regex over the same files\n * already walked by `prescanSignalExports`); set to `false` only if you\n * have a reason not to support `hydrateIslandsAuto()`.\n *\n * `hydrate: 'never'` islands are deliberately OMITTED from the auto-\n * registry — the whole point of the strategy is shipping zero client JS,\n * so registering a loader (which would pull the component module into the\n * client bundle graph) defeats it.\n *\n * @example\n * pyreon({ islands: true })\n *\n * // src/entry-client.ts\n * import { hydrateIslandsAuto } from '@pyreon/server/client'\n * hydrateIslandsAuto()\n */\n islands?: boolean\n\n /**\n * **LPIH auto-bridge** — zero-config Live Program Inlay Hints in dev.\n *\n * When `true` (the default in dev), the plugin auto-wires the LPIH\n * cache file: the browser-side activates devtools + polls fire data\n * every `intervalMs` (250ms default), and the dev-server middleware\n * receives the POST + writes `<project-root>/.pyreon-lpih.json` using\n * the atomic-rename pattern from `@pyreon/reactivity/lpih`. The LSP\n * (`pyreon-lint --lsp`) auto-discovers that file, so the end-to-end\n * \"save file → see fire counts\" loop needs ZERO user wiring.\n *\n * Set to `false` to opt out (e.g. if you're wiring `startLpihPolling()`\n * yourself from a non-browser runtime, or you want LPIH off entirely).\n * Pass an object to override the interval or the cache-file path.\n *\n * Build-only consumer: production builds skip injection entirely.\n *\n * @example\n * pyreon({ lpih: true }) // default in dev\n * pyreon({ lpih: false }) // opt out\n * pyreon({ lpih: { intervalMs: 500 } }) // slower poll\n * pyreon({ lpih: { cachePath: '/tmp/x.json' } }) // custom path\n */\n lpih?: boolean | PyreonLpihOptions\n\n /**\n * P0 — opt-in compile-time rocketstyle wrapper collapse. `true` uses\n * the default provider/theme/mode wiring (PyreonUI + theme +\n * useMode from @pyreon/ui-core / @pyreon/ui-theme). Pass an object to\n * override. OFF by default (zero behaviour change). When on, the\n * plugin SSR-resolves every literal-prop call site of a candidate\n * component (real component, light + dark) and the compiler collapses\n * the 5-layer wrapper mount into a single `_rsCollapse` cloneNode.\n * Only the CLIENT graph is collapsed — the SSR graph keeps the normal\n * mount (and the resolver itself uses SSR render).\n *\n * @example pyreon({ collapse: true })\n * @example pyreon({ collapse: { components: ['Button', 'Badge'] } })\n */\n collapse?: boolean | PyreonCollapseOptions\n}\n\nexport interface PyreonCollapseOptions {\n /**\n * Import sources whose components may collapse. Default:\n * `['@pyreon/ui-components']`. The compiler's AST scan only considers\n * a call site whose component was imported from one of these sources;\n * the conservative bail catalogue + the SSR resolver are the real\n * gate beyond that.\n */\n sources?: string[]\n /**\n * Optional local-name allowlist applied AFTER the source scan\n * (e.g. `['Button']`). Omit to collapse every collapsible component\n * from the configured sources.\n */\n components?: string[]\n /** Override the theme/mode provider. Default PyreonUI@@pyreon/ui-core. */\n provider?: { name: string; source: string }\n /** Override the theme object. Default theme@@pyreon/ui-theme. */\n theme?: { name: string; source: string }\n /** Override the live mode accessor. Default useMode@@pyreon/ui-core. */\n mode?: { name: string; source: string }\n}\n\nexport interface PyreonLpihOptions {\n /**\n * Poll interval in milliseconds. The browser-side bridge reads\n * `getFireSummaries()` and POSTs every `intervalMs` to the dev-server\n * middleware. Default 250ms — matches the LSP-debounce window so\n * editor hints settle within one frame of the typical save→hint cycle.\n *\n * Lower values (e.g. 100ms) trade dev-server CPU for snappier hints;\n * higher values (1000ms) reduce overhead for slow machines.\n */\n intervalMs?: number\n /**\n * Cache-file path override. Defaults to\n * `<projectRoot>/.pyreon-lpih.json` — the convention the LSP auto-\n * discovers (R2, #777). Override only if you need a non-default\n * location (shared mount, custom workspace layout).\n */\n cachePath?: string\n}\n\n// ── Compat alias maps ─────────────────────────────────────────────────────────\n\nconst COMPAT_ALIASES: Record<CompatFramework, Record<string, string>> = {\n react: {\n react: '@pyreon/react-compat',\n 'react/jsx-runtime': '@pyreon/react-compat/jsx-runtime',\n 'react/jsx-dev-runtime': '@pyreon/react-compat/jsx-runtime',\n 'react-dom': '@pyreon/react-compat/dom',\n 'react-dom/client': '@pyreon/react-compat/dom',\n },\n preact: {\n preact: '@pyreon/preact-compat',\n 'preact/hooks': '@pyreon/preact-compat/hooks',\n 'preact/jsx-runtime': '@pyreon/preact-compat/jsx-runtime',\n 'preact/jsx-dev-runtime': '@pyreon/preact-compat/jsx-runtime',\n '@preact/signals': '@pyreon/preact-compat/signals',\n },\n vue: {\n vue: '@pyreon/vue-compat',\n 'vue/jsx-runtime': '@pyreon/vue-compat/jsx-runtime',\n 'vue/jsx-dev-runtime': '@pyreon/vue-compat/jsx-runtime',\n },\n solid: {\n 'solid-js': '@pyreon/solid-compat',\n 'solid-js/jsx-runtime': '@pyreon/solid-compat/jsx-runtime',\n 'solid-js/jsx-dev-runtime': '@pyreon/solid-compat/jsx-runtime',\n },\n svelte: {\n svelte: '@pyreon/svelte-compat',\n 'svelte/store': '@pyreon/svelte-compat/store',\n 'svelte/internal': '@pyreon/svelte-compat',\n 'svelte/jsx-runtime': '@pyreon/svelte-compat/jsx-runtime',\n 'svelte/jsx-dev-runtime': '@pyreon/svelte-compat/jsx-runtime',\n },\n}\n\n/**\n * Detect whether a file id resolves to a `@pyreon/*` framework-package source\n * (i.e. a published Pyreon package whose .tsx is being pulled in via the\n * `bun` condition workspace-link, NOT user code, NOT an example app).\n *\n * Why this exists: in compat mode, OXC's per-project `importSource` is set\n * to `@pyreon/core` and the resolveId hook redirects `@pyreon/core/jsx-runtime`\n * to the compat package. That's correct for user code (the whole point of\n * compat mode) but WRONG for framework-internal sources like\n * `@pyreon/zero/src/link.tsx`, which need the real `@pyreon/core` runtime.\n * The fix skips the redirect when the importer is a `@pyreon/*` framework\n * file. Result: published-package consumers (where `@pyreon/zero` resolves\n * to its pre-built `lib/`) and workspace-dev consumers (where it resolves\n * to source) both get correct JSX runtime resolution.\n *\n * Detection heuristic: walk to nearest `package.json`, require BOTH:\n * 1. `name` starts with `@pyreon/` (workspace member of the @pyreon scope)\n * 2. file path contains `/packages/` AND NOT `/examples/`\n *\n * Step 2 excludes the existing `@pyreon/example-{react,vue,solid,preact}-compat`\n * apps under `examples/`. Without it, user code in those apps would skip the\n * compat-mode JSX-runtime redirect and import `@pyreon/core/jsx-runtime`\n * directly — breaking the React/Vue/Solid/Preact compat layer's contract.\n *\n * Result cached per directory. The `/packages/` + `/examples/` check is a\n * structural property of the monorepo (workspace layout), not the package\n * name — so it's robust against renames.\n */\nfunction isPyreonWorkspaceFile(id: string, cache: Map<string, boolean>): boolean {\n // Strip query strings (e.g. `?vue&type=script`) to get the bare path.\n const queryIdx = id.indexOf('?')\n const filePath = queryIdx === -1 ? id : id.slice(0, queryIdx)\n if (!filePath || filePath[0] === '\\0') return false\n\n // Path-based filter first (cheap): file must live under `<root>/packages/`\n // and not under `<root>/examples/`. This excludes example apps even when\n // they have `@pyreon/example-*` names.\n if (!filePath.includes('/packages/') || filePath.includes('/examples/')) {\n return false\n }\n\n let dir = dirname(filePath)\n // Walk up at most ~12 levels — enough for any realistic monorepo depth.\n for (let i = 0; i < 12; i++) {\n const cached = cache.get(dir)\n if (cached !== undefined) return cached\n\n const pkgPath = pathJoin(dir, 'package.json')\n if (existsSync(pkgPath)) {\n let isPyreon = false\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { name?: string }\n isPyreon = typeof pkg.name === 'string' && pkg.name.startsWith('@pyreon/')\n } catch {\n // Malformed package.json — treat as not-pyreon.\n }\n cache.set(dir, isPyreon)\n return isPyreon\n }\n\n const parent = dirname(dir)\n if (parent === dir) break // reached filesystem root\n dir = parent\n }\n return false\n}\n\n/**\n * Return the Pyreon compat target for an import specifier, or undefined if\n * the import should not be redirected.\n */\nfunction getCompatTarget(compat: CompatFramework | undefined, id: string): string | undefined {\n if (!compat) return undefined\n const aliased = COMPAT_ALIASES[compat][id]\n if (aliased) return aliased\n // OXC's JSX transform reads jsxImportSource from tsconfig (@pyreon/core),\n // not from our plugin config. Redirect JSX runtime imports in compat mode.\n if (id === '@pyreon/core/jsx-runtime' || id === '@pyreon/core/jsx-dev-runtime') {\n if (compat === 'react') return '@pyreon/react-compat/jsx-runtime'\n if (compat === 'preact') return '@pyreon/preact-compat/jsx-runtime'\n if (compat === 'vue') return '@pyreon/vue-compat/jsx-runtime'\n if (compat === 'solid') return '@pyreon/solid-compat/jsx-runtime'\n if (compat === 'svelte') return '@pyreon/svelte-compat/jsx-runtime'\n }\n return undefined\n}\n\n/**\n * Scan the consumer's package.json for `@pyreon/*` deps. Result is the\n * list of names to exclude from Vite's deps optimizer (avoids\n * `.vite/deps/@pyreon_*.js: File does not exist` runtime errors caused\n * by esbuild trying to pre-bundle TypeScript source files exposed via\n * the `bun` resolve condition).\n *\n * Reads dependencies + devDependencies + peerDependencies. Best-effort:\n * missing/malformed package.json returns an empty list so a typo in\n * the consumer's manifest doesn't break the build.\n */\nfunction scanPyreonDeps(root: string): string[] {\n const pkgPath = pathJoin(root, 'package.json')\n if (!existsSync(pkgPath)) return []\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n peerDependencies?: Record<string, string>\n }\n const all = {\n ...pkg.dependencies,\n ...pkg.devDependencies,\n ...pkg.peerDependencies,\n }\n return Object.keys(all).filter((name) => name.startsWith('@pyreon/'))\n } catch {\n return []\n }\n}\n\n/**\n * Walk up from `root` looking for the nearest `node_modules/@pyreon`\n * directory, then return every subdirectory name as `@pyreon/<name>`.\n *\n * This is the TRANSITIVE @pyreon/* dependency list — direct + every\n * indirect dep that any direct dep pulled in. Required for `resolve.dedupe`\n * because the original `scanPyreonDeps()` reads `package.json` only and\n * misses anything a direct dep transitively requires (a user with only\n * `@pyreon/zero` declared transitively pulls @pyreon/core, @pyreon/router,\n * @pyreon/runtime-dom, etc. — none of which appear in their package.json).\n *\n * Bun / npm / pnpm all create `node_modules/@pyreon/<name>` entries for\n * every resolved version, so a filesystem walk gives the exact set the\n * bundler will see. Returns an empty array if no `node_modules/@pyreon`\n * directory is reachable (fresh project before `bun install`, etc.) —\n * dedupe then has nothing to do, which is the correct degradation.\n */\nfunction scanPyreonDepsTransitive(root: string): string[] {\n let dir = root\n for (let i = 0; i < 10; i++) {\n const candidate = pathJoin(dir, 'node_modules', '@pyreon')\n if (existsSync(candidate)) {\n try {\n const entries = readdirSync(candidate, { withFileTypes: true })\n return entries\n .filter((e) => e.isDirectory() || e.isSymbolicLink())\n .map((e) => `@pyreon/${e.name}`)\n .sort()\n } catch {\n return []\n }\n }\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n return []\n}\n\n/**\n * Truthy env-var parser. Accepts `1` / `true` / `yes` / `on`\n * (case-insensitive). Returns `false` for `undefined`, empty string,\n * `0`, `false`, `no`, `off`, or any unrecognized value.\n *\n * Exported via `_internal` for unit-testability — the previous strict\n * `=== '1'` shape caught a user reporting `PYREON_DISABLE_DEDUPE=true`\n * silently not working. Env-var hatches are usually reached for under\n * stress; rejecting alternatives is exactly the wrong moment to be strict.\n *\n * @internal\n */\nexport function _isTruthyEnv(v: string | undefined): boolean {\n if (v === undefined) return false\n const lower = v.toLowerCase()\n return lower === '1' || lower === 'true' || lower === 'yes' || lower === 'on'\n}\nconst isTruthyEnv = _isTruthyEnv\n\nexport default function pyreonPlugin(options?: PyreonPluginOptions): Plugin {\n const ssrConfig = options?.ssr\n const compat = options?.compat\n // Default islands support to enabled — the prescan is cheap and the virtual\n // module is harmless if the user has no `island()` calls. Opt out only if\n // you have a specific reason.\n const islandsEnabled = options?.islands !== false\n\n // ── LPIH auto-bridge config ──────────────────────────────────────────────\n // Default `true` (zero-config Live Program Inlay Hints in dev). Set to\n // `false` to opt out. Object form overrides interval / cache path.\n const lpihOpt = options?.lpih\n const lpihEnabled = lpihOpt !== false\n const lpihUserCfg: PyreonLpihOptions =\n lpihOpt && lpihOpt !== true ? lpihOpt : {}\n const lpihIntervalMs = lpihUserCfg.intervalMs ?? 250\n\n // ── P0 rocketstyle-collapse config (opt-in) ───────────────────────────────\n const collapseOpt = options?.collapse\n const collapseEnabled = collapseOpt === true || (collapseOpt != null && collapseOpt !== false)\n const collapseUserCfg: PyreonCollapseOptions =\n collapseOpt && collapseOpt !== true ? collapseOpt : {}\n const collapseProvider = collapseUserCfg.provider ?? {\n name: 'PyreonUI',\n source: '@pyreon/ui-core',\n }\n const collapseTheme = collapseUserCfg.theme ?? { name: 'theme', source: '@pyreon/ui-theme' }\n const collapseMode = collapseUserCfg.mode ?? { name: 'useMode', source: '@pyreon/ui-core' }\n const collapseSources = new Set(collapseUserCfg.sources ?? ['@pyreon/ui-components'])\n const collapseComponentFilter = collapseUserCfg.components\n ? (n: string) => collapseUserCfg.components!.includes(n)\n : null\n // Lazily created on first client-graph transform; one Vite SSR server\n // reused for every resolve in the build. Disposed in closeBundle.\n let collapseResolver: import('./rocketstyle-collapse').CollapseResolver | null = null\n let collapseResolverInit: Promise<\n import('./rocketstyle-collapse').CollapseResolver | null\n > | null = null\n\n /**\n * Lazily spin ONE programmatic Vite SSR server (bound to the project's\n * own vite config) the first time a client-graph module actually has a\n * collapsible call site. Memoized via `collapseResolverInit` so\n * concurrent transforms share the single server. Returns null if the\n * server fails to start (graceful — every call site then keeps its\n * normal rocketstyle mount).\n */\n function ensureCollapseResolver(): Promise<\n import('./rocketstyle-collapse').CollapseResolver | null\n > {\n if (collapseResolver) return Promise.resolve(collapseResolver)\n if (collapseResolverInit) return collapseResolverInit\n collapseResolverInit = loadCreateCollapseResolver()\n .then((create) => create(projectRoot))\n .then((r) => {\n collapseResolver = r\n return r\n })\n .catch(() => null)\n return collapseResolverInit\n }\n\n let isBuild = false\n // Collapse is build-only by design: the resolver computes each site's\n // class from a SEPARATE nested Vite SSR server's module graph and caches\n // it. In dev that frozen class would NOT react to the user's theme-source\n // HMR edits — strictly worse than the normal mount, which IS reactive.\n // So dev keeps the normal mount; we surface that ONCE so an opted-in\n // consumer running `vite dev` isn't left wondering why nothing collapsed.\n let warnedDevCollapse = false\n let projectRoot = ''\n\n // ── Cross-module signal export registry ─────────────────────────────────\n // Tracks which modules export signal() declarations so imported signals\n // can be auto-called in JSX across file boundaries.\n // Key: normalized module ID, Value: set of exported signal names\n const signalExportRegistry = new Map<string, Set<string>>()\n // Cache resolved import specifiers to avoid redundant resolution calls\n const resolveCache = new Map<string, string | null>()\n // Cache `isPyreonWorkspaceFile` lookups by directory — package.json reads\n // happen at most once per containing directory across the build.\n const pyreonWorkspaceDirCache = new Map<string, boolean>()\n\n // ── Island declaration registry ─────────────────────────────────────────\n // Tracks every `island(() => import('PATH'), { name: 'X', hydrate: 'Y' })`\n // call across the source tree. Keyed by absolute source-file path of the\n // declaration site so HMR can invalidate per-file. Each entry's loader path\n // is resolved relative to the file where the call was written.\n const islandRegistry = new Map<string, IslandDecl[]>()\n\n return {\n name: 'pyreon',\n enforce: 'pre',\n\n config(userConfig, env) {\n isBuild = env.command === 'build'\n // Capture the project root for package resolution in resolveId\n projectRoot = userConfig.root ?? process.cwd()\n\n // Tell Vite's dep scanner not to pre-bundle the aliased framework imports —\n // they resolve to workspace packages via our resolveId hook, not node_modules.\n const compatExclude = compat ? Object.keys(COMPAT_ALIASES[compat]) : []\n // Auto-detect `@pyreon/*` deps in the consumer's package.json and add\n // them to optimizeDeps.exclude. Vite's deps optimizer pre-bundles\n // node_modules deps via esbuild, but the plugin's `bun` resolve\n // condition redirects every `@pyreon/*` import to source `.ts(x)`\n // files. Esbuild's pre-bundler can't process raw TS source from a\n // published package and silently produces broken bundles in\n // `.vite/deps/`, surfacing as `File does not exist at\n // .../node_modules/.vite/deps/@pyreon_styler.js` errors at runtime.\n // Excluding them sidesteps the optimizer entirely — they're resolved\n // on demand via the plugin's resolveId hook + Vite's normal source\n // pipeline. Workspace-linked apps in this monorepo aren't affected\n // because Vite never tries to pre-bundle workspace deps.\n const pyreonExclude = scanPyreonDeps(projectRoot)\n const optimizeDepsExclude = Array.from(\n new Set([...compatExclude, ...pyreonExclude]),\n )\n\n // Transitive @pyreon/* dedupe — default-on. Eliminates the dual-load\n // bug class at the bundler layer by forcing every @pyreon/* import to\n // resolve to ONE copy regardless of how the import chain looks (direct\n // vs transitive, hoisted vs nested). The singleton sentinel\n // (@pyreon/reactivity:registerSingleton, default-on per PR A) is the\n // detection layer for any case this misses. Together they form the\n // defense-in-depth — bundler PREVENTS, sentinel DETECTS.\n //\n // scanPyreonDeps() reads the consumer's direct package.json only and\n // therefore misses anything a direct dep transitively pulls in (a user\n // with only @pyreon/zero declared transitively pulls @pyreon/core,\n // @pyreon/router, @pyreon/runtime-dom — none of which appear in their\n // package.json). scanPyreonDepsTransitive() walks node_modules to\n // capture the full set.\n //\n // Escape hatch: PYREON_DISABLE_DEDUPE turns the injection off — rare\n // (browser extensions / micro-frontends that legitimately dual-load).\n // Accept any truthy string: `1`, `true`, `yes`, `on` (case-insensitive).\n // Users reach for env-var escape hatches under stress; rejecting `true`\n // because it isn't literal `'1'` is exactly the wrong moment to be\n // strict.\n const procEnv =\n typeof process !== 'undefined' && process.env\n ? (process.env as unknown as Record<string, string | undefined>)\n : undefined\n const dedupeDisabled = isTruthyEnv(procEnv?.PYREON_DISABLE_DEDUPE)\n const dedupeList = dedupeDisabled ? [] : scanPyreonDepsTransitive(projectRoot)\n\n // Always set OXC's JSX importSource to `@pyreon/core`. In compat mode,\n // we redirect `@pyreon/core/jsx-runtime` imports to the compat package\n // VIA `resolveId` — but ONLY for user code, never for `@pyreon/*`\n // workspace-package files (zero, router, runtime-dom, etc.). Setting\n // OXC's importSource directly to the compat package would force the\n // compat runtime on framework internals too, which they cannot handle.\n const jsxSource = '@pyreon/core'\n\n return {\n // Use \"bun\" condition for workspace resolution — source .ts/.tsx files\n // for HMR, fast refresh, and type-safe imports. `dedupe` forces every\n // @pyreon/* import to resolve to ONE copy across the module graph.\n resolve: {\n conditions: ['bun'],\n ...(dedupeList.length > 0 ? { dedupe: dedupeList } : {}),\n },\n // Force every `@pyreon/*` package through Vite's transform pipeline\n // for SSR. Without this, Vite externalizes some `@pyreon/*` packages\n // (loads via Node's `import()`) while transforming others — producing\n // TWO module instances of `@pyreon/core` (one at `lib/index.js`, one\n // at `src/index.ts` via the `bun` condition). The two instances have\n // SEPARATE `_current` lifecycle state, so `runWithHooks` sets\n // `_current` on instance A while `provide()` reads `_current` from\n // instance B → null → `provide() outside setup` warning storm.\n //\n // Real-app symptom (bokisch.com dev-404 SSR, 0.24.4): 17 spurious\n // `[Pyreon] onUnmount() called outside component setup` warnings\n // per unmatched URL hit, even though every `provide()` IS structurally\n // inside a `runWithHooks` setup window. Fix is purely a Vite\n // module-graph reconciliation; no runtime behavior change.\n //\n // The regex `/@pyreon\\//` matches every framework package + every\n // user-side `@pyreon/*` import. Internal `@pyreon/*` resolution\n // chains (zero → runtime-server → core; user `_layout.tsx` →\n // ui-core → core) all converge on the same module instance.\n ssr: {\n noExternal: [/@pyreon\\//],\n },\n optimizeDeps: {\n exclude: optimizeDepsExclude,\n },\n // Vite 8 uses oxc for JSX transform (not esbuildOptions or rolldownOptions)\n oxc: {\n jsx: {\n runtime: 'automatic',\n importSource: jsxSource,\n },\n },\n // In SSR build mode, configure the entry\n ...(env.isSsrBuild && ssrConfig\n ? {\n build: {\n ssr: true,\n rollupOptions: {\n input: ssrConfig.entry,\n },\n },\n }\n : {}),\n }\n },\n\n // ── Pre-scan all source files for signal exports ──────────────────────\n async buildStart() {\n // Pre-scan all source files for signal exports so the registry\n // is complete before any transforms run. This solves the build\n // ordering problem where component.tsx is transformed before\n // store.ts — without pre-scanning, the registry would be empty.\n await prescanSignalExports(projectRoot, signalExportRegistry)\n\n // Mirror prescan for `island()` declarations. The result populates\n // `virtual:pyreon/islands-registry`, consumed by `hydrateIslandsAuto()`\n // in `@pyreon/server/client`. Eliminates the manual sync between\n // `island()` source-of-truth and the client `hydrateIslands({ ... })`\n // call — the #1 author foot-gun for islands.\n if (islandsEnabled) {\n await prescanIslandDeclarations(projectRoot, islandRegistry)\n }\n },\n\n // @internal — debug accessor for tests; returns live references to\n // the per-instance caches so `cache-eviction-on-delete.test.ts` can\n // assert on contents. Symbol.for-keyed so it's not part of the\n // plugin's documented surface but stays stable across reloads.\n [Symbol.for('pyreon/vite-plugin:caches')]: {\n signalExportRegistry,\n resolveCache,\n pyreonWorkspaceDirCache,\n islandRegistry,\n },\n\n // ── Cache invalidation on file delete (long-running `vite dev`) ─────\n // Vite's `watchChange` hook fires on filesystem events for files in\n // the watched module graph. Without this, the four per-instance\n // caches (`signalExportRegistry`, `resolveCache`, `islandRegistry`,\n // `pyreonWorkspaceDirCache`) accumulated stale entries for the\n // entire lifetime of the dev server — a long `vite dev` session\n // that edited / renamed / deleted source files would grow each\n // cache by one entry per dead file. Bounded by total source tree\n // size in practice, but a real leak over hours of editing.\n //\n // `'create' | 'update'` events are handled implicitly by the\n // existing transform-time `scanSignalExports` /\n // `scanIslandDeclarations` calls — they re-populate the registry\n // every time a file's `transform` hook fires, overwriting any\n // stale entry. So watchChange only needs to handle `'delete'`.\n watchChange(id: string, change: { event: 'create' | 'update' | 'delete' }) {\n if (change.event !== 'delete') return\n\n // Leak-class C diagnostic — emit per handled delete event. Bounded\n // by file-deletion count in a dev session; should grow strictly\n // monotonically with developer edit activity. Zero in a session\n // with known deletes = the watchChange hook regressed (and the\n // 4 per-instance caches will leak again).\n if (__DEV__) _countSink.__pyreon_count__?.('vite-plugin.watchChange.delete')\n\n const normalized = normalizeModuleId(id)\n\n // 1) signalExportRegistry — keyed by normalized module id.\n signalExportRegistry.delete(normalized)\n\n // 2) islandRegistry — keyed by absolute source path of the\n // declaration site (the original `id`, not normalized).\n islandRegistry.delete(id)\n // Also try the normalized form just in case the registry was\n // populated with a slightly different shape.\n if (normalized !== id) islandRegistry.delete(normalized)\n\n // 3) resolveCache — keyed by `${importer}::${source}` where\n // `importer` is normalized AND values can be the deleted\n // file's resolved path. Sweep both directions:\n // a) entries WHERE the deleted file is the importer (this\n // file's resolved imports are no longer relevant).\n // b) entries WHERE the deleted file is the resolved value\n // (other files importing the deleted file need to\n // re-resolve so they see `null` next time).\n const importerPrefix = `${normalized}::`\n for (const [key, value] of resolveCache) {\n if (key.startsWith(importerPrefix) || value === normalized) {\n resolveCache.delete(key)\n }\n }\n\n // 4) pyreonWorkspaceDirCache — keyed by DIRECTORY, not file. A\n // single file deletion doesn't invalidate the directory's\n // workspace status (other files may still live there), so\n // this cache stays. Bounded by source-tree directory count\n // in any case (small + finite).\n },\n\n // Tear down the one programmatic Vite SSR server the collapse\n // resolver holds (created lazily on first client-graph transform).\n async closeBundle() {\n if (collapseResolver) {\n await collapseResolver.dispose()\n collapseResolver = null\n collapseResolverInit = null\n }\n },\n\n // ── Virtual module + compat alias resolution ─────────────────────────────\n async resolveId(id, importer) {\n if (id === HMR_RUNTIME_IMPORT) return HMR_RUNTIME_ID\n if (id === ISLANDS_REGISTRY_IMPORT) return ISLANDS_REGISTRY_ID\n\n // `@pyreon/core/jsx-runtime` resolves to the compat package only for\n // user code — never for `@pyreon/*` framework files (zero, router,\n // runtime-dom, etc.). Without this importer guard, every JSX file in\n // the build (including framework internals resolved via the `bun`\n // workspace condition) would get redirected to a compat runtime that\n // doesn't match the framework's JSX shape. Caught by `cpa-smoke-app-*-compat`.\n if (\n compat &&\n (id === '@pyreon/core/jsx-runtime' || id === '@pyreon/core/jsx-dev-runtime') &&\n importer &&\n isPyreonWorkspaceFile(importer, pyreonWorkspaceDirCache)\n ) {\n return // let Vite resolve to the real `@pyreon/core/jsx-runtime`\n }\n\n const target = getCompatTarget(compat, id)\n if (!target) return\n\n // Vite 8 resolves the \"bun\" condition natively via resolve.conditions.\n // Delegate to Vite's resolver instead of manual package.json parsing.\n const resolved = await this.resolve(target, importer, { skipSelf: true })\n return resolved?.id\n },\n\n load(id) {\n if (id === HMR_RUNTIME_ID) {\n return HMR_RUNTIME_SOURCE\n }\n if (id === ISLANDS_REGISTRY_ID) {\n return renderIslandsRegistry(islandRegistry, islandsEnabled)\n }\n },\n\n async transform(code, id, transformOptions) {\n const ext = getExt(id)\n if (ext !== '.tsx' && ext !== '.jsx' && ext !== '.pyreon') return\n\n // In compat mode, skip Pyreon's reactive JSX transform but apply\n // attribute renames (className → class, htmlFor → for) so source code\n // that uses React-style attribute names works correctly.\n if (\n compat === 'react' ||\n compat === 'preact' ||\n compat === 'vue' ||\n compat === 'solid' ||\n compat === 'svelte'\n ) {\n if (compat === 'react' || compat === 'preact') {\n const transformed = transformCompatAttributes(code)\n if (transformed !== code) return { code: transformed, map: null }\n }\n return\n }\n\n // ── Scan for exported signal declarations (populate registry) ──────\n // This runs on every .tsx/.jsx file so the registry is built\n // incrementally. buildStart pre-scans all files, but this handles\n // files created/modified after buildStart (dev mode HMR).\n scanSignalExports(code, normalizeModuleId(id), signalExportRegistry)\n\n // ── Same incremental update for island() declarations ──────────────\n // HMR: when a user adds/renames/removes an island() call, the\n // virtual:pyreon/islands-registry module needs to reflect it on the\n // next dev-server module reload.\n if (islandsEnabled) scanIslandDeclarations(code, id, islandRegistry)\n\n // ── Inline-Defer pre-pass ──────────────────────────────────────────\n // Rewrites `<Defer when={x}><Modal /></Defer>` into the explicit\n // chunk-prop form so Rolldown emits a proper per-Defer chunk and\n // the main bundle drops the static `import { Modal } from ...`\n // when it's exclusively used inside this Defer's subtree. Runs\n // BEFORE the JSX→runtime transform so the downstream pipeline\n // sees an already-explicit `<Defer chunk={...}>` shape with no\n // special-casing needed in `transformJSX`. See\n // `@pyreon/compiler/defer-inline` for the rewrite contract.\n const deferResult = transformDeferInline(code, id)\n const sourceForJsx = deferResult.changed ? deferResult.code : code\n for (const w of deferResult.warnings) {\n this.warn(`${w.message} (${id}:${w.line}:${w.column})`)\n }\n\n // ── Resolve imported signals from the registry ─────────────────────\n // Check each import in this file: if the imported module has signal\n // exports in the registry, pass them as knownSignals to the compiler.\n const knownSignals = await resolveImportedSignals(sourceForJsx, id, signalExportRegistry, this, resolveCache)\n\n // Vite passes `ssr: true` when transforming for the SSR module graph\n // (both build --ssr and dev `ssrLoadModule`). The compiler emits plain\n // `h()` calls in that mode so `runtime-server` can render to a string.\n const isSsr = transformOptions?.ssr === true\n\n // ── P0 rocketstyle-collapse (opt-in, CLIENT graph only) ────────────\n // Never collapse the SSR graph: renderToString needs the real\n // VNode tree, AND the resolver itself SSR-renders the component —\n // collapsing the SSR graph would be circular. Resolve every\n // scanned literal-prop site once (real component, light + dark)\n // and hand the compiler a key→emission map; the compiler's AST\n // bail catalogue is the real gate, an unresolved key just falls\n // back to the normal mount.\n let collapseRocketstyle:\n | NonNullable<Parameters<typeof transformJSX>[2]>['collapseRocketstyle']\n | undefined\n if (collapseEnabled && !isBuild && !isSsr && !warnedDevCollapse) {\n warnedDevCollapse = true\n this.info(\n '[Pyreon] collapse is build-only — `vite dev` keeps the normal rocketstyle mount so theme-source edits stay HMR-reactive. Production `vite build` collapses the literal-prop sites.',\n )\n }\n if (collapseEnabled && isBuild && !isSsr) {\n const scanned: CollapsibleSite[] = scanCollapsibleSites(\n sourceForJsx,\n id,\n collapseSources,\n ).filter((s) => !collapseComponentFilter || collapseComponentFilter(s.componentName))\n if (scanned.length > 0) {\n const resolver = await ensureCollapseResolver()\n if (resolver) {\n const sites = new Map<\n string,\n {\n templateHtml: string\n lightClass: string\n darkClass: string\n rules: string[]\n ruleKey: string\n }\n >()\n const candidates = new Set<string>()\n for (const s of scanned) {\n const resolved = await resolver.resolve({\n component: { name: s.importedName, source: s.source },\n props: s.props,\n childrenText: s.childrenText,\n config: {\n provider: collapseProvider,\n theme: collapseTheme,\n mode: collapseMode,\n },\n })\n if (!resolved) continue\n candidates.add(s.componentName)\n sites.set(s.key, {\n templateHtml: resolved.templateHtml,\n lightClass: resolved.lightClass,\n darkClass: resolved.darkClass,\n rules: resolved.rules,\n ruleKey: resolved.key,\n })\n }\n if (sites.size > 0) {\n collapseRocketstyle = { candidates, sites, mode: collapseMode }\n }\n }\n }\n }\n\n const result = transformJSX(sourceForJsx, id, {\n ssr: isSsr,\n knownSignals,\n ...(collapseRocketstyle ? { collapseRocketstyle } : {}),\n })\n // Surface compiler warnings in the terminal\n for (const w of result.warnings) {\n this.warn(`${w.message} (${id}:${w.line}:${w.column})`)\n }\n\n let output = result.code\n\n // ── Dev-only transforms ────────────────────────────────────────────\n if (!isBuild) {\n output = injectHmr(output, id)\n // Inject debug names + LPIH source locations for signal() calls\n // not rewritten by HMR. `id` is Vite's resolved module path —\n // the same path the runtime would have parsed from new Error().\n output = injectSignalNames(output, id)\n }\n\n // R12: surface the compiler's V3 source map so stack traces /\n // breakpoints in Pyreon components resolve to the right source line\n // (the JS backend now emits one; substitutions shift line counts, so\n // `map: null` previously mislocated every frame app-wide). Exact in\n // build; in dev the small extra HMR / signal-name injections aren't\n // re-mapped (still vastly better than no map). The native backend\n // emits no map yet (its own scoped follow-up) → `null`, unchanged\n // behaviour for that path.\n return { code: output, map: result.map ?? null }\n },\n\n // ── SSR dev middleware ───────────────────────────────────────────────────\n configureServer(server: ViteDevServer) {\n // Generate .pyreon/context.json for AI tools on dev server start\n generateProjectContext(projectRoot)\n\n // Debounced regeneration on file changes\n let contextTimer: ReturnType<typeof setTimeout> | null = null\n server.watcher.on('change', (file) => {\n if (/\\.(tsx|jsx|ts|js)$/.test(file) && !file.includes('node_modules')) {\n if (contextTimer) clearTimeout(contextTimer)\n contextTimer = setTimeout(() => generateProjectContext(projectRoot), 500)\n }\n })\n\n // LPIH auto-bridge — accepts POST /__pyreon_lpih__ from the browser\n // client and atomically writes the cache file the LSP auto-discovers.\n // Registered BEFORE the SSR middleware so it short-circuits and never\n // falls through to handleSsrRequest.\n if (lpihEnabled) {\n registerLpihMiddleware(server, projectRoot, lpihUserCfg)\n }\n\n if (!ssrConfig) return\n\n // Return a function so the middleware runs AFTER Vite's built-in middleware\n // (static files, HMR, etc.) — only handle requests that Vite doesn't serve.\n return () => {\n server.middlewares.use(async (req, res, next) => {\n if (req.method !== 'GET') return next()\n const url = req.url ?? '/'\n if (isAssetRequest(url)) return next()\n\n try {\n await handleSsrRequest(server, ssrConfig.entry, url, req, res, next)\n } catch (err) {\n server.ssrFixStacktrace(err as Error)\n next(err)\n }\n })\n }\n },\n\n // ── LPIH auto-bridge client injection ────────────────────────────────────\n transformIndexHtml(html: string): string | undefined {\n if (isBuild || !lpihEnabled) return undefined\n // Inject a tiny <script type=\"module\"> that activates devtools + polls\n // getFireSummaries() and POSTs to /__pyreon_lpih__. The dev server\n // middleware (above) writes the body to <projectRoot>/.pyreon-lpih.json\n // using @pyreon/reactivity's atomic-rename pattern. The LSP\n // auto-discovers that file (R2, #777) so the user wires NOTHING.\n const script = buildLpihClientScript(lpihIntervalMs)\n return html.replace('</head>', `${script}\\n</head>`)\n },\n }\n}\n\nasync function handleSsrRequest(\n server: ViteDevServer,\n entry: string,\n url: string,\n req: import('node:http').IncomingMessage,\n res: import('node:http').ServerResponse,\n next: (err?: unknown) => void,\n): Promise<void> {\n const mod = await server.ssrLoadModule(entry)\n const handler = mod.handler ?? mod.default\n\n if (typeof handler !== 'function') {\n next()\n return\n }\n\n const origin = `http://${req.headers.host ?? 'localhost'}`\n const fullUrl = new URL(url, origin)\n const request = new Request(fullUrl.href, {\n method: req.method ?? 'GET',\n headers: Object.entries(req.headers).reduce((h, [k, v]) => {\n if (v) h.set(k, Array.isArray(v) ? v.join(', ') : v)\n return h\n }, new Headers()),\n })\n\n const response: Response = await handler(request)\n let html = await response.text()\n\n html = await server.transformIndexHtml(url, html)\n\n res.statusCode = response.status\n response.headers.forEach((v, k) => {\n res.setHeader(k, v)\n })\n res.end(html)\n}\n\n// ── AI context generation ─────────────────────────────────────────────────────\n\n/**\n * Generate .pyreon/context.json — project map for AI coding assistants.\n * Delegates to @pyreon/compiler's unified project scanner.\n */\nfunction generateProjectContext(root: string): void {\n try {\n const context = generateContext(root)\n const outDir = pathJoin(root, '.pyreon')\n if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true })\n writeFileSync(pathJoin(outDir, 'context.json'), JSON.stringify(context, null, 2), 'utf-8')\n } catch {\n // Silently fail — context generation is best-effort\n }\n}\n\n// ── LPIH auto-bridge helpers ───────────────────────────────────────────────\n\n/**\n * Resolve the LPIH cache-file path for a given project root. Matches the\n * convention `@pyreon/reactivity/lpih`'s `getDefaultLpihCachePath()` uses\n * AND the LSP auto-discovers (R2, #777): `<projectRoot>/.pyreon-lpih.json`.\n *\n * @internal — exported for tests.\n */\nexport function resolveLpihCachePath(projectRoot: string): string {\n return pathJoin(projectRoot, '.pyreon-lpih.json')\n}\n\n/**\n * Register the LPIH dev-server middleware on a Vite server. Extracted from\n * `configureServer` so the `cachePath` option reference lives at module\n * scope (top-level helper) rather than inside the plugin's inline body —\n * keeps `scripts/audit-types.ts` happy regardless of how its comment-\n * stripping handles the long inline `configureServer` block.\n *\n * @internal — exported for tests.\n */\nexport function registerLpihMiddleware(\n server: ViteDevServer,\n projectRoot: string,\n userCfg: PyreonLpihOptions,\n): void {\n const cachePath = userCfg.cachePath ?? resolveLpihCachePath(projectRoot)\n server.middlewares.use('/__pyreon_lpih__', (req, res) => {\n if (req.method !== 'POST') {\n res.statusCode = 405\n res.end('Method Not Allowed')\n return\n }\n let body = ''\n req.on('data', (chunk: Buffer | string) => {\n body += chunk.toString()\n // Defensive cap — fire payloads are tiny (a few KB at most);\n // anything larger is malicious or buggy. Drop the request.\n if (body.length > 1024 * 1024) {\n res.statusCode = 413\n res.end('Payload Too Large')\n req.destroy()\n }\n })\n req.on('end', () => {\n void writeLpihCacheFile(cachePath, body)\n .then(() => {\n res.statusCode = 204\n res.end()\n })\n .catch((err: unknown) => {\n // Don't crash the dev server — log + return 500 so the\n // browser-side bridge can back off + retry next interval.\n // oxlint-disable-next-line no-console\n console.warn(\n '[pyreon] LPIH cache write failed:',\n err instanceof Error ? err.message : err,\n )\n res.statusCode = 500\n res.end('LPIH cache write failed')\n })\n })\n })\n}\n\nlet _lpihSeq = 0\n\n/**\n * Atomically write a LPIH cache file (tmp + rename), mirroring the\n * `@pyreon/reactivity/lpih:writeLpihCache` implementation. The payload\n * comes pre-serialized from the browser-side bridge — we validate the\n * outer shape (`{ fires: [...] }`) and reject malformed bodies to stop a\n * buggy client from corrupting the file the LSP reads.\n *\n * @internal — exported for tests.\n */\nexport async function writeLpihCacheFile(path: string, body: string): Promise<void> {\n // Validate shape — must be a JSON object with `fires: array`. We re-\n // serialize so the on-disk format is stable regardless of how the\n // browser-side bridge encodes it.\n let parsed: unknown\n try {\n parsed = JSON.parse(body)\n } catch {\n throw new Error('LPIH bridge: payload is not valid JSON')\n }\n if (\n parsed === null ||\n typeof parsed !== 'object' ||\n !Array.isArray((parsed as { fires?: unknown }).fires)\n ) {\n throw new Error('LPIH bridge: payload is missing `fires` array')\n }\n const fs = await import('node:fs/promises')\n const pid = typeof process !== 'undefined' && 'pid' in process ? process.pid : 0\n const tmp = `${path}.tmp.${pid}.${++_lpihSeq}`\n // Single try/catch covering BOTH writeFile AND rename. The previous\n // shape only guarded the rename — if `fs.writeFile` itself threw (disk\n // full, EIO, EACCES, transient FS error), the partial tmp file leaked\n // on disk with a unique PID+seq name (so no conflict, but it accumulated\n // forever). Audit caught this in the LPIH followups round.\n try {\n await fs.writeFile(tmp, JSON.stringify(parsed), 'utf8')\n await fs.rename(tmp, path)\n } catch (err) {\n // Best-effort cleanup; original error is more useful than unlink's.\n // Covers BOTH the writeFile-failed (tmp may not exist) and the\n // rename-failed (tmp exists, rename didn't move it) cases —\n // `fs.unlink` of a non-existent file throws ENOENT, which we swallow.\n try {\n await fs.unlink(tmp)\n } catch {\n /* swallow — original error is the user-facing one */\n }\n throw err\n }\n}\n\n/**\n * Build the `<script type=\"module\">` body injected into the HTML head.\n * The script imports devtools activation + `getFireSummaries` from\n * `@pyreon/reactivity`, sets up a `setInterval` that POSTs every\n * `intervalMs` ms, and registers a `beforeunload` cleanup so the timer\n * doesn't outlive the page.\n *\n * Browser bundlers serve `@pyreon/reactivity` from the workspace via\n * Vite's normal module resolution — no virtual module needed.\n *\n * @internal — exported for tests.\n */\nexport function buildLpihClientScript(intervalMs: number): string {\n // Note: the script body is intentionally compact — the goal is zero\n // visible payload in DevTools \"Sources\" while still being readable\n // when someone DOES go looking. `JSON.stringify` for `intervalMs` is\n // defense against `__proto__` / NaN / non-finite values reaching the\n // emitted JS as a literal.\n // CRITICAL — top-level await on the dynamic import. `<script type=\"module\">`\n // tags execute in document order with `defer` semantics; the head-injected\n // LPIH script's body MUST fully evaluate (including this await) BEFORE the\n // body-injected app entry's module body runs. Otherwise activateReactiveDevtools()\n // would land AFTER the app has already created its module-scope signals,\n // and `_rdRegister` (gated on `if (!_active) return undefined`) would skip\n // them entirely — making the most common signal shape (top-of-file `const x = signal(0)`)\n // invisible to LPIH. With the `await`, the LPIH module doesn't complete\n // until activation finishes; the app's entry waits its turn.\n return `<script type=\"module\">\n // Pyreon LPIH auto-bridge — POSTs fire summaries to /__pyreon_lpih__\n // so the LSP (pyreon-lint --lsp) sees live fire data. Dev-only.\n const __px = await import('@pyreon/reactivity').catch(() => null)\n if (__px) {\n __px.activateReactiveDevtools()\n const __pxGet = __px.getFireSummaries\n const __pxInterval = ${JSON.stringify(intervalMs)}\n const __pxPost = () => {\n const summaries = __pxGet()\n const payload = JSON.stringify({\n fires: summaries.map((s) => ({\n file: s.loc.file,\n line: s.loc.line,\n count: s.count,\n kind: s.kind,\n lastFire: s.lastFire,\n rate1s: s.rate1s,\n })),\n })\n fetch('/__pyreon_lpih__', { method: 'POST', body: payload, headers: { 'content-type': 'application/json' } }).catch(() => {\n // Dev-server might be restarting; swallow + retry next interval.\n })\n }\n const __pxId = setInterval(__pxPost, __pxInterval)\n window.addEventListener('beforeunload', () => clearInterval(__pxId))\n }\n // If __px is null, @pyreon/reactivity isn't in the dep graph — stay silent,\n // LPIH is opt-in via the runtime API too. The dynamic-import catch returns\n // null instead of letting the rejection bubble so consumers without the\n // package don't see a console error.\n</script>`\n}\n\n// ── HMR injection ─────────────────────────────────────────────────────────────\n\n/**\n * Regex that detects signal declarations (prefix + variable name).\n * The arguments are extracted via balanced-paren matching in `injectHmr`.\n * A brace-depth check filters out matches inside functions/blocks — only\n * module-scope (depth 0) signals are rewritten for HMR state preservation.\n *\n * The optional `<...>` group accepts a TypeScript type parameter so that\n * `signal<T>(initial)` declarations are also rewritten — without it, any\n * generic-typed module-scope signal silently skipped HMR preservation.\n *\n * The inner `(?:[^<>]|<[^<>]*>)*` permits one level of generic nesting\n * (e.g. `signal<Array<Row>>([])`, `signal<Map<string, number>>(m)`).\n * Deeper nesting (`signal<Array<{ id: T<U> }>>(...)`) falls back to\n * not-rewritten — tracked as a follow-up if real consumers need it,\n * but unlikely at module scope where generics are usually shallow.\n */\nconst SIGNAL_PREFIX_RE =\n /^((?:export\\s+)?(?:const|let)\\s+(\\w+)\\s*=\\s*)signal(?:<(?:[^<>]|<[^<>]*>)*>)?\\(/gm\n\n/**\n * Detect whether the module exports any component-like functions\n * (uppercase first letter — standard convention for JSX components).\n */\nconst EXPORT_COMPONENT_RE =\n /export\\s+(?:default\\s+)?(?:function\\s+([A-Z]\\w*)|const\\s+([A-Z]\\w*)\\s*[=:])/\n\nfunction skipStringLiteral(code: string, start: number, quote: string): number {\n let j = start + 1\n while (j < code.length) {\n if (code[j] === '\\\\') {\n j += 2\n continue\n }\n if (code[j] === quote) break\n j++\n }\n return j\n}\n\nfunction extractBalancedArgs(code: string, start: number): string | null {\n let depth = 1\n for (let i = start; i < code.length; i++) {\n const ch = code[i]\n if (ch === '(') depth++\n else if (ch === ')') {\n depth--\n if (depth === 0) return code.slice(start, i)\n } else if (ch === '\"' || ch === \"'\" || ch === '`') {\n i = skipStringLiteral(code, i, ch)\n }\n }\n return null\n}\n\n/**\n * Compute brace depth at position `pos` — returns 0 for module scope.\n * Skips string literals to avoid counting braces inside strings.\n */\nfunction braceDepthAt(code: string, pos: number): number {\n let depth = 0\n for (let i = 0; i < pos; i++) {\n const ch = code[i]\n if (ch === '{') depth++\n else if (ch === '}') depth--\n else if (ch === '\"' || ch === \"'\" || ch === '`') {\n i = skipStringLiteral(code, i, ch)\n }\n }\n return depth\n}\n\n/** Rewrite module-scope `signal()` calls to `__hmr_signal()` for state preservation. */\nfunction rewriteSignals(code: string, moduleId: string): string {\n const escapedId = JSON.stringify(moduleId)\n const matches: {\n start: number\n end: number\n prefix: string\n name: string\n args: string\n }[] = []\n let m: RegExpExecArray | null = SIGNAL_PREFIX_RE.exec(code)\n while (m !== null) {\n const argsStart = m.index + m[0].length\n const args = extractBalancedArgs(code, argsStart)\n if (args === null) {\n m = SIGNAL_PREFIX_RE.exec(code)\n continue // unbalanced — skip\n }\n // Only rewrite module-scope signals (brace depth 0).\n if (braceDepthAt(code, m.index) === 0) {\n matches.push({\n start: m.index,\n end: argsStart + args.length + 1, // +1 for closing paren\n prefix: m[1] ?? '',\n name: m[2] ?? '',\n args,\n })\n }\n m = SIGNAL_PREFIX_RE.exec(code)\n }\n SIGNAL_PREFIX_RE.lastIndex = 0\n\n // Replace in reverse to preserve offsets\n let output = code\n for (let i = matches.length - 1; i >= 0; i--) {\n const { start, end, prefix, name, args } = matches[i] as (typeof matches)[number]\n const replacement = `${prefix}__hmr_signal(${escapedId}, ${JSON.stringify(name)}, signal, ${args})`\n output = output.slice(0, start) + replacement + output.slice(end)\n }\n return output\n}\n\n/** Check if an argument string contains a top-level comma (i.e. has multiple arguments). */\nfunction hasMultipleArgs(args: string): boolean {\n let depth = 0\n for (const ch of args) {\n if (ch === '(' || ch === '[' || ch === '{') depth++\n else if (ch === ')' || ch === ']' || ch === '}') depth--\n else if (ch === ',' && depth === 0) return true\n }\n return false\n}\n\n/**\n * Inject `{ name?, __sourceLocation: { file, line, col } }` into\n * `signal()` / `computed()` / `effect()` calls that don't already have\n * an options argument. Only runs in dev mode for debugging/devtools.\n *\n * Three forms covered:\n *\n * `const count = signal(0)` →\n * `const count = signal(0, { name: \"count\", __sourceLocation: {...} })`\n *\n * `const doubled = computed(() => count() * 2)` →\n * `const doubled = computed(() => count() * 2, { name: \"doubled\", __sourceLocation: {...} })`\n *\n * `effect(() => console.log(count()))` →\n * `effect(() => console.log(count()), { __sourceLocation: {...} })`\n * (no `name` — anonymous effects have no binding to derive from)\n *\n * Module-scope signals rewritten to __hmr_signal() are naturally skipped\n * because the regex matches `signal(` not `__hmr_signal(`.\n *\n * **LPIH integration**: `__sourceLocation` is consumed by\n * `@pyreon/reactivity`'s `signal()` / `computed()` / `effect()` to skip\n * the `new Error().stack` capture in `_rdRegister` — saves ~2.2µs per\n * creation when devtools is active. The injected literal is byte-for-byte\n * the same info the runtime would have parsed from the stack, so behavior\n * is identical except no stack-parse cost.\n *\n * **Anonymous-effect detection**: `effect(` can also appear as a property\n * access (`obj.effect(...)`), a longer identifier (`sideEffect(...)`), or\n * a previously-injected call (`effect(fn, { ... })`). The unbound-effect\n * pass guards against all three:\n * - preceded by NOT `[A-Za-z0-9_$.]` (so `.effect`/`sideEffect` skip)\n * - args do NOT already contain a 2nd arg (`hasMultipleArgs` check)\n *\n * @param code - source text\n * @param moduleId - the file path to embed in the injected `__sourceLocation`.\n * Vite passes the resolved module ID (absolute path).\n */\nfunction injectSignalNames(code: string, moduleId: string): string {\n // Pre-pass: mask string-literal, template-literal, and comment regions\n // so the regexes below don't false-fire on `effect(` inside docstrings,\n // help-text strings, JS-as-text test fixtures, or comments mentioning\n // reactive primitives. The regex runs against the MASKED code (positions\n // are preserved), so a match's index points at real code; args extraction\n // pulls from the ORIGINAL code for accurate output.\n //\n // Without this, user code like `const docs = \\`effect(() => x)\\`` would\n // get `, { __sourceLocation: ... }` injected INSIDE the template literal,\n // corrupting the help-text content at runtime.\n const masked = _maskStringsAndComments(code)\n\n // Pass 1: bound forms — `const X = (signal|computed|effect)(…)`.\n // Extract `X` as the debug name + the reactive primitive kind.\n const reBound = /(?:const|let)\\s+(\\w+)\\s*=\\s*(signal|computed|effect)\\(/gm\n // Pass 2: unbound effect — `effect(() => …)` at statement position,\n // not following a member-access (.) or identifier char ($_a-zA-Z0-9).\n // Reactive primitives other than `effect` are rare without binding,\n // so we skip the bare `signal(` / `computed(` form to stay conservative.\n const reUnboundEffect = /(?<![\\w$.])effect\\(/gm\n\n type Match = {\n start: number\n end: number\n name: string | null\n args: string\n matchIdx: number\n }\n const matches: Match[] = []\n // Track call positions covered by pass 1 so pass 2 can skip them.\n const covered = new Set<number>()\n\n let m: RegExpExecArray | null = reBound.exec(masked)\n while (m !== null) {\n const argsStart = m.index + m[0].length\n const args = extractBalancedArgs(code, argsStart)\n if (args !== null && !hasMultipleArgs(args)) {\n matches.push({\n start: argsStart,\n end: argsStart + args.length,\n name: m[1] ?? '',\n args,\n matchIdx: m.index,\n })\n // Mark the `effect(`/`signal(`/`computed(` token start so the\n // unbound-effect pass doesn't double-process it.\n const tokStart = m.index + m[0].length - (m[2]?.length ?? 0) - 1\n covered.add(tokStart)\n }\n m = reBound.exec(masked)\n }\n reBound.lastIndex = 0\n\n m = reUnboundEffect.exec(masked)\n while (m !== null) {\n if (!covered.has(m.index)) {\n const argsStart = m.index + m[0].length\n const args = extractBalancedArgs(code, argsStart)\n if (args !== null && !hasMultipleArgs(args)) {\n matches.push({\n start: argsStart,\n end: argsStart + args.length,\n name: null,\n args,\n matchIdx: m.index,\n })\n }\n }\n m = reUnboundEffect.exec(masked)\n }\n reUnboundEffect.lastIndex = 0\n\n if (matches.length === 0) return code\n\n // Sort by descending start so back-to-front rewriting doesn't shift\n // later indices (each splice leaves earlier offsets unchanged).\n matches.sort((a, b) => b.start - a.start)\n\n // Pre-compute line offsets ONCE — avoids O(N²) when many calls share\n // a file. Each lookup becomes O(log N) via binary search.\n const lineStarts = _computeLineStarts(code)\n\n let output = code\n for (let i = 0; i < matches.length; i++) {\n const { start, end, name, args, matchIdx } = matches[i] as Match\n const { line, col } = _offsetToLineCol(matchIdx, lineStarts)\n const locLiteral = `__sourceLocation: { file: ${JSON.stringify(moduleId)}, line: ${line}, col: ${col} }`\n const inner = name !== null\n ? `name: ${JSON.stringify(name)}, ${locLiteral}`\n : locLiteral\n output = `${output.slice(0, start)}${args}, { ${inner} }${output.slice(end)}`\n }\n return output\n}\n\n/**\n * Mask string-literal / template-literal / comment regions in `code` by\n * replacing their content with spaces. Returns a SAME-LENGTH string so\n * regex match positions in the masked version line up with the original.\n *\n * Used by `injectSignalNames` to skip false-positive matches against\n * reactive-primitive names that appear inside strings or comments. Without\n * masking, a user's `const docs = \\`effect(() => x)\\`` template literal\n * would get `, { __sourceLocation: ... }` injected INSIDE the string,\n * corrupting runtime values.\n *\n * Handles:\n * - `\"...\"` / `'...'` strings (escape-aware)\n * - `` `...` `` template literals; interpolations `${...}` are KEPT as\n * code (their content can contain real `signal()` calls worth catching)\n * - `// ...` line comments\n * - `/* ... *\\/` block comments\n *\n * Regex literals (`/foo/g`) are NOT special-cased — they're rare and the\n * downstream extractBalancedArgs handles unmatched parens by returning null.\n *\n * @internal — exported for tests.\n */\nexport function _maskStringsAndComments(code: string): string {\n const out: string[] = []\n let i = 0\n const n = code.length\n while (i < n) {\n const c = code[i]\n const c1 = code[i + 1]\n\n // Line comment `// ...`\n if (c === '/' && c1 === '/') {\n while (i < n && code[i] !== '\\n') {\n out.push(' ')\n i++\n }\n continue\n }\n // Block comment `/* ... */`\n if (c === '/' && c1 === '*') {\n out.push(' ', ' ')\n i += 2\n while (i < n) {\n if (code[i] === '*' && code[i + 1] === '/') {\n out.push(' ', ' ')\n i += 2\n break\n }\n // Preserve newlines so line numbers don't shift\n out.push(code[i] === '\\n' ? '\\n' : ' ')\n i++\n }\n continue\n }\n // String literal \"...\" or '...'\n if (c === '\"' || c === \"'\") {\n const quote = c\n out.push(' ')\n i++\n while (i < n && code[i] !== quote) {\n // Escape sequence — skip the next char too (handles `\\\"`, `\\\\`, etc.)\n if (code[i] === '\\\\' && i + 1 < n) {\n // Preserve a newline (line-continuation `\\<LF>`) as a newline.\n out.push(' ', code[i + 1] === '\\n' ? '\\n' : ' ')\n i += 2\n continue\n }\n // Unterminated string (legacy parsers stop at newline) — break\n if (code[i] === '\\n') break\n out.push(' ')\n i++\n }\n if (i < n && code[i] === quote) {\n out.push(' ')\n i++\n }\n continue\n }\n // Template literal `...` — preserve `${...}` interpolations as code\n if (c === '`') {\n out.push(' ')\n i++\n while (i < n && code[i] !== '`') {\n if (code[i] === '\\\\' && i + 1 < n) {\n out.push(' ', code[i + 1] === '\\n' ? '\\n' : ' ')\n i += 2\n continue\n }\n // `${...}` — keep the interpolation body as code (with nested\n // brace tracking so we find the matching `}`).\n if (code[i] === '$' && code[i + 1] === '{') {\n out.push(' ', ' ')\n i += 2\n let depth = 1\n while (i < n && depth > 0) {\n if (code[i] === '{') {\n depth++\n out.push(code[i] ?? ' ')\n i++\n continue\n }\n if (code[i] === '}') {\n depth--\n if (depth === 0) {\n out.push(' ')\n i++\n break\n }\n out.push(code[i] ?? ' ')\n i++\n continue\n }\n // Inside `${}` — pass through as code (might contain `signal(` etc).\n out.push(code[i] ?? ' ')\n i++\n }\n continue\n }\n // Preserve newlines so line numbers don't shift.\n out.push(code[i] === '\\n' ? '\\n' : ' ')\n i++\n }\n if (i < n && code[i] === '`') {\n out.push(' ')\n i++\n }\n continue\n }\n out.push(c ?? '')\n i++\n }\n return out.join('')\n}\n\n/**\n * Compute the 0-indexed character offset for the start of each line.\n * `lineStarts[i]` is the offset of the FIRST character on line i+1\n * (1-based, so `lineStarts[0]` = offset 0 = line 1).\n *\n * @internal — exported for tests.\n */\nexport function _computeLineStarts(code: string): number[] {\n const starts: number[] = [0]\n for (let i = 0; i < code.length; i++) {\n if (code.charCodeAt(i) === 10) starts.push(i + 1) // \\n\n }\n return starts\n}\n\n/**\n * Convert a 0-indexed offset to `{ line: 1-based, col: 1-based }` using a\n * pre-computed line-starts array. Binary search → O(log N) per lookup.\n *\n * @internal — exported for tests.\n */\nexport function _offsetToLineCol(\n offset: number,\n lineStarts: number[],\n): { line: number; col: number } {\n // Binary search for the largest lineStarts[i] <= offset.\n let lo = 0\n let hi = lineStarts.length - 1\n while (lo < hi) {\n const mid = (lo + hi + 1) >>> 1\n const v = lineStarts[mid]\n if (v !== undefined && v <= offset) lo = mid\n else hi = mid - 1\n }\n const lineStart = lineStarts[lo] ?? 0\n return { line: lo + 1, col: offset - lineStart + 1 }\n}\n\nfunction injectHmr(code: string, moduleId: string): string {\n const hasSignals = SIGNAL_PREFIX_RE.test(code)\n SIGNAL_PREFIX_RE.lastIndex = 0\n\n const hasComponentExport = EXPORT_COMPONENT_RE.test(code)\n\n // Only inject HMR if the module exports components or has module-scope signals\n if (!hasComponentExport && !hasSignals) return code\n\n let output = hasSignals ? rewriteSignals(code, moduleId) : code\n\n // Build the HMR footer\n const escapedId = JSON.stringify(moduleId)\n const lines: string[] = []\n\n if (hasSignals) {\n lines.push(`import { __hmr_signal, __hmr_dispose } from \"${HMR_RUNTIME_IMPORT}\";`)\n }\n\n lines.push(`if (import.meta.hot) {`)\n\n if (hasSignals) {\n lines.push(` import.meta.hot.dispose(() => __hmr_dispose(${escapedId}));`)\n }\n\n // Self-accept the module, then drive Pyreon's HMR coordinator.\n //\n // The OLD code emitted a bare `import.meta.hot.accept()` (no callback):\n // Vite re-evaluated the module but NOTHING re-rendered the mounted tree,\n // AND the self-accept suppressed Vite's full-reload fallback — so a\n // component/JSX edit produced a silently-stale UI until a MANUAL refresh.\n //\n // Now: the accept callback hands the FRESH module namespace Vite already\n // re-evaluated straight to `globalThis.__pyreon_hmr_swap__` (registered\n // by `@pyreon/router` in a dev browser — zero import coupling, same\n // pattern as the perf-harness counter sink), keyed by THIS module's id.\n // The coordinator finds every active matched route record whose lazy\n // `_hmrId` matches and swaps in the new component, re-rendering ONLY\n // that subtree IN PLACE (no page reload → `__pyreon_hmr_registry__`\n // survives → `__hmr_signal` restores module-scope signal values).\n //\n // Using the namespace Vite passes (not a re-run of the lazy thunk)\n // sidesteps the stale-`?t=` trap: the dynamic-import thunk lives in the\n // virtual routes module, which is NOT invalidated when this leaf route\n // self-accepts — re-importing it would return the OLD module.\n //\n // `__pyreon_hmr_swap__` returns falsy when the edit was outside the\n // active route tree (nested non-route component, unrelated route,\n // signal-only module) OR no coordinator is registered (plain\n // `@pyreon/runtime-dom` app, or module loaded before any router\n // mounted). Then `import.meta.hot.invalidate()` → Vite propagates → an\n // AUTOMATIC full reload. Either way the user never refreshes by hand.\n lines.push(` import.meta.hot.accept((__m) => {`)\n lines.push(` const __s = globalThis.__pyreon_hmr_swap__;`)\n lines.push(\n ` if (typeof __s === \"function\" && __m && __s(${escapedId}, __m)) return;`,\n )\n lines.push(` import.meta.hot.invalidate();`)\n lines.push(` });`)\n lines.push(`}`)\n\n output = `${output}\\n\\n${lines.join('\\n')}\\n`\n\n return output\n}\n\n// ── Compat attribute transforms ──────────────────────────────────────────────\n\n/**\n * Transform React-style JSX attribute names to standard HTML attribute names.\n * This is a lightweight string transform that runs on JSX source before OXC's\n * JSX transform converts it to jsx() calls.\n *\n * - `className` → `class`\n * - `htmlFor` → `for`\n *\n * Only matches attribute position in JSX (after `<tag ` or whitespace).\n * Does not transform property access (e.g. `props.className` stays as-is since\n * the compat JSX runtime handles that at call time).\n */\nfunction transformCompatAttributes(code: string): string {\n // Match className/htmlFor in JSX attribute position:\n // After < and tag name, or after whitespace between attributes\n // Pattern: word boundary + attribute name + = (with optional whitespace)\n return code\n .replace(/(\\s)className(\\s*=)/g, '$1class$2')\n .replace(/(\\s)htmlFor(\\s*=)/g, '$1for$2')\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction getExt(id: string): string {\n const clean = id.split('?')[0] ?? id\n const dot = clean.lastIndexOf('.')\n return dot >= 0 ? clean.slice(dot) : ''\n}\n\n/** Skip Vite-handled asset requests (CSS, images, HMR, etc.) */\nfunction isAssetRequest(url: string): boolean {\n return (\n url.startsWith('/@') || // @vite/client, @id, @fs, etc.\n url.startsWith('/__') || // __open-in-editor, etc.\n url.includes('/node_modules/') ||\n /\\.(css|js|ts|tsx|jsx|json|ico|png|jpg|jpeg|gif|svg|woff2?|ttf|eot|map)(\\?|$)/.test(url)\n )\n}\n\n// ── HMR runtime source (served as virtual module) ─────────────────────────────\n//\n// Inlined here so it's available without a filesystem read. This is the\n// compiled-to-JS version of hmr-runtime.ts — kept in sync manually.\n\n// ─── Cross-module signal auto-call helpers ──────────────────────────────────\n\n/**\n * Normalize a Vite module ID by stripping query strings (?v=..., ?t=...)\n * and resolving to an absolute path for consistent registry lookups.\n */\nfunction normalizeModuleId(id: string): string {\n const queryIndex = id.indexOf('?')\n return queryIndex >= 0 ? id.slice(0, queryIndex) : id\n}\n\n// ─── Island declaration scanner ────────────────────────────────────────────\n\n/**\n * One island() call site discovered in source.\n *\n * `loaderAbsPath` is the dynamic-import target resolved relative to the\n * source file where the call was written. Vite's resolver finds the actual\n * file (.tsx / .jsx / .ts / .js extension auto-added) when the registry\n * module emits `() => import('<loaderAbsPath>')`.\n */\ninterface IslandDecl {\n name: string\n hydrate: string\n loaderAbsPath: string\n}\n\n/**\n * Pre-scan all source files in the project for `island()` declarations.\n *\n * Called from `buildStart` (when `islands: true`) so the registry is fully\n * populated before any transforms run. Mirrors `prescanSignalExports` shape;\n * the per-file regex pattern matches:\n *\n * island(() => import('PATH'), { name: 'NAME', hydrate: 'STRATEGY' })\n *\n * Edge cases the regex deliberately doesn't cover (user falls back to manual\n * `hydrateIslands({ ... })`):\n * - Loader is a variable, not an inline arrow: `island(myLoader, { name })`\n * - Name is a variable: `island(() => import('./X'), { name: NAME_CONST })`\n * - Options come from a spread: `island(loader, { ...opts })`\n */\nasync function prescanIslandDeclarations(\n root: string,\n registry: Map<string, IslandDecl[]>,\n): Promise<void> {\n const files: string[] = []\n\n function walk(dir: string) {\n try {\n for (const entry of readdirSync(dir)) {\n if (\n entry.startsWith('.') ||\n entry === 'node_modules' ||\n entry === 'dist' ||\n entry === 'lib' ||\n entry === 'build'\n )\n continue\n const full = pathJoin(dir, entry)\n try {\n const stat = statSync(full)\n if (stat.isDirectory()) walk(full)\n else if (/\\.(ts|tsx|js|jsx)$/.test(entry)) files.push(full)\n } catch {\n /* permission error, etc. */\n }\n }\n } catch {\n /* dir doesn't exist */\n }\n }\n\n walk(root)\n\n for (const file of files) {\n try {\n const code = readFileSync(file, 'utf-8')\n scanIslandDeclarations(code, file, registry)\n } catch {\n /* read error */\n }\n }\n}\n\n/**\n * Scan a single source file for `island()` declarations and record them.\n *\n * The regex captures:\n * - Group 1: dynamic-import path (`./components/Counter`)\n * - Group 2: options block contents\n *\n * Then a follow-up regex pulls `name: 'X'` and `hydrate: 'Y'` from the\n * options block. Single-line and multi-line forms both work.\n *\n * Resolves the loader path relative to the file where the call lives so\n * the emitted virtual-module registry gets an absolute path Vite's resolver\n * can find.\n */\nfunction scanIslandDeclarations(\n code: string,\n filePath: string,\n registry: Map<string, IslandDecl[]>,\n): void {\n // `[\\s\\S]` lets the options block span multiple lines. The lazy `?` after\n // the options block prevents over-matching when several `island()` calls\n // appear in the same file.\n // `[^}]{0,500}` instead of `[\\s\\S]*?` — real island() option blocks\n // are tiny (`{ name: 'X', hydrate: 'load' }`); excluding `}` from\n // the inner class also tightens the match against the outer `\\}`.\n const ISLAND_CALL_RE =\n /island\\s*\\(\\s*\\(\\s*\\)\\s*=>\\s*import\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)\\s*,\\s*\\{([^}]{0,500})\\}\\s*\\)/g\n const decls: IslandDecl[] = []\n let match: RegExpExecArray | null\n while ((match = ISLAND_CALL_RE.exec(code)) !== null) {\n const importPath = match[1]!\n const optsBlock = match[2]!\n const nameMatch = /(?:^|[\\s,{])name\\s*:\\s*['\"]([^'\"]+)['\"]/.exec(optsBlock)\n if (!nameMatch) continue // can't auto-register without a name\n const hydrateMatch = /(?:^|[\\s,{])hydrate\\s*:\\s*['\"]([^'\"]+)['\"]/.exec(optsBlock)\n const hydrate = hydrateMatch ? hydrateMatch[1]! : 'load'\n const loaderAbsPath = importPath.startsWith('.')\n ? resolveRelative(filePath, importPath)\n : importPath\n decls.push({ name: nameMatch[1]!, hydrate, loaderAbsPath })\n }\n if (decls.length > 0) {\n registry.set(normalizeModuleId(filePath), decls)\n } else {\n // Clean up if file no longer declares islands (e.g. after edit)\n registry.delete(normalizeModuleId(filePath))\n }\n}\n\n/**\n * Resolve a dynamic-import specifier to an absolute path, mirroring how Node\n * / Vite resolve `import('./X')` from the source file's directory.\n */\nfunction resolveRelative(fromFile: string, relPath: string): string {\n return pathJoin(dirname(fromFile), relPath)\n}\n\n/**\n * Render the auto-generated `virtual:pyreon/islands-registry` source. Emits:\n *\n * export const __pyreonIslandRegistry = {\n * Counter: () => import('/abs/path/to/components/Counter'),\n * IdleClock: () => import('/abs/path/to/components/IdleClock'),\n * // never-strategy islands deliberately omitted\n * }\n *\n * `hydrate: 'never'` islands are skipped — registering a loader for them\n * would defeat the strategy by pulling the component module into the\n * client bundle graph. `hydrateIslandsAuto()` short-circuits never-islands\n * at runtime regardless; emitting here would still create the dynamic-\n * import chunk.\n *\n * Duplicate `name` across declarations: the LAST one wins. Documented as\n * an anti-pattern (caught by the planned `pyreon doctor --check-islands`).\n */\nfunction renderIslandsRegistry(\n registry: Map<string, IslandDecl[]>,\n enabled: boolean,\n): string {\n if (!enabled) {\n return [\n `// pyreon plugin: islands feature is disabled (pyreon({ islands: false })).`,\n `// hydrateIslandsAuto() will throw at runtime — re-enable via vite.config.ts`,\n `// or use manual hydrateIslands({ ... }) instead.`,\n `export const __pyreonIslandRegistry = {};`,\n `export const __pyreonIslandsEnabled = false;`,\n ].join('\\n')\n }\n const entries: string[] = []\n const seen = new Set<string>()\n // Deterministic order: sort by name for stable output / predictable HMR.\n const all = Array.from(registry.values()).flat()\n all.sort((a, b) => a.name.localeCompare(b.name))\n for (const { name, hydrate, loaderAbsPath } of all) {\n if (hydrate === 'never') continue\n if (seen.has(name)) continue\n seen.add(name)\n // JSON.stringify gives proper escaping for both name (object key) and path.\n entries.push(` ${JSON.stringify(name)}: () => import(${JSON.stringify(loaderAbsPath)}),`)\n }\n return [\n `// Auto-generated by @pyreon/vite-plugin (islands: true). Do not edit.`,\n `// Sourced from island() declarations in your project. Never-strategy`,\n `// islands are intentionally omitted — registering a loader for them`,\n `// would defeat the zero-JS contract.`,\n `export const __pyreonIslandRegistry = {`,\n ...entries,\n `};`,\n `export const __pyreonIslandsEnabled = true;`,\n ].join('\\n')\n}\n\n/**\n * Pre-scan all source files in the project for signal exports.\n *\n * Called from `buildStart` so the registry is fully populated before any\n * transforms run. This solves the build ordering problem where component.tsx\n * is transformed before store.ts — without pre-scanning, the registry would\n * be empty and imported signals would not be auto-called.\n */\nasync function prescanSignalExports(root: string, registry: Map<string, Set<string>>): Promise<void> {\n const files: string[] = []\n\n function walk(dir: string) {\n try {\n for (const entry of readdirSync(dir)) {\n if (entry.startsWith('.') || entry === 'node_modules' || entry === 'dist' || entry === 'lib' || entry === 'build') continue\n const full = pathJoin(dir, entry)\n try {\n const stat = statSync(full)\n if (stat.isDirectory()) walk(full)\n else if (/\\.(ts|tsx|js|jsx)$/.test(entry)) files.push(full)\n } catch {\n /* permission error, etc. */\n }\n }\n } catch {\n /* dir doesn't exist */\n }\n }\n\n walk(root)\n\n for (const file of files) {\n try {\n const code = readFileSync(file, 'utf-8')\n scanSignalExports(code, file, registry)\n } catch {\n /* read error */\n }\n }\n}\n\n/**\n * Scan a module's source for exported signal declarations and register them.\n *\n * Detects patterns:\n * 1. `export const x = signal(...)` or `export const x = computed(...)` — inline export\n * 2. `const x = signal(...); export { x }` — separate declaration + named export\n * 3. `export default signal(...)` — default export (tracked as 'default')\n *\n * Re-exports (`export { x } from './signals'`) are NOT detected — the source\n * module must be scanned directly. This is a known limitation.\n *\n * Uses simple regex — no AST parse needed.\n */\n// Bounded `\\s{1,10}` instead of unbounded `\\s+` to remove worst-case\n// backtracking; real import specifiers have 1-2 spaces around `as`.\nconst AS_SPLIT_RE = /\\s{1,10}as\\s{1,10}/\n\nfunction scanSignalExports(code: string, moduleId: string, registry: Map<string, Set<string>>): void {\n const normalizedId = normalizeModuleId(moduleId)\n let match: RegExpExecArray | null\n const signals = new Set<string>()\n\n // Pattern 1: export const x = signal(...) or export const x = computed(...)\n const EXPORT_CONST_RE = /export\\s+const\\s+(\\w+)\\s*=\\s*(?:signal|computed)\\s*[<(]/g\n while ((match = EXPORT_CONST_RE.exec(code)) !== null) {\n signals.add(match[1]!)\n }\n\n // Pattern 2: const x = signal(...) followed by export { x }\n // First, find all local `const x = signal(` or `const x = computed(` declarations\n const localSignals = new Set<string>()\n const LOCAL_SIGNAL_RE = /(?:^|[\\s;])const\\s+(\\w+)\\s*=\\s*(?:signal|computed)\\s*[<(]/gm\n while ((match = LOCAL_SIGNAL_RE.exec(code)) !== null) {\n localSignals.add(match[1]!)\n }\n\n // Then check named exports: export { x, y as z }\n if (localSignals.size > 0) {\n // Bounded `[^}]{1,500}` — real export blocks fit easily.\n const NAMED_EXPORT_RE = /export\\s*\\{([^}]{1,500})\\}/g\n while ((match = NAMED_EXPORT_RE.exec(code)) !== null) {\n // Skip re-exports (export { x } from '...')\n const afterBrace = code.slice(match.index + match[0].length).trimStart()\n if (afterBrace.startsWith('from')) continue\n\n for (const spec of match[1]!.split(',')) {\n const trimmed = spec.trim()\n if (!trimmed) continue\n const parts = trimmed.split(AS_SPLIT_RE)\n const localName = parts[0]!.trim()\n const exportedName = (parts[1] ?? parts[0])!.trim()\n if (localSignals.has(localName)) {\n signals.add(exportedName)\n }\n }\n }\n }\n\n // Pattern 3: export default signal(...) or export default computed(...) — tracked as 'default'\n if (/export\\s+default\\s+(?:signal|computed)\\s*[<(]/.test(code)) {\n signals.add('default')\n }\n\n if (signals.size > 0) {\n registry.set(normalizedId, signals)\n } else {\n // Clean up if module no longer exports signals (e.g. after edit)\n registry.delete(normalizedId)\n }\n}\n\n/**\n * Resolve imported signal names from the signal export registry.\n *\n * For each import in the source, resolves the module and checks if it has\n * signal exports in the registry. Returns the local names of imported signals.\n *\n * Handles named imports (`import { x } from ...`) and default imports\n * (`import x from ...` — matched against 'default' in the registry).\n */\nasync function resolveImportedSignals(\n code: string,\n _moduleId: string,\n registry: Map<string, Set<string>>,\n pluginCtx: { resolve: (id: string, importer?: string, options?: { skipSelf: boolean }) => Promise<{ id: string } | null> },\n resolveCache: Map<string, string | null>,\n): Promise<string[]> {\n if (registry.size === 0) return []\n\n const knownSignals: string[] = []\n let match: RegExpExecArray | null\n\n /** Resolve a source specifier to a normalized module ID, using the cache. */\n async function resolveSource(source: string): Promise<string | null> {\n const cacheKey = `${_moduleId}::${source}`\n if (resolveCache.has(cacheKey)) return resolveCache.get(cacheKey) ?? null\n let resolvedId: string | null = null\n try {\n const resolved = await pluginCtx.resolve(source, _moduleId, { skipSelf: true })\n resolvedId = resolved?.id ? normalizeModuleId(resolved.id) : null\n } catch {\n /* resolve error */\n }\n resolveCache.set(cacheKey, resolvedId)\n return resolvedId\n }\n\n // Named imports: import { name1, name2 as alias } from 'source'\n // Excludes `import type { ... }` — type-only imports have no runtime value\n const IMPORT_RE = /import\\s+(?!type\\s)\\{([^}]+)\\}\\s*from\\s*['\"]([^'\"]+)['\"]/g\n while ((match = IMPORT_RE.exec(code)) !== null) {\n const specifiers = match[1]!\n const source = match[2]!\n\n const resolvedId = await resolveSource(source)\n if (!resolvedId) continue\n const exportedSignals = registry.get(resolvedId)\n if (!exportedSignals) continue\n\n // Parse import specifiers: \"count, theme as t, other\"\n for (const spec of specifiers.split(',')) {\n const trimmed = spec.trim()\n if (!trimmed) continue\n\n const parts = trimmed.split(AS_SPLIT_RE)\n const importedName = parts[0]!.trim()\n const localName = (parts[1] ?? parts[0])!.trim()\n\n if (exportedSignals.has(importedName)) {\n knownSignals.push(localName)\n }\n }\n }\n\n // Default imports: import count from './store'\n // Excludes: `import { ... }`, `import type X`, `import * as X`\n const DEFAULT_IMPORT_RE = /import\\s+(?!type\\s)(\\w+)\\s+from\\s*['\"]([^'\"]+)['\"]/g\n while ((match = DEFAULT_IMPORT_RE.exec(code)) !== null) {\n // Skip if this is actually a `import type X from` pattern\n const fullMatch = match[0]\n if (/import\\s+type\\s+/.test(fullMatch)) continue\n\n const localName = match[1]!\n const source = match[2]!\n\n const resolvedId = await resolveSource(source)\n if (!resolvedId) continue\n const exportedSignals = registry.get(resolvedId)\n if (!exportedSignals) continue\n\n if (exportedSignals.has('default')) {\n knownSignals.push(localName)\n }\n }\n\n return knownSignals\n}\n\nconst HMR_RUNTIME_SOURCE = `\nconst REGISTRY_KEY = \"__pyreon_hmr_registry__\";\n\nfunction getRegistry() {\n if (!globalThis[REGISTRY_KEY]) {\n globalThis[REGISTRY_KEY] = new Map();\n }\n return globalThis[REGISTRY_KEY];\n}\n\nconst moduleSignals = new Map();\n\nexport function __hmr_signal(moduleId, name, signalFn, initialValue) {\n const registry = getRegistry();\n const saved = registry.get(moduleId);\n const value = saved?.has(name) ? saved.get(name) : initialValue;\n const s = signalFn(value, { name: name });\n\n let mod = moduleSignals.get(moduleId);\n if (!mod) {\n mod = { entries: new Map() };\n moduleSignals.set(moduleId, mod);\n }\n mod.entries.set(name, s);\n\n return s;\n}\n\nexport function __hmr_dispose(moduleId) {\n const mod = moduleSignals.get(moduleId);\n if (!mod) return;\n\n const registry = getRegistry();\n const saved = new Map();\n for (const [name, s] of mod.entries) {\n saved.set(name, s.peek());\n }\n registry.set(moduleId, saved);\n moduleSignals.delete(moduleId);\n}\n`\n"],"mappings":"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"}
@@ -1,4 +1,29 @@
1
+ import { withSilent } from "@pyreon/reactivity";
2
+
1
3
  //#region src/rocketstyle-collapse.ts
4
+ /**
5
+ * P0 — build-time rocketstyle-collapse resolver.
6
+ *
7
+ * For a collapsible call site (`<Button state="primary" size="md">Save</Button>`
8
+ * — every dimension prop a string literal, children static text) this
9
+ * resolves the FULL rocketstyle/styler pipeline ONCE by SSR-rendering the
10
+ * REAL component, light AND dark, and returns: the resolved styler class
11
+ * per mode, the styler rule text, and a class-stripped `_tpl` template.
12
+ *
13
+ * The render runs through a programmatic Vite SSR server bound to the
14
+ * CONSUMER's own `vite.config` — so module resolution is identical to
15
+ * the app's real build (workspace `bun` condition, app aliases,
16
+ * app-local relative imports, whatever). Parity with the runtime-mounted
17
+ * class is then guaranteed BY CONSTRUCTION: it is literally the same
18
+ * `renderToString` + `@pyreon/styler` code path the client uses, and
19
+ * styler's FNV-1a class hashing is identical in SSR and DOM (styler's
20
+ * hydration contract). No reimplementation, no closure re-execution, no
21
+ * drift (RFC decision 2).
22
+ *
23
+ * Every failure returns `null` (graceful bail → the call site keeps its
24
+ * normal rocketstyle mount). Correct-but-slow is acceptable; wrong
25
+ * output is not.
26
+ */
2
27
  function fnv1a(str) {
3
28
  let h = 2166136261;
4
29
  for (let i = 0; i < str.length; i++) {
@@ -44,7 +69,7 @@ function deriveCollapse(lightHtml, darkHtml, rules) {
44
69
  */
45
70
  async function createCollapseResolver(projectRoot) {
46
71
  const { createServer } = await import("vite");
47
- let server = await createServer({
72
+ const inline = {
48
73
  root: projectRoot,
49
74
  server: { middlewareMode: true },
50
75
  appType: "custom",
@@ -53,10 +78,11 @@ async function createCollapseResolver(projectRoot) {
53
78
  noDiscovery: true,
54
79
  include: []
55
80
  }
56
- });
81
+ };
82
+ let server = await withSilent(() => createServer(inline));
57
83
  const cache = /* @__PURE__ */ new Map();
58
84
  async function load(spec) {
59
- return await server.ssrLoadModule(spec);
85
+ return withSilent(() => server.ssrLoadModule(spec));
60
86
  }
61
87
  return {
62
88
  async resolve(input) {
@@ -110,4 +136,4 @@ async function createCollapseResolver(projectRoot) {
110
136
 
111
137
  //#endregion
112
138
  export { createCollapseResolver };
113
- //# sourceMappingURL=rocketstyle-collapse-C4eMAnwR.js.map
139
+ //# sourceMappingURL=rocketstyle-collapse-DGnwgDhC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rocketstyle-collapse-DGnwgDhC.js","names":[],"sources":["../src/rocketstyle-collapse.ts"],"sourcesContent":["/**\n * P0 — build-time rocketstyle-collapse resolver.\n *\n * For a collapsible call site (`<Button state=\"primary\" size=\"md\">Save</Button>`\n * — every dimension prop a string literal, children static text) this\n * resolves the FULL rocketstyle/styler pipeline ONCE by SSR-rendering the\n * REAL component, light AND dark, and returns: the resolved styler class\n * per mode, the styler rule text, and a class-stripped `_tpl` template.\n *\n * The render runs through a programmatic Vite SSR server bound to the\n * CONSUMER's own `vite.config` — so module resolution is identical to\n * the app's real build (workspace `bun` condition, app aliases,\n * app-local relative imports, whatever). Parity with the runtime-mounted\n * class is then guaranteed BY CONSTRUCTION: it is literally the same\n * `renderToString` + `@pyreon/styler` code path the client uses, and\n * styler's FNV-1a class hashing is identical in SSR and DOM (styler's\n * hydration contract). No reimplementation, no closure re-execution, no\n * drift (RFC decision 2).\n *\n * Every failure returns `null` (graceful bail → the call site keeps its\n * normal rocketstyle mount). Correct-but-slow is acceptable; wrong\n * output is not.\n */\nimport { withSilent } from '@pyreon/reactivity'\nimport type { InlineConfig, ViteDevServer } from 'vite'\n\n// Inline FNV-1a (same algorithm as @pyreon/styler/hash) — avoids pulling\n// the styler module graph into the vite-plugin's cheap entry path.\nfunction fnv1a(str: string): string {\n let h = 2166136261\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i)\n h = Math.imul(h, 16777619)\n }\n return (h >>> 0).toString(36)\n}\n\nexport interface CollapseImportSpec {\n /** Imported binding name, e.g. `PyreonUI` / `theme` / `useMode`. */\n name: string\n /** Module specifier, e.g. `@pyreon/ui-core` / `@pyreon/ui-theme`. */\n source: string\n}\n\nexport interface CollapseConfig {\n /** Theme/mode provider component. Default: PyreonUI from @pyreon/ui-core. */\n provider: CollapseImportSpec\n /** Theme object. Default: theme from @pyreon/ui-theme. */\n theme: CollapseImportSpec\n /** Live mode accessor — emitted into the collapsed site for dual-emit. */\n mode: CollapseImportSpec\n}\n\nexport const DEFAULT_COLLAPSE_CONFIG: CollapseConfig = {\n provider: { name: 'PyreonUI', source: '@pyreon/ui-core' },\n theme: { name: 'theme', source: '@pyreon/ui-theme' },\n mode: { name: 'useMode', source: '@pyreon/ui-core' },\n}\n\nexport interface ResolveInput {\n /** The collapsible component's import. */\n component: CollapseImportSpec\n /** Literal dimension/HTML props, e.g. `{ state: 'primary', size: 'md' }`. */\n props: Record<string, string>\n /** Static text children (empty ⇒ no children). */\n childrenText: string\n config: CollapseConfig\n}\n\nexport interface ResolvedCollapse {\n /** Element HTML with the root `class=\"...\"` removed (the `_tpl` template). */\n templateHtml: string\n lightClass: string\n darkClass: string\n /** Pre-resolved styler rule text (full snapshot) for `injectRules`. */\n rules: string[]\n /** FNV over the rule set — `injectRules` idempotency + cross-site dedupe. */\n key: string\n}\n\nconst FIRST_CLASS_RE = /^(\\s*<[a-zA-Z][\\w-]*)([^>]*?)\\sclass=\"([^\"]*)\"([^>]*>)/\n\n/** Strip the FIRST element's `class=\"...\"`, returning [stripped, class]. */\nexport function stripRootClass(html: string): { stripped: string; cls: string } | null {\n const m = FIRST_CLASS_RE.exec(html)\n if (!m) return null\n const stripped = html.replace(FIRST_CLASS_RE, '$1$2$4')\n return { stripped, cls: m[3] ?? '' }\n}\n\n/**\n * Pure extraction half — given the two rendered HTML strings and the\n * styler rule snapshot, derive the ResolvedCollapse (or null on a shape\n * the slice doesn't collapse). Separated for direct unit-testing without\n * spinning Vite.\n */\nexport function deriveCollapse(\n lightHtml: string,\n darkHtml: string,\n rules: string[],\n): ResolvedCollapse | null {\n const light = stripRootClass(lightHtml)\n const dark = stripRootClass(darkHtml)\n if (!light || !dark || !light.cls || !dark.cls) return null\n // The structural template must be identical between modes (only the\n // class differs). Divergent markup ⇒ not a simple single-root\n // collapsible — bail.\n if (light.stripped !== dark.stripped) return null\n return {\n templateHtml: light.stripped.trim(),\n lightClass: light.cls,\n darkClass: dark.cls,\n rules,\n key: fnv1a(rules.join('\\u0000')),\n }\n}\n\nexport interface CollapseResolver {\n resolve(input: ResolveInput): Promise<ResolvedCollapse | null>\n dispose(): Promise<void>\n}\n\n/**\n * Create a resolver backed by ONE programmatic Vite SSR server bound to\n * `projectRoot`'s vite config. Reused across every call site in a build;\n * `dispose()` at buildEnd. Module loads are cached by Vite's own SSR\n * module graph (provider/theme/component import once).\n */\nexport async function createCollapseResolver(projectRoot: string): Promise<CollapseResolver> {\n const { createServer } = (await import('vite')) as typeof import('vite')\n const inline: InlineConfig = {\n // No `configFile` override — Vite auto-loads the project's own\n // vite.config from `root`, so module resolution (workspace `bun`\n // condition, app aliases) matches the real build exactly.\n root: projectRoot,\n server: { middlewareMode: true },\n appType: 'custom',\n logLevel: 'silent',\n optimizeDeps: { noDiscovery: true, include: [] },\n }\n // `createServer` evaluates the consumer's vite.config + plugin chain,\n // which may itself load `@pyreon/*` packages via the `node` condition\n // (different path than the outer process's `bun`-conditioned `src/`).\n // That's a legitimate dual-load — scope the opt-out across the entire\n // server lifecycle since every `load(spec)` below also touches the\n // dual graph.\n let server: ViteDevServer | null = await withSilent(() => createServer(inline))\n\n // Resolved-bundle cache — identical input must hit the same result\n // without a second double-render (deterministic by construction).\n const cache = new Map<string, ResolvedCollapse | null>()\n\n async function load(spec: string): Promise<Record<string, unknown>> {\n // The nested Vite SSR server loads its own copy of @pyreon/* packages for\n // the SSR snapshot. This is a legitimate dual-load — the outer process has\n // its own @pyreon/* graph; the nested server has its own. `withSilent`\n // from @pyreon/reactivity scopes the sentinel opt-out via a refcount\n // (race-safe under concurrency; the prior env-var dance leaked `silent`\n // permanently when N opt-out scopes overlapped — see withSilent JSDoc).\n return withSilent(\n () => server!.ssrLoadModule(spec) as Promise<Record<string, unknown>>,\n )\n }\n\n return {\n async resolve(input) {\n const ck = JSON.stringify([\n input.component,\n input.props,\n input.childrenText,\n input.config.provider,\n input.config.theme,\n ])\n if (cache.has(ck)) return cache.get(ck) ?? null\n try {\n if (!server) return null\n const rs = await load('@pyreon/runtime-server')\n const core = await load('@pyreon/core')\n const styler = await load('@pyreon/styler')\n const prov = await load(input.config.provider.source)\n const thm = await load(input.config.theme.source)\n const comp = await load(input.component.source)\n const renderToString = rs.renderToString as (n: unknown) => Promise<string>\n const h = core.h as (t: unknown, p: unknown, ...c: unknown[]) => unknown\n const Provider = prov[input.config.provider.name]\n const themeVal = thm[input.config.theme.name]\n const Component = comp[input.component.name]\n const sheet = styler.sheet as { getStyleRules(): readonly string[] }\n if (typeof Component !== 'function' || Provider == null || themeVal == null) {\n cache.set(ck, null)\n return null\n }\n const childArgs = input.childrenText ? [input.childrenText] : []\n const node = (mode: string) =>\n h(Provider, { theme: themeVal, mode }, h(Component, input.props, ...childArgs))\n const lightHtml = await renderToString(node('light'))\n const darkHtml = await renderToString(node('dark'))\n const rules = sheet.getStyleRules().slice()\n const result = deriveCollapse(lightHtml, darkHtml, rules)\n cache.set(ck, result)\n return result\n } catch {\n cache.set(ck, null)\n return null\n }\n },\n async dispose() {\n const s = server\n server = null\n cache.clear()\n if (s) await s.close()\n },\n }\n}\n"],"mappings":"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"}
@@ -146,6 +146,19 @@ interface PyreonLpihOptions {
146
146
  */
147
147
  cachePath?: string;
148
148
  }
149
+ /**
150
+ * Truthy env-var parser. Accepts `1` / `true` / `yes` / `on`
151
+ * (case-insensitive). Returns `false` for `undefined`, empty string,
152
+ * `0`, `false`, `no`, `off`, or any unrecognized value.
153
+ *
154
+ * Exported via `_internal` for unit-testability — the previous strict
155
+ * `=== '1'` shape caught a user reporting `PYREON_DISABLE_DEDUPE=true`
156
+ * silently not working. Env-var hatches are usually reached for under
157
+ * stress; rejecting alternatives is exactly the wrong moment to be strict.
158
+ *
159
+ * @internal
160
+ */
161
+ declare function _isTruthyEnv(v: string | undefined): boolean;
149
162
  declare function pyreonPlugin(options?: PyreonPluginOptions): Plugin;
150
163
  /**
151
164
  * Resolve the LPIH cache-file path for a given project root. Matches the
@@ -231,5 +244,5 @@ declare function _offsetToLineCol(offset: number, lineStarts: number[]): {
231
244
  col: number;
232
245
  };
233
246
  //#endregion
234
- export { CompatFramework, PyreonCollapseOptions, PyreonLpihOptions, PyreonPluginOptions, _computeLineStarts, _maskStringsAndComments, _offsetToLineCol, buildLpihClientScript, pyreonPlugin as default, registerLpihMiddleware, resolveLpihCachePath, writeLpihCacheFile };
247
+ export { CompatFramework, PyreonCollapseOptions, PyreonLpihOptions, PyreonPluginOptions, _computeLineStarts, _isTruthyEnv, _maskStringsAndComments, _offsetToLineCol, buildLpihClientScript, pyreonPlugin as default, registerLpihMiddleware, resolveLpihCachePath, writeLpihCacheFile };
235
248
  //# sourceMappingURL=index2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/index.ts"],"mappings":";;;KA4EY,eAAA;AAAA,UAEK,mBAAA;EAsHf;;;;;;;AAE6B;AAG/B;;;;AAiBW;EA9HT,MAAA,GAAS,eAAA;EA8UiB;;;AAAsB;AAIjD;;;;;;EAtUC,GAAA;IAyUyE,4DAvUvE,KAAA;EAAA;EAm7BgC;;;AAAoB;AAaxD;;;;;;;;;;AAG4B;AAoD5B;;;;;;;;AAA6E;AAsD7E;EAjhCE,OAAA;;;AAihCsD;AA4UxD;;;;AAAoD;AAuHpD;;;;AAA+C;AAc/C;;;;;;;;;AAGsB;EA58CpB,IAAA,aAAiB,iBAAA;;;;;;;;;;;;;;;EAgBjB,QAAA,aAAqB,qBAAA;AAAA;AAAA,UAGN,qBAAA;;;;;;;;EAQf,OAAA;;;;;;EAMA,UAAA;;EAEA,QAAA;IAAa,IAAA;IAAc,MAAA;EAAA;;EAE3B,KAAA;IAAU,IAAA;IAAc,MAAA;EAAA;;EAExB,IAAA;IAAS,IAAA;IAAc,MAAA;EAAA;AAAA;AAAA,UAGR,iBAAA;;;;;;;;;;EAUf,UAAA;;;;;;;EAOA,SAAS;AAAA;;;;;;;;;;;;;iBAgNK,YAAA,CAAa,CAAqB;AAAA,iBAO1B,YAAA,CAAa,OAAA,GAAU,mBAAA,GAAsB,MAAM;;;;;;;;iBA4mB3D,oBAAA,CAAqB,WAAmB;;;;;;;;;;iBAaxC,sBAAA,CACd,MAAA,EAAQ,aAAA,EACR,WAAA,UACA,OAAA,EAAS,iBAAiB;;;;;;;;;;iBAoDN,kBAAA,CAAmB,IAAA,UAAc,IAAA,WAAe,OAAO;;;;;;;;;;;;;iBAsD7D,qBAAA,CAAsB,UAAkB;;;;;;;;;;;;;;;;;;;;;;;;iBA4UxC,uBAAA,CAAwB,IAAY;;;;;;;;iBAuHpC,kBAAA,CAAmB,IAAY;;;;;;;iBAc/B,gBAAA,CACd,MAAA,UACA,UAAA;EACG,IAAA;EAAc,GAAA;AAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/vite-plugin",
3
- "version": "0.24.6",
3
+ "version": "0.25.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": {
@@ -14,7 +14,6 @@
14
14
  },
15
15
  "files": [
16
16
  "lib",
17
- "!lib/**/*.map",
18
17
  "README.md",
19
18
  "LICENSE"
20
19
  ],
@@ -41,7 +40,8 @@
41
40
  "prepublishOnly": "bun run build"
42
41
  },
43
42
  "dependencies": {
44
- "@pyreon/compiler": "^0.24.6"
43
+ "@pyreon/compiler": "^0.25.1",
44
+ "@pyreon/reactivity": "^0.25.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "vite": "^8.0.0"