@pyreon/vite-plugin 0.11.5 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,8 +12,8 @@ bun add -D @pyreon/vite-plugin
12
12
 
13
13
  ```ts
14
14
  // vite.config.ts
15
- import pyreon from "@pyreon/vite-plugin"
16
- import { defineConfig } from "vite"
15
+ import pyreon from '@pyreon/vite-plugin'
16
+ import { defineConfig } from 'vite'
17
17
 
18
18
  export default defineConfig({
19
19
  plugins: [pyreon()],
@@ -26,11 +26,11 @@ Pass an `ssr` option to enable SSR dev middleware. The plugin will load your ser
26
26
 
27
27
  ```ts
28
28
  // vite.config.ts
29
- import pyreon from "@pyreon/vite-plugin"
30
- import { defineConfig } from "vite"
29
+ import pyreon from '@pyreon/vite-plugin'
30
+ import { defineConfig } from 'vite'
31
31
 
32
32
  export default defineConfig({
33
- plugins: [pyreon({ ssr: { entry: "./src/entry-server.ts" } })],
33
+ plugins: [pyreon({ ssr: { entry: './src/entry-server.ts' } })],
34
34
  })
35
35
  ```
36
36
 
@@ -38,13 +38,13 @@ Your server entry must export a `handler` (or default export) with the signature
38
38
 
39
39
  ```tsx
40
40
  // src/entry-server.ts
41
- import { renderToString } from "@pyreon/runtime-server"
42
- import App from "./App"
41
+ import { renderToString } from '@pyreon/runtime-server'
42
+ import App from './App'
43
43
 
44
44
  export async function handler(req: Request): Promise<Response> {
45
45
  const html = await renderToString(<App />)
46
46
  return new Response(html, {
47
- headers: { "Content-Type": "text/html" },
47
+ headers: { 'Content-Type': 'text/html' },
48
48
  })
49
49
  }
