brustjs 0.1.64-alpha → 0.1.66-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 +6 -0
- package/package.json +7 -7
- package/runtime/ai/actions.ts +271 -0
- package/runtime/ai/index.ts +90 -0
- package/runtime/ai/manifest.ts +68 -0
- package/runtime/ai/navigate.ts +157 -0
- package/runtime/ai/pages.ts +49 -0
- package/runtime/ai/refs.ts +87 -0
- package/runtime/ai/struct.ts +168 -0
- package/runtime/cli/build.ts +43 -4
- package/runtime/cli/dev.ts +6 -1
- package/runtime/cli/help.ts +8 -0
- package/runtime/cli/native-routes-emit.ts +33 -9
- package/runtime/config.ts +3 -0
- package/runtime/dev/coordinator.ts +70 -7
- package/runtime/dev/validate-change.ts +33 -0
- package/runtime/dev/watcher.ts +29 -16
- package/runtime/generator.ts +14 -0
- package/runtime/index.js +52 -52
- package/runtime/index.ts +90 -3
- package/runtime/islands/build.ts +42 -1
- package/runtime/md/emit.ts +5 -1
- package/runtime/render/inject-ai-client.ts +38 -0
- package/runtime/render/stream.ts +41 -4
- package/types/ai/manifest.d.ts +17 -0
- package/types/cli/native-routes-emit.d.ts +5 -1
- package/types/config.d.ts +2 -0
- package/types/dev/coordinator.d.ts +8 -1
- package/types/dev/validate-change.d.ts +1 -0
- package/types/dev/watcher.d.ts +5 -4
- package/types/generator.d.ts +5 -0
- package/types/index.d.ts +2 -0
- package/types/islands/build.d.ts +9 -0
- package/types/md/emit.d.ts +3 -0
- package/types/render/inject-ai-client.d.ts +6 -0
package/runtime/md/emit.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
14
14
|
import path from 'node:path'
|
|
15
|
-
import { insertGeneratorMeta, resolveGenerator } from '../generator.ts'
|
|
15
|
+
import { injectAiScriptIntoTemplate, insertGeneratorMeta, resolveGenerator } from '../generator.ts'
|
|
16
16
|
import {
|
|
17
17
|
bakeDirectivesIfUsed,
|
|
18
18
|
buildChainWrapperSource,
|
|
@@ -66,6 +66,9 @@ export interface MdEmitOpts {
|
|
|
66
66
|
* BRUST_DEV injection — md pages render Rust-side and never pass through the
|
|
67
67
|
* React renderer's dev-client injection). */
|
|
68
68
|
withDevClient?: boolean
|
|
69
|
+
/** Bake the AI runtime tag into document-style md templates. Fragments skip
|
|
70
|
+
* the injection entirely. */
|
|
71
|
+
aiEnabled?: boolean
|
|
69
72
|
/** What to do when a route's md file no longer exists on disk (deleted after
|
|
70
73
|
* the route table was built). emitMdTemplates serves BOTH `brust build` and
|
|
71
74
|
* the dev re-emit, and the two must diverge here:
|
|
@@ -400,6 +403,7 @@ export async function emitMdTemplates(opts: MdEmitOpts): Promise<{
|
|
|
400
403
|
}
|
|
401
404
|
final = bakeDirectivesIfUsed(final, hasDirectives)
|
|
402
405
|
if (opts.withDevClient) final = injectDevClientIntoTemplate(final)
|
|
406
|
+
if (!opts.withDevClient && opts.aiEnabled) final = injectAiScriptIntoTemplate(final)
|
|
403
407
|
writeFileSync(outPath, final)
|
|
404
408
|
}
|
|
405
409
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const ENC = new TextEncoder()
|
|
2
|
+
|
|
3
|
+
/** Splice `snippet` into `body` immediately before the first `</head>`
|
|
4
|
+
* (case-insensitive on the four ASCII letters only). Returns the original body
|
|
5
|
+
* untouched if `snippet` is null/empty or if `</head>` is absent.
|
|
6
|
+
*
|
|
7
|
+
* AI pages are document-only: fragment templates skip this injection entirely. */
|
|
8
|
+
export function injectAiClient(body: Uint8Array, snippet: string | null): Uint8Array {
|
|
9
|
+
if (!snippet) return body
|
|
10
|
+
const pos = findHeadCloseTag(body)
|
|
11
|
+
if (pos < 0) return body
|
|
12
|
+
const tagBytes = ENC.encode(snippet)
|
|
13
|
+
const out = new Uint8Array(body.length + tagBytes.length)
|
|
14
|
+
out.set(body.subarray(0, pos), 0)
|
|
15
|
+
out.set(tagBytes, pos)
|
|
16
|
+
out.set(body.subarray(pos), pos + tagBytes.length)
|
|
17
|
+
return out
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findHeadCloseTag(body: Uint8Array): number {
|
|
21
|
+
const LT = 0x3c,
|
|
22
|
+
SL = 0x2f,
|
|
23
|
+
GT = 0x3e
|
|
24
|
+
for (let i = 0, max = body.length - 6; i < max; i++) {
|
|
25
|
+
if (body[i] !== LT || body[i + 1] !== SL) continue
|
|
26
|
+
if (!isLetter(body[i + 2], 0x48)) continue
|
|
27
|
+
if (!isLetter(body[i + 3], 0x45)) continue
|
|
28
|
+
if (!isLetter(body[i + 4], 0x41)) continue
|
|
29
|
+
if (!isLetter(body[i + 5], 0x44)) continue
|
|
30
|
+
if (body[i + 6] !== GT) continue
|
|
31
|
+
return i
|
|
32
|
+
}
|
|
33
|
+
return -1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isLetter(b: number, u: number): boolean {
|
|
37
|
+
return b === u || b === (u | 0x20)
|
|
38
|
+
}
|
package/runtime/render/stream.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Writable } from 'node:stream'
|
|
|
7
7
|
import { IslandUsedContext, createIslandUsedBox } from '../islands/island.tsx'
|
|
8
8
|
import { ISLANDS_IMPORTMAP_AND_BOOTSTRAP } from '../islands/importmap.ts'
|
|
9
9
|
import { injectCssLink } from './inject-css-link.ts'
|
|
10
|
+
import { injectAiClient } from './inject-ai-client.ts'
|
|
10
11
|
import { getCssHrefs, getCssHrefsForRoute } from '../css.ts'
|
|
11
12
|
import { injectDevClient } from './inject-dev-client.ts'
|
|
12
13
|
import { injectActionPrefix, getActionPrefixSnippet } from './inject-action-prefix.ts'
|
|
@@ -14,6 +15,7 @@ import { injectBrustStore, buildStoreScripts } from './inject-store.ts'
|
|
|
14
15
|
import { getDevClientSnippet } from '../dev/inject.ts'
|
|
15
16
|
import { getGeneratorMeta, injectGeneratorMeta } from './inject-generator.ts'
|
|
16
17
|
import { injectShellMeta, shellMetaTag } from './inject-shell-meta.ts'
|
|
18
|
+
import { aiScriptTag } from '../generator.ts'
|
|
17
19
|
|
|
18
20
|
export interface RenderBranchStreamingArgs {
|
|
19
21
|
element: ReactNode
|
|
@@ -64,6 +66,7 @@ export interface RenderBranchStreamingArgs {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
const encoder = new TextEncoder()
|
|
69
|
+
const decoder = new TextDecoder()
|
|
67
70
|
|
|
68
71
|
/** JSON.stringify the per-chunk meta. Defaults match the renderToString
|
|
69
72
|
* path so single-chunk responses keep their existing wire shape. */
|
|
@@ -120,6 +123,18 @@ function concatBuffers(parts: Uint8Array[], withBootstrap: boolean): Uint8Array
|
|
|
120
123
|
return out
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
function injectOrPrependDevClient(body: Uint8Array, snippet: string | null): Uint8Array {
|
|
127
|
+
if (!snippet) return body
|
|
128
|
+
if (decoder.decode(body).toLowerCase().includes('</head>')) {
|
|
129
|
+
return injectDevClient(body, snippet)
|
|
130
|
+
}
|
|
131
|
+
const snippetBytes = encoder.encode(snippet)
|
|
132
|
+
const out = new Uint8Array(snippetBytes.length + body.length)
|
|
133
|
+
out.set(snippetBytes, 0)
|
|
134
|
+
out.set(body, snippetBytes.length)
|
|
135
|
+
return out
|
|
136
|
+
}
|
|
137
|
+
|
|
123
138
|
export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<void> {
|
|
124
139
|
const { element, view, workerId, napi, errorBoundary } = args
|
|
125
140
|
const slot = args.slot ?? 0
|
|
@@ -184,6 +199,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
|
|
|
184
199
|
body = injectGeneratorMeta(body, getGeneratorMeta())
|
|
185
200
|
body = injectShellMeta(body, args.shellId ?? '')
|
|
186
201
|
body = injectDevClient(body, getDevClientSnippet())
|
|
202
|
+
body = injectAiClient(body, process.env.BRUST_AI === '1' ? aiScriptTag() : null)
|
|
187
203
|
body = injectActionPrefix(body, getActionPrefixSnippet())
|
|
188
204
|
body = injectBrustStore(body, args.storeSnapshot ?? null)
|
|
189
205
|
const meta = makeMeta({
|
|
@@ -246,6 +262,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
|
|
|
246
262
|
.map((h) => `<link rel="stylesheet" href="${h}">`)
|
|
247
263
|
.join('')
|
|
248
264
|
const devTag = getDevClientSnippet() ?? ''
|
|
265
|
+
const aiTag = process.env.BRUST_AI === '1' ? aiScriptTag() : ''
|
|
249
266
|
const prefixTag = getActionPrefixSnippet() ?? ''
|
|
250
267
|
const storeTag = buildStoreScripts(args.storeSnapshot ?? null)
|
|
251
268
|
const genTag = getGeneratorMeta() ?? ''
|
|
@@ -253,6 +270,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
|
|
|
253
270
|
if (
|
|
254
271
|
linkTagsStr.length > 0 ||
|
|
255
272
|
devTag.length > 0 ||
|
|
273
|
+
aiTag.length > 0 ||
|
|
256
274
|
prefixTag.length > 0 ||
|
|
257
275
|
storeTag.length > 0 ||
|
|
258
276
|
genTag.length > 0 ||
|
|
@@ -261,9 +279,10 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
|
|
|
261
279
|
const prepend = encoder.encode(
|
|
262
280
|
genTag + shellTag + linkTagsStr + prefixTag + devTag + storeTag,
|
|
263
281
|
)
|
|
264
|
-
const out = new Uint8Array(
|
|
265
|
-
out.set(
|
|
266
|
-
out.set(
|
|
282
|
+
const out = new Uint8Array(prepend.length + aiTag.length + flushed.length)
|
|
283
|
+
out.set(prepend, 0)
|
|
284
|
+
out.set(encoder.encode(aiTag), prepend.length)
|
|
285
|
+
out.set(flushed, prepend.length + aiTag.length)
|
|
267
286
|
flushed = out
|
|
268
287
|
}
|
|
269
288
|
const meta = makeMeta({ status: successStatus, streaming: true, headers: extraHeaders })
|
|
@@ -298,11 +317,12 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
|
|
|
298
317
|
onShellError(err) {
|
|
299
318
|
try {
|
|
300
319
|
const html = renderToString(createElement(errorBoundary, { error: err as Error }))
|
|
320
|
+
const body = injectOrPrependDevClient(encoder.encode(html), getDevClientSnippet())
|
|
301
321
|
const meta = makeMeta({ status: 500, streaming: false })
|
|
302
322
|
mode = 'done'
|
|
303
323
|
;(async () => {
|
|
304
324
|
try {
|
|
305
|
-
const len = encodeFirstChunk(view, meta,
|
|
325
|
+
const len = encodeFirstChunk(view, meta, body)
|
|
306
326
|
await napi.renderChunkFinal(workerId, slot, len, view)
|
|
307
327
|
finalSent = true
|
|
308
328
|
resolve()
|
|
@@ -312,6 +332,23 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
|
|
|
312
332
|
})()
|
|
313
333
|
} catch (e2) {
|
|
314
334
|
console.error('[brust] errorBoundary threw during shell error:', e2)
|
|
335
|
+
const devSnippet = getDevClientSnippet()
|
|
336
|
+
if (devSnippet) {
|
|
337
|
+
const meta = makeMeta({ status: 500, streaming: false })
|
|
338
|
+
const html = `<!doctype html><html><head>${devSnippet}</head><body>Internal Server Error</body></html>`
|
|
339
|
+
mode = 'done'
|
|
340
|
+
;(async () => {
|
|
341
|
+
try {
|
|
342
|
+
const len = encodeFirstChunk(view, meta, encoder.encode(html))
|
|
343
|
+
await napi.renderChunkFinal(workerId, slot, len, view)
|
|
344
|
+
finalSent = true
|
|
345
|
+
resolve()
|
|
346
|
+
} catch (e) {
|
|
347
|
+
reject(e)
|
|
348
|
+
}
|
|
349
|
+
})()
|
|
350
|
+
return
|
|
351
|
+
}
|
|
315
352
|
const meta = makeMeta({
|
|
316
353
|
status: 500,
|
|
317
354
|
streaming: false,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FlatRoute } from '../routes.ts';
|
|
2
|
+
export interface AiPageEntry {
|
|
3
|
+
path: string;
|
|
4
|
+
params: string[];
|
|
5
|
+
catchAll: boolean;
|
|
6
|
+
kind: 'react' | 'native' | 'md';
|
|
7
|
+
shellId: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AiManifest {
|
|
12
|
+
version: 1;
|
|
13
|
+
pages: AiPageEntry[];
|
|
14
|
+
}
|
|
15
|
+
export declare function extractAiManifest(routes: FlatRoute[]): AiManifest;
|
|
16
|
+
export declare function writeManifest(cwd: string, manifest: AiManifest): Promise<void>;
|
|
17
|
+
export declare function readManifest(cwd: string): Promise<AiManifest | null>;
|
|
@@ -71,7 +71,11 @@ export declare function countMainTags(template: string): number;
|
|
|
71
71
|
*
|
|
72
72
|
* Exported for the md emit step (runtime/md/emit.ts), which bakes the same tag
|
|
73
73
|
* under its `withDevClient` option — md pages render Rust-side too, so without
|
|
74
|
-
* it they never auto-reload in dev.
|
|
74
|
+
* it they never auto-reload in dev.
|
|
75
|
+
*
|
|
76
|
+
* The AI runtime script is injected here as well when BRUST_AI=1. The tag is
|
|
77
|
+
* document-only: the compiler emits a head anchor for full documents, while
|
|
78
|
+
* fragment templates (no head) are left unchanged. */
|
|
75
79
|
export declare function injectDevClientIntoTemplate(template: string): string;
|
|
76
80
|
/** Bake the directive runtime loader into a native template iff it uses any
|
|
77
81
|
* x-data directive. Idempotent. Wrapped in {% raw %} for symmetry with the islands
|
package/types/config.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface BrustConfig {
|
|
|
10
10
|
cacheMaxEntries?: number;
|
|
11
11
|
/** L2 page-cache capacity (entries). Undefined → Rust default of 1000. */
|
|
12
12
|
cachePageMaxEntries?: number;
|
|
13
|
+
/** AI runtime toggle from BRUST_AI. Dev mode enables it separately. */
|
|
14
|
+
ai?: boolean;
|
|
13
15
|
/** R9 cross-process cache invalidation: redis/dragonfly URL. Absent →
|
|
14
16
|
* feature disabled (current single-process behavior). */
|
|
15
17
|
cacheSyncUrl?: string;
|
|
@@ -7,6 +7,8 @@ export interface CoordinatorDeps {
|
|
|
7
7
|
};
|
|
8
8
|
buildCss: () => Promise<void>;
|
|
9
9
|
buildIslands: () => Promise<void>;
|
|
10
|
+
/** Parse changed JS/TS modules before mutating the live generation. */
|
|
11
|
+
validateChanges: (paths: string[]) => Promise<void>;
|
|
10
12
|
/** Recompile native-route `.jinja` templates from source and reload them into
|
|
11
13
|
* the minijinja env, so `native: true` routes pick up .tsx edits on reload. */
|
|
12
14
|
reEmitJinja: () => Promise<void>;
|
|
@@ -24,10 +26,15 @@ export interface CoordinatorDeps {
|
|
|
24
26
|
}
|
|
25
27
|
export declare class Coordinator {
|
|
26
28
|
private deps;
|
|
27
|
-
private
|
|
29
|
+
private readonly pending;
|
|
30
|
+
private drainPromise;
|
|
28
31
|
constructor(deps: CoordinatorDeps);
|
|
29
32
|
handleChange(ev: {
|
|
30
33
|
paths: string[];
|
|
31
34
|
kind: ChangeKind;
|
|
32
35
|
}): Promise<void>;
|
|
36
|
+
private startDrain;
|
|
37
|
+
private drainPending;
|
|
38
|
+
private takeNext;
|
|
39
|
+
private runBatch;
|
|
33
40
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function validateChangedModules(paths: string[]): Promise<void>;
|
package/types/dev/watcher.d.ts
CHANGED
|
@@ -26,9 +26,10 @@ export interface CreateWatcherOptions {
|
|
|
26
26
|
export interface Watcher {
|
|
27
27
|
close(): void;
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*
|
|
29
|
+
/** Internal — exposed for deterministic callback-contract tests. */
|
|
30
|
+
export declare function _testDispatchChanges(paths: string[], opts: Pick<CreateWatcherOptions, 'root' | 'hasMdRoutes' | 'onChange'>): void;
|
|
31
|
+
/** Watch `root` recursively. Emits one `onChange` call for every distinct kind
|
|
32
|
+
* retained in a debounce window. Priority orders delivery; it never discards
|
|
33
|
+
* lower-priority kinds from a mixed window. */
|
|
33
34
|
export declare function createWatcher(opts: CreateWatcherOptions): Watcher;
|
|
34
35
|
export {};
|
package/types/generator.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export interface GeneratorStrings {
|
|
|
4
4
|
/** X-Powered-By value, e.g. `brust/0.1.48-alpha` */
|
|
5
5
|
header: string;
|
|
6
6
|
}
|
|
7
|
+
/** Full browser entry tag for the AI runtime chunk. */
|
|
8
|
+
export declare function aiScriptTag(): string;
|
|
9
|
+
/** Insert the AI runtime tag immediately before the first `</head>`.
|
|
10
|
+
* Document-only: fragment templates with no head are left unchanged. */
|
|
11
|
+
export declare function injectAiScriptIntoTemplate(template: string): string;
|
|
7
12
|
/** Build the resolved strings. Version comes from the brustjs package.json
|
|
8
13
|
* (readVersion never throws — "unknown" degrades to name-only, never a crash).
|
|
9
14
|
* The version is sanitized to attr/header-safe bytes; semver chars only. */
|
package/types/index.d.ts
CHANGED
|
@@ -183,6 +183,8 @@ export declare const brust: {
|
|
|
183
183
|
/** Optional global CORS policy — see {@link CorsOptions}. Threaded to
|
|
184
184
|
* serve() like `actionPrefix`. */
|
|
185
185
|
cors?: CorsOptions;
|
|
186
|
+
/** AI runtime toggle. Dev mode enables it automatically. */
|
|
187
|
+
ai?: boolean;
|
|
186
188
|
/** Overrides merged into the underlying `serve()` call (main thread). */
|
|
187
189
|
serve?: Partial<Omit<ServeOptions, "entry" | "actions" | "mcp">>;
|
|
188
190
|
/** Per-worker SAB size in bytes. Default 256 KB. */
|
package/types/islands/build.d.ts
CHANGED
|
@@ -18,6 +18,14 @@ export interface BuildIslandsOptions {
|
|
|
18
18
|
* on the output filename (X.module.css + X.tsx → both X.js). */
|
|
19
19
|
plugins?: BunPlugin[];
|
|
20
20
|
}
|
|
21
|
+
export interface BuildAiRuntimeOptions {
|
|
22
|
+
/** Override the output directory. Default: `<cwd>/.brust/islands`. */
|
|
23
|
+
outDir?: string;
|
|
24
|
+
/** Override the browser entry file. Default: `runtime/ai/index.ts`. */
|
|
25
|
+
entryFile?: string;
|
|
26
|
+
/** Build plugins passed straight to `Bun.build` for the AI runtime chunk. */
|
|
27
|
+
plugins?: BunPlugin[];
|
|
28
|
+
}
|
|
21
29
|
/** Scan a routes entry file for `<Island component={X} />` usage and derive the
|
|
22
30
|
* island chunk list (componentName → absolute source path). Replaces the old
|
|
23
31
|
* static config-file lookup — the chunk set is derived from source.
|
|
@@ -46,4 +54,5 @@ export declare function scanIslandChunks(routesEntryFile: string, extraIslands?:
|
|
|
46
54
|
/** Build the runtime chunks + all island chunks + bootstrap. Returns the
|
|
47
55
|
* absolute output directory; caller passes it to `brust.configureIslandsDir`. */
|
|
48
56
|
export declare function buildIslands(islands: Map<string, string>, options?: BuildIslandsOptions): Promise<IslandsBuildResult>;
|
|
57
|
+
export declare function buildAiRuntime(options?: BuildAiRuntimeOptions): Promise<string | null>;
|
|
49
58
|
export declare function buildOne(entrypoints: string[], outdir: string, naming: string, external: string[], plugins?: BunPlugin[]): Promise<void>;
|
package/types/md/emit.d.ts
CHANGED
|
@@ -27,6 +27,9 @@ export interface MdEmitOpts {
|
|
|
27
27
|
* BRUST_DEV injection — md pages render Rust-side and never pass through the
|
|
28
28
|
* React renderer's dev-client injection). */
|
|
29
29
|
withDevClient?: boolean;
|
|
30
|
+
/** Bake the AI runtime tag into document-style md templates. Fragments skip
|
|
31
|
+
* the injection entirely. */
|
|
32
|
+
aiEnabled?: boolean;
|
|
30
33
|
/** What to do when a route's md file no longer exists on disk (deleted after
|
|
31
34
|
* the route table was built). emitMdTemplates serves BOTH `brust build` and
|
|
32
35
|
* the dev re-emit, and the two must diverge here:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Splice `snippet` into `body` immediately before the first `</head>`
|
|
2
|
+
* (case-insensitive on the four ASCII letters only). Returns the original body
|
|
3
|
+
* untouched if `snippet` is null/empty or if `</head>` is absent.
|
|
4
|
+
*
|
|
5
|
+
* AI pages are document-only: fragment templates skip this injection entirely. */
|
|
6
|
+
export declare function injectAiClient(body: Uint8Array, snippet: string | null): Uint8Array;
|