@pyreon/vite-plugin 0.24.5 → 0.24.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/package.json +2 -4
- package/src/hmr-runtime.ts +0 -92
- package/src/index.ts +0 -2116
- package/src/rocketstyle-collapse.ts +0 -199
- package/src/tests/cache-eviction-on-delete.test.ts +0 -187
- package/src/tests/compat-resolve.test.ts +0 -260
- package/src/tests/cross-module-signals.test.ts +0 -425
- package/src/tests/dev-server.test.ts +0 -171
- package/src/tests/islands-registry.test.ts +0 -236
- package/src/tests/lpih-auto-bridge.test.ts +0 -408
- package/src/tests/lpih-injection.test.ts +0 -559
- package/src/tests/rocketstyle-collapse-dev.test.ts +0 -119
- package/src/tests/rocketstyle-collapse.test.ts +0 -352
- package/src/tests/ssr-no-external.test.ts +0 -82
- package/src/tests/vite-plugin.test.ts +0 -503
|
@@ -1,503 +0,0 @@
|
|
|
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 { mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
9
|
-
import { join as pathJoin } from 'node:path'
|
|
10
|
-
import { describe, expect, it } from 'vitest'
|
|
11
|
-
|
|
12
|
-
// ── Import internals ─────────────────────────────────────────────────────────
|
|
13
|
-
// We import the default export and call it to get the plugin object,
|
|
14
|
-
// then invoke its hooks directly.
|
|
15
|
-
|
|
16
|
-
import type { PyreonPluginOptions } from '../index'
|
|
17
|
-
import pyreonPlugin from '../index'
|
|
18
|
-
|
|
19
|
-
type ConfigHook = (
|
|
20
|
-
userConfig: Record<string, unknown>,
|
|
21
|
-
env: { command: string; isSsrBuild?: boolean },
|
|
22
|
-
) => Record<string, unknown>
|
|
23
|
-
|
|
24
|
-
function getConfigHook(plugin: ReturnType<typeof pyreonPlugin>): ConfigHook {
|
|
25
|
-
return plugin.config as unknown as ConfigHook
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function createPlugin(opts?: PyreonPluginOptions) {
|
|
29
|
-
const plugin = pyreonPlugin(opts)
|
|
30
|
-
// Simulate Vite calling config() so isBuild / projectRoot are set
|
|
31
|
-
getConfigHook(plugin)({}, { command: 'serve' })
|
|
32
|
-
return plugin
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function createBuildPlugin(opts?: PyreonPluginOptions) {
|
|
36
|
-
const plugin = pyreonPlugin(opts)
|
|
37
|
-
getConfigHook(plugin)({}, { command: 'build' })
|
|
38
|
-
return plugin
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function transform(plugin: ReturnType<typeof pyreonPlugin>, code: string, id: string) {
|
|
42
|
-
const transformHook = plugin.transform as (
|
|
43
|
-
this: { warn: (msg: string) => void; resolve: (id: string, importer?: string, options?: { skipSelf: boolean }) => Promise<{ id: string } | null> },
|
|
44
|
-
code: string,
|
|
45
|
-
id: string,
|
|
46
|
-
) => Promise<{ code: string; map: null } | undefined>
|
|
47
|
-
const warnings: string[] = []
|
|
48
|
-
return transformHook.call(
|
|
49
|
-
{
|
|
50
|
-
warn: (msg: string) => warnings.push(msg),
|
|
51
|
-
resolve: async () => null, // no cross-module resolution in unit tests
|
|
52
|
-
},
|
|
53
|
-
code,
|
|
54
|
-
id,
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ─── HMR injection ──────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
describe('HMR injection', () => {
|
|
61
|
-
it('injects a coordinator-driven HMR accept for modules with component exports', async () => {
|
|
62
|
-
const plugin = createPlugin()
|
|
63
|
-
const code = `
|
|
64
|
-
import { h } from "@pyreon/core"
|
|
65
|
-
export function App() { return h("div", null, "hello") }
|
|
66
|
-
`
|
|
67
|
-
const result = await transform(plugin, code, '/src/App.tsx')
|
|
68
|
-
expect(result).toBeDefined()
|
|
69
|
-
// Self-accept WITH a callback receiving the fresh module (the bare
|
|
70
|
-
// `accept()` was the bug — it suppressed Vite's reload fallback while
|
|
71
|
-
// re-rendering nothing).
|
|
72
|
-
expect(result!.code).toContain('import.meta.hot.accept((__m) => {')
|
|
73
|
-
expect(result!.code).not.toContain('import.meta.hot.accept();')
|
|
74
|
-
// Hands the fresh module to the router-registered HMR coordinator,
|
|
75
|
-
// keyed by THIS module's id (zero import coupling).
|
|
76
|
-
expect(result!.code).toContain('globalThis.__pyreon_hmr_swap__')
|
|
77
|
-
expect(result!.code).toContain('__s("/src/App.tsx", __m)')
|
|
78
|
-
// Falls back to an automatic full reload when the edit was outside the
|
|
79
|
-
// active route tree or no coordinator is registered.
|
|
80
|
-
expect(result!.code).toContain('import.meta.hot.invalidate()')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('injects HMR for exported const components', async () => {
|
|
84
|
-
const plugin = createPlugin()
|
|
85
|
-
const code = `
|
|
86
|
-
import { h } from "@pyreon/core"
|
|
87
|
-
export const Header = () => h("header", null, "nav")
|
|
88
|
-
`
|
|
89
|
-
const result = await transform(plugin, code, '/src/Header.tsx')
|
|
90
|
-
expect(result).toBeDefined()
|
|
91
|
-
expect(result!.code).toContain('import.meta.hot')
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('does not inject HMR for modules without component exports or signals', async () => {
|
|
95
|
-
const plugin = createPlugin()
|
|
96
|
-
// Only lowercase exports — no component-like names (uppercase first letter)
|
|
97
|
-
const code = `
|
|
98
|
-
export const formatDate = (d) => d.toISOString()
|
|
99
|
-
export const maxItems = 100
|
|
100
|
-
`
|
|
101
|
-
const result = await transform(plugin, code, '/src/utils.tsx')
|
|
102
|
-
expect(result).toBeDefined()
|
|
103
|
-
expect(result!.code).not.toContain('import.meta.hot')
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('does not inject HMR in build mode', async () => {
|
|
107
|
-
const plugin = createBuildPlugin()
|
|
108
|
-
const code = `
|
|
109
|
-
import { h } from "@pyreon/core"
|
|
110
|
-
export function App() { return h("div", null, "hello") }
|
|
111
|
-
`
|
|
112
|
-
const result = await transform(plugin, code, '/src/App.tsx')
|
|
113
|
-
expect(result).toBeDefined()
|
|
114
|
-
expect(result!.code).not.toContain('import.meta.hot')
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
// ─── Signal rewriting ────────────────────────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
describe('signal rewriting', () => {
|
|
121
|
-
it('rewrites module-scope signal() to __hmr_signal()', async () => {
|
|
122
|
-
const plugin = createPlugin()
|
|
123
|
-
const code = `
|
|
124
|
-
import { signal } from "@pyreon/reactivity"
|
|
125
|
-
import { h } from "@pyreon/core"
|
|
126
|
-
const count = signal(0)
|
|
127
|
-
export function Counter() { return h("div", null, count()) }
|
|
128
|
-
`
|
|
129
|
-
const result = await transform(plugin, code, '/src/Counter.tsx')
|
|
130
|
-
expect(result).toBeDefined()
|
|
131
|
-
expect(result!.code).toContain('__hmr_signal(')
|
|
132
|
-
expect(result!.code).toContain('"count"')
|
|
133
|
-
expect(result!.code).toContain('"/src/Counter.tsx"')
|
|
134
|
-
expect(result!.code).toContain('__hmr_dispose')
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('rewrites exported signals', async () => {
|
|
138
|
-
const plugin = createPlugin()
|
|
139
|
-
const code = `
|
|
140
|
-
import { signal } from "@pyreon/reactivity"
|
|
141
|
-
export const theme = signal("light")
|
|
142
|
-
export function App() { return null }
|
|
143
|
-
`
|
|
144
|
-
const result = await transform(plugin, code, '/src/theme.tsx')
|
|
145
|
-
expect(result).toBeDefined()
|
|
146
|
-
expect(result!.code).toContain('__hmr_signal("/src/theme.tsx", "theme", signal, "light")')
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('rewrites generic-typed signals (signal<T>(value))', async () => {
|
|
150
|
-
// Regression for the silent-skip bug: SIGNAL_PREFIX_RE used to match
|
|
151
|
-
// `signal(` but not `signal<T>(`. Pre-rewrite TypeScript still has type
|
|
152
|
-
// parameters; declarations like `signal<string>('')` would skip HMR
|
|
153
|
-
// preservation silently and produce an empty-string-valued signal that
|
|
154
|
-
// — under a separate `__hmr_signal` interaction — could read as
|
|
155
|
-
// undefined. Discovered via PR #329 (perf-dashboard form section).
|
|
156
|
-
const plugin = createPlugin()
|
|
157
|
-
const code = `
|
|
158
|
-
import { signal } from "@pyreon/reactivity"
|
|
159
|
-
export const password = signal<string>("")
|
|
160
|
-
export const items = signal<Array<{ id: number }>>([])
|
|
161
|
-
export const count = signal<number>(0)
|
|
162
|
-
export function App() { return null }
|
|
163
|
-
`
|
|
164
|
-
const result = await transform(plugin, code, '/src/state.tsx')
|
|
165
|
-
expect(result).toBeDefined()
|
|
166
|
-
expect(result!.code).toContain(
|
|
167
|
-
'__hmr_signal("/src/state.tsx", "password", signal, "")',
|
|
168
|
-
)
|
|
169
|
-
expect(result!.code).toContain(
|
|
170
|
-
'__hmr_signal("/src/state.tsx", "items", signal, [])',
|
|
171
|
-
)
|
|
172
|
-
expect(result!.code).toContain('__hmr_signal("/src/state.tsx", "count", signal, 0)')
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('does not rewrite signal() inside functions to __hmr_signal (but injects name)', async () => {
|
|
176
|
-
const plugin = createPlugin()
|
|
177
|
-
const code = `
|
|
178
|
-
import { signal } from "@pyreon/reactivity"
|
|
179
|
-
import { h } from "@pyreon/core"
|
|
180
|
-
export function Counter() {
|
|
181
|
-
const local = signal(0)
|
|
182
|
-
return h("div", null, local())
|
|
183
|
-
}
|
|
184
|
-
`
|
|
185
|
-
const result = await transform(plugin, code, '/src/Counter.tsx')
|
|
186
|
-
expect(result).toBeDefined()
|
|
187
|
-
// The signal inside the function body should NOT be rewritten to __hmr_signal
|
|
188
|
-
expect(result!.code).not.toContain('__hmr_signal')
|
|
189
|
-
// But should get a debug name + source location injected (the LPIH
|
|
190
|
-
// build-time injection, R4 follow-up — see lpih.md docs).
|
|
191
|
-
expect(result!.code).toMatch(
|
|
192
|
-
/signal\(0, \{ name: "local", __sourceLocation: \{ file: "\/src\/Counter\.tsx", line: \d+, col: \d+ \} \}\)/,
|
|
193
|
-
)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
it('rewrites multiple module-scope signals', async () => {
|
|
197
|
-
const plugin = createPlugin()
|
|
198
|
-
const code = `
|
|
199
|
-
import { signal } from "@pyreon/reactivity"
|
|
200
|
-
const count = signal(0)
|
|
201
|
-
const name = signal("world")
|
|
202
|
-
export function App() { return null }
|
|
203
|
-
`
|
|
204
|
-
const result = await transform(plugin, code, '/src/App.tsx')
|
|
205
|
-
expect(result).toBeDefined()
|
|
206
|
-
expect(result!.code).toContain('"count"')
|
|
207
|
-
expect(result!.code).toContain('"name"')
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('handles signal with complex initial values', async () => {
|
|
211
|
-
const plugin = createPlugin()
|
|
212
|
-
const code = `
|
|
213
|
-
import { signal } from "@pyreon/reactivity"
|
|
214
|
-
const items = signal([1, 2, 3])
|
|
215
|
-
const config = signal({ theme: "dark", size: 14 })
|
|
216
|
-
export function App() { return null }
|
|
217
|
-
`
|
|
218
|
-
const result = await transform(plugin, code, '/src/App.tsx')
|
|
219
|
-
expect(result).toBeDefined()
|
|
220
|
-
expect(result!.code).toContain('__hmr_signal')
|
|
221
|
-
expect(result!.code).toContain('[1, 2, 3]')
|
|
222
|
-
expect(result!.code).toContain('{ theme: "dark", size: 14 }')
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it('does not rewrite signal in build mode', async () => {
|
|
226
|
-
const plugin = createBuildPlugin()
|
|
227
|
-
const code = `
|
|
228
|
-
import { signal } from "@pyreon/reactivity"
|
|
229
|
-
const count = signal(0)
|
|
230
|
-
export function App() { return null }
|
|
231
|
-
`
|
|
232
|
-
const result = await transform(plugin, code, '/src/App.tsx')
|
|
233
|
-
expect(result).toBeDefined()
|
|
234
|
-
expect(result!.code).not.toContain('__hmr_signal')
|
|
235
|
-
// No signal names in production builds
|
|
236
|
-
expect(result!.code).toContain('signal(0)')
|
|
237
|
-
expect(result!.code).not.toContain('{ name:')
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
it('skips signal naming when options already provided', async () => {
|
|
241
|
-
const plugin = createPlugin()
|
|
242
|
-
const code = `
|
|
243
|
-
import { signal } from "@pyreon/reactivity"
|
|
244
|
-
export function App() {
|
|
245
|
-
const count = signal(0, { name: "custom" })
|
|
246
|
-
return null
|
|
247
|
-
}
|
|
248
|
-
`
|
|
249
|
-
const result = await transform(plugin, code, '/src/App.tsx')
|
|
250
|
-
expect(result).toBeDefined()
|
|
251
|
-
// Should not double-inject name
|
|
252
|
-
expect(result!.code).toContain('signal(0, { name: "custom" })')
|
|
253
|
-
})
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
// ─── File extension filtering ────────────────────────────────────────────────
|
|
257
|
-
|
|
258
|
-
describe('file extension filtering', () => {
|
|
259
|
-
it('transforms .tsx files', async () => {
|
|
260
|
-
const plugin = createPlugin()
|
|
261
|
-
const code = `export function App() { return null }`
|
|
262
|
-
const result = await transform(plugin, code, '/src/App.tsx')
|
|
263
|
-
expect(result).toBeDefined()
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
it('transforms .jsx files', async () => {
|
|
267
|
-
const plugin = createPlugin()
|
|
268
|
-
const code = `export function App() { return null }`
|
|
269
|
-
const result = await transform(plugin, code, '/src/App.jsx')
|
|
270
|
-
expect(result).toBeDefined()
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
it('ignores .ts files', async () => {
|
|
274
|
-
const plugin = createPlugin()
|
|
275
|
-
const code = `export const x = 1`
|
|
276
|
-
const result = await transform(plugin, code, '/src/utils.ts')
|
|
277
|
-
expect(result).toBeUndefined()
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
it('ignores .js files', async () => {
|
|
281
|
-
const plugin = createPlugin()
|
|
282
|
-
const code = `export const x = 1`
|
|
283
|
-
const result = await transform(plugin, code, '/src/utils.js')
|
|
284
|
-
expect(result).toBeUndefined()
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
it('handles query strings in file paths', async () => {
|
|
288
|
-
const plugin = createPlugin()
|
|
289
|
-
const code = `export function App() { return null }`
|
|
290
|
-
const result = await transform(plugin, code, '/src/App.tsx?v=123')
|
|
291
|
-
expect(result).toBeDefined()
|
|
292
|
-
})
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
// ─── Compat mode ─────────────────────────────────────────────────────────────
|
|
296
|
-
|
|
297
|
-
describe('compat mode', () => {
|
|
298
|
-
it('skips Pyreon JSX transform in react compat mode', async () => {
|
|
299
|
-
const plugin = createPlugin({ compat: 'react' })
|
|
300
|
-
const code = `
|
|
301
|
-
import { useState } from "react"
|
|
302
|
-
export function App() { const [x] = useState(0); return null }
|
|
303
|
-
`
|
|
304
|
-
const result = await transform(plugin, code, '/src/App.tsx')
|
|
305
|
-
expect(result).toBeUndefined()
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
it('skips transform in preact compat mode', async () => {
|
|
309
|
-
const plugin = createPlugin({ compat: 'preact' })
|
|
310
|
-
const result = await transform(plugin, 'export function App() { return null }', '/src/App.tsx')
|
|
311
|
-
expect(result).toBeUndefined()
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
it('skips transform in vue compat mode', async () => {
|
|
315
|
-
const plugin = createPlugin({ compat: 'vue' })
|
|
316
|
-
const result = await transform(plugin, 'export function App() { return null }', '/src/App.tsx')
|
|
317
|
-
expect(result).toBeUndefined()
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
it('skips transform in solid compat mode', async () => {
|
|
321
|
-
const plugin = createPlugin({ compat: 'solid' })
|
|
322
|
-
const result = await transform(plugin, 'export function App() { return null }', '/src/App.tsx')
|
|
323
|
-
expect(result).toBeUndefined()
|
|
324
|
-
})
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
// ─── Plugin config ───────────────────────────────────────────────────────────
|
|
328
|
-
|
|
329
|
-
describe('plugin config', () => {
|
|
330
|
-
it('sets resolve.conditions: ["bun"] for workspace source resolution', async () => {
|
|
331
|
-
const plugin = pyreonPlugin()
|
|
332
|
-
const config = getConfigHook(plugin)({}, { command: 'serve' }) as Record<string, any>
|
|
333
|
-
expect(config.resolve.conditions).toEqual(['bun'])
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
it('sets JSX import source to @pyreon/core by default', async () => {
|
|
337
|
-
const plugin = pyreonPlugin()
|
|
338
|
-
const config = getConfigHook(plugin)({}, { command: 'serve' }) as {
|
|
339
|
-
oxc: { jsx: { importSource: string } }
|
|
340
|
-
}
|
|
341
|
-
expect(config.oxc.jsx.importSource).toBe('@pyreon/core')
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
it('keeps JSX import source as @pyreon/core in compat mode', async () => {
|
|
345
|
-
// OXC's `importSource` is project-wide (one setting for the whole build),
|
|
346
|
-
// so pointing it at the compat package would force the compat runtime
|
|
347
|
-
// on `@pyreon/*` framework files too — which they cannot handle. Instead
|
|
348
|
-
// the plugin keeps OXC at `@pyreon/core` and redirects the resulting
|
|
349
|
-
// `@pyreon/core/jsx-runtime` import to the compat package via `resolveId`,
|
|
350
|
-
// gated on the importer (user code only). See `compat-resolve.test.ts`
|
|
351
|
-
// "framework-importer carve-out". Caught by `cpa-smoke-app-*-compat`.
|
|
352
|
-
const plugin = pyreonPlugin({ compat: 'react' })
|
|
353
|
-
const config = getConfigHook(plugin)({}, { command: 'serve' }) as {
|
|
354
|
-
oxc: { jsx: { importSource: string } }
|
|
355
|
-
}
|
|
356
|
-
expect(config.oxc.jsx.importSource).toBe('@pyreon/core')
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
it('excludes compat packages from optimizeDeps', async () => {
|
|
360
|
-
const plugin = pyreonPlugin({ compat: 'react' })
|
|
361
|
-
const config = getConfigHook(plugin)({}, { command: 'serve' }) as {
|
|
362
|
-
optimizeDeps: { exclude: string[] }
|
|
363
|
-
}
|
|
364
|
-
expect(config.optimizeDeps.exclude).toContain('react')
|
|
365
|
-
expect(config.optimizeDeps.exclude).toContain('react-dom')
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
// Regression: pre-fix, the plugin's `bun` resolve condition redirected
|
|
369
|
-
// every `@pyreon/*` import to source `.ts(x)` files. In a non-monorepo
|
|
370
|
-
// consumer app, Vite's deps optimizer (esbuild) tried to pre-bundle
|
|
371
|
-
// those packages from `node_modules` and silently produced broken
|
|
372
|
-
// bundles in `.vite/deps/`, surfacing as
|
|
373
|
-
// `File does not exist at .../node_modules/.vite/deps/@pyreon_styler.js`
|
|
374
|
-
// at runtime. Fix scans the consumer's package.json for `@pyreon/*`
|
|
375
|
-
// deps and adds them to optimizeDeps.exclude so the optimizer skips
|
|
376
|
-
// them (resolution then goes through the plugin's own resolveId hook
|
|
377
|
-
// and Vite's normal source pipeline).
|
|
378
|
-
it("auto-excludes consumer's @pyreon/* deps from optimizeDeps (Vite optimizer fix)", async () => {
|
|
379
|
-
// Build a fake consumer package.json with a few @pyreon/* deps.
|
|
380
|
-
const tmpRoot = pathJoin(import.meta.dirname, 'fixtures', 'pyreon-deps-consumer')
|
|
381
|
-
rmSync(tmpRoot, { recursive: true, force: true })
|
|
382
|
-
mkdirSync(tmpRoot, { recursive: true })
|
|
383
|
-
writeFileSync(
|
|
384
|
-
pathJoin(tmpRoot, 'package.json'),
|
|
385
|
-
JSON.stringify({
|
|
386
|
-
name: 'fake-consumer',
|
|
387
|
-
dependencies: {
|
|
388
|
-
'@pyreon/core': '^0.15.0',
|
|
389
|
-
'@pyreon/styler': '^0.15.0',
|
|
390
|
-
'@pyreon/runtime-dom': '^0.15.0',
|
|
391
|
-
// Non-@pyreon dep MUST NOT leak into the exclude list.
|
|
392
|
-
react: '^19.0.0',
|
|
393
|
-
},
|
|
394
|
-
devDependencies: {
|
|
395
|
-
'@pyreon/vite-plugin': '^0.15.0',
|
|
396
|
-
},
|
|
397
|
-
}),
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
const plugin = pyreonPlugin()
|
|
401
|
-
const config = getConfigHook(plugin)({ root: tmpRoot }, { command: 'serve' }) as {
|
|
402
|
-
optimizeDeps: { exclude: string[] }
|
|
403
|
-
}
|
|
404
|
-
expect(config.optimizeDeps.exclude).toContain('@pyreon/core')
|
|
405
|
-
expect(config.optimizeDeps.exclude).toContain('@pyreon/styler')
|
|
406
|
-
expect(config.optimizeDeps.exclude).toContain('@pyreon/runtime-dom')
|
|
407
|
-
expect(config.optimizeDeps.exclude).toContain('@pyreon/vite-plugin')
|
|
408
|
-
expect(config.optimizeDeps.exclude).not.toContain('react')
|
|
409
|
-
|
|
410
|
-
rmSync(tmpRoot, { recursive: true, force: true })
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
it("merges @pyreon/* deps with compat aliases without dup'ing", async () => {
|
|
414
|
-
const tmpRoot = pathJoin(import.meta.dirname, 'fixtures', 'pyreon-deps-compat')
|
|
415
|
-
rmSync(tmpRoot, { recursive: true, force: true })
|
|
416
|
-
mkdirSync(tmpRoot, { recursive: true })
|
|
417
|
-
writeFileSync(
|
|
418
|
-
pathJoin(tmpRoot, 'package.json'),
|
|
419
|
-
JSON.stringify({ dependencies: { '@pyreon/core': '^0.15.0' } }),
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
const plugin = pyreonPlugin({ compat: 'react' })
|
|
423
|
-
const config = getConfigHook(plugin)({ root: tmpRoot }, { command: 'serve' }) as {
|
|
424
|
-
optimizeDeps: { exclude: string[] }
|
|
425
|
-
}
|
|
426
|
-
// Compat list still present
|
|
427
|
-
expect(config.optimizeDeps.exclude).toContain('react')
|
|
428
|
-
// Pyreon list also present
|
|
429
|
-
expect(config.optimizeDeps.exclude).toContain('@pyreon/core')
|
|
430
|
-
// Deduplicated (Set)
|
|
431
|
-
const occurrences = config.optimizeDeps.exclude.filter((d) => d === 'react').length
|
|
432
|
-
expect(occurrences).toBe(1)
|
|
433
|
-
|
|
434
|
-
rmSync(tmpRoot, { recursive: true, force: true })
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
it('handles missing/malformed consumer package.json gracefully', async () => {
|
|
438
|
-
const plugin = pyreonPlugin()
|
|
439
|
-
// Point at a directory that doesn't exist — should not throw.
|
|
440
|
-
const config = getConfigHook(plugin)(
|
|
441
|
-
{ root: '/nonexistent/path/that/does/not/exist' },
|
|
442
|
-
{ command: 'serve' },
|
|
443
|
-
) as { optimizeDeps: { exclude: string[] } }
|
|
444
|
-
expect(config.optimizeDeps.exclude).toEqual([])
|
|
445
|
-
})
|
|
446
|
-
|
|
447
|
-
it('adds SSR build config when isSsrBuild', async () => {
|
|
448
|
-
const plugin = pyreonPlugin({ ssr: { entry: './src/entry-server.ts' } })
|
|
449
|
-
const config = getConfigHook(plugin)({}, { command: 'build', isSsrBuild: true }) as {
|
|
450
|
-
build: { ssr: boolean; rollupOptions: { input: string } }
|
|
451
|
-
}
|
|
452
|
-
expect(config.build.ssr).toBe(true)
|
|
453
|
-
expect(config.build.rollupOptions.input).toBe('./src/entry-server.ts')
|
|
454
|
-
})
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
// ─── Virtual module (HMR runtime) ────────────────────────────────────────────
|
|
458
|
-
|
|
459
|
-
describe('virtual module resolution', () => {
|
|
460
|
-
it('resolves virtual:pyreon/hmr-runtime to internal ID', async () => {
|
|
461
|
-
const plugin = createPlugin()
|
|
462
|
-
const resolveId = plugin.resolveId as (
|
|
463
|
-
id: string,
|
|
464
|
-
) => string | undefined | Promise<string | undefined>
|
|
465
|
-
const resolved = await resolveId('virtual:pyreon/hmr-runtime')
|
|
466
|
-
expect(resolved).toBe('\0pyreon/hmr-runtime')
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
it('loads HMR runtime source for internal ID', async () => {
|
|
470
|
-
const plugin = createPlugin()
|
|
471
|
-
const load = plugin.load as (id: string) => string | undefined
|
|
472
|
-
const source = load('\0pyreon/hmr-runtime')
|
|
473
|
-
expect(source).toBeDefined()
|
|
474
|
-
expect(source).toContain('__hmr_signal')
|
|
475
|
-
expect(source).toContain('__hmr_dispose')
|
|
476
|
-
expect(source).toContain('__pyreon_hmr_registry__')
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
it('returns undefined for non-virtual IDs', async () => {
|
|
480
|
-
const plugin = createPlugin()
|
|
481
|
-
const load = plugin.load as (id: string) => string | undefined
|
|
482
|
-
expect(load('/src/App.tsx')).toBeUndefined()
|
|
483
|
-
})
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
// ─── Asset request detection ────────────────────────────────────────────────
|
|
487
|
-
|
|
488
|
-
describe('asset request filtering', () => {
|
|
489
|
-
// The SSR middleware uses isAssetRequest internally.
|
|
490
|
-
// We test it via the configureServer middleware behavior.
|
|
491
|
-
// For direct testing, we'd need to export it — instead we verify
|
|
492
|
-
// the plugin's SSR middleware config exists when ssr option is set.
|
|
493
|
-
|
|
494
|
-
it('configureServer returns middleware function when SSR enabled', async () => {
|
|
495
|
-
const plugin = pyreonPlugin({ ssr: { entry: './src/entry-server.ts' } })
|
|
496
|
-
expect(plugin.configureServer).toBeDefined()
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
it('configureServer is defined even without SSR (for context generation)', async () => {
|
|
500
|
-
const plugin = pyreonPlugin()
|
|
501
|
-
expect(plugin.configureServer).toBeDefined()
|
|
502
|
-
})
|
|
503
|
-
})
|