@pyreon/vite-plugin 0.14.0 → 0.16.0

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.
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Auto-discovered island-registry tests for @pyreon/vite-plugin.
3
+ *
4
+ * Exercises the `pyreon({ islands: true })` path:
5
+ * 1. Materialize synthetic source files containing `island()` calls
6
+ * 2. Drive plugin.config() + plugin.buildStart() to populate the registry
7
+ * 3. Drive plugin.load('\0pyreon/islands-registry') + assert the emitted
8
+ * source contains the expected loader entries (and excludes
9
+ * hydrate: 'never' islands)
10
+ *
11
+ * Companion to `cross-module-signals.test.ts` — same harness shape.
12
+ */
13
+
14
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
15
+ import { tmpdir } from 'node:os'
16
+ import { join } from 'node:path'
17
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'
18
+ import pyreonPlugin, { type PyreonPluginOptions } from '../index'
19
+
20
+ type ConfigHook = (
21
+ userConfig: Record<string, unknown>,
22
+ env: { command: string; isSsrBuild?: boolean },
23
+ ) => Record<string, unknown>
24
+
25
+ type BuildStartHook = (this: unknown) => Promise<void>
26
+ type LoadHook = (id: string) => string | undefined
27
+ type ResolveIdHook = (
28
+ this: unknown,
29
+ id: string,
30
+ importer?: string,
31
+ ) => Promise<string | null | undefined>
32
+
33
+ let root: string
34
+
35
+ beforeAll(() => {
36
+ root = mkdtempSync(join(tmpdir(), 'pyreon-islands-registry-'))
37
+ })
38
+ afterAll(() => {
39
+ rmSync(root, { recursive: true, force: true })
40
+ })
41
+ beforeEach(() => {
42
+ rmSync(root, { recursive: true, force: true })
43
+ mkdirSync(root, { recursive: true })
44
+ })
45
+
46
+ function writeFile(rel: string, contents: string): string {
47
+ const full = join(root, rel)
48
+ const dir = full.slice(0, full.lastIndexOf('/'))
49
+ mkdirSync(dir, { recursive: true })
50
+ writeFileSync(full, contents)
51
+ return full
52
+ }
53
+
54
+ function bootstrap(opts?: PyreonPluginOptions) {
55
+ const plugin = pyreonPlugin(opts)
56
+ ;(plugin.config as unknown as ConfigHook)({ root }, { command: 'build' })
57
+ return plugin
58
+ }
59
+
60
+ async function runBuildStart(plugin: ReturnType<typeof pyreonPlugin>) {
61
+ const buildStart = plugin.buildStart as BuildStartHook
62
+ await buildStart.call({})
63
+ }
64
+
65
+ function runLoad(plugin: ReturnType<typeof pyreonPlugin>, id: string): string {
66
+ const result = (plugin.load as LoadHook)(id)
67
+ if (typeof result !== 'string') {
68
+ throw new Error(`load('${id}') returned ${typeof result}, expected string`)
69
+ }
70
+ return result
71
+ }
72
+
73
+ async function runResolveId(
74
+ plugin: ReturnType<typeof pyreonPlugin>,
75
+ id: string,
76
+ ): Promise<string | null | undefined> {
77
+ return (plugin.resolveId as ResolveIdHook).call({}, id)
78
+ }
79
+
80
+ const ISLANDS_REGISTRY_IMPORT = 'virtual:pyreon/islands-registry'
81
+ const ISLANDS_REGISTRY_ID = '\0pyreon/islands-registry'
82
+
83
+ describe('vite-plugin — islands virtual module', () => {
84
+ it('resolveId redirects virtual:pyreon/islands-registry to the \\0-prefixed id', async () => {
85
+ const plugin = bootstrap()
86
+ expect(await runResolveId(plugin, ISLANDS_REGISTRY_IMPORT)).toBe(ISLANDS_REGISTRY_ID)
87
+ })
88
+
89
+ it('emits an empty registry when no island() calls exist', async () => {
90
+ writeFile('src/App.tsx', `export const App = () => null`)
91
+ const plugin = bootstrap()
92
+ await runBuildStart(plugin)
93
+ const source = runLoad(plugin, ISLANDS_REGISTRY_ID)
94
+ expect(source).toContain('__pyreonIslandsEnabled = true')
95
+ expect(source).toContain('__pyreonIslandRegistry = {')
96
+ // No entries beyond the opening and closing braces
97
+ expect(source).not.toMatch(/import\(.+\)/)
98
+ })
99
+
100
+ it('discovers `island(() => import("./X"), { name, hydrate: "load" })` calls', async () => {
101
+ writeFile(
102
+ 'src/islands.ts',
103
+ `import { island } from '@pyreon/server'
104
+ export const Counter = island(() => import('./components/Counter'), {
105
+ name: 'Counter',
106
+ hydrate: 'load',
107
+ })`,
108
+ )
109
+ const plugin = bootstrap()
110
+ await runBuildStart(plugin)
111
+ const source = runLoad(plugin, ISLANDS_REGISTRY_ID)
112
+ expect(source).toContain('"Counter":')
113
+ // Loader path was resolved relative to the file where the call lives
114
+ expect(source).toContain(`/components/Counter`)
115
+ })
116
+
117
+ it('omits hydrate: "never" islands from the registry', async () => {
118
+ writeFile(
119
+ 'src/islands.ts',
120
+ `import { island } from '@pyreon/server'
121
+ export const Counter = island(() => import('./Counter'), { name: 'Counter', hydrate: 'load' })
122
+ export const StaticBadge = island(() => import('./StaticBadge'), { name: 'StaticBadge', hydrate: 'never' })`,
123
+ )
124
+ const plugin = bootstrap()
125
+ await runBuildStart(plugin)
126
+ const source = runLoad(plugin, ISLANDS_REGISTRY_ID)
127
+ expect(source).toContain('"Counter":')
128
+ expect(source).not.toContain('"StaticBadge":')
129
+ expect(source).not.toContain('StaticBadge')
130
+ })
131
+
132
+ it('handles `media(...)` and `interaction` strategy strings without omitting them', async () => {
133
+ writeFile(
134
+ 'src/islands.ts',
135
+ `import { island } from '@pyreon/server'
136
+ export const Mobile = island(() => import('./Mobile'), { name: 'Mobile', hydrate: 'media((max-width: 768px))' })
137
+ export const Idle = island(() => import('./Idle'), { name: 'Idle', hydrate: 'idle' })
138
+ export const Visible = island(() => import('./Visible'), { name: 'Visible', hydrate: 'visible' })`,
139
+ )
140
+ const plugin = bootstrap()
141
+ await runBuildStart(plugin)
142
+ const source = runLoad(plugin, ISLANDS_REGISTRY_ID)
143
+ expect(source).toContain('"Mobile":')
144
+ expect(source).toContain('"Idle":')
145
+ expect(source).toContain('"Visible":')
146
+ })
147
+
148
+ it('discovers island() calls across multiple source files', async () => {
149
+ writeFile(
150
+ 'src/foo/A.ts',
151
+ `import { island } from '@pyreon/server'
152
+ export const A = island(() => import('./component'), { name: 'A', hydrate: 'load' })`,
153
+ )
154
+ writeFile(
155
+ 'src/bar/B.ts',
156
+ `import { island } from '@pyreon/server'
157
+ export const B = island(() => import('./component'), { name: 'B', hydrate: 'idle' })`,
158
+ )
159
+ const plugin = bootstrap()
160
+ await runBuildStart(plugin)
161
+ const source = runLoad(plugin, ISLANDS_REGISTRY_ID)
162
+ expect(source).toContain('"A":')
163
+ expect(source).toContain('"B":')
164
+ })
165
+
166
+ it('skips node_modules / dist / lib / build during the prescan walk', async () => {
167
+ writeFile(
168
+ 'node_modules/some-pkg/island.ts',
169
+ `island(() => import('./X'), { name: 'IgnoreMe', hydrate: 'load' })`,
170
+ )
171
+ writeFile(
172
+ 'dist/build-output.ts',
173
+ `island(() => import('./X'), { name: 'AlsoIgnoreMe', hydrate: 'load' })`,
174
+ )
175
+ writeFile(
176
+ 'src/Real.ts',
177
+ `import { island } from '@pyreon/server'
178
+ export const Real = island(() => import('./X'), { name: 'Real', hydrate: 'load' })`,
179
+ )
180
+ const plugin = bootstrap()
181
+ await runBuildStart(plugin)
182
+ const source = runLoad(plugin, ISLANDS_REGISTRY_ID)
183
+ expect(source).toContain('"Real":')
184
+ expect(source).not.toContain('"IgnoreMe":')
185
+ expect(source).not.toContain('"AlsoIgnoreMe":')
186
+ })
187
+
188
+ it('emits a stub registry when islands: false is set', async () => {
189
+ writeFile(
190
+ 'src/islands.ts',
191
+ `import { island } from '@pyreon/server'
192
+ export const Counter = island(() => import('./Counter'), { name: 'Counter', hydrate: 'load' })`,
193
+ )
194
+ const plugin = bootstrap({ islands: false })
195
+ await runBuildStart(plugin)
196
+ const source = runLoad(plugin, ISLANDS_REGISTRY_ID)
197
+ // Stub flips the enabled flag so hydrateIslandsAuto() throws at runtime
198
+ // with a clear message — better than a silent empty registry.
199
+ expect(source).toContain('__pyreonIslandsEnabled = false')
200
+ expect(source).not.toContain('"Counter":')
201
+ })
202
+
203
+ it('deduplicates duplicate names (last-wins order)', async () => {
204
+ writeFile(
205
+ 'src/a.ts',
206
+ `import { island } from '@pyreon/server'
207
+ export const A = island(() => import('./a-comp'), { name: 'Same', hydrate: 'load' })`,
208
+ )
209
+ writeFile(
210
+ 'src/b.ts',
211
+ `import { island } from '@pyreon/server'
212
+ export const B = island(() => import('./b-comp'), { name: 'Same', hydrate: 'load' })`,
213
+ )
214
+ const plugin = bootstrap()
215
+ await runBuildStart(plugin)
216
+ const source = runLoad(plugin, ISLANDS_REGISTRY_ID)
217
+ // Only one entry for "Same" emitted — registry can't have duplicate keys.
218
+ const matches = source.match(/"Same":/g) ?? []
219
+ expect(matches).toHaveLength(1)
220
+ })
221
+
222
+ it('skips island() calls without a name field (auto-registry has nothing to key on)', async () => {
223
+ writeFile(
224
+ 'src/islands.ts',
225
+ `import { island } from '@pyreon/server'
226
+ // Anomaly: island() without a name option. Auto-registry can't include this.
227
+ export const Bad = island(() => import('./X'), { hydrate: 'load' } as any)
228
+ export const Good = island(() => import('./Y'), { name: 'Good', hydrate: 'load' })`,
229
+ )
230
+ const plugin = bootstrap()
231
+ await runBuildStart(plugin)
232
+ const source = runLoad(plugin, ISLANDS_REGISTRY_ID)
233
+ expect(source).toContain('"Good":')
234
+ expect(source).not.toContain('"Bad":')
235
+ })
236
+ })
@@ -5,6 +5,8 @@
5
5
  * These test the plugin's transform logic directly (no Vite required).