50
50
  ```
@@ -64,8 +64,8 @@ Default export. Returns a Vite `Plugin`.
64
64
 
65
65
  ### Options
66
66
 
67
- | Option | Type | Description |
68
- |---|---|---|
67
+ | Option | Type | Description |
68
+ | ----------- | -------- | --------------------------------------------------- |
69
69
  | `ssr.entry` | `string` | Server entry file path. Enables SSR dev middleware. |
70
70
 
71
71
  ## What It Does
package/lib/index.js.map CHANGED
@@ -1 +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, 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 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 return {\n optimizeDeps: {\n exclude: optimizeDepsExclude,\n },\n oxc: {\n jsx: {\n runtime: \"automatic\",\n importSource: compat ? COMPAT_JSX_SOURCE[compat] : \"@pyreon/core\",\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 // ── 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 transform(code, id) {\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.\n // OXC's built-in JSX transform handles jsx() calls; the compat\n // JSX runtime wraps components for re-render support.\n if (compat === \"react\" || compat === \"preact\" || compat === \"vue\" || compat === \"solid\")\n return\n\n const result = transformJSX(code, id)\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// ── 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\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
+ {"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, 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 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 return {\n optimizeDeps: {\n exclude: optimizeDepsExclude,\n },\n oxc: {\n jsx: {\n runtime: 'automatic',\n importSource: compat ? COMPAT_JSX_SOURCE[compat] : '@pyreon/core',\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 // ── 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 transform(code, id) {\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.\n // OXC's built-in JSX transform handles jsx() calls; the compat\n // JSX runtime wraps components for re-render support.\n if (compat === 'react' || compat === 'preact' || compat === 'vue' || compat === 'solid')\n return\n\n const result = transformJSX(code, id)\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// ── 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\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,7 +1,7 @@
1
1
  import { Plugin } from "vite";
2
2
 
3
3
  //#region src/index.d.ts
4
- type CompatFramework = "react" | "preact" | "vue" | "solid";
4
+ type CompatFramework = 'react' | 'preact' | 'vue' | 'solid';
5
5
  interface PyreonPluginOptions {
6
6
  /**
7
7
  * Alias imports from an existing framework to Pyreon's compat layer.
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "@pyreon/vite-plugin",
3
- "version": "0.11.5",
3
+ "version": "0.11.6",
4
4
  "description": "Vite plugin for Pyreon — .pyreon SFC support, HMR, compiler integration",
5
+ "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/vite-plugin#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/pyreon/pyreon/issues"
8
+ },
5
9
  "license": "MIT",
6
10
  "repository": {
7
11
  "type": "git",
8
12
  "url": "https://github.com/pyreon/pyreon.git",
9
13
  "directory": "packages/tools/vite-plugin"
10
14
  },
11
- "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/vite-plugin#readme",
12
- "bugs": {
13
- "url": "https://github.com/pyreon/pyreon/issues"
14
- },
15
15
  "files": [
16
16
  "lib",
17
17
  "src",
18
18
  "README.md",
19
19
  "LICENSE"
20
20
  ],
21
- "sideEffects": false,
22
21
  "type": "module",
22
+ "sideEffects": false,
23
23
  "main": "./lib/index.js",
24
24
  "module": "./lib/index.js",
25
25
  "types": "./lib/types/index.d.ts",
@@ -30,24 +30,24 @@
30
30
  "types": "./lib/types/index.d.ts"
31
31
  }
32
32
  },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
33
36
  "scripts": {
34
37
  "build": "vl_rolldown_build",
35
38
  "dev": "vl_rolldown_build-watch",
36
39
  "test": "vitest run",
37
40
  "typecheck": "tsc --noEmit",
38
- "lint": "biome check .",
41
+ "lint": "oxlint .",
39
42
  "prepublishOnly": "bun run build"
40
43
  },
41
44
  "dependencies": {
42
- "@pyreon/compiler": "^0.11.5"
43
- },
44
- "peerDependencies": {
45
- "vite": ">=8.0.0"
45
+ "@pyreon/compiler": "^0.11.6"
46
46
  },
47
47
  "devDependencies": {
48
48
  "vite": "^8.0.0"
49
49
  },
50
- "publishConfig": {
51
- "access": "public"
50
+ "peerDependencies": {
51
+ "vite": ">=8.0.0"
52
52
  }
53
53
  }
@@ -29,7 +29,7 @@ interface ModuleSignals {
29
29
  entries: Map<string, SignalLike>
30
30
  }
31
31
 
32
- const REGISTRY_KEY = "__pyreon_hmr_registry__"
32
+ const REGISTRY_KEY = '__pyreon_hmr_registry__'
33
33
 
34
34
  type Registry = Map<string, Map<string, unknown>>
35
35
 
package/src/index.ts CHANGED
@@ -32,16 +32,16 @@
32
32
  * vite build --ssr src/entry-server.ts --outDir dist/server # server bundle
33
33
  */
34
34
 
35
- import { existsSync, mkdirSync, writeFileSync } from "node:fs"
36
- import { join as pathJoin } from "node:path"
37
- import { generateContext, transformJSX } from "@pyreon/compiler"
38
- import type { Plugin, ViteDevServer } from "vite"
35
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
36
+ import { join as pathJoin } from 'node:path'
37
+ import { generateContext, transformJSX } from '@pyreon/compiler'
38
+ import type { Plugin, ViteDevServer } from 'vite'
39
39
 
40
40
  // Virtual module ID for the HMR runtime
41
- const HMR_RUNTIME_ID = "\0pyreon/hmr-runtime"
42
- const HMR_RUNTIME_IMPORT = "virtual:pyreon/hmr-runtime"
41
+ const HMR_RUNTIME_ID = '\0pyreon/hmr-runtime'
42
+ const HMR_RUNTIME_IMPORT = 'virtual:pyreon/hmr-runtime'
43
43
 
44
- export type CompatFramework = "react" | "preact" | "vue" | "solid"
44
+ export type CompatFramework = 'react' | 'preact' | 'vue' | 'solid'
45
45
 
46
46
  export interface PyreonPluginOptions {
47
47
  /**
@@ -77,38 +77,38 @@ export interface PyreonPluginOptions {
77
77
  // ── Compat JSX import sources ─────────────────────────────────────────────────
78
78
 
79
79
  const COMPAT_JSX_SOURCE: Record<CompatFramework, string> = {
80
- react: "@pyreon/react-compat",
81
- preact: "@pyreon/preact-compat",
82
- vue: "@pyreon/vue-compat",
83
- solid: "@pyreon/solid-compat",
80
+ react: '@pyreon/react-compat',
81
+ preact: '@pyreon/preact-compat',
82
+ vue: '@pyreon/vue-compat',
83
+ solid: '@pyreon/solid-compat',
84
84
  }
85
85
 
86
86
  // ── Compat alias maps ─────────────────────────────────────────────────────────
87
87
 
88
88
  const COMPAT_ALIASES: Record<CompatFramework, Record<string, string>> = {
89
89
  react: {
90
- react: "@pyreon/react-compat",
91
- "react/jsx-runtime": "@pyreon/react-compat/jsx-runtime",
92
- "react/jsx-dev-runtime": "@pyreon/react-compat/jsx-runtime",
93
- "react-dom": "@pyreon/react-compat/dom",
94
- "react-dom/client": "@pyreon/react-compat/dom",
90
+ react: '@pyreon/react-compat',
91
+ 'react/jsx-runtime': '@pyreon/react-compat/jsx-runtime',
92
+ 'react/jsx-dev-runtime': '@pyreon/react-compat/jsx-runtime',
93
+ 'react-dom': '@pyreon/react-compat/dom',
94
+ 'react-dom/client': '@pyreon/react-compat/dom',
95
95
  },
96
96
  preact: {
97
- preact: "@pyreon/preact-compat",
98
- "preact/hooks": "@pyreon/preact-compat/hooks",
99
- "preact/jsx-runtime": "@pyreon/preact-compat/jsx-runtime",
100
- "preact/jsx-dev-runtime": "@pyreon/preact-compat/jsx-runtime",
101
- "@preact/signals": "@pyreon/preact-compat/signals",
97
+ preact: '@pyreon/preact-compat',
98
+ 'preact/hooks': '@pyreon/preact-compat/hooks',
99
+ 'preact/jsx-runtime': '@pyreon/preact-compat/jsx-runtime',
100
+ 'preact/jsx-dev-runtime': '@pyreon/preact-compat/jsx-runtime',
101
+ '@preact/signals': '@pyreon/preact-compat/signals',
102
102
  },
103
103
  vue: {
104
- vue: "@pyreon/vue-compat",
105
- "vue/jsx-runtime": "@pyreon/vue-compat/jsx-runtime",
106
- "vue/jsx-dev-runtime": "@pyreon/vue-compat/jsx-runtime",
104
+ vue: '@pyreon/vue-compat',
105
+ 'vue/jsx-runtime': '@pyreon/vue-compat/jsx-runtime',
106
+ 'vue/jsx-dev-runtime': '@pyreon/vue-compat/jsx-runtime',
107
107
  },
108
108
  solid: {
109
- "solid-js": "@pyreon/solid-compat",
110
- "solid-js/jsx-runtime": "@pyreon/solid-compat/jsx-runtime",
111
- "solid-js/jsx-dev-runtime": "@pyreon/solid-compat/jsx-runtime",
109
+ 'solid-js': '@pyreon/solid-compat',
110
+ 'solid-js/jsx-runtime': '@pyreon/solid-compat/jsx-runtime',
111
+ 'solid-js/jsx-dev-runtime': '@pyreon/solid-compat/jsx-runtime',
112
112
  },
113
113
  }
114
114
 
@@ -122,11 +122,11 @@ function getCompatTarget(compat: CompatFramework | undefined, id: string): strin
122
122
  if (aliased) return aliased
123
123
  // OXC's JSX transform reads jsxImportSource from tsconfig (@pyreon/core),
124
124
  // not from our plugin config. Redirect JSX runtime imports in compat mode.
125
- if (id === "@pyreon/core/jsx-runtime" || id === "@pyreon/core/jsx-dev-runtime") {
126
- if (compat === "react") return "@pyreon/react-compat/jsx-runtime"
127
- if (compat === "preact") return "@pyreon/preact-compat/jsx-runtime"
128
- if (compat === "vue") return "@pyreon/vue-compat/jsx-runtime"
129
- if (compat === "solid") return "@pyreon/solid-compat/jsx-runtime"
125
+ if (id === '@pyreon/core/jsx-runtime' || id === '@pyreon/core/jsx-dev-runtime') {
126
+ if (compat === 'react') return '@pyreon/react-compat/jsx-runtime'
127
+ if (compat === 'preact') return '@pyreon/preact-compat/jsx-runtime'
128
+ if (compat === 'vue') return '@pyreon/vue-compat/jsx-runtime'
129
+ if (compat === 'solid') return '@pyreon/solid-compat/jsx-runtime'
130
130
  }
131
131
  return undefined
132
132
  }
@@ -135,14 +135,14 @@ export default function pyreonPlugin(options?: PyreonPluginOptions): Plugin {
135
135
  const ssrConfig = options?.ssr
136
136
  const compat = options?.compat
137
137
  let isBuild = false
138
- let projectRoot = ""
138
+ let projectRoot = ''
139
139
 
140
140
  return {
141
- name: "pyreon",
142
- enforce: "pre",
141
+ name: 'pyreon',
142
+ enforce: 'pre',
143
143
 
144
144
  config(userConfig, env) {
145
- isBuild = env.command === "build"
145
+ isBuild = env.command === 'build'
146
146
  // Capture the project root for package resolution in resolveId
147
147
  projectRoot = userConfig.root ?? process.cwd()
148
148
 
@@ -156,8 +156,8 @@ export default function pyreonPlugin(options?: PyreonPluginOptions): Plugin {
156
156
  },
157
157
  oxc: {
158
158
  jsx: {
159
- runtime: "automatic",
160
- importSource: compat ? COMPAT_JSX_SOURCE[compat] : "@pyreon/core",
159
+ runtime: 'automatic',
160
+ importSource: compat ? COMPAT_JSX_SOURCE[compat] : '@pyreon/core',
161
161
  },
162
162
  },
163
163
  // In SSR build mode, configure the entry
@@ -194,12 +194,12 @@ export default function pyreonPlugin(options?: PyreonPluginOptions): Plugin {
194
194
 
195
195
  transform(code, id) {
196
196
  const ext = getExt(id)
197
- if (ext !== ".tsx" && ext !== ".jsx" && ext !== ".pyreon") return
197
+ if (ext !== '.tsx' && ext !== '.jsx' && ext !== '.pyreon') return
198
198
 
199
199
  // In compat mode, skip Pyreon's reactive JSX transform.
200
200
  // OXC's built-in JSX transform handles jsx() calls; the compat
201
201
  // JSX runtime wraps components for re-render support.
202
- if (compat === "react" || compat === "preact" || compat === "vue" || compat === "solid")
202
+ if (compat === 'react' || compat === 'preact' || compat === 'vue' || compat === 'solid')
203
203
  return
204
204
 
205
205
  const result = transformJSX(code, id)
@@ -227,8 +227,8 @@ export default function pyreonPlugin(options?: PyreonPluginOptions): Plugin {
227
227
 
228
228
  // Debounced regeneration on file changes
229
229
  let contextTimer: ReturnType<typeof setTimeout> | null = null
230
- server.watcher.on("change", (file) => {
231
- if (/\.(tsx|jsx|ts|js)$/.test(file) && !file.includes("node_modules")) {
230
+ server.watcher.on('change', (file) => {
231
+ if (/\.(tsx|jsx|ts|js)$/.test(file) && !file.includes('node_modules')) {
232
232
  if (contextTimer) clearTimeout(contextTimer)
233
233
  contextTimer = setTimeout(() => generateProjectContext(projectRoot), 500)
234
234
  }
@@ -240,8 +240,8 @@ export default function pyreonPlugin(options?: PyreonPluginOptions): Plugin {
240
240
  // (static files, HMR, etc.) — only handle requests that Vite doesn't serve.
241
241
  return () => {
242
242
  server.middlewares.use(async (req, res, next) => {
243
- if (req.method !== "GET") return next()
244
- const url = req.url ?? "/"
243
+ if (req.method !== 'GET') return next()
244
+ const url = req.url ?? '/'
245
245
  if (isAssetRequest(url)) return next()
246
246
 
247
247
  try {
@@ -260,24 +260,24 @@ async function handleSsrRequest(
260
260
  server: ViteDevServer,
261
261
  entry: string,
262
262
  url: string,
263
- req: import("node:http").IncomingMessage,
264
- res: import("node:http").ServerResponse,
263
+ req: import('node:http').IncomingMessage,
264
+ res: import('node:http').ServerResponse,
265
265
  next: (err?: unknown) => void,
266
266
  ): Promise<void> {
267
267
  const mod = await server.ssrLoadModule(entry)
268
268
  const handler = mod.handler ?? mod.default
269
269
 
270
- if (typeof handler !== "function") {
270
+ if (typeof handler !== 'function') {
271
271
  next()
272
272
  return
273
273
  }
274
274
 
275
- const origin = `http://${req.headers.host ?? "localhost"}`
275
+ const origin = `http://${req.headers.host ?? 'localhost'}`
276
276
  const fullUrl = new URL(url, origin)
