brustjs 0.1.10-alpha → 0.1.12-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/README.md +14 -8
- package/package.json +9 -8
- package/runtime/actions.ts +7 -65
- package/runtime/cli/build.ts +152 -42
- package/runtime/cli/help.ts +125 -0
- package/runtime/cli/index.ts +57 -22
- package/runtime/client/index.ts +5 -104
- package/runtime/define-actions.ts +179 -0
- package/runtime/index.d.ts +11 -5
- package/runtime/index.js +52 -52
- package/runtime/index.ts +80 -56
- package/runtime/islands/native-render.ts +23 -6
- package/runtime/mcp/extractor.ts +240 -88
- package/runtime/mcp/manifest.ts +2 -1
- package/runtime/mcp/server.ts +28 -37
- package/runtime/render/inject-action-prefix.ts +60 -0
- package/runtime/render/stream.ts +5 -2
- package/runtime/routes.ts +110 -56
- package/runtime/standard-schema.ts +29 -0
- package/runtime/treaty.ts +131 -0
- package/runtime/cli/actions-prebuilt-plugin.ts +0 -97
- package/runtime/scan-actions.ts +0 -172
package/runtime/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as native from './index.js'
|
|
2
|
-
import type {
|
|
3
|
-
import { isValidActionId } from './actions.ts'
|
|
2
|
+
import type { EndpointDef } from './define-actions.ts'
|
|
4
3
|
import { loadConfig } from './config.ts'
|
|
5
4
|
import { configureCssEnabled, configureCssHrefsForRoute } from './css.ts'
|
|
5
|
+
import { configureJinjaDir } from './islands/native-render.ts'
|
|
6
6
|
|
|
7
7
|
export interface ServeOptions {
|
|
8
8
|
/** Host/address to bind on. A hostname (e.g. `localhost`, resolved Rust-side)
|
|
@@ -12,10 +12,13 @@ export interface ServeOptions {
|
|
|
12
12
|
workers: number
|
|
13
13
|
entry: string
|
|
14
14
|
bootTimeoutMs?: number
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
* Optional — omit if the app has no
|
|
18
|
-
actions?:
|
|
15
|
+
/** Actions builder from `defineActions(...)`. When present, `serve`
|
|
16
|
+
* registers its endpoints (method/path, keyed by registration index)
|
|
17
|
+
* before the listener binds. Optional — omit if the app has no actions. */
|
|
18
|
+
actions?: import('./define-actions.ts').ActionsBuilder
|
|
19
|
+
/** URL prefix the action router mounts under (e.g. `/_actions`). Threaded
|
|
20
|
+
* into Rust's ServeOptions.action_prefix. */
|
|
21
|
+
actionPrefix?: string
|
|
19
22
|
/** MCP support — pass a manifest built via brust.buildMcpManifest. brust.serve
|
|
20
23
|
* does NOT auto-wire MCP into workers; the worker branch of the entry file must
|
|
21
24
|
* call brust.loadMcpManifest() + makeMcpServer() itself and pass the McpServer
|
|
@@ -82,21 +85,8 @@ function loadJinjaOnce(dir: string): void {
|
|
|
82
85
|
_jinjaLoaded = true
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
function registerActionsInternal(
|
|
86
|
-
|
|
87
|
-
for (const a of actions) {
|
|
88
|
-
if (!isValidActionId(a.id)) {
|
|
89
|
-
throw new Error(
|
|
90
|
-
`action id ${JSON.stringify(a.id)} contains invalid characters; ` +
|
|
91
|
-
`allowed: [A-Za-z0-9_-]+ (max 128 chars)`,
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
if (seen.has(a.id)) {
|
|
95
|
-
throw new Error(`action id ${JSON.stringify(a.id)} registered more than once`)
|
|
96
|
-
}
|
|
97
|
-
seen.add(a.id)
|
|
98
|
-
}
|
|
99
|
-
return (native as any).registerActions(actions.map((a) => a.id))
|
|
88
|
+
function registerActionsInternal(endpoints: Array<{ method: string; path: string }>): number {
|
|
89
|
+
return (native as any).registerActions(endpoints.map((e) => ({ method: e.method, path: e.path })))
|
|
100
90
|
}
|
|
101
91
|
|
|
102
92
|
/** Read and schema-validate a prebuilt mcp-manifest.json at an absolute path.
|
|
@@ -121,11 +111,10 @@ async function readManifestFromPath(
|
|
|
121
111
|
|
|
122
112
|
export const brust = {
|
|
123
113
|
async serve(opts: ServeOptions): Promise<void> {
|
|
124
|
-
if (opts.actions
|
|
125
|
-
// Register
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
registerActionsInternal(opts.actions)
|
|
114
|
+
if (opts.actions) {
|
|
115
|
+
// Register endpoint method/path with Rust. The router keys each by its
|
|
116
|
+
// registration index; the worker dispatches on that same index string.
|
|
117
|
+
registerActionsInternal(opts.actions.endpoints)
|
|
129
118
|
}
|
|
130
119
|
;(native as any).beginServe({
|
|
131
120
|
host: opts.host,
|
|
@@ -133,6 +122,10 @@ export const brust = {
|
|
|
133
122
|
workers: opts.workers,
|
|
134
123
|
entry: opts.entry,
|
|
135
124
|
tuning: opts.tuning,
|
|
125
|
+
// napi-rs camelCases #[napi(object)] fields, so the addon reads
|
|
126
|
+
// `actionPrefix` (not snake_case). A snake_case key is silently dropped,
|
|
127
|
+
// leaving the prefix at its default — which broke custom-prefix routing.
|
|
128
|
+
actionPrefix: opts.actionPrefix,
|
|
136
129
|
})
|
|
137
130
|
const baseEnv = { ...process.env }
|
|
138
131
|
const workersArr: Worker[] = []
|
|
@@ -217,18 +210,6 @@ export const brust = {
|
|
|
217
210
|
configureCssDir(dir: string): void {
|
|
218
211
|
;(native as any).configureCssDir(dir)
|
|
219
212
|
},
|
|
220
|
-
/** Walk the project for files marked `'use server'`, import them, and
|
|
221
|
-
* return all named function exports as ActionDef[] plus the list of source
|
|
222
|
-
* files (needed by brust.buildMcpManifest). Both the main process and each
|
|
223
|
-
* worker should call this once at module top-level and pass `actions` to
|
|
224
|
-
* `brust.serve({ actions, ... })` (main) and `makeRenderer(..., { actions,
|
|
225
|
-
* ... })` (worker). See ScanOptions for roots / ignore overrides. */
|
|
226
|
-
async scanActions(
|
|
227
|
-
opts?: import('./scan-actions.ts').ScanOptions,
|
|
228
|
-
): Promise<import('./scan-actions.ts').ScanActionsResult> {
|
|
229
|
-
const { scanActions } = await import('./scan-actions.ts')
|
|
230
|
-
return scanActions(opts)
|
|
231
|
-
},
|
|
232
213
|
/** Extract the MCP manifest from TypeScript source using the compiler API,
|
|
233
214
|
* write it to `.brust/mcp-manifest.json`, and return it. Call once in the
|
|
234
215
|
* main process after `brust.registerRoutes(routes)`. Workers must read the
|
|
@@ -236,10 +217,9 @@ export const brust = {
|
|
|
236
217
|
* branch and pass an `McpServer` (built via `makeMcpServer`) to
|
|
237
218
|
* `makeRenderer` via `opts.mcp`. See example/hello-world/index.ts. */
|
|
238
219
|
async buildMcpManifest(opts: {
|
|
239
|
-
|
|
220
|
+
actionsFile?: string
|
|
240
221
|
routesFile: string
|
|
241
222
|
sourceRoots: string[]
|
|
242
|
-
actions: import('./actions.ts').ActionDef[]
|
|
243
223
|
routes: import('./routes.ts').FlatRoute[]
|
|
244
224
|
cwd?: string
|
|
245
225
|
}): Promise<import('./mcp/manifest.ts').McpManifest> {
|
|
@@ -293,6 +273,11 @@ export const brust = {
|
|
|
293
273
|
/** TCP port to bind on. Default 1337. Overridable by env/toml — see
|
|
294
274
|
* `address`. */
|
|
295
275
|
port?: number
|
|
276
|
+
/** Actions builder from `defineActions(...)`. Registered with Rust and
|
|
277
|
+
* threaded to each worker's renderer. Omit if the app has no actions. */
|
|
278
|
+
actions?: import('./define-actions.ts').ActionsBuilder
|
|
279
|
+
/** URL prefix the action router mounts under. Threaded to serve(). */
|
|
280
|
+
actionPrefix?: string
|
|
296
281
|
/** Overrides merged into the underlying `serve()` call (main thread). */
|
|
297
282
|
serve?: Partial<Omit<ServeOptions, 'entry' | 'actions' | 'mcp'>>
|
|
298
283
|
/** Per-worker SAB size in bytes. Default 256 KB. */
|
|
@@ -324,9 +309,20 @@ export const brust = {
|
|
|
324
309
|
]
|
|
325
310
|
configureDevClientSnippet(buildDevClientTag())
|
|
326
311
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
312
|
+
{
|
|
313
|
+
const { configureActionPrefixSnippet } = await import('./render/inject-action-prefix.ts')
|
|
314
|
+
const ap = opts.actionPrefix
|
|
315
|
+
configureActionPrefixSnippet(
|
|
316
|
+
ap && ap !== '/_brust/action'
|
|
317
|
+
? `<script>globalThis.__BRUST_ACTION_PREFIX__=${JSON.stringify(ap)}</script>`
|
|
318
|
+
: null,
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
// Actions now come from the explicit `defineActions(...)` builder passed in
|
|
322
|
+
// opts (the `'use server'` scanner is gone). `endpoints` is the EndpointDef[]
|
|
323
|
+
// threaded to serve()/makeRenderer; the worker keys dispatch by registration
|
|
324
|
+
// index.
|
|
325
|
+
const endpoints: EndpointDef[] = opts.actions?.endpoints ?? []
|
|
330
326
|
|
|
331
327
|
if (!isWorker) {
|
|
332
328
|
const { host, port, workers, cacheMaxEntries } = await loadConfig(process.cwd(), {
|
|
@@ -419,10 +415,20 @@ export const brust = {
|
|
|
419
415
|
}
|
|
420
416
|
}
|
|
421
417
|
|
|
422
|
-
// Sub-project J — load
|
|
423
|
-
// the startup-validation warning in registerRoutes can compare
|
|
424
|
-
// a populated registry. Loads once at startup; dev hot reload
|
|
425
|
-
|
|
418
|
+
// Sub-project J — load the compiled `*.jinja` templates into the minijinja
|
|
419
|
+
// env so the startup-validation warning in registerRoutes can compare
|
|
420
|
+
// against a populated registry. Loads once at startup; dev hot reload
|
|
421
|
+
// reloads it. The templates ship INSIDE the build output, so a pre-built
|
|
422
|
+
// run reads them (and their islands/components sidecars) from
|
|
423
|
+
// `<distDir>/jinja`; dev compiles them to `cwd/.brust/jinja`.
|
|
424
|
+
const jinjaDir = prebuilt
|
|
425
|
+
? path.join(distDir!, 'jinja')
|
|
426
|
+
: path.resolve(process.cwd(), '.brust/jinja')
|
|
427
|
+
configureJinjaDir(jinjaDir)
|
|
428
|
+
loadJinjaOnce(jinjaDir)
|
|
429
|
+
if (prebuilt && existsSync(jinjaDir)) {
|
|
430
|
+
console.log(`[brust] main: using pre-built jinja at ${jinjaDir}`)
|
|
431
|
+
}
|
|
426
432
|
|
|
427
433
|
this.registerRoutes(routes)
|
|
428
434
|
const ssePaths = routes
|
|
@@ -442,7 +448,7 @@ export const brust = {
|
|
|
442
448
|
console.log(`[brust] main: registered ${wsPaths.length} ws path(s): ${wsPaths.join(', ')}`)
|
|
443
449
|
}
|
|
444
450
|
console.log(
|
|
445
|
-
`[brust] main:
|
|
451
|
+
`[brust] main: registered ${endpoints.length} action endpoint(s): ${endpoints.map((e) => `${e.method} ${e.path}`).join(', ')}`,
|
|
446
452
|
)
|
|
447
453
|
|
|
448
454
|
if (dev) {
|
|
@@ -565,11 +571,11 @@ export const brust = {
|
|
|
565
571
|
)
|
|
566
572
|
}
|
|
567
573
|
} else {
|
|
574
|
+
const actionsFile = path.join(scanRoot, 'actions.ts')
|
|
568
575
|
mcpManifest = await this.buildMcpManifest({
|
|
569
|
-
|
|
576
|
+
actionsFile: existsSync(actionsFile) ? actionsFile : undefined,
|
|
570
577
|
routesFile: path.join(scanRoot, 'routes.tsx'),
|
|
571
578
|
sourceRoots: [scanRoot],
|
|
572
|
-
actions,
|
|
573
579
|
routes,
|
|
574
580
|
})
|
|
575
581
|
console.log(
|
|
@@ -582,7 +588,8 @@ export const brust = {
|
|
|
582
588
|
port,
|
|
583
589
|
workers,
|
|
584
590
|
entry: opts.entry,
|
|
585
|
-
actions,
|
|
591
|
+
actions: opts.actions,
|
|
592
|
+
actionPrefix: opts.actionPrefix,
|
|
586
593
|
...(mcpManifest ? { mcp: { manifest: mcpManifest } } : {}),
|
|
587
594
|
...opts.serve,
|
|
588
595
|
})
|
|
@@ -603,6 +610,15 @@ export const brust = {
|
|
|
603
610
|
...opts.routes,
|
|
604
611
|
]
|
|
605
612
|
}
|
|
613
|
+
{
|
|
614
|
+
const { configureActionPrefixSnippet } = await import('./render/inject-action-prefix.ts')
|
|
615
|
+
const ap = opts.actionPrefix
|
|
616
|
+
configureActionPrefixSnippet(
|
|
617
|
+
ap && ap !== '/_brust/action'
|
|
618
|
+
? `<script>globalThis.__BRUST_ACTION_PREFIX__=${JSON.stringify(ap)}</script>`
|
|
619
|
+
: null,
|
|
620
|
+
)
|
|
621
|
+
}
|
|
606
622
|
// Worker: detect CSS the same way main did (no compile, no configureCssDir
|
|
607
623
|
// — Rust state is shared, but the per-worker renderer needs the hrefs).
|
|
608
624
|
if (prebuilt) {
|
|
@@ -644,7 +660,7 @@ export const brust = {
|
|
|
644
660
|
let mcpServer: import('./mcp/server.ts').McpServer | undefined
|
|
645
661
|
if (mcpManifest) {
|
|
646
662
|
const { makeMcpServer } = await import('./mcp/server.ts')
|
|
647
|
-
mcpServer = makeMcpServer({ manifest: mcpManifest,
|
|
663
|
+
mcpServer = makeMcpServer({ manifest: mcpManifest, endpoints, routes: workerRoutes })
|
|
648
664
|
console.log(`[brust] worker: mcp server ready (${mcpManifest.tools.length} tools)`)
|
|
649
665
|
}
|
|
650
666
|
|
|
@@ -656,7 +672,11 @@ export const brust = {
|
|
|
656
672
|
|
|
657
673
|
const { makeRenderer: make } = await import('./routes.ts')
|
|
658
674
|
let wid: number | null = null
|
|
659
|
-
const renderer = make(workerRoutes, view, {
|
|
675
|
+
const renderer = make(workerRoutes, view, {
|
|
676
|
+
actions: endpoints,
|
|
677
|
+
getWorkerId: () => wid,
|
|
678
|
+
mcp: mcpServer,
|
|
679
|
+
})
|
|
660
680
|
wid = this.registerRenderer(view, renderer)
|
|
661
681
|
}
|
|
662
682
|
},
|
|
@@ -674,9 +694,13 @@ export type {
|
|
|
674
694
|
Middleware,
|
|
675
695
|
} from './routes.ts'
|
|
676
696
|
|
|
677
|
-
export {
|
|
678
|
-
export type {
|
|
679
|
-
|
|
697
|
+
export { defineActions, isValidEndpointPath } from './define-actions.ts'
|
|
698
|
+
export type {
|
|
699
|
+
EndpointDef,
|
|
700
|
+
ActionContext,
|
|
701
|
+
EndpointOptions,
|
|
702
|
+
ActionsBuilder,
|
|
703
|
+
} from './define-actions.ts'
|
|
680
704
|
|
|
681
705
|
export { loadConfig, BrustConfigError } from './config.ts'
|
|
682
706
|
export type { BrustConfig } from './config.ts'
|
|
@@ -20,6 +20,23 @@ import path from 'node:path'
|
|
|
20
20
|
import { renderToString } from 'react-dom/server.node'
|
|
21
21
|
import { createElement } from 'react'
|
|
22
22
|
|
|
23
|
+
// Sub-project J — the directory holding the compiled `<Name>.jinja` templates
|
|
24
|
+
// and their `.islands.json` / `.components.json` sidecars. Configured ONCE at
|
|
25
|
+
// boot (`configureJinjaDir`) so it tracks the same location `loadJinjaOnce`
|
|
26
|
+
// loads from: `<BRUST_DIST_DIR>/jinja` for a pre-built run, `cwd/.brust/jinja`
|
|
27
|
+
// in dev. Falls back to the dev location when unset (tests pass an explicit dir).
|
|
28
|
+
let _configuredJinjaDir: string | null = null
|
|
29
|
+
|
|
30
|
+
/** Set the directory the sidecar-manifest readers resolve against. Called once
|
|
31
|
+
* at boot from `index.ts`, mirroring `configureIslandsDir`/`configureCssDir`. */
|
|
32
|
+
export function configureJinjaDir(dir: string): void {
|
|
33
|
+
_configuredJinjaDir = dir
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function defaultJinjaDir(): string {
|
|
37
|
+
return _configuredJinjaDir ?? path.resolve(process.cwd(), '.brust/jinja')
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
/** One entry of a `<template>.islands.json` manifest (enriched by T6). */
|
|
24
41
|
export interface NativeIslandEntry {
|
|
25
42
|
component: string
|
|
@@ -90,14 +107,14 @@ export function entityEncode(s: string): string {
|
|
|
90
107
|
const manifestCache = new Map<string, NativeIslandEntry[] | null>()
|
|
91
108
|
|
|
92
109
|
/** Read `<jinjaDir>/<templateName>.islands.json` and return the parsed entry
|
|
93
|
-
* array, or `null` if the file doesn't exist. `jinjaDir` defaults to
|
|
94
|
-
* `
|
|
95
|
-
* are cached by absolute path. */
|
|
110
|
+
* array, or `null` if the file doesn't exist. `jinjaDir` defaults to the
|
|
111
|
+
* boot-configured jinja dir (`configureJinjaDir`), or `cwd/.brust/jinja` when
|
|
112
|
+
* unset; tests pass a temp dir. Both hits and misses are cached by absolute path. */
|
|
96
113
|
export function loadIslandManifest(
|
|
97
114
|
templateName: string,
|
|
98
115
|
jinjaDir?: string,
|
|
99
116
|
): NativeIslandEntry[] | null {
|
|
100
|
-
const dir = jinjaDir ??
|
|
117
|
+
const dir = jinjaDir ?? defaultJinjaDir()
|
|
101
118
|
const abs = path.resolve(dir, `${templateName}.islands.json`)
|
|
102
119
|
if (manifestCache.has(abs)) return manifestCache.get(abs)!
|
|
103
120
|
let parsed: NativeIslandEntry[] | null
|
|
@@ -251,7 +268,7 @@ export function loadComponentManifest(
|
|
|
251
268
|
templateName: string,
|
|
252
269
|
jinjaDir?: string,
|
|
253
270
|
): NativeComponentEntry[] | null {
|
|
254
|
-
const dir = jinjaDir ??
|
|
271
|
+
const dir = jinjaDir ?? defaultJinjaDir()
|
|
255
272
|
const abs = path.resolve(dir, `${templateName}.components.json`)
|
|
256
273
|
if (componentManifestCache.has(abs)) return componentManifestCache.get(abs)!
|
|
257
274
|
let parsed: NativeComponentEntry[] | null
|
|
@@ -282,7 +299,7 @@ export async function resolveComponentContext(
|
|
|
282
299
|
const out: Record<string, string> = {}
|
|
283
300
|
if (!manifest.length) return out
|
|
284
301
|
|
|
285
|
-
const dir = jinjaDir ??
|
|
302
|
+
const dir = jinjaDir ?? defaultJinjaDir()
|
|
286
303
|
const factoryPath = path.resolve(dir, `${templateName}.factory.ts`)
|
|
287
304
|
|
|
288
305
|
let factoryMod = factoryCache.get(factoryPath)
|