6
6
  */
7
7
 
8
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
9
+ import { join as pathJoin } from 'node:path'
8
10
  import { describe, expect, it } from 'vitest'
9
11
 
10
12
  // ── Import internals ─────────────────────────────────────────────────────────
@@ -133,6 +135,32 @@ export function App() { return null }
133
135
  expect(result!.code).toContain('__hmr_signal("/src/theme.tsx", "theme", signal, "light")')
134
136
  })
135
137
 
138
+ it('rewrites generic-typed signals (signal<T>(value))', async () => {
139
+ // Regression for the silent-skip bug: SIGNAL_PREFIX_RE used to match
140
+ // `signal(` but not `signal<T>(`. Pre-rewrite TypeScript still has type
141
+ // parameters; declarations like `signal<string>('')` would skip HMR
142
+ // preservation silently and produce an empty-string-valued signal that
143
+ // — under a separate `__hmr_signal` interaction — could read as
144
+ // undefined. Discovered via PR #329 (perf-dashboard form section).
145
+ const plugin = createPlugin()
146
+ const code = `
147
+ import { signal } from "@pyreon/reactivity"
148
+ export const password = signal<string>("")
149
+ export const items = signal<Array<{ id: number }>>([])
150
+ export const count = signal<number>(0)
151
+ export function App() { return null }
152
+ `
153
+ const result = await transform(plugin, code, '/src/state.tsx')
154
+ expect(result).toBeDefined()
155
+ expect(result!.code).toContain(
156
+ '__hmr_signal("/src/state.tsx", "password", signal, "")',
157
+ )
158
+ expect(result!.code).toContain(
159
+ '__hmr_signal("/src/state.tsx", "items", signal, [])',
160
+ )
161
+ expect(result!.code).toContain('__hmr_signal("/src/state.tsx", "count", signal, 0)')
162
+ })
163
+
136
164
  it('does not rewrite signal() inside functions to __hmr_signal (but injects name)', async () => {
137
165
  const plugin = createPlugin()
138
166
  const code = `
@@ -299,12 +327,19 @@ describe('plugin config', () => {
299
327
  expect(config.oxc.jsx.importSource).toBe('@pyreon/core')
300
328
  })
301
329
 
302
- it('sets JSX import source to compat package in compat mode', async () => {
330
+ it('keeps JSX import source as @pyreon/core in compat mode', async () => {
331
+ // OXC's `importSource` is project-wide (one setting for the whole build),
332
+ // so pointing it at the compat package would force the compat runtime
333
+ // on `@pyreon/*` framework files too — which they cannot handle. Instead
334
+ // the plugin keeps OXC at `@pyreon/core` and redirects the resulting
335
+ // `@pyreon/core/jsx-runtime` import to the compat package via `resolveId`,
336
+ // gated on the importer (user code only). See `compat-resolve.test.ts`
337
+ // "framework-importer carve-out". Caught by `cpa-smoke-app-*-compat`.
303
338
  const plugin = pyreonPlugin({ compat: 'react' })
304
339
  const config = getConfigHook(plugin)({}, { command: 'serve' }) as {
305
340
  oxc: { jsx: { importSource: string } }
306
341
  }
307
- expect(config.oxc.jsx.importSource).toBe('@pyreon/react-compat')
342
+ expect(config.oxc.jsx.importSource).toBe('@pyreon/core')
308
343
  })
309
344
 
310
345
  it('excludes compat packages from optimizeDeps', async () => {
@@ -316,6 +351,85 @@ describe('plugin config', () => {
316
351
  expect(config.optimizeDeps.exclude).toContain('react-dom')
317
352
  })
318
353
 
354
+ // Regression: pre-fix, the plugin's `bun` resolve condition redirected
355
+ // every `@pyreon/*` import to source `.ts(x)` files. In a non-monorepo
356
+ // consumer app, Vite's deps optimizer (esbuild) tried to pre-bundle
357
+ // those packages from `node_modules` and silently produced broken
358
+ // bundles in `.vite/deps/`, surfacing as
359
+ // `File does not exist at .../node_modules/.vite/deps/@pyreon_styler.js`
360
+ // at runtime. Fix scans the consumer's package.json for `@pyreon/*`
361
+ // deps and adds them to optimizeDeps.exclude so the optimizer skips
362
+ // them (resolution then goes through the plugin's own resolveId hook
363
+ // and Vite's normal source pipeline).
364
+ it("auto-excludes consumer's @pyreon/* deps from optimizeDeps (Vite optimizer fix)", async () => {
365
+ // Build a fake consumer package.json with a few @pyreon/* deps.
366
+ const tmpRoot = pathJoin(import.meta.dirname, 'fixtures', 'pyreon-deps-consumer')
367
+ rmSync(tmpRoot, { recursive: true, force: true })
368
+ mkdirSync(tmpRoot, { recursive: true })
369
+ writeFileSync(
370
+ pathJoin(tmpRoot, 'package.json'),
371
+ JSON.stringify({
372
+ name: 'fake-consumer',
373
+ dependencies: {
374
+ '@pyreon/core': '^0.15.0',
375
+ '@pyreon/styler': '^0.15.0',
376
+ '@pyreon/runtime-dom': '^0.15.0',
377
+ // Non-@pyreon dep MUST NOT leak into the exclude list.
378
+ react: '^19.0.0',
379
+ },
380
+ devDependencies: {
381
+ '@pyreon/vite-plugin': '^0.15.0',
382
+ },
383
+ }),
384
+ )
385
+
386
+ const plugin = pyreonPlugin()
387
+ const config = getConfigHook(plugin)({ root: tmpRoot }, { command: 'serve' }) as {
388
+ optimizeDeps: { exclude: string[] }
389
+ }
390
+ expect(config.optimizeDeps.exclude).toContain('@pyreon/core')
391
+ expect(config.optimizeDeps.exclude).toContain('@pyreon/styler')
392
+ expect(config.optimizeDeps.exclude).toContain('@pyreon/runtime-dom')
393
+ expect(config.optimizeDeps.exclude).toContain('@pyreon/vite-plugin')
394
+ expect(config.optimizeDeps.exclude).not.toContain('react')
395
+
396
+ rmSync(tmpRoot, { recursive: true, force: true })
397
+ })
398
+
399
+ it("merges @pyreon/* deps with compat aliases without dup'ing", async () => {
400
+ const tmpRoot = pathJoin(import.meta.dirname, 'fixtures', 'pyreon-deps-compat')
401
+ rmSync(tmpRoot, { recursive: true, force: true })
402
+ mkdirSync(tmpRoot, { recursive: true })
403
+ writeFileSync(
404
+ pathJoin(tmpRoot, 'package.json'),
405
+ JSON.stringify({ dependencies: { '@pyreon/core': '^0.15.0' } }),
406
+ )
407
+
408
+ const plugin = pyreonPlugin({ compat: 'react' })
409
+ const config = getConfigHook(plugin)({ root: tmpRoot }, { command: 'serve' }) as {
410
+ optimizeDeps: { exclude: string[] }
411
+ }
412
+ // Compat list still present
413
+ expect(config.optimizeDeps.exclude).toContain('react')
414
+ // Pyreon list also present
415
+ expect(config.optimizeDeps.exclude).toContain('@pyreon/core')
416
+ // Deduplicated (Set)
417
+ const occurrences = config.optimizeDeps.exclude.filter((d) => d === 'react').length
418
+ expect(occurrences).toBe(1)
419
+
420
+ rmSync(tmpRoot, { recursive: true, force: true })
421
+ })
422
+
423
+ it('handles missing/malformed consumer package.json gracefully', async () => {
424
+ const plugin = pyreonPlugin()
425
+ // Point at a directory that doesn't exist — should not throw.
426
+ const config = getConfigHook(plugin)(
427
+ { root: '/nonexistent/path/that/does/not/exist' },
428
+ { command: 'serve' },
429
+ ) as { optimizeDeps: { exclude: string[] } }
430
+ expect(config.optimizeDeps.exclude).toEqual([])
431
+ })
432
+
319
433
  it('adds SSR build config when isSsrBuild', async () => {
320
434
  const plugin = pyreonPlugin({ ssr: { entry: './src/entry-server.ts' } })
321
435
  const config = getConfigHook(plugin)({}, { command: 'build', isSsrBuild: true }) as {
package/lib/index.js.map DELETED
@@ -1 +0,0 @@
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 { join as pathJoin } from 'node:path'\nimport { generateContext, transformJSX } from '@pyreon/compiler'\nimport type { Plugin, ViteDevServer } from 'vite'\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\nexport type CompatFramework = 'react' | 'preact' | 'vue' | 'solid'\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: \"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// ── Compat JSX import sources ─────────────────────────────────────────────────\n\nconst COMPAT_JSX_SOURCE: Record<CompatFramework, string> = {\n react: '@pyreon/react-compat',\n preact: '@pyreon/preact-compat',\n vue: '@pyreon/vue-compat',\n solid: '@pyreon/solid-compat',\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}\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 }\n return undefined\n}\n\nexport default function pyreonPlugin(options?: PyreonPluginOptions): Plugin {\n const ssrConfig = options?.ssr\n const compat = options?.compat\n let isBuild = 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\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 optimizeDepsExclude = compat ? Object.keys(COMPAT_ALIASES[compat]) : []\n\n const jsxSource = compat ? COMPAT_JSX_SOURCE[compat] : '@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.\n resolve: { conditions: ['bun'] },\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\n // ── Virtual module + compat alias resolution ─────────────────────────────\n async resolveId(id, importer) {\n if (id === HMR_RUNTIME_IMPORT) return HMR_RUNTIME_ID\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 },\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 (compat === 'react' || compat === 'preact' || compat === 'vue' || compat === 'solid') {\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 // ── 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(code, 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 const result = transformJSX(code, id, { ssr: isSsr, knownSignals })\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 for signal() calls not rewritten by HMR\n output = injectSignalNames(output)\n }\n\n return { code: output, 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 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}\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// ── 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 */\nconst SIGNAL_PREFIX_RE = /^((?: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: \"varName\" }` into signal() calls that don't already have\n * an options argument. Only runs in dev mode for debugging/devtools.\n *\n * `const count = signal(0)` → `const count = signal(0, { name: \"count\" })`\n *\n * Module-scope signals rewritten to __hmr_signal() are naturally skipped\n * because the regex matches `signal(` not `__hmr_signal(`.\n */\nfunction injectSignalNames(code: string): string {\n const re = /(?:const|let)\\s+(\\w+)\\s*=\\s*signal\\(/gm\n const matches: { start: number; end: number; name: string; args: string }[] = []\n\n let m: RegExpExecArray | null = 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 && !hasMultipleArgs(args)) {\n matches.push({ start: argsStart, end: argsStart + args.length, name: m[1] ?? '', args })\n }\n m = re.exec(code)\n }\n re.lastIndex = 0\n\n let output = code\n for (let i = matches.length - 1; i >= 0; i--) {\n const { start, end, name, args } = matches[i] as (typeof matches)[number]\n output = `${output.slice(0, start)}${args}, { name: ${JSON.stringify(name)} }${output.slice(end)}`\n }\n return output\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 lines.push(` import.meta.hot.accept();`)\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/**\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 */\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 const NAMED_EXPORT_RE = /export\\s*\\{([^}]+)\\}/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(/\\s+as\\s+/)\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(/\\s+as\\s+/)\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 +0,0 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/index.ts"],"mappings":";;;KA2CY,eAAA;AAAA,UAEK,mBAAA;;;;;;;;;;;;;EAaf,MAAA,GAAS,eAAA;;;;;;;;;;;EAYT,GAAA;gEAEE,KAAA;EAAA;AAAA;AAAA,iBA6DoB,YAAA,CAAa,OAAA,GAAU,mBAAA,GAAsB,MAAA"}