277
277
  const request = new Request(fullUrl.href, {
278
- method: req.method ?? "GET",
278
+ method: req.method ?? 'GET',
279
279
  headers: Object.entries(req.headers).reduce((h, [k, v]) => {
280
- if (v) h.set(k, Array.isArray(v) ? v.join(", ") : v)
280
+ if (v) h.set(k, Array.isArray(v) ? v.join(', ') : v)
281
281
  return h
282
282
  }, new Headers()),
283
283
  })
@@ -303,9 +303,9 @@ async function handleSsrRequest(
303
303
  function generateProjectContext(root: string): void {
304
304
  try {
305
305
  const context = generateContext(root)
306
- const outDir = pathJoin(root, ".pyreon")
306
+ const outDir = pathJoin(root, '.pyreon')
307
307
  if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true })
308
- writeFileSync(pathJoin(outDir, "context.json"), JSON.stringify(context, null, 2), "utf-8")
308
+ writeFileSync(pathJoin(outDir, 'context.json'), JSON.stringify(context, null, 2), 'utf-8')
309
309
  } catch {
310
310
  // Silently fail — context generation is best-effort
311
311
  }
@@ -331,7 +331,7 @@ const EXPORT_COMPONENT_RE =
331
331
  function skipStringLiteral(code: string, start: number, quote: string): number {
332
332
  let j = start + 1
333
333
  while (j < code.length) {
334
- if (code[j] === "\\") {
334
+ if (code[j] === '\\') {
335
335
  j += 2
336
336
  continue
337
337
  }
@@ -345,11 +345,11 @@ function extractBalancedArgs(code: string, start: number): string | null {
345
345
  let depth = 1
346
346
  for (let i = start; i < code.length; i++) {
347
347
  const ch = code[i]
348
- if (ch === "(") depth++
349
- else if (ch === ")") {
348
+ if (ch === '(') depth++
349
+ else if (ch === ')') {
350
350
  depth--
351
351
  if (depth === 0) return code.slice(start, i)
352
- } else if (ch === '"' || ch === "'" || ch === "`") {
352
+ } else if (ch === '"' || ch === "'" || ch === '`') {
353
353
  i = skipStringLiteral(code, i, ch)
354
354
  }
355
355
  }
@@ -364,9 +364,9 @@ function braceDepthAt(code: string, pos: number): number {
364
364
  let depth = 0
365
365
  for (let i = 0; i < pos; i++) {
366
366
  const ch = code[i]
367
- if (ch === "{") depth++
368
- else if (ch === "}") depth--
369
- else if (ch === '"' || ch === "'" || ch === "`") {
367
+ if (ch === '{') depth++
368
+ else if (ch === '}') depth--
369
+ else if (ch === '"' || ch === "'" || ch === '`') {
370
370
  i = skipStringLiteral(code, i, ch)
371
371
  }
372
372
  }
@@ -396,8 +396,8 @@ function rewriteSignals(code: string, moduleId: string): string {
396
396
  matches.push({
397
397
  start: m.index,
398
398
  end: argsStart + args.length + 1, // +1 for closing paren
399
- prefix: m[1] ?? "",
400
- name: m[2] ?? "",
399
+ prefix: m[1] ?? '',
400
+ name: m[2] ?? '',
401
401
  args,
402
402
  })
403
403
  }
@@ -419,9 +419,9 @@ function rewriteSignals(code: string, moduleId: string): string {
419
419
  function hasMultipleArgs(args: string): boolean {
420
420
  let depth = 0
421
421
  for (const ch of args) {
422
- if (ch === "(" || ch === "[" || ch === "{") depth++
423
- else if (ch === ")" || ch === "]" || ch === "}") depth--
424
- else if (ch === "," && depth === 0) return true
422
+ if (ch === '(' || ch === '[' || ch === '{') depth++
423
+ else if (ch === ')' || ch === ']' || ch === '}') depth--
424
+ else if (ch === ',' && depth === 0) return true
425
425
  }
426
426
  return false
427
427
  }
@@ -444,7 +444,7 @@ function injectSignalNames(code: string): string {
444
444
  const argsStart = m.index + m[0].length
445
445
  const args = extractBalancedArgs(code, argsStart)
446
446
  if (args !== null && !hasMultipleArgs(args)) {
447
- matches.push({ start: argsStart, end: argsStart + args.length, name: m[1] ?? "", args })
447
+ matches.push({ start: argsStart, end: argsStart + args.length, name: m[1] ?? '', args })
448
448
  }
449
449
  m = re.exec(code)
450
450
  }
@@ -486,7 +486,7 @@ function injectHmr(code: string, moduleId: string): string {
486
486
  lines.push(` import.meta.hot.accept();`)
487
487
  lines.push(`}`)
488
488
 
489
- output = `${output}\n\n${lines.join("\n")}\n`
489
+ output = `${output}\n\n${lines.join('\n')}\n`
490
490
 
491
491
  return output
492
492
  }
@@ -494,17 +494,17 @@ function injectHmr(code: string, moduleId: string): string {
494
494
  // ── Helpers ───────────────────────────────────────────────────────────────────
495
495
 
496
496
  function getExt(id: string): string {
497
- const clean = id.split("?")[0] ?? id
498
- const dot = clean.lastIndexOf(".")
499
- return dot >= 0 ? clean.slice(dot) : ""
497
+ const clean = id.split('?')[0] ?? id
498
+ const dot = clean.lastIndexOf('.')
499
+ return dot >= 0 ? clean.slice(dot) : ''
500
500
  }
501
501
 
502
502
  /** Skip Vite-handled asset requests (CSS, images, HMR, etc.) */
503
503
  function isAssetRequest(url: string): boolean {
504
504
  return (
505
- url.startsWith("/@") || // @vite/client, @id, @fs, etc.
506
- url.startsWith("/__") || // __open-in-editor, etc.
507
- url.includes("/node_modules/") ||
505
+ url.startsWith('/@') || // @vite/client, @id, @fs, etc.
506
+ url.startsWith('/__') || // __open-in-editor, etc.
507
+ url.includes('/node_modules/') ||
508
508
  /\.(css|js|ts|tsx|jsx|json|ico|png|jpg|jpeg|gif|svg|woff2?|ttf|eot|map)(\?|$)/.test(url)
509
509
  )
510
510
  }
@@ -5,14 +5,14 @@
5
5
  * These test the plugin's transform logic directly (no Vite required).
6
6
  */
7
7
 
8
- import { describe, expect, it } from "vitest"
8
+ import { describe, expect, it } from 'vitest'
9
9
 
10
10
  // ── Import internals ─────────────────────────────────────────────────────────
11
11
  // We import the default export and call it to get the plugin object,
12
12
  // then invoke its hooks directly.
13
13
 
14
- import type { PyreonPluginOptions } from "../index"
15
- import pyreonPlugin from "../index"
14
+ import type { PyreonPluginOptions } from '../index'
15
+ import pyreonPlugin from '../index'
16
16
 
17
17
  type ConfigHook = (
18
18
  userConfig: Record<string, unknown>,
@@ -26,13 +26,13 @@ function getConfigHook(plugin: ReturnType<typeof pyreonPlugin>): ConfigHook {
26
26
  function createPlugin(opts?: PyreonPluginOptions) {
27
27
  const plugin = pyreonPlugin(opts)
28
28
  // Simulate Vite calling config() so isBuild / projectRoot are set
29
- getConfigHook(plugin)({}, { command: "serve" })
29
+ getConfigHook(plugin)({}, { command: 'serve' })
30
30
  return plugin
31
31
  }
32
32
 
33
33
  function createBuildPlugin(opts?: PyreonPluginOptions) {
34
34
  const plugin = pyreonPlugin(opts)
35
- getConfigHook(plugin)({}, { command: "build" })
35
+ getConfigHook(plugin)({}, { command: 'build' })
36
36
  return plugin
37
37
  }
38
38
 
@@ -48,57 +48,57 @@ function transform(plugin: ReturnType<typeof pyreonPlugin>, code: string, id: st
48
48
 
49
49
  // ─── HMR injection ──────────────────────────────────────────────────────────
50
50
 
51
- describe("HMR injection", () => {
52
- it("injects HMR accept for modules with component exports", () => {
51
+ describe('HMR injection', () => {
52
+ it('injects HMR accept for modules with component exports', () => {
53
53
  const plugin = createPlugin()
54
54
  const code = `
55
55
  import { h } from "@pyreon/core"
56
56
  export function App() { return h("div", null, "hello") }
57
57
  `
58
- const result = transform(plugin, code, "/src/App.tsx")
58
+ const result = transform(plugin, code, '/src/App.tsx')
59
59
  expect(result).toBeDefined()
60
- expect(result!.code).toContain("import.meta.hot.accept()")
60
+ expect(result!.code).toContain('import.meta.hot.accept()')
61
61
  })
62
62
 
63
- it("injects HMR for exported const components", () => {
63
+ it('injects HMR for exported const components', () => {
64
64
  const plugin = createPlugin()
65
65
  const code = `
66
66
  import { h } from "@pyreon/core"
67
67
  export const Header = () => h("header", null, "nav")
68
68
  `
69
- const result = transform(plugin, code, "/src/Header.tsx")
69
+ const result = transform(plugin, code, '/src/Header.tsx')
70
70
  expect(result).toBeDefined()
71
- expect(result!.code).toContain("import.meta.hot")
71
+ expect(result!.code).toContain('import.meta.hot')
72
72
  })
73
73
 
74
- it("does not inject HMR for modules without component exports or signals", () => {
74
+ it('does not inject HMR for modules without component exports or signals', () => {
75
75
  const plugin = createPlugin()
76
76
  // Only lowercase exports — no component-like names (uppercase first letter)
77
77
  const code = `
78
78
  export const formatDate = (d) => d.toISOString()
79
79
  export const maxItems = 100
80
80
  `
81
- const result = transform(plugin, code, "/src/utils.tsx")
81
+ const result = transform(plugin, code, '/src/utils.tsx')
82
82
  expect(result).toBeDefined()
83
- expect(result!.code).not.toContain("import.meta.hot")
83
+ expect(result!.code).not.toContain('import.meta.hot')
84
84
  })
85
85
 
86
- it("does not inject HMR in build mode", () => {
86
+ it('does not inject HMR in build mode', () => {
87
87
  const plugin = createBuildPlugin()
88
88
  const code = `
89
89
  import { h } from "@pyreon/core"
90
90
  export function App() { return h("div", null, "hello") }
91
91
  `
92
- const result = transform(plugin, code, "/src/App.tsx")
92
+ const result = transform(plugin, code, '/src/App.tsx')
93
93
  expect(result).toBeDefined()
94
- expect(result!.code).not.toContain("import.meta.hot")
94
+ expect(result!.code).not.toContain('import.meta.hot')
95
95
  })
96
96
  })
97
97
 
98
98
  // ─── Signal rewriting ────────────────────────────────────────────────────────
99
99
 
100
- describe("signal rewriting", () => {
101
- it("rewrites module-scope signal() to __hmr_signal()", () => {
100
+ describe('signal rewriting', () => {
101
+ it('rewrites module-scope signal() to __hmr_signal()', () => {
102
102
  const plugin = createPlugin()
103
103
  const code = `
104
104
  import { signal } from "@pyreon/reactivity"
@@ -106,27 +106,27 @@ import { h } from "@pyreon/core"
106
106
  const count = signal(0)
107
107
  export function Counter() { return h("div", null, count()) }
108
108
  `
109
- const result = transform(plugin, code, "/src/Counter.tsx")
109
+ const result = transform(plugin, code, '/src/Counter.tsx')
110
110
  expect(result).toBeDefined()
111
- expect(result!.code).toContain("__hmr_signal(")
111
+ expect(result!.code).toContain('__hmr_signal(')
112
112
  expect(result!.code).toContain('"count"')
113
113
  expect(result!.code).toContain('"/src/Counter.tsx"')
114
- expect(result!.code).toContain("__hmr_dispose")
114
+ expect(result!.code).toContain('__hmr_dispose')
115
115
  })
116
116
 
117
- it("rewrites exported signals", () => {
117
+ it('rewrites exported signals', () => {
118
118
  const plugin = createPlugin()
119
119
  const code = `
120
120
  import { signal } from "@pyreon/reactivity"
121
121
  export const theme = signal("light")
122
122
  export function App() { return null }
123
123
  `
124
- const result = transform(plugin, code, "/src/theme.tsx")
124
+ const result = transform(plugin, code, '/src/theme.tsx')
125
125
  expect(result).toBeDefined()
126
126
  expect(result!.code).toContain('__hmr_signal("/src/theme.tsx", "theme", signal, "light")')
127
127
  })
128
128
 
129
- it("does not rewrite signal() inside functions to __hmr_signal (but injects name)", () => {
129
+ it('does not rewrite signal() inside functions to __hmr_signal (but injects name)', () => {
130
130
  const plugin = createPlugin()
131
131
  const code = `
132
132
  import { signal } from "@pyreon/reactivity"
@@ -136,15 +136,15 @@ export function Counter() {
136
136
  return h("div", null, local())
137
137
  }
138
138
  `
139
- const result = transform(plugin, code, "/src/Counter.tsx")
139
+ const result = transform(plugin, code, '/src/Counter.tsx')
140
140
  expect(result).toBeDefined()
141
141
  // The signal inside the function body should NOT be rewritten to __hmr_signal
142
- expect(result!.code).not.toContain("__hmr_signal")
142
+ expect(result!.code).not.toContain('__hmr_signal')
143
143
  // But should get a debug name injected
144
144
  expect(result!.code).toContain('signal(0, { name: "local" })')
145
145
  })
146
146
 
147
- it("rewrites multiple module-scope signals", () => {
147
+ it('rewrites multiple module-scope signals', () => {
148
148
  const plugin = createPlugin()
149
149
  const code = `
150
150
  import { signal } from "@pyreon/reactivity"
@@ -152,13 +152,13 @@ const count = signal(0)
152
152
  const name = signal("world")
153
153
  export function App() { return null }
154
154
  `
155
- const result = transform(plugin, code, "/src/App.tsx")
155
+ const result = transform(plugin, code, '/src/App.tsx')
156
156
  expect(result).toBeDefined()
157
157
  expect(result!.code).toContain('"count"')
158
158
  expect(result!.code).toContain('"name"')
159
159
  })
160
160
 
161
- it("handles signal with complex initial values", () => {
161
+ it('handles signal with complex initial values', () => {
162
162
  const plugin = createPlugin()
163
163
  const code = `
164
164
  import { signal } from "@pyreon/reactivity"
@@ -166,29 +166,29 @@ const items = signal([1, 2, 3])
166
166
  const config = signal({ theme: "dark", size: 14 })
167
167
  export function App() { return null }
168
168
  `
169
- const result = transform(plugin, code, "/src/App.tsx")
169
+ const result = transform(plugin, code, '/src/App.tsx')
170
170
  expect(result).toBeDefined()
171
- expect(result!.code).toContain("__hmr_signal")
172
- expect(result!.code).toContain("[1, 2, 3]")
171
+ expect(result!.code).toContain('__hmr_signal')
172
+ expect(result!.code).toContain('[1, 2, 3]')
173
173
  expect(result!.code).toContain('{ theme: "dark", size: 14 }')
174
174
  })
175
175
 
176
- it("does not rewrite signal in build mode", () => {
176
+ it('does not rewrite signal in build mode', () => {
177
177
  const plugin = createBuildPlugin()
178
178
  const code = `
179
179
  import { signal } from "@pyreon/reactivity"
180
180
  const count = signal(0)
181
181
  export function App() { return null }
182
182
  `
183
- const result = transform(plugin, code, "/src/App.tsx")
183
+ const result = transform(plugin, code, '/src/App.tsx')
184
184
  expect(result).toBeDefined()
185
- expect(result!.code).not.toContain("__hmr_signal")
185
+ expect(result!.code).not.toContain('__hmr_signal')
186
186
  // No signal names in production builds
187
- expect(result!.code).toContain("signal(0)")
188
- expect(result!.code).not.toContain("{ name:")
187
+ expect(result!.code).toContain('signal(0)')
188
+ expect(result!.code).not.toContain('{ name:')
189
189
  })
190
190
 
191
- it("skips signal naming when options already provided", () => {
191
+ it('skips signal naming when options already provided', () => {
192
192
  const plugin = createPlugin()
193
193
  const code = `
194
194
  import { signal } from "@pyreon/reactivity"
@@ -197,7 +197,7 @@ export function App() {
197
197
  return null
198
198
  }
199
199
  `
200
- const result = transform(plugin, code, "/src/App.tsx")
200
+ const result = transform(plugin, code, '/src/App.tsx')
201
201
  expect(result).toBeDefined()
202
202
  // Should not double-inject name
203
203
  expect(result!.code).toContain('signal(0, { name: "custom" })')
@@ -206,162 +206,162 @@ export function App() {
206
206
 
207
207
  // ─── File extension filtering ────────────────────────────────────────────────
208
208
 
209
- describe("file extension filtering", () => {
210
- it("transforms .tsx files", () => {
209
+ describe('file extension filtering', () => {
210
+ it('transforms .tsx files', () => {
211
211
  const plugin = createPlugin()
212
212
  const code = `export function App() { return null }`
213
- const result = transform(plugin, code, "/src/App.tsx")
213
+ const result = transform(plugin, code, '/src/App.tsx')
214
214
  expect(result).toBeDefined()
215
215
  })
216
216
 
217
- it("transforms .jsx files", () => {
217
+ it('transforms .jsx files', () => {
218
218
  const plugin = createPlugin()
219
219
  const code = `export function App() { return null }`
220
- const result = transform(plugin, code, "/src/App.jsx")
220
+ const result = transform(plugin, code, '/src/App.jsx')
221
221
  expect(result).toBeDefined()
222
222
  })
223
223
 
224
- it("ignores .ts files", () => {
224
+ it('ignores .ts files', () => {
225
225
  const plugin = createPlugin()
226
226
  const code = `export const x = 1`
227
- const result = transform(plugin, code, "/src/utils.ts")
227
+ const result = transform(plugin, code, '/src/utils.ts')
228
228
  expect(result).toBeUndefined()
229
229
  })
230
230
 
231
- it("ignores .js files", () => {
231
+ it('ignores .js files', () => {
232
232
  const plugin = createPlugin()
233
233
  const code = `export const x = 1`
234
- const result = transform(plugin, code, "/src/utils.js")
234
+ const result = transform(plugin, code, '/src/utils.js')
235
235
  expect(result).toBeUndefined()
236
236
  })
237
237
 
238
- it("handles query strings in file paths", () => {
238
+ it('handles query strings in file paths', () => {
239
239
  const plugin = createPlugin()
240
240
  const code = `export function App() { return null }`
241
- const result = transform(plugin, code, "/src/App.tsx?v=123")
241
+ const result = transform(plugin, code, '/src/App.tsx?v=123')
242
242
  expect(result).toBeDefined()
243
243
  })
244
244
  })
245
245
 
246
246
  // ─── Compat mode ─────────────────────────────────────────────────────────────
247
247
 
248
- describe("compat mode", () => {
249
- it("skips Pyreon JSX transform in react compat mode", () => {
250
- const plugin = createPlugin({ compat: "react" })
248
+ describe('compat mode', () => {
249
+ it('skips Pyreon JSX transform in react compat mode', () => {
250
+ const plugin = createPlugin({ compat: 'react' })
251
251
  const code = `
252
252
  import { useState } from "react"
253
253
  export function App() { const [x] = useState(0); return null }
254
254
  `
255
- const result = transform(plugin, code, "/src/App.tsx")
255
+ const result = transform(plugin, code, '/src/App.tsx')
256
256
  expect(result).toBeUndefined()
257
257
  })
258
258
 
259
- it("skips transform in preact compat mode", () => {
260
- const plugin = createPlugin({ compat: "preact" })
261
- const result = transform(plugin, "export function App() { return null }", "/src/App.tsx")
259
+ it('skips transform in preact compat mode', () => {
260
+ const plugin = createPlugin({ compat: 'preact' })
261
+ const result = transform(plugin, 'export function App() { return null }', '/src/App.tsx')
262
262
  expect(result).toBeUndefined()
263
263
  })
264
264
 
265
- it("skips transform in vue compat mode", () => {
266
- const plugin = createPlugin({ compat: "vue" })
267
- const result = transform(plugin, "export function App() { return null }", "/src/App.tsx")
265
+ it('skips transform in vue compat mode', () => {
266
+ const plugin = createPlugin({ compat: 'vue' })
267
+ const result = transform(plugin, 'export function App() { return null }', '/src/App.tsx')
268
268
  expect(result).toBeUndefined()
269
269
  })
270
270
 
271
- it("skips transform in solid compat mode", () => {
272
- const plugin = createPlugin({ compat: "solid" })
273
- const result = transform(plugin, "export function App() { return null }", "/src/App.tsx")
271
+ it('skips transform in solid compat mode', () => {
272
+ const plugin = createPlugin({ compat: 'solid' })
273
+ const result = transform(plugin, 'export function App() { return null }', '/src/App.tsx')
274
274
  expect(result).toBeUndefined()
275
275
  })
276
276
  })
277
277
 
278
278
  // ─── Plugin config ───────────────────────────────────────────────────────────
279
279
 
280
- describe("plugin config", () => {
281
- it("does not set resolve.conditions (consumer manages their own)", () => {
280
+ describe('plugin config', () => {
281
+ it('does not set resolve.conditions (consumer manages their own)', () => {
282
282
  const plugin = pyreonPlugin()
283
- const config = getConfigHook(plugin)({}, { command: "serve" }) as Record<string, unknown>
283
+ const config = getConfigHook(plugin)({}, { command: 'serve' }) as Record<string, unknown>
284
284
  expect(config.resolve).toBeUndefined()
285
285
  })
286
286
 
287
- it("sets JSX import source to @pyreon/core by default", () => {
287
+ it('sets JSX import source to @pyreon/core by default', () => {
288
288
  const plugin = pyreonPlugin()
289
- const config = getConfigHook(plugin)({}, { command: "serve" }) as {
289
+ const config = getConfigHook(plugin)({}, { command: 'serve' }) as {
290
290
  oxc: { jsx: { importSource: string } }
291
291
  }
292
- expect(config.oxc.jsx.importSource).toBe("@pyreon/core")
292
+ expect(config.oxc.jsx.importSource).toBe('@pyreon/core')
293
293
  })
294
294
 
295
- it("sets JSX import source to compat package in compat mode", () => {
296
- const plugin = pyreonPlugin({ compat: "react" })
297
- const config = getConfigHook(plugin)({}, { command: "serve" }) as {
295
+ it('sets JSX import source to compat package in compat mode', () => {
296
+ const plugin = pyreonPlugin({ compat: 'react' })
297
+ const config = getConfigHook(plugin)({}, { command: 'serve' }) as {
298
298
  oxc: { jsx: { importSource: string } }
299
299
  }
300
- expect(config.oxc.jsx.importSource).toBe("@pyreon/react-compat")
300
+ expect(config.oxc.jsx.importSource).toBe('@pyreon/react-compat')
301
301
  })
302
302
 
303
- it("excludes compat packages from optimizeDeps", () => {
304
- const plugin = pyreonPlugin({ compat: "react" })
305
- const config = getConfigHook(plugin)({}, { command: "serve" }) as {
303
+ it('excludes compat packages from optimizeDeps', () => {
304
+ const plugin = pyreonPlugin({ compat: 'react' })
305
+ const config = getConfigHook(plugin)({}, { command: 'serve' }) as {
306
306
  optimizeDeps: { exclude: string[] }
307
307
  }
308
- expect(config.optimizeDeps.exclude).toContain("react")
309
- expect(config.optimizeDeps.exclude).toContain("react-dom")
308
+ expect(config.optimizeDeps.exclude).toContain('react')
309
+ expect(config.optimizeDeps.exclude).toContain('react-dom')
310
310
  })
311
311
 
312
- it("adds SSR build config when isSsrBuild", () => {
313
- const plugin = pyreonPlugin({ ssr: { entry: "./src/entry-server.ts" } })
314
- const config = getConfigHook(plugin)({}, { command: "build", isSsrBuild: true }) as {
312
+ it('adds SSR build config when isSsrBuild', () => {
313
+ const plugin = pyreonPlugin({ ssr: { entry: './src/entry-server.ts' } })
314
+ const config = getConfigHook(plugin)({}, { command: 'build', isSsrBuild: true }) as {
315
315
  build: { ssr: boolean; rollupOptions: { input: string } }
316
316
  }
317
317
  expect(config.build.ssr).toBe(true)
318
- expect(config.build.rollupOptions.input).toBe("./src/entry-server.ts")
318
+ expect(config.build.rollupOptions.input).toBe('./src/entry-server.ts')
319
319
  })
320
320
  })
321
321
 
322
322
  // ─── Virtual module (HMR runtime) ────────────────────────────────────────────
323
323
 
324
- describe("virtual module resolution", () => {
325
- it("resolves virtual:pyreon/hmr-runtime to internal ID", async () => {
324
+ describe('virtual module resolution', () => {
325
+ it('resolves virtual:pyreon/hmr-runtime to internal ID', async () => {
326
326
  const plugin = createPlugin()
327
327
  const resolveId = plugin.resolveId as (
328
328
  id: string,
329
329
  ) => string | undefined | Promise<string | undefined>
330
- const resolved = await resolveId("virtual:pyreon/hmr-runtime")
331
- expect(resolved).toBe("\0pyreon/hmr-runtime")
330
+ const resolved = await resolveId('virtual:pyreon/hmr-runtime')
331
+ expect(resolved).toBe('\0pyreon/hmr-runtime')
332
332
  })
333
333
 
334
- it("loads HMR runtime source for internal ID", () => {
334
+ it('loads HMR runtime source for internal ID', () => {
335
335
  const plugin = createPlugin()
336
336
  const load = plugin.load as (id: string) => string | undefined
337
- const source = load("\0pyreon/hmr-runtime")
337
+ const source = load('\0pyreon/hmr-runtime')
338
338
  expect(source).toBeDefined()
339
- expect(source).toContain("__hmr_signal")
340
- expect(source).toContain("__hmr_dispose")
341
- expect(source).toContain("__pyreon_hmr_registry__")
339
+ expect(source).toContain('__hmr_signal')
340
+ expect(source).toContain('__hmr_dispose')
341
+ expect(source).toContain('__pyreon_hmr_registry__')
342
342
  })
343
343
 
344
- it("returns undefined for non-virtual IDs", () => {
344
+ it('returns undefined for non-virtual IDs', () => {
345
345
  const plugin = createPlugin()
346
346
  const load = plugin.load as (id: string) => string | undefined
347
- expect(load("/src/App.tsx")).toBeUndefined()
347
+ expect(load('/src/App.tsx')).toBeUndefined()
348
348
  })
349
349
  })
350
350
 
351
351
  // ─── Asset request detection ────────────────────────────────────────────────
352
352
 
353
- describe("asset request filtering", () => {
353
+ describe('asset request filtering', () => {
354
354
  // The SSR middleware uses isAssetRequest internally.
355
355
  // We test it via the configureServer middleware behavior.
356
356
  // For direct testing, we'd need to export it — instead we verify
357
357
  // the plugin's SSR middleware config exists when ssr option is set.
358
358
 
359
- it("configureServer returns middleware function when SSR enabled", () => {
360
- const plugin = pyreonPlugin({ ssr: { entry: "./src/entry-server.ts" } })
359
+ it('configureServer returns middleware function when SSR enabled', () => {
360
+ const plugin = pyreonPlugin({ ssr: { entry: './src/entry-server.ts' } })
361
361
  expect(plugin.configureServer).toBeDefined()
362
362
  })
363
363
 
364
- it("configureServer is defined even without SSR (for context generation)", () => {
364
+ it('configureServer is defined even without SSR (for context generation)', () => {
365
365
  const plugin = pyreonPlugin()
366
366
  expect(plugin.configureServer).toBeDefined()
367
367
  })