@pyreon/vite-plugin 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +144 -3
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +236 -3
- package/src/tests/compat-resolve.test.ts +178 -0
- package/src/tests/cross-module-signals.test.ts +425 -0
- package/src/tests/dev-server.test.ts +167 -0
- package/src/tests/vite-plugin.test.ts +60 -53
|
@@ -36,60 +36,67 @@ function createBuildPlugin(opts?: PyreonPluginOptions) {
|
|
|
36
36
|
return plugin
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function transform(plugin: ReturnType<typeof pyreonPlugin>, code: string, id: string) {
|
|
39
|
+
async function transform(plugin: ReturnType<typeof pyreonPlugin>, code: string, id: string) {
|
|
40
40
|
const transformHook = plugin.transform as (
|
|
41
|
-
this: { warn: (msg: string) => void },
|
|
41
|
+
this: { warn: (msg: string) => void; resolve: (id: string, importer?: string, options?: { skipSelf: boolean }) => Promise<{ id: string } | null> },
|
|
42
42
|
code: string,
|
|
43
43
|
id: string,
|
|
44
|
-
) => { code: string; map: null } | undefined
|
|
44
|
+
) => Promise<{ code: string; map: null } | undefined>
|
|
45
45
|
const warnings: string[] = []
|
|
46
|
-
return transformHook.call(
|
|
46
|
+
return transformHook.call(
|
|
47
|
+
{
|
|
48
|
+
warn: (msg: string) => warnings.push(msg),
|
|
49
|
+
resolve: async () => null, // no cross-module resolution in unit tests
|
|
50
|
+
},
|
|
51
|
+
code,
|
|
52
|
+
id,
|
|
53
|
+
)
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
// ─── HMR injection ──────────────────────────────────────────────────────────
|
|
50
57
|
|
|
51
58
|
describe('HMR injection', () => {
|
|
52
|
-
it('injects HMR accept for modules with component exports', () => {
|
|
59
|
+
it('injects HMR accept for modules with component exports', async () => {
|
|
53
60
|
const plugin = createPlugin()
|
|
54
61
|
const code = `
|
|
55
62
|
import { h } from "@pyreon/core"
|
|
56
63
|
export function App() { return h("div", null, "hello") }
|
|
57
64
|
`
|
|
58
|
-
const result = transform(plugin, code, '/src/App.tsx')
|
|
65
|
+
const result = await transform(plugin, code, '/src/App.tsx')
|
|
59
66
|
expect(result).toBeDefined()
|
|
60
67
|
expect(result!.code).toContain('import.meta.hot.accept()')
|
|
61
68
|
})
|
|
62
69
|
|
|
63
|
-
it('injects HMR for exported const components', () => {
|
|
70
|
+
it('injects HMR for exported const components', async () => {
|
|
64
71
|
const plugin = createPlugin()
|
|
65
72
|
const code = `
|
|
66
73
|
import { h } from "@pyreon/core"
|
|
67
74
|
export const Header = () => h("header", null, "nav")
|
|
68
75
|
`
|
|
69
|
-
const result = transform(plugin, code, '/src/Header.tsx')
|
|
76
|
+
const result = await transform(plugin, code, '/src/Header.tsx')
|
|
70
77
|
expect(result).toBeDefined()
|
|
71
78
|
expect(result!.code).toContain('import.meta.hot')
|
|
72
79
|
})
|
|
73
80
|
|
|
74
|
-
it('does not inject HMR for modules without component exports or signals', () => {
|
|
81
|
+
it('does not inject HMR for modules without component exports or signals', async () => {
|
|
75
82
|
const plugin = createPlugin()
|
|
76
83
|
// Only lowercase exports — no component-like names (uppercase first letter)
|
|
77
84
|
const code = `
|
|
78
85
|
export const formatDate = (d) => d.toISOString()
|
|
79
86
|
export const maxItems = 100
|
|
80
87
|
`
|
|
81
|
-
const result = transform(plugin, code, '/src/utils.tsx')
|
|
88
|
+
const result = await transform(plugin, code, '/src/utils.tsx')
|
|
82
89
|
expect(result).toBeDefined()
|
|
83
90
|
expect(result!.code).not.toContain('import.meta.hot')
|
|
84
91
|
})
|
|
85
92
|
|
|
86
|
-
it('does not inject HMR in build mode', () => {
|
|
93
|
+
it('does not inject HMR in build mode', async () => {
|
|
87
94
|
const plugin = createBuildPlugin()
|
|
88
95
|
const code = `
|
|
89
96
|
import { h } from "@pyreon/core"
|
|
90
97
|
export function App() { return h("div", null, "hello") }
|
|
91
98
|
`
|
|
92
|
-
const result = transform(plugin, code, '/src/App.tsx')
|
|
99
|
+
const result = await transform(plugin, code, '/src/App.tsx')
|
|
93
100
|
expect(result).toBeDefined()
|
|
94
101
|
expect(result!.code).not.toContain('import.meta.hot')
|
|
95
102
|
})
|
|
@@ -98,7 +105,7 @@ export function App() { return h("div", null, "hello") }
|
|
|
98
105
|
// ─── Signal rewriting ────────────────────────────────────────────────────────
|
|
99
106
|
|
|
100
107
|
describe('signal rewriting', () => {
|
|
101
|
-
it('rewrites module-scope signal() to __hmr_signal()', () => {
|
|
108
|
+
it('rewrites module-scope signal() to __hmr_signal()', async () => {
|
|
102
109
|
const plugin = createPlugin()
|
|
103
110
|
const code = `
|
|
104
111
|
import { signal } from "@pyreon/reactivity"
|
|
@@ -106,7 +113,7 @@ import { h } from "@pyreon/core"
|
|
|
106
113
|
const count = signal(0)
|
|
107
114
|
export function Counter() { return h("div", null, count()) }
|
|
108
115
|
`
|
|
109
|
-
const result = transform(plugin, code, '/src/Counter.tsx')
|
|
116
|
+
const result = await transform(plugin, code, '/src/Counter.tsx')
|
|
110
117
|
expect(result).toBeDefined()
|
|
111
118
|
expect(result!.code).toContain('__hmr_signal(')
|
|
112
119
|
expect(result!.code).toContain('"count"')
|
|
@@ -114,19 +121,19 @@ export function Counter() { return h("div", null, count()) }
|
|
|
114
121
|
expect(result!.code).toContain('__hmr_dispose')
|
|
115
122
|
})
|
|
116
123
|
|
|
117
|
-
it('rewrites exported signals', () => {
|
|
124
|
+
it('rewrites exported signals', async () => {
|
|
118
125
|
const plugin = createPlugin()
|
|
119
126
|
const code = `
|
|
120
127
|
import { signal } from "@pyreon/reactivity"
|
|
121
128
|
export const theme = signal("light")
|
|
122
129
|
export function App() { return null }
|
|
123
130
|
`
|
|
124
|
-
const result = transform(plugin, code, '/src/theme.tsx')
|
|
131
|
+
const result = await transform(plugin, code, '/src/theme.tsx')
|
|
125
132
|
expect(result).toBeDefined()
|
|
126
133
|
expect(result!.code).toContain('__hmr_signal("/src/theme.tsx", "theme", signal, "light")')
|
|
127
134
|
})
|
|
128
135
|
|
|
129
|
-
it('does not rewrite signal() inside functions to __hmr_signal (but injects name)', () => {
|
|
136
|
+
it('does not rewrite signal() inside functions to __hmr_signal (but injects name)', async () => {
|
|
130
137
|
const plugin = createPlugin()
|
|
131
138
|
const code = `
|
|
132
139
|
import { signal } from "@pyreon/reactivity"
|
|
@@ -136,7 +143,7 @@ export function Counter() {
|
|
|
136
143
|
return h("div", null, local())
|
|
137
144
|
}
|
|
138
145
|
`
|
|
139
|
-
const result = transform(plugin, code, '/src/Counter.tsx')
|
|
146
|
+
const result = await transform(plugin, code, '/src/Counter.tsx')
|
|
140
147
|
expect(result).toBeDefined()
|
|
141
148
|
// The signal inside the function body should NOT be rewritten to __hmr_signal
|
|
142
149
|
expect(result!.code).not.toContain('__hmr_signal')
|
|
@@ -144,7 +151,7 @@ export function Counter() {
|
|
|
144
151
|
expect(result!.code).toContain('signal(0, { name: "local" })')
|
|
145
152
|
})
|
|
146
153
|
|
|
147
|
-
it('rewrites multiple module-scope signals', () => {
|
|
154
|
+
it('rewrites multiple module-scope signals', async () => {
|
|
148
155
|
const plugin = createPlugin()
|
|
149
156
|
const code = `
|
|
150
157
|
import { signal } from "@pyreon/reactivity"
|
|
@@ -152,13 +159,13 @@ const count = signal(0)
|
|
|
152
159
|
const name = signal("world")
|
|
153
160
|
export function App() { return null }
|
|
154
161
|
`
|
|
155
|
-
const result = transform(plugin, code, '/src/App.tsx')
|
|
162
|
+
const result = await transform(plugin, code, '/src/App.tsx')
|
|
156
163
|
expect(result).toBeDefined()
|
|
157
164
|
expect(result!.code).toContain('"count"')
|
|
158
165
|
expect(result!.code).toContain('"name"')
|
|
159
166
|
})
|
|
160
167
|
|
|
161
|
-
it('handles signal with complex initial values', () => {
|
|
168
|
+
it('handles signal with complex initial values', async () => {
|
|
162
169
|
const plugin = createPlugin()
|
|
163
170
|
const code = `
|
|
164
171
|
import { signal } from "@pyreon/reactivity"
|
|
@@ -166,21 +173,21 @@ const items = signal([1, 2, 3])
|
|
|
166
173
|
const config = signal({ theme: "dark", size: 14 })
|
|
167
174
|
export function App() { return null }
|
|
168
175
|
`
|
|
169
|
-
const result = transform(plugin, code, '/src/App.tsx')
|
|
176
|
+
const result = await transform(plugin, code, '/src/App.tsx')
|
|
170
177
|
expect(result).toBeDefined()
|
|
171
178
|
expect(result!.code).toContain('__hmr_signal')
|
|
172
179
|
expect(result!.code).toContain('[1, 2, 3]')
|
|
173
180
|
expect(result!.code).toContain('{ theme: "dark", size: 14 }')
|
|
174
181
|
})
|
|
175
182
|
|
|
176
|
-
it('does not rewrite signal in build mode', () => {
|
|
183
|
+
it('does not rewrite signal in build mode', async () => {
|
|
177
184
|
const plugin = createBuildPlugin()
|
|
178
185
|
const code = `
|
|
179
186
|
import { signal } from "@pyreon/reactivity"
|
|
180
187
|
const count = signal(0)
|
|
181
188
|
export function App() { return null }
|
|
182
189
|
`
|
|
183
|
-
const result = transform(plugin, code, '/src/App.tsx')
|
|
190
|
+
const result = await transform(plugin, code, '/src/App.tsx')
|
|
184
191
|
expect(result).toBeDefined()
|
|
185
192
|
expect(result!.code).not.toContain('__hmr_signal')
|
|
186
193
|
// No signal names in production builds
|
|
@@ -188,7 +195,7 @@ export function App() { return null }
|
|
|
188
195
|
expect(result!.code).not.toContain('{ name:')
|
|
189
196
|
})
|
|
190
197
|
|
|
191
|
-
it('skips signal naming when options already provided', () => {
|
|
198
|
+
it('skips signal naming when options already provided', async () => {
|
|
192
199
|
const plugin = createPlugin()
|
|
193
200
|
const code = `
|
|
194
201
|
import { signal } from "@pyreon/reactivity"
|
|
@@ -197,7 +204,7 @@ export function App() {
|
|
|
197
204
|
return null
|
|
198
205
|
}
|
|
199
206
|
`
|
|
200
|
-
const result = transform(plugin, code, '/src/App.tsx')
|
|
207
|
+
const result = await transform(plugin, code, '/src/App.tsx')
|
|
201
208
|
expect(result).toBeDefined()
|
|
202
209
|
// Should not double-inject name
|
|
203
210
|
expect(result!.code).toContain('signal(0, { name: "custom" })')
|
|
@@ -207,38 +214,38 @@ export function App() {
|
|
|
207
214
|
// ─── File extension filtering ────────────────────────────────────────────────
|
|
208
215
|
|
|
209
216
|
describe('file extension filtering', () => {
|
|
210
|
-
it('transforms .tsx files', () => {
|
|
217
|
+
it('transforms .tsx files', async () => {
|
|
211
218
|
const plugin = createPlugin()
|
|
212
219
|
const code = `export function App() { return null }`
|
|
213
|
-
const result = transform(plugin, code, '/src/App.tsx')
|
|
220
|
+
const result = await transform(plugin, code, '/src/App.tsx')
|
|
214
221
|
expect(result).toBeDefined()
|
|
215
222
|
})
|
|
216
223
|
|
|
217
|
-
it('transforms .jsx files', () => {
|
|
224
|
+
it('transforms .jsx files', async () => {
|
|
218
225
|
const plugin = createPlugin()
|
|
219
226
|
const code = `export function App() { return null }`
|
|
220
|
-
const result = transform(plugin, code, '/src/App.jsx')
|
|
227
|
+
const result = await transform(plugin, code, '/src/App.jsx')
|
|
221
228
|
expect(result).toBeDefined()
|
|
222
229
|
})
|
|
223
230
|
|
|
224
|
-
it('ignores .ts files', () => {
|
|
231
|
+
it('ignores .ts files', async () => {
|
|
225
232
|
const plugin = createPlugin()
|
|
226
233
|
const code = `export const x = 1`
|
|
227
|
-
const result = transform(plugin, code, '/src/utils.ts')
|
|
234
|
+
const result = await transform(plugin, code, '/src/utils.ts')
|
|
228
235
|
expect(result).toBeUndefined()
|
|
229
236
|
})
|
|
230
237
|
|
|
231
|
-
it('ignores .js files', () => {
|
|
238
|
+
it('ignores .js files', async () => {
|
|
232
239
|
const plugin = createPlugin()
|
|
233
240
|
const code = `export const x = 1`
|
|
234
|
-
const result = transform(plugin, code, '/src/utils.js')
|
|
241
|
+
const result = await transform(plugin, code, '/src/utils.js')
|
|
235
242
|
expect(result).toBeUndefined()
|
|
236
243
|
})
|
|
237
244
|
|
|
238
|
-
it('handles query strings in file paths', () => {
|
|
245
|
+
it('handles query strings in file paths', async () => {
|
|
239
246
|
const plugin = createPlugin()
|
|
240
247
|
const code = `export function App() { return null }`
|
|
241
|
-
const result = transform(plugin, code, '/src/App.tsx?v=123')
|
|
248
|
+
const result = await transform(plugin, code, '/src/App.tsx?v=123')
|
|
242
249
|
expect(result).toBeDefined()
|
|
243
250
|
})
|
|
244
251
|
})
|
|
@@ -246,31 +253,31 @@ describe('file extension filtering', () => {
|
|
|
246
253
|
// ─── Compat mode ─────────────────────────────────────────────────────────────
|
|
247
254
|
|
|
248
255
|
describe('compat mode', () => {
|
|
249
|
-
it('skips Pyreon JSX transform in react compat mode', () => {
|
|
256
|
+
it('skips Pyreon JSX transform in react compat mode', async () => {
|
|
250
257
|
const plugin = createPlugin({ compat: 'react' })
|
|
251
258
|
const code = `
|
|
252
259
|
import { useState } from "react"
|
|
253
260
|
export function App() { const [x] = useState(0); return null }
|
|
254
261
|
`
|
|
255
|
-
const result = transform(plugin, code, '/src/App.tsx')
|
|
262
|
+
const result = await transform(plugin, code, '/src/App.tsx')
|
|
256
263
|
expect(result).toBeUndefined()
|
|
257
264
|
})
|
|
258
265
|
|
|
259
|
-
it('skips transform in preact compat mode', () => {
|
|
266
|
+
it('skips transform in preact compat mode', async () => {
|
|
260
267
|
const plugin = createPlugin({ compat: 'preact' })
|
|
261
|
-
const result = transform(plugin, 'export function App() { return null }', '/src/App.tsx')
|
|
268
|
+
const result = await transform(plugin, 'export function App() { return null }', '/src/App.tsx')
|
|
262
269
|
expect(result).toBeUndefined()
|
|
263
270
|
})
|
|
264
271
|
|
|
265
|
-
it('skips transform in vue compat mode', () => {
|
|
272
|
+
it('skips transform in vue compat mode', async () => {
|
|
266
273
|
const plugin = createPlugin({ compat: 'vue' })
|
|
267
|
-
const result = transform(plugin, 'export function App() { return null }', '/src/App.tsx')
|
|
274
|
+
const result = await transform(plugin, 'export function App() { return null }', '/src/App.tsx')
|
|
268
275
|
expect(result).toBeUndefined()
|
|
269
276
|
})
|
|
270
277
|
|
|
271
|
-
it('skips transform in solid compat mode', () => {
|
|
278
|
+
it('skips transform in solid compat mode', async () => {
|
|
272
279
|
const plugin = createPlugin({ compat: 'solid' })
|
|
273
|
-
const result = transform(plugin, 'export function App() { return null }', '/src/App.tsx')
|
|
280
|
+
const result = await transform(plugin, 'export function App() { return null }', '/src/App.tsx')
|
|
274
281
|
expect(result).toBeUndefined()
|
|
275
282
|
})
|
|
276
283
|
})
|
|
@@ -278,13 +285,13 @@ export function App() { const [x] = useState(0); return null }
|
|
|
278
285
|
// ─── Plugin config ───────────────────────────────────────────────────────────
|
|
279
286
|
|
|
280
287
|
describe('plugin config', () => {
|
|
281
|
-
it('sets resolve.conditions: ["bun"] for workspace source resolution', () => {
|
|
288
|
+
it('sets resolve.conditions: ["bun"] for workspace source resolution', async () => {
|
|
282
289
|
const plugin = pyreonPlugin()
|
|
283
290
|
const config = getConfigHook(plugin)({}, { command: 'serve' }) as Record<string, any>
|
|
284
291
|
expect(config.resolve.conditions).toEqual(['bun'])
|
|
285
292
|
})
|
|
286
293
|
|
|
287
|
-
it('sets JSX import source to @pyreon/core by default', () => {
|
|
294
|
+
it('sets JSX import source to @pyreon/core by default', async () => {
|
|
288
295
|
const plugin = pyreonPlugin()
|
|
289
296
|
const config = getConfigHook(plugin)({}, { command: 'serve' }) as {
|
|
290
297
|
oxc: { jsx: { importSource: string } }
|
|
@@ -292,7 +299,7 @@ describe('plugin config', () => {
|
|
|
292
299
|
expect(config.oxc.jsx.importSource).toBe('@pyreon/core')
|
|
293
300
|
})
|
|
294
301
|
|
|
295
|
-
it('sets JSX import source to compat package in compat mode', () => {
|
|
302
|
+
it('sets JSX import source to compat package in compat mode', async () => {
|
|
296
303
|
const plugin = pyreonPlugin({ compat: 'react' })
|
|
297
304
|
const config = getConfigHook(plugin)({}, { command: 'serve' }) as {
|
|
298
305
|
oxc: { jsx: { importSource: string } }
|
|
@@ -300,7 +307,7 @@ describe('plugin config', () => {
|
|
|
300
307
|
expect(config.oxc.jsx.importSource).toBe('@pyreon/react-compat')
|
|
301
308
|
})
|
|
302
309
|
|
|
303
|
-
it('excludes compat packages from optimizeDeps', () => {
|
|
310
|
+
it('excludes compat packages from optimizeDeps', async () => {
|
|
304
311
|
const plugin = pyreonPlugin({ compat: 'react' })
|
|
305
312
|
const config = getConfigHook(plugin)({}, { command: 'serve' }) as {
|
|
306
313
|
optimizeDeps: { exclude: string[] }
|
|
@@ -309,7 +316,7 @@ describe('plugin config', () => {
|
|
|
309
316
|
expect(config.optimizeDeps.exclude).toContain('react-dom')
|
|
310
317
|
})
|
|
311
318
|
|
|
312
|
-
it('adds SSR build config when isSsrBuild', () => {
|
|
319
|
+
it('adds SSR build config when isSsrBuild', async () => {
|
|
313
320
|
const plugin = pyreonPlugin({ ssr: { entry: './src/entry-server.ts' } })
|
|
314
321
|
const config = getConfigHook(plugin)({}, { command: 'build', isSsrBuild: true }) as {
|
|
315
322
|
build: { ssr: boolean; rollupOptions: { input: string } }
|
|
@@ -331,7 +338,7 @@ describe('virtual module resolution', () => {
|
|
|
331
338
|
expect(resolved).toBe('\0pyreon/hmr-runtime')
|
|
332
339
|
})
|
|
333
340
|
|
|
334
|
-
it('loads HMR runtime source for internal ID', () => {
|
|
341
|
+
it('loads HMR runtime source for internal ID', async () => {
|
|
335
342
|
const plugin = createPlugin()
|
|
336
343
|
const load = plugin.load as (id: string) => string | undefined
|
|
337
344
|
const source = load('\0pyreon/hmr-runtime')
|
|
@@ -341,7 +348,7 @@ describe('virtual module resolution', () => {
|
|
|
341
348
|
expect(source).toContain('__pyreon_hmr_registry__')
|
|
342
349
|
})
|
|
343
350
|
|
|
344
|
-
it('returns undefined for non-virtual IDs', () => {
|
|
351
|
+
it('returns undefined for non-virtual IDs', async () => {
|
|
345
352
|
const plugin = createPlugin()
|
|
346
353
|
const load = plugin.load as (id: string) => string | undefined
|
|
347
354
|
expect(load('/src/App.tsx')).toBeUndefined()
|
|
@@ -356,12 +363,12 @@ describe('asset request filtering', () => {
|
|
|
356
363
|
// For direct testing, we'd need to export it — instead we verify
|
|
357
364
|
// the plugin's SSR middleware config exists when ssr option is set.
|
|
358
365
|
|
|
359
|
-
it('configureServer returns middleware function when SSR enabled', () => {
|
|
366
|
+
it('configureServer returns middleware function when SSR enabled', async () => {
|
|
360
367
|
const plugin = pyreonPlugin({ ssr: { entry: './src/entry-server.ts' } })
|
|
361
368
|
expect(plugin.configureServer).toBeDefined()
|
|
362
369
|
})
|
|
363
370
|
|
|
364
|
-
it('configureServer is defined even without SSR (for context generation)', () => {
|
|
371
|
+
it('configureServer is defined even without SSR (for context generation)', async () => {
|
|
365
372
|
const plugin = pyreonPlugin()
|
|
366
373
|
expect(plugin.configureServer).toBeDefined()
|
|
367
374
|
})
|