@brust/core 2.0.0-alpha.3 → 2.0.0-alpha.30

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.
Files changed (64) hide show
  1. package/README.md +66 -6
  2. package/native/index.d.ts +63 -4
  3. package/native/index.js +57 -54
  4. package/package.json +29 -15
  5. package/src/actions/client.ts +53 -7
  6. package/src/actions/elysia.ts +1 -1
  7. package/src/actions/load.ts +24 -1
  8. package/src/actions/prefix.ts +24 -2
  9. package/src/actions-bridge.ts +2 -2
  10. package/src/assets.d.ts +55 -0
  11. package/src/browser.ts +6 -1
  12. package/src/build/actions-only.ts +86 -0
  13. package/src/build/build-eval.ts +332 -0
  14. package/src/build/build-icons.ts +69 -0
  15. package/src/build/bundle.ts +466 -54
  16. package/src/build/cf-assets.ts +4 -2
  17. package/src/build/cf-entry.ts +7 -3
  18. package/src/build/cf-wrangler.ts +3 -1
  19. package/src/build/cloudflare.ts +21 -11
  20. package/src/build/compile.ts +263 -6
  21. package/src/build/config.ts +85 -11
  22. package/src/build/css.ts +353 -0
  23. package/src/build/esm-resolve.ts +170 -0
  24. package/src/build/features.ts +119 -0
  25. package/src/build/index.ts +210 -27
  26. package/src/build/manifest.ts +88 -7
  27. package/src/build/pkg-src.ts +514 -0
  28. package/src/build/recognised.ts +19 -0
  29. package/src/build/scan.ts +71 -20
  30. package/src/build/server-only.ts +62 -0
  31. package/src/build/ssg-render.ts +65 -0
  32. package/src/build/ssg.ts +146 -0
  33. package/src/build/static-worker.ts +91 -0
  34. package/src/build/static.ts +259 -0
  35. package/src/build/tailwind.ts +117 -0
  36. package/src/build/tsconfig-paths.ts +67 -0
  37. package/src/build-time.ts +10 -0
  38. package/src/cache-adapter/types.ts +1 -1
  39. package/src/cf.ts +4 -1
  40. package/src/cli.ts +40 -4
  41. package/src/config.ts +111 -25
  42. package/src/css-modules.d.ts +8 -0
  43. package/src/dev/bun.ts +371 -0
  44. package/src/dev/client.ts +92 -0
  45. package/src/dev/devtools-data.ts +23 -0
  46. package/src/dev/devtools.ts +358 -0
  47. package/src/dev/diag.ts +30 -0
  48. package/src/dev/proxy.ts +236 -0
  49. package/src/dev/tui.ts +144 -0
  50. package/src/dev/watch.ts +130 -0
  51. package/src/dev.ts +15 -28
  52. package/src/handlers.ts +115 -9
  53. package/src/index.ts +6 -1
  54. package/src/native.ts +18 -0
  55. package/src/navigate-browser.ts +19 -0
  56. package/src/navigate.ts +19 -0
  57. package/src/navigation.ts +91 -0
  58. package/src/routes.ts +31 -1
  59. package/src/run.ts +53 -11
  60. package/src/shadcn/init.ts +513 -0
  61. package/src/shadcn/jsonc.ts +129 -0
  62. package/src/shadcn/registry.ts +149 -0
  63. package/src/store/builtin.ts +4 -0
  64. package/src/worker.ts +98 -22
@@ -6,6 +6,7 @@
6
6
  import type { FlatRoute } from '../routes'
7
7
  import type { Compiled, ComponentIR } from './compile'
8
8
  import { BuildError } from './errors'
9
+ import { featuresOf } from './features'
9
10
 
