@pyreon/vite-plugin 0.5.3 → 0.5.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/vite-plugin",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Vite plugin for Pyreon — .pyreon SFC support, HMR, compiler integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,13 +33,13 @@
33
33
  "scripts": {
34
34
  "build": "vl_rolldown_build",
35
35
  "dev": "vl_rolldown_build-watch",
36
- "test": "echo 'no tests yet'",
36
+ "test": "vitest run",
37
37
  "typecheck": "tsc --noEmit",
38
38
  "lint": "biome check .",
39
39
  "prepublishOnly": "bun run build"
40
40
  },
41
41
  "dependencies": {
42
- "@pyreon/compiler": "^0.5.3"
42
+ "@pyreon/compiler": "^0.5.5"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "vite": ">=8.0.0"
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Tests for @pyreon/vite-plugin — HMR injection, signal rewriting,
3
+ * compat alias resolution, and helper functions.
4
+ *
5
+ * These test the plugin's transform logic directly (no Vite required).
6
+ */
7
+
8
+ import { describe, expect, it } from "vitest"
9
+
10
+ // ── Import internals ─────────────────────────────────────────────────────────
11
+ // We import the default export and call it to get the plugin object,
12
+ // then invoke its hooks directly.
13
+
14
+ import type { PyreonPluginOptions } from "../index"
15
+ import pyreonPlugin from "../index"
16
+
17
+ type ConfigHook = (
18
+ userConfig: Record<string, unknown>,
19
+ env: { command: string; isSsrBuild?: boolean },
20
+ ) => Record<string, unknown>
21
+
22
+ function getConfigHook(plugin: ReturnType<typeof pyreonPlugin>): ConfigHook {
23
+ return plugin.config as unknown as ConfigHook
24
+ }
25
+
26
+ function createPlugin(opts?: PyreonPluginOptions) {
27
+ const plugin = pyreonPlugin(opts)
28
+ // Simulate Vite calling config() so isBuild / projectRoot are set
29
+ getConfigHook(plugin)({}, { command: "serve" })
30
+ return plugin
31
+ }
32
+
33
+ function createBuildPlugin(opts?: PyreonPluginOptions) {
34
+ const plugin = pyreonPlugin(opts)
35
+ getConfigHook(plugin)({}, { command: "build" })
36
+ return plugin
37
+ }
38
+
39
+ function transform(plugin: ReturnType<typeof pyreonPlugin>, code: string, id: string) {
40
+ const transformHook = plugin.transform as (
41
+ this: { warn: (msg: string) => void },
42
+ code: string,
43
+ id: string,
44
+ ) => { code: string; map: null } | undefined
45
+ const warnings: string[] = []
46
+ return transformHook.call({ warn: (msg: string) => warnings.push(msg) }, code, id)
47
+ }
48
+
49
+ // ─── HMR injection ──────────────────────────────────────────────────────────
50
+
51
+ describe("HMR injection", () => {
52
+ it("injects HMR accept for modules with component exports", () => {
53
+ const plugin = createPlugin()
54
+ const code = `
55
+ import { h } from "@pyreon/core"
56
+ export function App() { return h("div", null, "hello") }
57
+ `
58
+ const result = transform(plugin, code, "/src/App.tsx")
59
+ expect(result).toBeDefined()
60
+ expect(result!.code).toContain("import.meta.hot.accept()")
61
+ })
62
+
63
+ it("injects HMR for exported const components", () => {
64
+ const plugin = createPlugin()
65
+ const code = `
66
+ import { h } from "@pyreon/core"
67
+ export const Header = () => h("header", null, "nav")
68
+ `
69
+ const result = transform(plugin, code, "/src/Header.tsx")
70
+ expect(result).toBeDefined()
71
+ expect(result!.code).toContain("import.meta.hot")
72
+ })
73
+
74
+ it("does not inject HMR for modules without component exports or signals", () => {
75
+ const plugin = createPlugin()
76
+ // Only lowercase exports — no component-like names (uppercase first letter)
77
+ const code = `
78
+ export const formatDate = (d) => d.toISOString()
79
+ export const maxItems = 100
80
+ `
81
+ const result = transform(plugin, code, "/src/utils.tsx")
82
+ expect(result).toBeDefined()
83
+ expect(result!.code).not.toContain("import.meta.hot")
84
+ })
85
+
86
+ it("does not inject HMR in build mode", () => {
87
+ const plugin = createBuildPlugin()
88
+ const code = `
89
+ import { h } from "@pyreon/core"
90
+ export function App() { return h("div", null, "hello") }
91
+ `
92
+ const result = transform(plugin, code, "/src/App.tsx")
93
+ expect(result).toBeDefined()
94
+ expect(result!.code).not.toContain("import.meta.hot")
95
+ })
96
+ })
97
+
98
+ // ─── Signal rewriting ────────────────────────────────────────────────────────
99
+
100
+ describe("signal rewriting", () => {
101
+ it("rewrites module-scope signal() to __hmr_signal()", () => {
102
+ const plugin = createPlugin()
103
+ const code = `
104
+ import { signal } from "@pyreon/reactivity"
105
+ import { h } from "@pyreon/core"
106
+ const count = signal(0)
107
+ export function Counter() { return h("div", null, count()) }
108
+ `
109
+ const result = transform(plugin, code, "/src/Counter.tsx")
110
+ expect(result).toBeDefined()
111
+ expect(result!.code).toContain("__hmr_signal(")
112
+ expect(result!.code).toContain('"count"')
113
+ expect(result!.code).toContain('"/src/Counter.tsx"')
114
+ expect(result!.code).toContain("__hmr_dispose")
115
+ })
116
+
117
+ it("rewrites exported signals", () => {
118
+ const plugin = createPlugin()
119
+ const code = `
120
+ import { signal } from "@pyreon/reactivity"
121
+ export const theme = signal("light")
122
+ export function App() { return null }
123
+ `
124
+ const result = transform(plugin, code, "/src/theme.tsx")
125
+ expect(result).toBeDefined()
126
+ expect(result!.code).toContain('__hmr_signal("/src/theme.tsx", "theme", signal, "light")')
127
+ })
128
+
129
+ it("does not rewrite signal() inside functions (non-module scope)", () => {
130
+ const plugin = createPlugin()
131
+ const code = `
132
+ import { signal } from "@pyreon/reactivity"
133
+ import { h } from "@pyreon/core"
134
+ export function Counter() {
135
+ const local = signal(0)
136
+ return h("div", null, local())
137
+ }
138
+ `
139
+ const result = transform(plugin, code, "/src/Counter.tsx")
140
+ expect(result).toBeDefined()
141
+ // The signal inside the function body should NOT be rewritten
142
+ expect(result!.code).toContain("const local = signal(0)")
143
+ })
144
+
145
+ it("rewrites multiple module-scope signals", () => {
146
+ const plugin = createPlugin()
147
+ const code = `
148
+ import { signal } from "@pyreon/reactivity"
149
+ const count = signal(0)
150
+ const name = signal("world")
151
+ export function App() { return null }
152
+ `
153
+ const result = transform(plugin, code, "/src/App.tsx")
154
+ expect(result).toBeDefined()
155
+ expect(result!.code).toContain('"count"')
156
+ expect(result!.code).toContain('"name"')
157
+ })
158
+
159
+ it("handles signal with complex initial values", () => {
160
+ const plugin = createPlugin()
161
+ const code = `
162
+ import { signal } from "@pyreon/reactivity"
163
+ const items = signal([1, 2, 3])
164
+ const config = signal({ theme: "dark", size: 14 })
165
+ export function App() { return null }
166
+ `
167
+ const result = transform(plugin, code, "/src/App.tsx")
168
+ expect(result).toBeDefined()
169
+ expect(result!.code).toContain("__hmr_signal")
170
+ expect(result!.code).toContain("[1, 2, 3]")
171
+ expect(result!.code).toContain('{ theme: "dark", size: 14 }')
172
+ })
173
+
174
+ it("does not rewrite signal in build mode", () => {
175
+ const plugin = createBuildPlugin()
176
+ const code = `
177
+ import { signal } from "@pyreon/reactivity"
178
+ const count = signal(0)
179
+ export function App() { return null }
180
+ `
181
+ const result = transform(plugin, code, "/src/App.tsx")
182
+ expect(result).toBeDefined()
183
+ expect(result!.code).not.toContain("__hmr_signal")
184
+ expect(result!.code).toContain("signal(0)")
185
+ })
186
+ })
187
+
188
+ // ─── File extension filtering ────────────────────────────────────────────────
189
+
190
+ describe("file extension filtering", () => {
191
+ it("transforms .tsx files", () => {
192
+ const plugin = createPlugin()
193
+ const code = `export function App() { return null }`
194
+ const result = transform(plugin, code, "/src/App.tsx")
195
+ expect(result).toBeDefined()
196
+ })
197
+
198
+ it("transforms .jsx files", () => {
199
+ const plugin = createPlugin()
200
+ const code = `export function App() { return null }`
201
+ const result = transform(plugin, code, "/src/App.jsx")
202
+ expect(result).toBeDefined()
203
+ })
204
+
205
+ it("ignores .ts files", () => {
206
+ const plugin = createPlugin()
207
+ const code = `export const x = 1`
208
+ const result = transform(plugin, code, "/src/utils.ts")
209
+ expect(result).toBeUndefined()
210
+ })
211
+
212
+ it("ignores .js files", () => {
213
+ const plugin = createPlugin()
214
+ const code = `export const x = 1`
215
+ const result = transform(plugin, code, "/src/utils.js")
216
+ expect(result).toBeUndefined()
217
+ })
218
+
219
+ it("handles query strings in file paths", () => {
220
+ const plugin = createPlugin()
221
+ const code = `export function App() { return null }`
222
+ const result = transform(plugin, code, "/src/App.tsx?v=123")
223
+ expect(result).toBeDefined()
224
+ })
225
+ })
226
+
227
+ // ─── Compat mode ─────────────────────────────────────────────────────────────
228
+
229
+ describe("compat mode", () => {
230
+ it("skips Pyreon JSX transform in react compat mode", () => {
231
+ const plugin = createPlugin({ compat: "react" })
232
+ const code = `
233
+ import { useState } from "react"
234
+ export function App() { const [x] = useState(0); return null }
235
+ `
236
+ const result = transform(plugin, code, "/src/App.tsx")
237
+ expect(result).toBeUndefined()
238
+ })
239
+
240
+ it("skips transform in preact compat mode", () => {
241
+ const plugin = createPlugin({ compat: "preact" })
242
+ const result = transform(plugin, "export function App() { return null }", "/src/App.tsx")
243
+ expect(result).toBeUndefined()
244
+ })
245
+
246
+ it("skips transform in vue compat mode", () => {
247
+ const plugin = createPlugin({ compat: "vue" })
248
+ const result = transform(plugin, "export function App() { return null }", "/src/App.tsx")
249
+ expect(result).toBeUndefined()
250
+ })
251
+
252
+ it("skips transform in solid compat mode", () => {
253
+ const plugin = createPlugin({ compat: "solid" })
254
+ const result = transform(plugin, "export function App() { return null }", "/src/App.tsx")
255
+ expect(result).toBeUndefined()
256
+ })
257
+ })
258
+
259
+ // ─── Plugin config ───────────────────────────────────────────────────────────
260
+
261
+ describe("plugin config", () => {
262
+ it("sets resolve conditions to ['bun']", () => {
263
+ const plugin = pyreonPlugin()
264
+ const config = getConfigHook(plugin)({}, { command: "serve" }) as {
265
+ resolve: { conditions: string[] }
266
+ }
267
+ expect(config.resolve.conditions).toContain("bun")
268
+ })
269
+
270
+ it("sets JSX import source to @pyreon/core by default", () => {
271
+ const plugin = pyreonPlugin()
272
+ const config = getConfigHook(plugin)({}, { command: "serve" }) as {
273
+ oxc: { jsx: { importSource: string } }
274
+ }
275
+ expect(config.oxc.jsx.importSource).toBe("@pyreon/core")
276
+ })
277
+
278
+ it("sets JSX import source to compat package in compat mode", () => {
279
+ const plugin = pyreonPlugin({ compat: "react" })
280
+ const config = getConfigHook(plugin)({}, { command: "serve" }) as {
281
+ oxc: { jsx: { importSource: string } }
282
+ }
283
+ expect(config.oxc.jsx.importSource).toBe("@pyreon/react-compat")
284
+ })
285
+
286
+ it("excludes compat packages from optimizeDeps", () => {
287
+ const plugin = pyreonPlugin({ compat: "react" })
288
+ const config = getConfigHook(plugin)({}, { command: "serve" }) as {
289
+ optimizeDeps: { exclude: string[] }
290
+ }
291
+ expect(config.optimizeDeps.exclude).toContain("react")
292
+ expect(config.optimizeDeps.exclude).toContain("react-dom")
293
+ })
294
+
295
+ it("adds SSR build config when isSsrBuild", () => {
296
+ const plugin = pyreonPlugin({ ssr: { entry: "./src/entry-server.ts" } })
297
+ const config = getConfigHook(plugin)({}, { command: "build", isSsrBuild: true }) as {
298
+ build: { ssr: boolean; rollupOptions: { input: string } }
299
+ }
300
+ expect(config.build.ssr).toBe(true)
301
+ expect(config.build.rollupOptions.input).toBe("./src/entry-server.ts")
302
+ })
303
+ })
304
+
305
+ // ─── Virtual module (HMR runtime) ────────────────────────────────────────────
306
+
307
+ describe("virtual module resolution", () => {
308
+ it("resolves virtual:pyreon/hmr-runtime to internal ID", () => {
309
+ const plugin = createPlugin()
310
+ const resolveId = plugin.resolveId as (id: string) => string | undefined
311
+ const resolved = resolveId("virtual:pyreon/hmr-runtime")
312
+ expect(resolved).toBe("\0pyreon/hmr-runtime")
313
+ })
314
+
315
+ it("loads HMR runtime source for internal ID", () => {
316
+ const plugin = createPlugin()
317
+ const load = plugin.load as (id: string) => string | undefined
318
+ const source = load("\0pyreon/hmr-runtime")
319
+ expect(source).toBeDefined()
320
+ expect(source).toContain("__hmr_signal")
321
+ expect(source).toContain("__hmr_dispose")
322
+ expect(source).toContain("__pyreon_hmr_registry__")
323
+ })
324
+
325
+ it("returns undefined for non-virtual IDs", () => {
326
+ const plugin = createPlugin()
327
+ const load = plugin.load as (id: string) => string | undefined
328
+ expect(load("/src/App.tsx")).toBeUndefined()
329
+ })
330
+ })
331
+
332
+ // ─── Asset request detection ────────────────────────────────────────────────
333
+
334
+ describe("asset request filtering", () => {
335
+ // The SSR middleware uses isAssetRequest internally.
336
+ // We test it via the configureServer middleware behavior.
337
+ // For direct testing, we'd need to export it — instead we verify
338
+ // the plugin's SSR middleware config exists when ssr option is set.
339
+
340
+ it("configureServer returns middleware function when SSR enabled", () => {
341
+ const plugin = pyreonPlugin({ ssr: { entry: "./src/entry-server.ts" } })
342
+ expect(plugin.configureServer).toBeDefined()
343
+ })
344
+
345
+ it("configureServer is defined even without SSR (for context generation)", () => {
346
+ const plugin = pyreonPlugin()
347
+ expect(plugin.configureServer).toBeDefined()
348
+ })
349
+ })