@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 +10 -10
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +1 -1
- package/package.json +13 -13
- package/src/hmr-runtime.ts +1 -1
- package/src/index.ts +72 -72
- package/src/tests/vite-plugin.test.ts +99 -99
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
|
|
16
|
-
import { defineConfig } from
|
|
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
|
|
30
|
-
import { defineConfig } from
|
|
29
|
+
import pyreon from '@pyreon/vite-plugin'
|
|
30
|
+
import { defineConfig } from 'vite'
|
|
31
31
|
|
|
32
32
|
export default defineConfig({
|
|
33
|
-
plugins: [pyreon({ ssr: { entry:
|
|
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
|
|
42
|
-
import App from
|
|
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: {
|
|
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
|
|
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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"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Plugin } from "vite";
|
|
2
2
|
|
|
3
3
|
//#region src/index.d.ts
|
|
4
|
-
type CompatFramework =
|
|
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.
|
|
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": "
|
|
41
|
+
"lint": "oxlint .",
|
|
39
42
|
"prepublishOnly": "bun run build"
|
|
40
43
|
},
|
|
41
44
|
"dependencies": {
|
|
42
|
-
"@pyreon/compiler": "^0.11.
|
|
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
|
-
"
|
|
51
|
-
"
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"vite": ">=8.0.0"
|
|
52
52
|
}
|
|
53
53
|
}
|
package/src/hmr-runtime.ts
CHANGED
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
|
|
36
|
-
import { join as pathJoin } from
|
|
37
|
-
import { generateContext, transformJSX } from
|
|
38
|
-
import type { Plugin, ViteDevServer } from
|
|
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 =
|
|
42
|
-
const HMR_RUNTIME_IMPORT =
|
|
41
|
+
const HMR_RUNTIME_ID = '\0pyreon/hmr-runtime'
|
|
42
|
+
const HMR_RUNTIME_IMPORT = 'virtual:pyreon/hmr-runtime'
|
|
43
43
|
|
|
44
|
-
export type CompatFramework =
|
|
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:
|
|
81
|
-
preact:
|
|
82
|
-
vue:
|
|
83
|
-
solid:
|
|
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:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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:
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 ===
|
|
126
|
-
if (compat ===
|
|
127
|
-
if (compat ===
|
|
128
|
-
if (compat ===
|
|
129
|
-
if (compat ===
|
|
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:
|
|
142
|
-
enforce:
|
|
141
|
+
name: 'pyreon',
|
|
142
|
+
enforce: 'pre',
|
|
143
143
|
|
|
144
144
|
config(userConfig, env) {
|
|
145
|
-
isBuild = env.command ===
|
|
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:
|
|
160
|
-
importSource: compat ? COMPAT_JSX_SOURCE[compat] :
|
|
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 !==
|
|
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 ===
|
|
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(
|
|
231
|
-
if (/\.(tsx|jsx|ts|js)$/.test(file) && !file.includes(
|
|
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 !==
|
|
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(
|
|
264
|
-
res: import(
|
|
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 !==
|
|
270
|
+
if (typeof handler !== 'function') {
|
|
271
271
|
next()
|
|
272
272
|
return
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
const origin = `http://${req.headers.host ??
|
|
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 ??
|
|
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(
|
|
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,
|
|
306
|
+
const outDir = pathJoin(root, '.pyreon')
|
|
307
307
|
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true })
|
|
308
|
-
writeFileSync(pathJoin(outDir,
|
|
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 ===
|
|
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 ===
|
|
368
|
-
else if (ch ===
|
|
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 ===
|
|
423
|
-
else if (ch ===
|
|
424
|
-
else if (ch ===
|
|
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] ??
|
|
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(
|
|
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(
|
|
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(
|
|
506
|
-
url.startsWith(
|
|
507
|
-
url.includes(
|
|
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
|
|
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
|
|
15
|
-
import pyreonPlugin from
|
|
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:
|
|
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:
|
|
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(
|
|
52
|
-
it(
|
|
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,
|
|
58
|
+
const result = transform(plugin, code, '/src/App.tsx')
|
|
59
59
|
expect(result).toBeDefined()
|
|
60
|
-
expect(result!.code).toContain(
|
|
60
|
+
expect(result!.code).toContain('import.meta.hot.accept()')
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
it(
|
|
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,
|
|
69
|
+
const result = transform(plugin, code, '/src/Header.tsx')
|
|
70
70
|
expect(result).toBeDefined()
|
|
71
|
-
expect(result!.code).toContain(
|
|
71
|
+
expect(result!.code).toContain('import.meta.hot')
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
-
it(
|
|
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,
|
|
81
|
+
const result = transform(plugin, code, '/src/utils.tsx')
|
|
82
82
|
expect(result).toBeDefined()
|
|
83
|
-
expect(result!.code).not.toContain(
|
|
83
|
+
expect(result!.code).not.toContain('import.meta.hot')
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
-
it(
|
|
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,
|
|
92
|
+
const result = transform(plugin, code, '/src/App.tsx')
|
|
93
93
|
expect(result).toBeDefined()
|
|
94
|
-
expect(result!.code).not.toContain(
|
|
94
|
+
expect(result!.code).not.toContain('import.meta.hot')
|
|
95
95
|
})
|
|
96
96
|
})
|
|
97
97
|
|
|
98
98
|
// ─── Signal rewriting ────────────────────────────────────────────────────────
|
|
99
99
|
|
|
100
|
-
describe(
|
|
101
|
-
it(
|
|
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,
|
|
109
|
+
const result = transform(plugin, code, '/src/Counter.tsx')
|
|
110
110
|
expect(result).toBeDefined()
|
|
111
|
-
expect(result!.code).toContain(
|
|
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(
|
|
114
|
+
expect(result!.code).toContain('__hmr_dispose')
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
-
it(
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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,
|
|
169
|
+
const result = transform(plugin, code, '/src/App.tsx')
|
|
170
170
|
expect(result).toBeDefined()
|
|
171
|
-
expect(result!.code).toContain(
|
|
172
|
-
expect(result!.code).toContain(
|
|
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(
|
|
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,
|
|
183
|
+
const result = transform(plugin, code, '/src/App.tsx')
|
|
184
184
|
expect(result).toBeDefined()
|
|
185
|
-
expect(result!.code).not.toContain(
|
|
185
|
+
expect(result!.code).not.toContain('__hmr_signal')
|
|
186
186
|
// No signal names in production builds
|
|
187
|
-
expect(result!.code).toContain(
|
|
188
|
-
expect(result!.code).not.toContain(
|
|
187
|
+
expect(result!.code).toContain('signal(0)')
|
|
188
|
+
expect(result!.code).not.toContain('{ name:')
|
|
189
189
|
})
|
|
190
190
|
|
|
191
|
-
it(
|
|
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,
|
|
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(
|
|
210
|
-
it(
|
|
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,
|
|
213
|
+
const result = transform(plugin, code, '/src/App.tsx')
|
|
214
214
|
expect(result).toBeDefined()
|
|
215
215
|
})
|
|
216
216
|
|
|
217
|
-
it(
|
|
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,
|
|
220
|
+
const result = transform(plugin, code, '/src/App.jsx')
|
|
221
221
|
expect(result).toBeDefined()
|
|
222
222
|
})
|
|
223
223
|
|
|
224
|
-
it(
|
|
224
|
+
it('ignores .ts files', () => {
|
|
225
225
|
const plugin = createPlugin()
|
|
226
226
|
const code = `export const x = 1`
|
|
227
|
-
const result = transform(plugin, code,
|
|
227
|
+
const result = transform(plugin, code, '/src/utils.ts')
|
|
228
228
|
expect(result).toBeUndefined()
|
|
229
229
|
})
|
|
230
230
|
|
|
231
|
-
it(
|
|
231
|
+
it('ignores .js files', () => {
|
|
232
232
|
const plugin = createPlugin()
|
|
233
233
|
const code = `export const x = 1`
|
|
234
|
-
const result = transform(plugin, code,
|
|
234
|
+
const result = transform(plugin, code, '/src/utils.js')
|
|
235
235
|
expect(result).toBeUndefined()
|
|
236
236
|
})
|
|
237
237
|
|
|
238
|
-
it(
|
|
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,
|
|
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(
|
|
249
|
-
it(
|
|
250
|
-
const plugin = createPlugin({ compat:
|
|
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,
|
|
255
|
+
const result = transform(plugin, code, '/src/App.tsx')
|
|
256
256
|
expect(result).toBeUndefined()
|
|
257
257
|
})
|
|
258
258
|
|
|
259
|
-
it(
|
|
260
|
-
const plugin = createPlugin({ compat:
|
|
261
|
-
const result = transform(plugin,
|
|
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(
|
|
266
|
-
const plugin = createPlugin({ compat:
|
|
267
|
-
const result = transform(plugin,
|
|
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(
|
|
272
|
-
const plugin = createPlugin({ compat:
|
|
273
|
-
const result = transform(plugin,
|
|
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(
|
|
281
|
-
it(
|
|
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:
|
|
283
|
+
const config = getConfigHook(plugin)({}, { command: 'serve' }) as Record<string, unknown>
|
|
284
284
|
expect(config.resolve).toBeUndefined()
|
|
285
285
|
})
|
|
286
286
|
|
|
287
|
-
it(
|
|
287
|
+
it('sets JSX import source to @pyreon/core by default', () => {
|
|
288
288
|
const plugin = pyreonPlugin()
|
|
289
|
-
const config = getConfigHook(plugin)({}, { command:
|
|
289
|
+
const config = getConfigHook(plugin)({}, { command: 'serve' }) as {
|
|
290
290
|
oxc: { jsx: { importSource: string } }
|
|
291
291
|
}
|
|
292
|
-
expect(config.oxc.jsx.importSource).toBe(
|
|
292
|
+
expect(config.oxc.jsx.importSource).toBe('@pyreon/core')
|
|
293
293
|
})
|
|
294
294
|
|
|
295
|
-
it(
|
|
296
|
-
const plugin = pyreonPlugin({ compat:
|
|
297
|
-
const config = getConfigHook(plugin)({}, { command:
|
|
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(
|
|
300
|
+
expect(config.oxc.jsx.importSource).toBe('@pyreon/react-compat')
|
|
301
301
|
})
|
|
302
302
|
|
|
303
|
-
it(
|
|
304
|
-
const plugin = pyreonPlugin({ compat:
|
|
305
|
-
const config = getConfigHook(plugin)({}, { command:
|
|
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(
|
|
309
|
-
expect(config.optimizeDeps.exclude).toContain(
|
|
308
|
+
expect(config.optimizeDeps.exclude).toContain('react')
|
|
309
|
+
expect(config.optimizeDeps.exclude).toContain('react-dom')
|
|
310
310
|
})
|
|
311
311
|
|
|
312
|
-
it(
|
|
313
|
-
const plugin = pyreonPlugin({ ssr: { entry:
|
|
314
|
-
const config = getConfigHook(plugin)({}, { command:
|
|
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(
|
|
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(
|
|
325
|
-
it(
|
|
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(
|
|
331
|
-
expect(resolved).toBe(
|
|
330
|
+
const resolved = await resolveId('virtual:pyreon/hmr-runtime')
|
|
331
|
+
expect(resolved).toBe('\0pyreon/hmr-runtime')
|
|
332
332
|
})
|
|
333
333
|
|
|
334
|
-
it(
|
|
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(
|
|
337
|
+
const source = load('\0pyreon/hmr-runtime')
|
|
338
338
|
expect(source).toBeDefined()
|
|
339
|
-
expect(source).toContain(
|
|
340
|
-
expect(source).toContain(
|
|
341
|
-
expect(source).toContain(
|
|
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(
|
|
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(
|
|
347
|
+
expect(load('/src/App.tsx')).toBeUndefined()
|
|
348
348
|
})
|
|
349
349
|
})
|
|
350
350
|
|
|
351
351
|
// ─── Asset request detection ────────────────────────────────────────────────
|
|
352
352
|
|
|
353
|
-
describe(
|
|
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(
|
|
360
|
-
const plugin = pyreonPlugin({ ssr: { entry:
|
|
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(
|
|
364
|
+
it('configureServer is defined even without SSR (for context generation)', () => {
|
|
365
365
|
const plugin = pyreonPlugin()
|
|
366
366
|
expect(plugin.configureServer).toBeDefined()
|
|
367
367
|
})
|