brustjs 0.1.28-alpha → 0.1.30-alpha
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/example/pokedex/app.css +8 -1712
- package/example/pokedex/components/AddToTeamButton.tsx +36 -19
- package/example/pokedex/components/AppLayout.tsx +48 -50
- package/example/pokedex/components/Breadcrumb.tsx +49 -0
- package/example/pokedex/components/DexFilter.tsx +121 -0
- package/example/pokedex/components/HeroSearch.tsx +51 -0
- package/example/pokedex/components/NavLink.tsx +16 -23
- package/example/pokedex/components/NavPreloader.tsx +7 -3
- package/example/pokedex/components/TeamBuilder.tsx +48 -131
- package/example/pokedex/components/ThemeToggle.tsx +22 -11
- package/example/pokedex/lib/loaders.ts +125 -115
- package/example/pokedex/lib/pokeapi.ts +21 -21
- package/example/pokedex/lib/team-store.ts +1 -1
- package/example/pokedex/lib/types.ts +73 -94
- package/example/pokedex/pages/BrowsePage.tsx +31 -0
- package/example/pokedex/pages/DetailPage.tsx +176 -91
- package/example/pokedex/pages/HomePage.tsx +229 -0
- package/example/pokedex/pages/TypeChart.tsx +46 -27
- package/example/pokedex/routes.tsx +9 -20
- package/example/pokedex/stores/team.ts +1 -1
- package/package.json +8 -7
- package/runtime/cli/native-routes-emit.ts +223 -63
- package/runtime/cli/templates.ts +7 -4
- package/runtime/index.js +52 -52
- package/runtime/native/runtime.ts +160 -16
- package/example/pokedex/pages/ListPage.tsx +0 -76
|
@@ -17,12 +17,12 @@ import { DIRECTIVES_BOOTSTRAP, ISLANDS_IMPORTMAP_AND_BOOTSTRAP } from '../island
|
|
|
17
17
|
* compiler. */
|
|
18
18
|
export function gatherComponentSources(pageSourcePath: string): {
|
|
19
19
|
sources: Record<string, string>
|
|
20
|
-
mergedImports: Map<string,
|
|
20
|
+
mergedImports: Map<string, ResolvedImport>
|
|
21
21
|
} {
|
|
22
22
|
const sources: Record<string, string> = {}
|
|
23
|
-
// mergedImports accumulates ident→
|
|
23
|
+
// mergedImports accumulates ident→ResolvedImport across ALL visited files.
|
|
24
24
|
// The page's own imports win on conflict (inserted last below).
|
|
25
|
-
const mergedImports = new Map<string,
|
|
25
|
+
const mergedImports = new Map<string, ResolvedImport>()
|
|
26
26
|
const visited = new Set<string>()
|
|
27
27
|
|
|
28
28
|
// Queue items: { ident, resolvedPath } — ident is how the PARENT imported it.
|
|
@@ -36,11 +36,11 @@ export function gatherComponentSources(pageSourcePath: string): {
|
|
|
36
36
|
// Record source keyed by ident — detect ambiguity.
|
|
37
37
|
if (ident in sources) {
|
|
38
38
|
// Same ident already seen — only an error if it resolves to a different path.
|
|
39
|
-
// We get the previous
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
39
|
+
// We get the previous spec from mergedImports (which maps ident → ResolvedImport).
|
|
40
|
+
const existing = mergedImports.get(ident)
|
|
41
|
+
if (existing && existing.spec !== filePath) {
|
|
42
42
|
throw new Error(
|
|
43
|
-
`native build: ambiguous component ident "${ident}" resolves to two paths: ${
|
|
43
|
+
`native build: ambiguous component ident "${ident}" resolves to two paths: ${existing.spec} and ${filePath}`,
|
|
44
44
|
)
|
|
45
45
|
}
|
|
46
46
|
} else {
|
|
@@ -48,36 +48,43 @@ export function gatherComponentSources(pageSourcePath: string): {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// Scan this file's imports and recurse into local ones.
|
|
51
|
-
const childImports =
|
|
52
|
-
for (const [childIdent,
|
|
53
|
-
// Merge into mergedImports — check for ambiguity.
|
|
51
|
+
const childImports = scanImportRefs(filePath)
|
|
52
|
+
for (const [childIdent, ref] of childImports) {
|
|
53
|
+
// Merge into mergedImports — check for ambiguity (compare by spec). Only
|
|
54
|
+
// Capitalized idents can be COMPONENT sources; lowercase idents (stores,
|
|
55
|
+
// hooks, utils) legitimately repeat across files and must not throw.
|
|
54
56
|
const existing = mergedImports.get(childIdent)
|
|
55
|
-
if (existing !== undefined && existing !==
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if (existing !== undefined && existing.spec !== ref.spec) {
|
|
58
|
+
if (isComponentIdent(childIdent)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`native build: ambiguous component ident "${childIdent}" resolves to two paths: ${existing.spec} and ${ref.spec}`,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
// Non-component collision — keep the first, don't recurse the duplicate.
|
|
64
|
+
continue
|
|
59
65
|
}
|
|
60
66
|
if (existing === undefined) {
|
|
61
|
-
mergedImports.set(childIdent,
|
|
67
|
+
mergedImports.set(childIdent, ref)
|
|
62
68
|
}
|
|
63
|
-
// Recurse into
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
// Recurse into LOCAL files only (a bare spec has no readable file —
|
|
70
|
+
// !bare, not the old node_modules string-check which a bare spec evades).
|
|
71
|
+
if (!ref.bare) {
|
|
72
|
+
visit(ref.spec, childIdent)
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
// Seed: scan the page's own imports and visit each local file.
|
|
71
|
-
const pageImports =
|
|
72
|
-
for (const [ident,
|
|
73
|
-
if (!
|
|
74
|
-
visit(
|
|
78
|
+
const pageImports = scanImportRefs(pageSourcePath)
|
|
79
|
+
for (const [ident, ref] of pageImports) {
|
|
80
|
+
if (!ref.bare) {
|
|
81
|
+
visit(ref.spec, ident)
|
|
75
82
|
}
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
// Page's own imports win on ident conflict — merge them last.
|
|
79
|
-
for (const [ident,
|
|
80
|
-
mergedImports.set(ident,
|
|
86
|
+
for (const [ident, ref] of pageImports) {
|
|
87
|
+
mergedImports.set(ident, ref)
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
return { sources, mergedImports }
|
|
@@ -99,9 +106,9 @@ export function gatherComponentSources(pageSourcePath: string): {
|
|
|
99
106
|
export function gatherChainSources(
|
|
100
107
|
chainNames: string[],
|
|
101
108
|
importMap: Map<string, string>,
|
|
102
|
-
): { sources: Record<string, string>; mergedImports: Map<string,
|
|
109
|
+
): { sources: Record<string, string>; mergedImports: Map<string, ResolvedImport> } {
|
|
103
110
|
const sources: Record<string, string> = {}
|
|
104
|
-
const mergedImports = new Map<string,
|
|
111
|
+
const mergedImports = new Map<string, ResolvedImport>()
|
|
105
112
|
|
|
106
113
|
for (const compName of chainNames) {
|
|
107
114
|
const compPath = importMap.get(compName)
|
|
@@ -116,20 +123,28 @@ export function gatherChainSources(
|
|
|
116
123
|
const { sources: subSources, mergedImports: subImports } = gatherComponentSources(compPath)
|
|
117
124
|
for (const [ident, src] of Object.entries(subSources)) {
|
|
118
125
|
if (ident in sources && sources[ident] !== src) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
126
|
+
// Only Capitalized idents are COMPONENT sources; a lowercase collision
|
|
127
|
+
// (e.g. two stores both named `teamStore`) is legitimate — keep first.
|
|
128
|
+
if (isComponentIdent(ident)) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`native build: ambiguous component ident "${ident}" — two different sources in one chain`,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
continue
|
|
122
134
|
}
|
|
123
135
|
sources[ident] = src
|
|
124
136
|
}
|
|
125
|
-
for (const [ident,
|
|
137
|
+
for (const [ident, ref] of subImports) {
|
|
126
138
|
const existing = mergedImports.get(ident)
|
|
127
|
-
if (existing !== undefined && existing !==
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
if (existing !== undefined && existing.spec !== ref.spec) {
|
|
140
|
+
if (isComponentIdent(ident)) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`native build: ambiguous component ident "${ident}" resolves to two paths: ${existing.spec} and ${ref.spec}`,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
continue
|
|
131
146
|
}
|
|
132
|
-
mergedImports.set(ident,
|
|
147
|
+
mergedImports.set(ident, ref)
|
|
133
148
|
}
|
|
134
149
|
|
|
135
150
|
// Inject the chain component's OWN source keyed by its ident — it is the
|
|
@@ -142,7 +157,9 @@ export function gatherChainSources(
|
|
|
142
157
|
)
|
|
143
158
|
}
|
|
144
159
|
sources[compName] = ownSrc
|
|
145
|
-
|
|
160
|
+
// The chain component is resolved by name from the routes entry — a LOCAL
|
|
161
|
+
// default-imported source file (route-name resolution is local-only).
|
|
162
|
+
mergedImports.set(compName, { spec: compPath, bare: false, kind: 'default' })
|
|
146
163
|
}
|
|
147
164
|
|
|
148
165
|
return { sources, mergedImports }
|
|
@@ -332,7 +349,7 @@ function toRelativeSpecifier(from: string, to: string): string {
|
|
|
332
349
|
function emitComponentArtifacts(
|
|
333
350
|
jinjaPath: string,
|
|
334
351
|
componentsJsonStr: string,
|
|
335
|
-
pageImports: Map<string,
|
|
352
|
+
pageImports: Map<string, ResolvedImport>,
|
|
336
353
|
routeName: string,
|
|
337
354
|
): { islandIdsFromComponents: string[] } {
|
|
338
355
|
const raw = JSON.parse(componentsJsonStr) as RawComponentEntry[]
|
|
@@ -341,25 +358,27 @@ function emitComponentArtifacts(
|
|
|
341
358
|
const jinjaDir = dirname(jinjaPath)
|
|
342
359
|
const projectRoot = process.cwd()
|
|
343
360
|
|
|
344
|
-
// Enrich with
|
|
345
|
-
// absolute for the readFileSync island scan
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
361
|
+
// Enrich with the resolved import ref. For local imports `ref.spec` is an
|
|
362
|
+
// ABSOLUTE path (kept absolute for the readFileSync island scan below); for
|
|
363
|
+
// bare imports it's the verbatim package specifier.
|
|
364
|
+
const enriched: Array<EnrichedComponentEntry & { ref: ResolvedImport }> = raw.map((entry) => {
|
|
365
|
+
const ref = pageImports.get(entry.component)
|
|
366
|
+
if (!ref) {
|
|
349
367
|
throw new Error(
|
|
350
368
|
`SSR component "${entry.component}" in native route "${routeName}" has no matching import in the page source (expected \`import ${entry.component} from "..."\`)`,
|
|
351
369
|
)
|
|
352
370
|
}
|
|
353
|
-
return { ...entry, sourcePath }
|
|
371
|
+
return { ...entry, sourcePath: ref.spec, ref }
|
|
354
372
|
})
|
|
355
373
|
|
|
356
|
-
// Write <Name>.components.json
|
|
357
|
-
//
|
|
358
|
-
//
|
|
374
|
+
// Write <Name>.components.json. For LOCAL imports sourcePath is PROJECT-RELATIVE
|
|
375
|
+
// (cwd-relative — no build-machine path baked in); for BARE imports it's the
|
|
376
|
+
// package spec verbatim. (sourcePath is build-time metadata — the factory
|
|
377
|
+
// import is what's load-bearing at runtime.)
|
|
359
378
|
const compJsonPath = jinjaPath.replace(/\.jinja$/, '.components.json')
|
|
360
|
-
const compJsonEntries = enriched.map((e) => ({
|
|
379
|
+
const compJsonEntries = enriched.map(({ ref, ...e }) => ({
|
|
361
380
|
...e,
|
|
362
|
-
sourcePath: relative(projectRoot,
|
|
381
|
+
sourcePath: ref.bare ? ref.spec : relative(projectRoot, ref.spec).replaceAll('\\', '/'),
|
|
363
382
|
}))
|
|
364
383
|
writeFileSync(compJsonPath, JSON.stringify(compJsonEntries))
|
|
365
384
|
|
|
@@ -374,25 +393,38 @@ function emitComponentArtifacts(
|
|
|
374
393
|
importLines.push("import { Island } from 'brustjs'")
|
|
375
394
|
}
|
|
376
395
|
|
|
377
|
-
// Import each referenced component
|
|
378
|
-
//
|
|
379
|
-
//
|
|
396
|
+
// Import each referenced component, regenerating the correct import FORM per
|
|
397
|
+
// kind. Local specs are relativized to the factory file's own dir (so
|
|
398
|
+
// `await import(factory)` resolves them at runtime regardless of project
|
|
399
|
+
// location); bare specs are kept verbatim.
|
|
380
400
|
const allReferenced = [...new Set(enriched.flatMap((e) => e.referencedComponents))]
|
|
381
401
|
for (const compName of allReferenced) {
|
|
382
402
|
if (seen.has(compName)) continue
|
|
383
403
|
seen.add(compName)
|
|
384
|
-
const
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
|
|
404
|
+
const ref = pageImports.get(compName)
|
|
405
|
+
if (!ref) continue
|
|
406
|
+
const spec = ref.bare ? ref.spec : toRelativeSpecifier(jinjaDir, ref.spec)
|
|
407
|
+
const specStr = JSON.stringify(spec)
|
|
408
|
+
if (ref.kind === 'namespace') {
|
|
409
|
+
importLines.push(`import * as ${compName} from ${specStr}`)
|
|
410
|
+
} else if (ref.kind === 'named') {
|
|
411
|
+
// Collapse the redundant `{ X as X }` to `{ X }` when the local name equals
|
|
412
|
+
// the imported name (idiomatic + avoids no-useless-rename lint on the factory).
|
|
413
|
+
const named =
|
|
414
|
+
ref.imported && ref.imported !== compName ? `${ref.imported} as ${compName}` : compName
|
|
415
|
+
importLines.push(`import { ${named} } from ${specStr}`)
|
|
416
|
+
} else {
|
|
417
|
+
importLines.push(`import ${compName} from ${specStr}`)
|
|
388
418
|
}
|
|
389
419
|
}
|
|
390
420
|
|
|
391
421
|
// Scan SSR component sources for <Island component={X}> to discover Island
|
|
392
|
-
// chunk identifiers that don't appear in the page's own .islands.json.
|
|
422
|
+
// chunk identifiers that don't appear in the page's own .islands.json. Bare
|
|
423
|
+
// imports have no readable local file — skip them (no readFileSync attempt).
|
|
393
424
|
const islandIdsFromComponents: string[] = []
|
|
394
425
|
const islandAttrRe = /<Island\s[^>]*component=\{(\w+)\}/g
|
|
395
426
|
for (const entry of enriched) {
|
|
427
|
+
if (entry.ref.bare) continue
|
|
396
428
|
try {
|
|
397
429
|
const src = readFileSync(entry.sourcePath, 'utf8')
|
|
398
430
|
islandAttrRe.lastIndex = 0
|
|
@@ -512,7 +544,7 @@ export async function emitNativeTemplates(opts: NativeRouteEmitOpts): Promise<vo
|
|
|
512
544
|
let routeSource: string
|
|
513
545
|
let routeSourcePath: string
|
|
514
546
|
let sources: Record<string, string>
|
|
515
|
-
let mergedImports: Map<string,
|
|
547
|
+
let mergedImports: Map<string, ResolvedImport>
|
|
516
548
|
if (chainNames.length > 1) {
|
|
517
549
|
routeSource = buildChainWrapperSource(chainNames)
|
|
518
550
|
// Synthetic path: a placeholder under the leaf's dir. The compiler keys
|
|
@@ -585,9 +617,129 @@ export async function emitNativeTemplates(opts: NativeRouteEmitOpts): Promise<vo
|
|
|
585
617
|
)
|
|
586
618
|
}
|
|
587
619
|
|
|
620
|
+
/** A JSX SSR-component ident is always Capitalized — `<Search/>` lowers to an
|
|
621
|
+
* SsrComponent while `<search/>` is a host element. The Rust compiler only lists
|
|
622
|
+
* Capitalized idents in `componentsJson`/`islandsJson`, so only Capitalized
|
|
623
|
+
* idents can collide as ambiguous COMPONENT sources. Lowercase idents (hooks,
|
|
624
|
+
* store singletons like `teamStore`, util fns) are never components — two local
|
|
625
|
+
* files legitimately sharing such a name (e.g. `teamStore` from two stores) must
|
|
626
|
+
* NOT trip the component-ambiguity guard. (Pre-`scanImportRefs`, the
|
|
627
|
+
* default-only scanner never saw named/namespace lowercase imports at all.) */
|
|
628
|
+
function isComponentIdent(ident: string): boolean {
|
|
629
|
+
return /^[A-Z]/.test(ident)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** A resolved import reference, capturing the import FORM so the SSR factory can
|
|
633
|
+
* regenerate the correct `import` statement. Used only by the SSR-component path
|
|
634
|
+
* (`gatherComponentSources`/`gatherChainSources` → `emitComponentArtifacts` /
|
|
635
|
+
* `reconcileIslandManifest`), NOT by `scanImports` (which stays local-default
|
|
636
|
+
* string-valued for the two external callers in islands/native build). */
|
|
637
|
+
export interface ResolvedImport {
|
|
638
|
+
/** Module specifier: an ABSOLUTE file path for local imports, or the verbatim
|
|
639
|
+
* bare specifier for package imports. */
|
|
640
|
+
spec: string
|
|
641
|
+
/** true ⇒ `spec` is a package/bare specifier (keep verbatim; do not
|
|
642
|
+
* readFileSync/relativize/recurse). */
|
|
643
|
+
bare: boolean
|
|
644
|
+
/** How the symbol was imported, so the factory regenerates the right import. */
|
|
645
|
+
kind: 'default' | 'named' | 'namespace'
|
|
646
|
+
/** For `named`, the exported name (may differ from the local alias). */
|
|
647
|
+
imported?: string
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Resolve a module specifier as it appears in an import: a `.`/`..`-prefixed
|
|
651
|
+
* (local) spec resolves to an absolute file path via the `.tsx/.ts/index.*`
|
|
652
|
+
* candidate logic; any other (bare/package) spec is kept verbatim. Returns
|
|
653
|
+
* `undefined` for a local spec that resolves to no existing file (matches
|
|
654
|
+
* scanImports' silent-drop behavior). */
|
|
655
|
+
function resolveSpec(spec: string, fromFile: string): ResolvedImport | undefined {
|
|
656
|
+
if (!spec.startsWith('.')) {
|
|
657
|
+
// Package/bare specifier — keep verbatim, never resolve/readFileSync.
|
|
658
|
+
return { spec, bare: true, kind: 'default' }
|
|
659
|
+
}
|
|
660
|
+
const baseDir = dirname(fromFile)
|
|
661
|
+
const resolved = resolve(baseDir, spec)
|
|
662
|
+
const candidates = [
|
|
663
|
+
`${resolved}.tsx`,
|
|
664
|
+
`${resolved}.ts`,
|
|
665
|
+
`${resolved}/index.tsx`,
|
|
666
|
+
`${resolved}/index.ts`,
|
|
667
|
+
]
|
|
668
|
+
const found = candidates.find((p) => existsSync(p))
|
|
669
|
+
if (!found) return undefined
|
|
670
|
+
return { spec: found, bare: false, kind: 'default' }
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Scan ALL import forms in `file` and resolve each local-name binding to a
|
|
674
|
+
* {@link ResolvedImport}. Unlike {@link scanImports} (default-local only, kept
|
|
675
|
+
* stable for external callers), this:
|
|
676
|
+
* - parses default / `* as ns` / `{ a, b as c }` / mixed `d, { a }` forms;
|
|
677
|
+
* - keeps package/bare specifiers verbatim (`bare:true`) instead of skipping
|
|
678
|
+
* them — the SSR path needs them to regenerate `import` lines and to SSR
|
|
679
|
+
* third-party components (e.g. lucide-react icons).
|
|
680
|
+
*
|
|
681
|
+
* Returns `Map<localName, ResolvedImport>` (localName = the in-source identifier
|
|
682
|
+
* used in JSX). Namespace imports are recorded parse-only (the Rust compiler
|
|
683
|
+
* rejects member-expression elements, so `<Ns.Member/>` isn't renderable yet). */
|
|
684
|
+
export function scanImportRefs(file: string): Map<string, ResolvedImport> {
|
|
685
|
+
const source = readFileSync(file, 'utf8')
|
|
686
|
+
const map = new Map<string, ResolvedImport>()
|
|
687
|
+
// Match any import statement's clause + specifier. The clause is parsed below.
|
|
688
|
+
const re = /^import\s+([^'"]+?)\s+from\s+['"]([^'"]+)['"]/gm
|
|
689
|
+
for (let m = re.exec(source); m !== null; m = re.exec(source)) {
|
|
690
|
+
const clause = m[1]!.trim()
|
|
691
|
+
// `import type …` / `import type { … }` / `import type * as …` are erased at
|
|
692
|
+
// build — they bind no runtime value, so a type alias must never enter the
|
|
693
|
+
// map (a Capitalized type name could otherwise collide with a real component).
|
|
694
|
+
if (/^type[\s{*]/.test(clause)) continue
|
|
695
|
+
const spec = m[2]!
|
|
696
|
+
const resolved = resolveSpec(spec, file)
|
|
697
|
+
if (!resolved) continue // local spec that resolves to no file — silent drop
|
|
698
|
+
|
|
699
|
+
// Namespace: `* as Ns`
|
|
700
|
+
const nsMatch = /^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(clause)
|
|
701
|
+
if (nsMatch) {
|
|
702
|
+
map.set(nsMatch[1]!, { ...resolved, kind: 'namespace' })
|
|
703
|
+
continue
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Split a possible mixed clause `Default, { a, b as c }` into default +
|
|
707
|
+
// named parts. The default ident (if any) is the leading bare identifier.
|
|
708
|
+
const rest = clause
|
|
709
|
+
const namedStart = rest.indexOf('{')
|
|
710
|
+
let defaultPart = namedStart === -1 ? rest : rest.slice(0, namedStart)
|
|
711
|
+
defaultPart = defaultPart.replace(/,\s*$/, '').trim()
|
|
712
|
+
if (defaultPart) {
|
|
713
|
+
const defMatch = /^([A-Za-z_$][\w$]*)$/.exec(defaultPart)
|
|
714
|
+
if (defMatch) {
|
|
715
|
+
map.set(defMatch[1]!, { ...resolved, kind: 'default' })
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (namedStart !== -1) {
|
|
720
|
+
const namedEnd = rest.indexOf('}', namedStart)
|
|
721
|
+
const inner = rest.slice(namedStart + 1, namedEnd === -1 ? undefined : namedEnd)
|
|
722
|
+
for (const raw of inner.split(',')) {
|
|
723
|
+
const piece = raw.trim()
|
|
724
|
+
if (!piece) continue
|
|
725
|
+
const aliasMatch = /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(piece)
|
|
726
|
+
if (aliasMatch) {
|
|
727
|
+
map.set(aliasMatch[2]!, { ...resolved, kind: 'named', imported: aliasMatch[1]! })
|
|
728
|
+
} else if (/^[A-Za-z_$][\w$]*$/.test(piece)) {
|
|
729
|
+
map.set(piece, { ...resolved, kind: 'named', imported: piece })
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return map
|
|
735
|
+
}
|
|
736
|
+
|
|
588
737
|
/** Scan the entry file's `import Name from './path'` declarations and build a
|
|
589
|
-
* map of localName -> resolved absolute path
|
|
590
|
-
* `.tsx`, `.ts`, `/index.tsx`,
|
|
738
|
+
* map of localName -> resolved absolute path (DEFAULT-LOCAL only — package
|
|
739
|
+
* specifiers skipped). Extension resolution tries `.tsx`, `.ts`, `/index.tsx`,
|
|
740
|
+
* `/index.ts` in order. Kept string-valued + default-local for the external
|
|
741
|
+
* callers in islands/build.ts + native/build.ts; the SSR-component path uses the
|
|
742
|
+
* richer {@link scanImportRefs} instead. */
|
|
591
743
|
export function scanImports(entryFile: string): Map<string, string> {
|
|
592
744
|
const source = readFileSync(entryFile, 'utf8')
|
|
593
745
|
const map = new Map<string, string>()
|
|
@@ -628,7 +780,7 @@ export function scanImports(entryFile: string): Map<string, string> {
|
|
|
628
780
|
export function reconcileIslandManifest(
|
|
629
781
|
jinjaPath: string,
|
|
630
782
|
islandsJsonPath: string,
|
|
631
|
-
pageImports: Map<string,
|
|
783
|
+
pageImports: Map<string, ResolvedImport>,
|
|
632
784
|
routeName: string,
|
|
633
785
|
): void {
|
|
634
786
|
if (!existsSync(islandsJsonPath)) return
|
|
@@ -644,13 +796,21 @@ export function reconcileIslandManifest(
|
|
|
644
796
|
// the SSR import. Mirrors the .components.json contract (emitComponentArtifacts).
|
|
645
797
|
const projectRoot = process.cwd()
|
|
646
798
|
const enriched: EnrichedIslandEntry[] = raw.map((entry) => {
|
|
647
|
-
const
|
|
648
|
-
if (!
|
|
799
|
+
const ref = pageImports.get(entry.component)
|
|
800
|
+
if (!ref) {
|
|
649
801
|
throw new Error(
|
|
650
802
|
`island component "${entry.component}" in native route "${routeName}" has no matching import in the page source (expected \`import ${entry.component} from "..."\`)`,
|
|
651
803
|
)
|
|
652
804
|
}
|
|
653
|
-
|
|
805
|
+
// Islands are LOCAL-only: the runtime (`loadIslandManifest`) rehydrates
|
|
806
|
+
// sourcePath against cwd, so a bare/package spec would break hydration. A
|
|
807
|
+
// bare-import island is a hard error, not a silently-written bad manifest.
|
|
808
|
+
if (ref.bare) {
|
|
809
|
+
throw new Error(
|
|
810
|
+
`island component "${entry.component}" in native route "${routeName}" resolves to a bare/package import "${ref.spec}" — islands must be imported from a local file (the manifest's sourcePath is rehydrated against cwd at runtime)`,
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
return { ...entry, sourcePath: relative(projectRoot, ref.spec).replaceAll('\\', '/') }
|
|
654
814
|
})
|
|
655
815
|
|
|
656
816
|
writeFileSync(islandsJsonPath, JSON.stringify(enriched))
|
package/runtime/cli/templates.ts
CHANGED
|
@@ -60,10 +60,11 @@ export function findBrustPackageRoot(startDir: string = import.meta.dir): string
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function pokedexExtraFiles(ctx: ScaffoldCtx): EmittedFile[] {
|
|
63
|
-
// pokedex's dependency set
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// import)
|
|
63
|
+
// pokedex's dependency set extends minimal: it adds `zod` (action/query
|
|
64
|
+
// schemas) and `lucide-react` (icons SSR-rendered in native routes + islands).
|
|
65
|
+
// `tailwindcss` is declared like minimal (the redesigned app.css is Tailwind v4
|
|
66
|
+
// — `@import "tailwindcss"`). `react-dom` is the framework's SSR/hydration peer
|
|
67
|
+
// (not a direct pokedex import) — do not "tidy" it away.
|
|
67
68
|
const pkg = {
|
|
68
69
|
name: ctx.projectName,
|
|
69
70
|
version: '0.0.1',
|
|
@@ -78,6 +79,8 @@ function pokedexExtraFiles(ctx: ScaffoldCtx): EmittedFile[] {
|
|
|
78
79
|
react: '^19.2.6',
|
|
79
80
|
'react-dom': '^19.2.6',
|
|
80
81
|
zod: '^4.4.3',
|
|
82
|
+
tailwindcss: '^4.3.0',
|
|
83
|
+
'lucide-react': '^1.17.0',
|
|
81
84
|
},
|
|
82
85
|
devDependencies: {
|
|
83
86
|
'@types/bun': 'latest',
|