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

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 +82 -4
  3. package/native/index.js +58 -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 +82 -1
  8. package/src/actions/prefix.ts +44 -2
  9. package/src/actions-bridge.ts +44 -9
  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 +9 -4
  18. package/src/build/cf-wrangler.ts +3 -1
  19. package/src/build/cloudflare.ts +34 -13
  20. package/src/build/compile.ts +263 -6
  21. package/src/build/config.ts +138 -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 +94 -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 +378 -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 +256 -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 +116 -10
  53. package/src/index.ts +6 -1
  54. package/src/native.ts +32 -1
  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 +65 -13
  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 +105 -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. */
@@ -58,17 +69,79 @@ export interface ActionsRecord {
58
69
  maxBodyBytes: number
59
70
  maxResponseBytes: number
60
71
  crossSite: 'reject' | 'allow'
72
+ /** F475: `[actions] stream` and its bounds; all absent without a `stream` list. */
73
+ stream?: string[]
74
+ maxStreamBytes?: number
75
+ streamIdleMs?: number
76
+ streamTotalMs?: number
77
+ maxStreams?: number
61
78
  }
62
79
 
80
+ /** The manifest format the Rust loader accepts (`MANIFEST_VERSION` in brust-core `manifest.rs`): the one place TS names it. */
81
+ export const MANIFEST_VERSION = 3 as const
82
+
63
83
  export interface ManifestJson {
64
- version: 1
84
+ version: typeof MANIFEST_VERSION
65
85
  routes: RouteRecord[]
66
86
  components: Record<string, ComponentRecord>
67
- assets: { runtime: string }
87
+ /** `runtime` is the core; `features` maps each feature module name to its file (L33). */
88
+ assets: {
89
+ runtime: string
90
+ features: Record<string, string>
91
+ /** F453: the SPA-navigation module `client/nav-<hex>.js`; the `nav` tag is emitted only when it is set. */
92
+ nav?: string
93
+ /** F453: the build id (`data-brust-build`): a digest of the rest of the manifest, set last by `brust build`. */
94
+ build?: string
95
+ }
68
96
  jobs_module: string
69
97
  actions: ActionsRecord | null
98
+ /** L16 R16.3: an actions-only build (no routes, no templates); everything but the prefix is a 404 problem+json. */
99
+ actions_only?: true
100
+ /** L16 R16.4b: a page-only build: no local actions app, `base_url` points at the deployment that has it. */
101
+ actions_remote?: { prefix: string }
70
102
  /** `[cache]` (cache adapters); absent without one (the manifest is then byte-identical). */
71
103
  cache?: CacheRecord
104
+ /** F165 (R40.1): `brust start` spawns its worker pool (`needsWorkers`). Read by `run.ts` only; an
105
+ * absent value (an older or actions-only manifest) means the pool is needed. */
106
+ needs_workers?: boolean
107
+ }
108
+
109
+ /** F165 (R40.1): whether any request can call a worker. Workers answer four kinds of call
110
+ * (worker.ts): `loader`, `jobs` (precompute and SSR), `fetch` (the local actions app) and `cache`
111
+ * (the adapter). Only the jobs a request can plan count (`reachedJobs`, F183); a store read counts too (`readsStores`). */
112
+ export function needsWorkers(m: Pick<ManifestJson, 'routes' | 'components' | 'actions' | 'cache'>): boolean {
113
+ const planned = [...reachedJobs(m).planned].some((id) => (m.components[id]?.jobs.length ?? 0) > 0)
114
+ return planned || m.routes.some((r) => r.loaders.length > 0) || readsStores(m) || m.actions !== null || m.cache !== undefined
115
+ }
116
+
117
+ /** A route whose chain reads a store (its components, their children and job targets, transitively) gets a loader call
118
+ * from brust-core even with no loader of its own: the worker builds the `__store` snapshot (pipeline.rs `reads_store`).
119
+ * Without a pool that call answers 503. */
120
+ function readsStores(m: Pick<ManifestJson, 'routes' | 'components'>): boolean {
121
+ const seen = new Set<string>()
122
+ const reads = (id: string): boolean => {
123
+ if (seen.has(id)) return false
124
+ seen.add(id)
125
+ const c = m.components[id]
126
+ return !!c && (c.stores.length > 0 || c.children.some((k) => reads(k.id)) || c.jobs.some((j) => j.target !== undefined && reads(j.target)))
127
+ }
128
+ return m.routes.some((r) => r.chain.some(reads))
129
+ }
130
+
131
+ /** F183: the jobs a request can call, as brust-core plans them (`collect_jobs`, pipeline.rs): every job of each
132
+ * route chain component and of each of its direct `children[]` records (`planned`; nothing deeper has a job,
133
+ * `linkNativeChunks`), and the `ssr` of each component one of those jobs targets (`ssr`; the worker runs the
134
+ * target's, handlers.ts). `jobs.js` leaves out every other entry: no request reaches it. */
135
+ export function reachedJobs(m: Pick<ManifestJson, 'routes' | 'components'>): { planned: Set<string>; ssr: Set<string> } {
136
+ const planned = new Set<string>()
137
+ for (const r of m.routes)
138
+ for (const id of r.chain) {
139
+ planned.add(id)
140
+ for (const ch of m.components[id]?.children ?? []) planned.add(ch.id)
141
+ }
142
+ const ssr = new Set<string>()
143
+ for (const id of planned) for (const j of m.components[id]?.jobs ?? []) if (j.kind === 'ssr' && j.target !== undefined) ssr.add(j.target)
144
+ return { planned, ssr }
72
145
  }
