@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
package/src/index.ts
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* vite build --ssr src/entry-server.ts --outDir dist/server # server bundle
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
35
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
36
36
|
import { join as pathJoin } from 'node:path'
|
|
37
37
|
import { generateContext, transformJSX } from '@pyreon/compiler'
|
|
38
38
|
import type { Plugin, ViteDevServer } from 'vite'
|
|
@@ -137,6 +137,14 @@ export default function pyreonPlugin(options?: PyreonPluginOptions): Plugin {
|
|
|
137
137
|
let isBuild = false
|
|
138
138
|
let projectRoot = ''
|
|
139
139
|
|
|
140
|
+
// ── Cross-module signal export registry ─────────────────────────────────
|
|
141
|
+
// Tracks which modules export signal() declarations so imported signals
|
|
142
|
+
// can be auto-called in JSX across file boundaries.
|
|
143
|
+
// Key: normalized module ID, Value: set of exported signal names
|
|
144
|
+
const signalExportRegistry = new Map<string, Set<string>>()
|
|
145
|
+
// Cache resolved import specifiers to avoid redundant resolution calls
|
|
146
|
+
const resolveCache = new Map<string, string | null>()
|
|
147
|
+
|
|
140
148
|
return {
|
|
141
149
|
name: 'pyreon',
|
|
142
150
|
enforce: 'pre',
|
|
@@ -180,6 +188,15 @@ export default function pyreonPlugin(options?: PyreonPluginOptions): Plugin {
|
|
|
180
188
|
}
|
|
181
189
|
},
|
|
182
190
|
|
|
191
|
+
// ── Pre-scan all source files for signal exports ──────────────────────
|
|
192
|
+
async buildStart() {
|
|
193
|
+
// Pre-scan all source files for signal exports so the registry
|
|
194
|
+
// is complete before any transforms run. This solves the build
|
|
195
|
+
// ordering problem where component.tsx is transformed before
|
|
196
|
+
// store.ts — without pre-scanning, the registry would be empty.
|
|
197
|
+
await prescanSignalExports(projectRoot, signalExportRegistry)
|
|
198
|
+
},
|
|
199
|
+
|
|
183
200
|
// ── Virtual module + compat alias resolution ─────────────────────────────
|
|
184
201
|
async resolveId(id, importer) {
|
|
185
202
|
if (id === HMR_RUNTIME_IMPORT) return HMR_RUNTIME_ID
|
|
@@ -198,7 +215,7 @@ export default function pyreonPlugin(options?: PyreonPluginOptions): Plugin {
|
|
|
198
215
|
}
|
|
199
216
|
},
|
|
200
217
|
|
|
201
|
-
transform(code, id, transformOptions) {
|
|
218
|
+
async transform(code, id, transformOptions) {
|
|
202
219
|
const ext = getExt(id)
|
|
203
220
|
if (ext !== '.tsx' && ext !== '.jsx' && ext !== '.pyreon') return
|
|
204
221
|
|
|
@@ -213,11 +230,22 @@ export default function pyreonPlugin(options?: PyreonPluginOptions): Plugin {
|
|
|
213
230
|
return
|
|
214
231
|
}
|
|
215
232
|
|
|
233
|
+
// ── Scan for exported signal declarations (populate registry) ──────
|
|
234
|
+
// This runs on every .tsx/.jsx file so the registry is built
|
|
235
|
+
// incrementally. buildStart pre-scans all files, but this handles
|
|
236
|
+
// files created/modified after buildStart (dev mode HMR).
|
|
237
|
+
scanSignalExports(code, normalizeModuleId(id), signalExportRegistry)
|
|
238
|
+
|
|
239
|
+
// ── Resolve imported signals from the registry ─────────────────────
|
|
240
|
+
// Check each import in this file: if the imported module has signal
|
|
241
|
+
// exports in the registry, pass them as knownSignals to the compiler.
|
|
242
|
+
const knownSignals = await resolveImportedSignals(code, id, signalExportRegistry, this, resolveCache)
|
|
243
|
+
|
|
216
244
|
// Vite passes `ssr: true` when transforming for the SSR module graph
|
|
217
245
|
// (both build --ssr and dev `ssrLoadModule`). The compiler emits plain
|
|
218
246
|
// `h()` calls in that mode so `runtime-server` can render to a string.
|
|
219
247
|
const isSsr = transformOptions?.ssr === true
|
|
220
|
-
const result = transformJSX(code, id, { ssr: isSsr })
|
|
248
|
+
const result = transformJSX(code, id, { ssr: isSsr, knownSignals })
|
|
221
249
|
// Surface compiler warnings in the terminal
|
|
222
250
|
for (const w of result.warnings) {
|
|
223
251
|
this.warn(`${w.message} (${id}:${w.line}:${w.column})`)
|
|
@@ -552,6 +580,211 @@ function isAssetRequest(url: string): boolean {
|
|
|
552
580
|
// Inlined here so it's available without a filesystem read. This is the
|
|
553
581
|
// compiled-to-JS version of hmr-runtime.ts — kept in sync manually.
|
|
554
582
|
|
|
583
|
+
// ─── Cross-module signal auto-call helpers ──────────────────────────────────
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Normalize a Vite module ID by stripping query strings (?v=..., ?t=...)
|
|
587
|
+
* and resolving to an absolute path for consistent registry lookups.
|
|
588
|
+
*/
|
|
589
|
+
function normalizeModuleId(id: string): string {
|
|
590
|
+
const queryIndex = id.indexOf('?')
|
|
591
|
+
return queryIndex >= 0 ? id.slice(0, queryIndex) : id
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Pre-scan all source files in the project for signal exports.
|
|
596
|
+
*
|
|
597
|
+
* Called from `buildStart` so the registry is fully populated before any
|
|
598
|
+
* transforms run. This solves the build ordering problem where component.tsx
|
|
599
|
+
* is transformed before store.ts — without pre-scanning, the registry would
|
|
600
|
+
* be empty and imported signals would not be auto-called.
|
|
601
|
+
*/
|
|
602
|
+
async function prescanSignalExports(root: string, registry: Map<string, Set<string>>): Promise<void> {
|
|
603
|
+
const files: string[] = []
|
|
604
|
+
|
|
605
|
+
function walk(dir: string) {
|
|
606
|
+
try {
|
|
607
|
+
for (const entry of readdirSync(dir)) {
|
|
608
|
+
if (entry.startsWith('.') || entry === 'node_modules' || entry === 'dist' || entry === 'lib' || entry === 'build') continue
|
|
609
|
+
const full = pathJoin(dir, entry)
|
|
610
|
+
try {
|
|
611
|
+
const stat = statSync(full)
|
|
612
|
+
if (stat.isDirectory()) walk(full)
|
|
613
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(entry)) files.push(full)
|
|
614
|
+
} catch {
|
|
615
|
+
/* permission error, etc. */
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
/* dir doesn't exist */
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
walk(root)
|
|
624
|
+
|
|
625
|
+
for (const file of files) {
|
|
626
|
+
try {
|
|
627
|
+
const code = readFileSync(file, 'utf-8')
|
|
628
|
+
scanSignalExports(code, file, registry)
|
|
629
|
+
} catch {
|
|
630
|
+
/* read error */
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Scan a module's source for exported signal declarations and register them.
|
|
637
|
+
*
|
|
638
|
+
* Detects patterns:
|
|
639
|
+
* 1. `export const x = signal(...)` or `export const x = computed(...)` — inline export
|
|
640
|
+
* 2. `const x = signal(...); export { x }` — separate declaration + named export
|
|
641
|
+
* 3. `export default signal(...)` — default export (tracked as 'default')
|
|
642
|
+
*
|
|
643
|
+
* Re-exports (`export { x } from './signals'`) are NOT detected — the source
|
|
644
|
+
* module must be scanned directly. This is a known limitation.
|
|
645
|
+
*
|
|
646
|
+
* Uses simple regex — no AST parse needed.
|
|
647
|
+
*/
|
|
648
|
+
function scanSignalExports(code: string, moduleId: string, registry: Map<string, Set<string>>): void {
|
|
649
|
+
const normalizedId = normalizeModuleId(moduleId)
|
|
650
|
+
let match: RegExpExecArray | null
|
|
651
|
+
const signals = new Set<string>()
|
|
652
|
+
|
|
653
|
+
// Pattern 1: export const x = signal(...) or export const x = computed(...)
|
|
654
|
+
const EXPORT_CONST_RE = /export\s+const\s+(\w+)\s*=\s*(?:signal|computed)\s*[<(]/g
|
|
655
|
+
while ((match = EXPORT_CONST_RE.exec(code)) !== null) {
|
|
656
|
+
signals.add(match[1]!)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Pattern 2: const x = signal(...) followed by export { x }
|
|
660
|
+
// First, find all local `const x = signal(` or `const x = computed(` declarations
|
|
661
|
+
const localSignals = new Set<string>()
|
|
662
|
+
const LOCAL_SIGNAL_RE = /(?:^|[\s;])const\s+(\w+)\s*=\s*(?:signal|computed)\s*[<(]/gm
|
|
663
|
+
while ((match = LOCAL_SIGNAL_RE.exec(code)) !== null) {
|
|
664
|
+
localSignals.add(match[1]!)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Then check named exports: export { x, y as z }
|
|
668
|
+
if (localSignals.size > 0) {
|
|
669
|
+
const NAMED_EXPORT_RE = /export\s*\{([^}]+)\}/g
|
|
670
|
+
while ((match = NAMED_EXPORT_RE.exec(code)) !== null) {
|
|
671
|
+
// Skip re-exports (export { x } from '...')
|
|
672
|
+
const afterBrace = code.slice(match.index + match[0].length).trimStart()
|
|
673
|
+
if (afterBrace.startsWith('from')) continue
|
|
674
|
+
|
|
675
|
+
for (const spec of match[1]!.split(',')) {
|
|
676
|
+
const trimmed = spec.trim()
|
|
677
|
+
if (!trimmed) continue
|
|
678
|
+
const parts = trimmed.split(/\s+as\s+/)
|
|
679
|
+
const localName = parts[0]!.trim()
|
|
680
|
+
const exportedName = (parts[1] ?? parts[0])!.trim()
|
|
681
|
+
if (localSignals.has(localName)) {
|
|
682
|
+
signals.add(exportedName)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Pattern 3: export default signal(...) or export default computed(...) — tracked as 'default'
|
|
689
|
+
if (/export\s+default\s+(?:signal|computed)\s*[<(]/.test(code)) {
|
|
690
|
+
signals.add('default')
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (signals.size > 0) {
|
|
694
|
+
registry.set(normalizedId, signals)
|
|
695
|
+
} else {
|
|
696
|
+
// Clean up if module no longer exports signals (e.g. after edit)
|
|
697
|
+
registry.delete(normalizedId)
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Resolve imported signal names from the signal export registry.
|
|
703
|
+
*
|
|
704
|
+
* For each import in the source, resolves the module and checks if it has
|
|
705
|
+
* signal exports in the registry. Returns the local names of imported signals.
|
|
706
|
+
*
|
|
707
|
+
* Handles named imports (`import { x } from ...`) and default imports
|
|
708
|
+
* (`import x from ...` — matched against 'default' in the registry).
|
|
709
|
+
*/
|
|
710
|
+
async function resolveImportedSignals(
|
|
711
|
+
code: string,
|
|
712
|
+
_moduleId: string,
|
|
713
|
+
registry: Map<string, Set<string>>,
|
|
714
|
+
pluginCtx: { resolve: (id: string, importer?: string, options?: { skipSelf: boolean }) => Promise<{ id: string } | null> },
|
|
715
|
+
resolveCache: Map<string, string | null>,
|
|
716
|
+
): Promise<string[]> {
|
|
717
|
+
if (registry.size === 0) return []
|
|
718
|
+
|
|
719
|
+
const knownSignals: string[] = []
|
|
720
|
+
let match: RegExpExecArray | null
|
|
721
|
+
|
|
722
|
+
/** Resolve a source specifier to a normalized module ID, using the cache. */
|
|
723
|
+
async function resolveSource(source: string): Promise<string | null> {
|
|
724
|
+
const cacheKey = `${_moduleId}::${source}`
|
|
725
|
+
if (resolveCache.has(cacheKey)) return resolveCache.get(cacheKey) ?? null
|
|
726
|
+
let resolvedId: string | null = null
|
|
727
|
+
try {
|
|
728
|
+
const resolved = await pluginCtx.resolve(source, _moduleId, { skipSelf: true })
|
|
729
|
+
resolvedId = resolved?.id ? normalizeModuleId(resolved.id) : null
|
|
730
|
+
} catch {
|
|
731
|
+
/* resolve error */
|
|
732
|
+
}
|
|
733
|
+
resolveCache.set(cacheKey, resolvedId)
|
|
734
|
+
return resolvedId
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Named imports: import { name1, name2 as alias } from 'source'
|
|
738
|
+
// Excludes `import type { ... }` — type-only imports have no runtime value
|
|
739
|
+
const IMPORT_RE = /import\s+(?!type\s)\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g
|
|
740
|
+
while ((match = IMPORT_RE.exec(code)) !== null) {
|
|
741
|
+
const specifiers = match[1]!
|
|
742
|
+
const source = match[2]!
|
|
743
|
+
|
|
744
|
+
const resolvedId = await resolveSource(source)
|
|
745
|
+
if (!resolvedId) continue
|
|
746
|
+
const exportedSignals = registry.get(resolvedId)
|
|
747
|
+
if (!exportedSignals) continue
|
|
748
|
+
|
|
749
|
+
// Parse import specifiers: "count, theme as t, other"
|
|
750
|
+
for (const spec of specifiers.split(',')) {
|
|
751
|
+
const trimmed = spec.trim()
|
|
752
|
+
if (!trimmed) continue
|
|
753
|
+
|
|
754
|
+
const parts = trimmed.split(/\s+as\s+/)
|
|
755
|
+
const importedName = parts[0]!.trim()
|
|
756
|
+
const localName = (parts[1] ?? parts[0])!.trim()
|
|
757
|
+
|
|
758
|
+
if (exportedSignals.has(importedName)) {
|
|
759
|
+
knownSignals.push(localName)
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Default imports: import count from './store'
|
|
765
|
+
// Excludes: `import { ... }`, `import type X`, `import * as X`
|
|
766
|
+
const DEFAULT_IMPORT_RE = /import\s+(?!type\s)(\w+)\s+from\s*['"]([^'"]+)['"]/g
|
|
767
|
+
while ((match = DEFAULT_IMPORT_RE.exec(code)) !== null) {
|
|
768
|
+
// Skip if this is actually a `import type X from` pattern
|
|
769
|
+
const fullMatch = match[0]
|
|
770
|
+
if (/import\s+type\s+/.test(fullMatch)) continue
|
|
771
|
+
|
|
772
|
+
const localName = match[1]!
|
|
773
|
+
const source = match[2]!
|
|
774
|
+
|
|
775
|
+
const resolvedId = await resolveSource(source)
|
|
776
|
+
if (!resolvedId) continue
|
|
777
|
+
const exportedSignals = registry.get(resolvedId)
|
|
778
|
+
if (!exportedSignals) continue
|
|
779
|
+
|
|
780
|
+
if (exportedSignals.has('default')) {
|
|
781
|
+
knownSignals.push(localName)
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return knownSignals
|
|
786
|
+
}
|
|
787
|
+
|
|
555
788
|
const HMR_RUNTIME_SOURCE = `
|
|
556
789
|
const REGISTRY_KEY = "__pyreon_hmr_registry__";
|
|
557
790
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compat-mode `resolveId` and `getCompatTarget` coverage for
|
|
3
|
+
* @pyreon/vite-plugin (PR #323). The existing test file covers
|
|
4
|
+
* compat-mode `transform` short-circuiting; this covers the
|
|
5
|
+
* resolveId hook + the JSX-runtime aliasing branch.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from 'vitest'
|
|
9
|
+
import pyreonPlugin, { type PyreonPluginOptions } from '../index'
|
|
10
|
+
|
|
11
|
+
type ConfigHook = (
|
|
12
|
+
userConfig: Record<string, unknown>,
|
|
13
|
+
env: { command: string; isSsrBuild?: boolean },
|
|
14
|
+
) => Record<string, unknown>
|
|
15
|
+
|
|
16
|
+
type ResolveIdCtx = {
|
|
17
|
+
resolve: (
|
|
18
|
+
id: string,
|
|
19
|
+
importer?: string,
|
|
20
|
+
options?: { skipSelf: boolean },
|
|
21
|
+
) => Promise<{ id: string } | null>
|
|
22
|
+
}
|
|
23
|
+
type ResolveIdHook = (
|
|
24
|
+
this: ResolveIdCtx,
|
|
25
|
+
id: string,
|
|
26
|
+
importer?: string,
|
|
27
|
+
) => Promise<string | undefined>
|
|
28
|
+
|
|
29
|
+
function bootstrap(opts?: PyreonPluginOptions) {
|
|
30
|
+
const plugin = pyreonPlugin(opts)
|
|
31
|
+
;(plugin.config as unknown as ConfigHook)({}, { command: 'serve' })
|
|
32
|
+
return plugin
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function callResolveId(
|
|
36
|
+
plugin: ReturnType<typeof pyreonPlugin>,
|
|
37
|
+
id: string,
|
|
38
|
+
resolveMap: Record<string, string> = {},
|
|
39
|
+
): Promise<string | undefined> {
|
|
40
|
+
const hook = plugin.resolveId as ResolveIdHook
|
|
41
|
+
return hook.call(
|
|
42
|
+
{
|
|
43
|
+
resolve: async (specifier: string) => {
|
|
44
|
+
const resolved = resolveMap[specifier]
|
|
45
|
+
return resolved ? { id: resolved } : null
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
id,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('compat-mode resolveId — react', () => {
|
|
53
|
+
it('redirects "react" → @pyreon/react-compat', async () => {
|
|
54
|
+
const plugin = bootstrap({ compat: 'react' })
|
|
55
|
+
const resolved = await callResolveId(plugin, 'react', {
|
|
56
|
+
'@pyreon/react-compat': '/abs/react-compat/index.ts',
|
|
57
|
+
})
|
|
58
|
+
expect(resolved).toBe('/abs/react-compat/index.ts')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('redirects "react/jsx-runtime" → @pyreon/react-compat/jsx-runtime', async () => {
|
|
62
|
+
const plugin = bootstrap({ compat: 'react' })
|
|
63
|
+
const resolved = await callResolveId(plugin, 'react/jsx-runtime', {
|
|
64
|
+
'@pyreon/react-compat/jsx-runtime': '/abs/react-compat/jsx-runtime.ts',
|
|
65
|
+
})
|
|
66
|
+
expect(resolved).toBe('/abs/react-compat/jsx-runtime.ts')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('redirects @pyreon/core/jsx-runtime → @pyreon/react-compat/jsx-runtime in react compat', async () => {
|
|
70
|
+
const plugin = bootstrap({ compat: 'react' })
|
|
71
|
+
const resolved = await callResolveId(plugin, '@pyreon/core/jsx-runtime', {
|
|
72
|
+
'@pyreon/react-compat/jsx-runtime': '/abs/react-compat/jsx-runtime.ts',
|
|
73
|
+
})
|
|
74
|
+
expect(resolved).toBe('/abs/react-compat/jsx-runtime.ts')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('redirects @pyreon/core/jsx-dev-runtime in react compat', async () => {
|
|
78
|
+
const plugin = bootstrap({ compat: 'react' })
|
|
79
|
+
const resolved = await callResolveId(plugin, '@pyreon/core/jsx-dev-runtime', {
|
|
80
|
+
'@pyreon/react-compat/jsx-runtime': '/abs/react-compat/jsx-runtime.ts',
|
|
81
|
+
})
|
|
82
|
+
expect(resolved).toBe('/abs/react-compat/jsx-runtime.ts')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('returns undefined for non-aliased imports', async () => {
|
|
86
|
+
const plugin = bootstrap({ compat: 'react' })
|
|
87
|
+
const resolved = await callResolveId(plugin, 'lodash', {})
|
|
88
|
+
expect(resolved).toBeUndefined()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('compat-mode resolveId — preact', () => {
|
|
93
|
+
it('redirects "preact" → @pyreon/preact-compat', async () => {
|
|
94
|
+
const plugin = bootstrap({ compat: 'preact' })
|
|
95
|
+
const resolved = await callResolveId(plugin, 'preact', {
|
|
96
|
+
'@pyreon/preact-compat': '/abs/preact-compat/index.ts',
|
|
97
|
+
})
|
|
98
|
+
expect(resolved).toBe('/abs/preact-compat/index.ts')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('redirects "preact/hooks" → @pyreon/preact-compat/hooks', async () => {
|
|
102
|
+
const plugin = bootstrap({ compat: 'preact' })
|
|
103
|
+
const resolved = await callResolveId(plugin, 'preact/hooks', {
|
|
104
|
+
'@pyreon/preact-compat/hooks': '/abs/preact-compat/hooks.ts',
|
|
105
|
+
})
|
|
106
|
+
expect(resolved).toBe('/abs/preact-compat/hooks.ts')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('redirects @pyreon/core/jsx-runtime in preact compat', async () => {
|
|
110
|
+
const plugin = bootstrap({ compat: 'preact' })
|
|
111
|
+
const resolved = await callResolveId(plugin, '@pyreon/core/jsx-runtime', {
|
|
112
|
+
'@pyreon/preact-compat/jsx-runtime': '/abs/preact-compat/jsx-runtime.ts',
|
|
113
|
+
})
|
|
114
|
+
expect(resolved).toBe('/abs/preact-compat/jsx-runtime.ts')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('redirects @preact/signals → @pyreon/preact-compat/signals', async () => {
|
|
118
|
+
const plugin = bootstrap({ compat: 'preact' })
|
|
119
|
+
const resolved = await callResolveId(plugin, '@preact/signals', {
|
|
120
|
+
'@pyreon/preact-compat/signals': '/abs/preact-compat/signals.ts',
|
|
121
|
+
})
|
|
122
|
+
expect(resolved).toBe('/abs/preact-compat/signals.ts')
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('compat-mode resolveId — vue', () => {
|
|
127
|
+
it('redirects "vue" → @pyreon/vue-compat', async () => {
|
|
128
|
+
const plugin = bootstrap({ compat: 'vue' })
|
|
129
|
+
const resolved = await callResolveId(plugin, 'vue', {
|
|
130
|
+
'@pyreon/vue-compat': '/abs/vue-compat/index.ts',
|
|
131
|
+
})
|
|
132
|
+
expect(resolved).toBe('/abs/vue-compat/index.ts')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('redirects @pyreon/core/jsx-runtime in vue compat', async () => {
|
|
136
|
+
const plugin = bootstrap({ compat: 'vue' })
|
|
137
|
+
const resolved = await callResolveId(plugin, '@pyreon/core/jsx-runtime', {
|
|
138
|
+
'@pyreon/vue-compat/jsx-runtime': '/abs/vue-compat/jsx-runtime.ts',
|
|
139
|
+
})
|
|
140
|
+
expect(resolved).toBe('/abs/vue-compat/jsx-runtime.ts')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('compat-mode resolveId — solid', () => {
|
|
145
|
+
it('redirects "solid-js" → @pyreon/solid-compat', async () => {
|
|
146
|
+
const plugin = bootstrap({ compat: 'solid' })
|
|
147
|
+
const resolved = await callResolveId(plugin, 'solid-js', {
|
|
148
|
+
'@pyreon/solid-compat': '/abs/solid-compat/index.ts',
|
|
149
|
+
})
|
|
150
|
+
expect(resolved).toBe('/abs/solid-compat/index.ts')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('redirects @pyreon/core/jsx-runtime in solid compat', async () => {
|
|
154
|
+
const plugin = bootstrap({ compat: 'solid' })
|
|
155
|
+
const resolved = await callResolveId(plugin, '@pyreon/core/jsx-runtime', {
|
|
156
|
+
'@pyreon/solid-compat/jsx-runtime': '/abs/solid-compat/jsx-runtime.ts',
|
|
157
|
+
})
|
|
158
|
+
expect(resolved).toBe('/abs/solid-compat/jsx-runtime.ts')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('compat-mode resolveId — no compat', () => {
|
|
163
|
+
it('returns undefined for any framework alias when compat is unset', async () => {
|
|
164
|
+
const plugin = bootstrap()
|
|
165
|
+
expect(await callResolveId(plugin, 'react', {})).toBeUndefined()
|
|
166
|
+
expect(await callResolveId(plugin, 'vue', {})).toBeUndefined()
|
|
167
|
+
expect(await callResolveId(plugin, 'preact', {})).toBeUndefined()
|
|
168
|
+
expect(await callResolveId(plugin, 'solid-js', {})).toBeUndefined()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('still resolves the HMR runtime virtual id (independent of compat)', async () => {
|
|
172
|
+
const plugin = bootstrap()
|
|
173
|
+
const resolved = await callResolveId(plugin, 'virtual:pyreon/hmr-runtime', {})
|
|
174
|
+
// Internal ID — has the leading '\0' marker convention or similar
|
|
175
|
+
expect(resolved).toBeDefined()
|
|
176
|
+
expect(typeof resolved).toBe('string')
|
|
177
|
+
})
|
|
178
|
+
})
|