10
11
  export interface JobCache {
11
12
  key: string | null
@@ -29,6 +30,12 @@ export interface ChildRecord {
29
30
  id: string
30
31
  instances: string
31
32
  props: Record<string, string>
33
+ /** F105: constant props; part of the child's job keys. */
34
+ literals?: Record<string, unknown>
35
+ /** F105: jinja over `__props` (and `__row`/`__idx` per row) the server evaluates. */
36
+ exprs?: Record<string, string>
37
+ /** F142 R34.43: the list of a `per-row-literal` instance (a constant the compiler read). */
38
+ rows?: unknown[]
32
39
  }
33
40
 
34
41
  export interface ComponentRecord {
@@ -40,6 +47,8 @@ export interface ComponentRecord {
40
47
  needs_worker: boolean
41
48
  use_id_slots: number
42
49
  stores: string[]
50
+ /** L33: the client runtime feature modules (`assets.features` keys) this component's output needs, in load order. */
51
+ features: string[]
43
52
  }
44
53
 
45
54
  export interface RouteRecord {
@@ -49,6 +58,8 @@ export interface RouteRecord {
49
58
  loaders: string[]
50
59
  cache: { ttl_seconds: number; prefix: string | null; bypass: true | string | null; tags: string[] } | null
51
60
  catch_all: boolean
61
+ /** L19 (R19.3): `client/<file>.css` in cascade order; absent when the route has none. */
62
+ css?: string[]
52
63
  }
53
64
 
54
65
  /** `manifest.actions` (A3): `entry` is relative to dist; `maxBodyBytes`/`maxResponseBytes` are camelCase by contract. */
@@ -60,15 +71,71 @@ export interface ActionsRecord {
60
71
  crossSite: 'reject' | 'allow'
61
72
  }
62
73
 
74
+ /** The manifest format the Rust loader accepts (`MANIFEST_VERSION` in brust-core `manifest.rs`): the one place TS names it. */
75
+ export const MANIFEST_VERSION = 3 as const
76
+
63
77
  export interface ManifestJson {
64
- version: 1
78
+ version: typeof MANIFEST_VERSION
65
79
  routes: RouteRecord[]
66
80
  components: Record<string, ComponentRecord>
67
- assets: { runtime: string }
81
+ /** `runtime` is the core; `features` maps each feature module name to its file (L33). */
82
+ assets: {
83
+ runtime: string
84
+ features: Record<string, string>
85
+ /** F453: the SPA-navigation module `client/nav-<hex>.js`; the `nav` tag is emitted only when it is set. */
86
+ nav?: string
87
+ /** F453: the build id (`data-brust-build`): a digest of the rest of the manifest, set last by `brust build`. */
88
+ build?: string
89
+ }
68
90
  jobs_module: string
69
91
  actions: ActionsRecord | null
92
+ /** L16 R16.3: an actions-only build (no routes, no templates); everything but the prefix is a 404 problem+json. */
93
+ actions_only?: true
94
+ /** L16 R16.4b: a page-only build: no local actions app, `base_url` points at the deployment that has it. */
95
+ actions_remote?: { prefix: string }
70
96
  /** `[cache]` (cache adapters); absent without one (the manifest is then byte-identical). */
71
97
  cache?: CacheRecord
98
+ /** F165 (R40.1): `brust start` spawns its worker pool (`needsWorkers`). Read by `run.ts` only; an
99
+ * absent value (an older or actions-only manifest) means the pool is needed. */
100
+ needs_workers?: boolean
101
+ }
102
+
103
+ /** F165 (R40.1): whether any request can call a worker. Workers answer four kinds of call
104
+ * (worker.ts): `loader`, `jobs` (precompute and SSR), `fetch` (the local actions app) and `cache`
105
+ * (the adapter). Only the jobs a request can plan count (`reachedJobs`, F183); a store read counts too (`readsStores`). */
106
+ export function needsWorkers(m: Pick<ManifestJson, 'routes' | 'components' | 'actions' | 'cache'>): boolean {
107
+ const planned = [...reachedJobs(m).planned].some((id) => (m.components[id]?.jobs.length ?? 0) > 0)
108
+ return planned || m.routes.some((r) => r.loaders.length > 0) || readsStores(m) || m.actions !== null || m.cache !== undefined
109
+ }
110
+
111
+ /** A route whose chain reads a store (its components, their children and job targets, transitively) gets a loader call
112
+ * from brust-core even with no loader of its own: the worker builds the `__store` snapshot (pipeline.rs `reads_store`).
113
+ * Without a pool that call answers 503. */
114
+ function readsStores(m: Pick<ManifestJson, 'routes' | 'components'>): boolean {
115
+ const seen = new Set<string>()
116
+ const reads = (id: string): boolean => {
117
+ if (seen.has(id)) return false
118
+ seen.add(id)
119
+ const c = m.components[id]
120
+ return !!c && (c.stores.length > 0 || c.children.some((k) => reads(k.id)) || c.jobs.some((j) => j.target !== undefined && reads(j.target)))
121
+ }
122
+ return m.routes.some((r) => r.chain.some(reads))
123
+ }
124
+
125
+ /** F183: the jobs a request can call, as brust-core plans them (`collect_jobs`, pipeline.rs): every job of each
126
+ * route chain component and of each of its direct `children[]` records (`planned`; nothing deeper has a job,
127
+ * `linkNativeChunks`), and the `ssr` of each component one of those jobs targets (`ssr`; the worker runs the
128
+ * target's, handlers.ts). `jobs.js` leaves out every other entry: no request reaches it. */
129
+ export function reachedJobs(m: Pick<ManifestJson, 'routes' | 'components'>): { planned: Set<string>; ssr: Set<string> } {
130
+ const planned = new Set<string>()
131
+ for (const r of m.routes)
132
+ for (const id of r.chain) {
133
+ planned.add(id)
134
+ for (const ch of m.components[id]?.children ?? []) planned.add(ch.id)
135
+ }
136
+ const ssr = new Set<string>()
137
+ for (const id of planned) for (const j of m.components[id]?.jobs ?? []) if (j.kind === 'ssr' && j.target !== undefined) ssr.add(j.target)
138
+ return { planned, ssr }
72
139
  }
73
140
 
74
141
  /** `manifest.cache` / `CONFIG.cache`: the adapter the runtime loads (`cache-adapter/load.ts`). */
@@ -226,15 +293,23 @@ export function childRecords(c: Compiled, clientOnly: string[] = []): ChildRecor
226
293
  if (inst.loops.length > 1)
227
294
  throw new BuildError('nested-instance', `${ir.id}: child ${inst.child_id} sits in nested lists (M3)`)
228
295
  const list = inst.loops[0]
229
- if (list === null)
296
+ const literal = list === null && Array.isArray(inst.rows_literal)
297
+ if (list === null && !literal)
230
298
  throw new BuildError('instance-list-path', `${ir.id}: child ${inst.child_id} sits in a list that is not a props path`)
231
299
  const props: Record<string, string> = {}
232
300
  for (const [name, path] of Object.entries(inst.props)) {
233
301
  if (path === null)
234
- throw new BuildError('instance-prop-path', `${ir.id}: child ${inst.child_id} prop ${name} is not a props path`)
302
+ throw new BuildError(
303
+ 'instance-prop-path',
304
+ `${ir.id}: child ${inst.child_id} prop ${name} is not a props path, a JSON literal or a template expression over props; compute it in a loader, or wrap the child's derived value in cache()`,
305
+ )
235
306
  props[name] = path
236
307
  }
237
- return { id: inst.child_id, instances: list === undefined ? 'static' : `per-row:${list}`, props }
308
+ const rec: ChildRecord = { id: inst.child_id, instances: literal ? 'per-row-literal' : list === undefined ? 'static' : `per-row:${list}`, props }
309
+ if (inst.literals && Object.keys(inst.literals).length > 0) rec.literals = { ...inst.literals }
310
+ if (inst.exprs && Object.keys(inst.exprs).length > 0) rec.exprs = { ...inst.exprs }
311
+ if (literal) rec.rows = inst.rows_literal
312
+ return rec
238
313
  })
239
314
  for (const id of clientOnly) out.push({ id, instances: 'static', props: {} })
240
315
  return out
@@ -261,7 +336,8 @@ export function writeManifest(opts: {
261
336
  leaves: FlatRoute[]
262
337
  routeComponent: Map<string, string>
263
338
  compiled: Map<string, Compiled>
264
- runtime: string
339
+ /** The runtime files `buildRuntime` wrote: the core and the feature modules. */
340
+ runtime: { core: string; features: Record<string, string>; nav?: string }
265
341
  /** component id → `client/<file>` (client chunk or react island chunk). */
266
342
  chunks: Map<string, string>
267
343
  actions?: ActionsRecord | null
@@ -280,7 +356,10 @@ export function writeManifest(opts: {
280
356
  needs_worker: jobs.length > 0,
281
357
  use_id_slots: c.ir.use_id_slots ?? 0,
282
358
  stores: storesOf(compiled, c.id),
359
+ features: featuresOf(c.id, c.jinja, tierOf(c.ir) === 'react'),
283
360
  }
361
+ for (const f of components[c.id]!.features)
362
+ if (!(f in opts.runtime.features)) throw new BuildError('runtime-feature-missing', `${c.id}: needs the client runtime feature ${f}, which the build did not ship`)
284
363
  }
285
364
  const routes: RouteRecord[] = opts.leaves.map((l) => {
286
365
  // A node without a Component (a pure path/loader group) renders nothing: not a chain entry.
@@ -302,7 +381,7 @@ export function writeManifest(opts: {
302
381
  }
303
382
  })
304
383
  linkNativeChunks(routes, compiled, components)
305
- return { version: 1, routes, components, assets: { runtime: opts.runtime }, jobs_module: 'jobs.js', actions: opts.actions ?? null }
384
+ return { version: MANIFEST_VERSION, routes, components, assets: { runtime: opts.runtime.core, features: opts.runtime.features, ...(opts.runtime.nav === undefined ? {} : { nav: opts.runtime.nav }) }, jobs_module: 'jobs.js', actions: opts.actions ?? null }
306
385
  }
307
386
 
308
387
  /** S6 amendment (F66): the server's asset injection walks only each chain component's direct
@@ -315,6 +394,8 @@ function linkNativeChunks(routes: RouteRecord[], compiled: Map<string, Compiled>
315
394
  const chainIds = new Set(routes.flatMap((r) => r.chain))
316
395
  for (const root of chainIds) {
317
396
  const rec = components[root]!
397
+ // A React root renders its whole subtree itself: whatever native chunks its IR lists are not served.
398
+ if (rec.tier === 'react') continue
318
399
  const linked = new Set(rec.children.map((ch) => ch.id))
319
400
  const seen = new Set([root])
320
401
  const walk = (id: string) => {