73
146
 
74
147
  /** `manifest.cache` / `CONFIG.cache`: the adapter the runtime loads (`cache-adapter/load.ts`). */
@@ -226,15 +299,23 @@ export function childRecords(c: Compiled, clientOnly: string[] = []): ChildRecor
226
299
  if (inst.loops.length > 1)
227
300
  throw new BuildError('nested-instance', `${ir.id}: child ${inst.child_id} sits in nested lists (M3)`)
228
301
  const list = inst.loops[0]
229
- if (list === null)
302
+ const literal = list === null && Array.isArray(inst.rows_literal)
303
+ if (list === null && !literal)
230
304
  throw new BuildError('instance-list-path', `${ir.id}: child ${inst.child_id} sits in a list that is not a props path`)
231
305
  const props: Record<string, string> = {}
232
306
  for (const [name, path] of Object.entries(inst.props)) {
233
307
  if (path === null)
234
- throw new BuildError('instance-prop-path', `${ir.id}: child ${inst.child_id} prop ${name} is not a props path`)
308
+ throw new BuildError(
309
+ 'instance-prop-path',
310
+ `${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()`,
311
+ )
235
312
  props[name] = path
236
313
  }
237
- return { id: inst.child_id, instances: list === undefined ? 'static' : `per-row:${list}`, props }
314
+ const rec: ChildRecord = { id: inst.child_id, instances: literal ? 'per-row-literal' : list === undefined ? 'static' : `per-row:${list}`, props }
315
+ if (inst.literals && Object.keys(inst.literals).length > 0) rec.literals = { ...inst.literals }
316
+ if (inst.exprs && Object.keys(inst.exprs).length > 0) rec.exprs = { ...inst.exprs }
317
+ if (literal) rec.rows = inst.rows_literal
318
+ return rec
238
319
  })
239
320
  for (const id of clientOnly) out.push({ id, instances: 'static', props: {} })
240
321
  return out
@@ -261,7 +342,8 @@ export function writeManifest(opts: {
261
342
  leaves: FlatRoute[]
262
343
  routeComponent: Map<string, string>
263
344
  compiled: Map<string, Compiled>
264
- runtime: string
345
+ /** The runtime files `buildRuntime` wrote: the core and the feature modules. */
346
+ runtime: { core: string; features: Record<string, string>; nav?: string }
265
347
  /** component id → `client/<file>` (client chunk or react island chunk). */
266
348
  chunks: Map<string, string>
267
349
  actions?: ActionsRecord | null
@@ -280,7 +362,10 @@ export function writeManifest(opts: {
280
362
  needs_worker: jobs.length > 0,
281
363
  use_id_slots: c.ir.use_id_slots ?? 0,
282
364
  stores: storesOf(compiled, c.id),
365
+ features: featuresOf(c.id, c.jinja, tierOf(c.ir) === 'react'),
283
366
  }
367
+ for (const f of components[c.id]!.features)
368
+ 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
369
  }
285
370
  const routes: RouteRecord[] = opts.leaves.map((l) => {
286
371
  // A node without a Component (a pure path/loader group) renders nothing: not a chain entry.
@@ -302,7 +387,7 @@ export function writeManifest(opts: {
302
387
  }
303
388
  })
304
389
  linkNativeChunks(routes, compiled, components)
305
- return { version: 1, routes, components, assets: { runtime: opts.runtime }, jobs_module: 'jobs.js', actions: opts.actions ?? null }
390
+ 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
391
  }
307
392
 
308
393
  /** S6 amendment (F66): the server's asset injection walks only each chain component's direct
@@ -315,6 +400,8 @@ function linkNativeChunks(routes: RouteRecord[], compiled: Map<string, Compiled>
315
400
  const chainIds = new Set(routes.flatMap((r) => r.chain))
316
401
  for (const root of chainIds) {
317
402
  const rec = components[root]!
403
+ // A React root renders its whole subtree itself: whatever native chunks its IR lists are not served.
404
+ if (rec.tier === 'react') continue
318
405
  const linked = new Set(rec.children.map((ch) => ch.id))
319
406
  const seen = new Set([root])
320
407
  const walk = (id: string) => {