brustjs 0.1.47-alpha → 0.1.49-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/package.json +7 -7
- package/runtime/cli/build.ts +21 -1
- package/runtime/cli/dev.ts +14 -2
- package/runtime/cli/help.ts +8 -0
- package/runtime/cli/native-routes-emit.ts +9 -1
- package/runtime/generator.ts +76 -0
- package/runtime/index.d.ts +6 -0
- package/runtime/index.js +52 -52
- package/runtime/index.ts +27 -0
- package/runtime/islands/bootstrap.ts +37 -22
- package/runtime/islands/view-transition.ts +50 -0
- package/runtime/md/emit.ts +11 -1
- package/runtime/render/inject-generator.ts +71 -0
- package/runtime/render/stream.ts +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brustjs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.49-alpha",
|
|
4
4
|
"description": "Bun + Rust SSR framework — React on the server, Rust everywhere else (napi cdylib + per-worker SharedArrayBuffer).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"typescript": "^6.0.3"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"brustjs-darwin-x64": "0.1.
|
|
45
|
-
"brustjs-darwin-arm64": "0.1.
|
|
46
|
-
"brustjs-linux-x64-gnu": "0.1.
|
|
47
|
-
"brustjs-linux-arm64-gnu": "0.1.
|
|
48
|
-
"brustjs-linux-x64-musl": "0.1.
|
|
49
|
-
"brustjs-linux-arm64-musl": "0.1.
|
|
44
|
+
"brustjs-darwin-x64": "0.1.49-alpha",
|
|
45
|
+
"brustjs-darwin-arm64": "0.1.49-alpha",
|
|
46
|
+
"brustjs-linux-x64-gnu": "0.1.49-alpha",
|
|
47
|
+
"brustjs-linux-arm64-gnu": "0.1.49-alpha",
|
|
48
|
+
"brustjs-linux-x64-musl": "0.1.49-alpha",
|
|
49
|
+
"brustjs-linux-arm64-musl": "0.1.49-alpha"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": "^19.2.6",
|
package/runtime/cli/build.ts
CHANGED
|
@@ -145,6 +145,7 @@ export interface ParsedArgs {
|
|
|
145
145
|
target: string // --target value (default 'auto')
|
|
146
146
|
ssg: boolean // --ssg — prerender static routes after the build
|
|
147
147
|
ssgOut: string | null // --ssg-out value (absolute); null → <outDir>/static computed later
|
|
148
|
+
generatorVersion: boolean // false ⇔ --no-generator-version (name-only generator tag)
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
/** Parse `brust build` argv. Pure (no fs access, no process.exit) so it's
|
|
@@ -155,6 +156,7 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
155
156
|
let target = 'auto'
|
|
156
157
|
let ssg = false
|
|
157
158
|
let ssgOut: string | undefined
|
|
159
|
+
let generatorVersion = true
|
|
158
160
|
|
|
159
161
|
for (let i = 0; i < args.length; i++) {
|
|
160
162
|
const a = args[i]
|
|
@@ -177,6 +179,8 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
177
179
|
}
|
|
178
180
|
} else if (a === '--ssg') {
|
|
179
181
|
ssg = true
|
|
182
|
+
} else if (a === '--no-generator-version') {
|
|
183
|
+
generatorVersion = false
|
|
180
184
|
} else if (a === '--ssg-out') {
|
|
181
185
|
ssgOut = args[++i]
|
|
182
186
|
if (!ssgOut) {
|
|
@@ -214,7 +218,7 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
214
218
|
}
|
|
215
219
|
const ssgOutPath = ssgOut ? (isAbsolute(ssgOut) ? ssgOut : resolve(cwd, ssgOut)) : null
|
|
216
220
|
|
|
217
|
-
return { entry: entryPath, outDir: outPath, target, ssg, ssgOut: ssgOutPath }
|
|
221
|
+
return { entry: entryPath, outDir: outPath, target, ssg, ssgOut: ssgOutPath, generatorVersion }
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
export async function runBuild(args: string[]): Promise<void> {
|
|
@@ -322,6 +326,22 @@ export async function runBuild(args: string[]): Promise<void> {
|
|
|
322
326
|
// the same dual-emit the jinja mirror below does). Strict no-op without md
|
|
323
327
|
// routes: no files, no dirs, byte-identical dist.
|
|
324
328
|
const jinjaDir = path.join(outDir, 'jinja')
|
|
329
|
+
|
|
330
|
+
// Generator tag decision — baked at build time into BOTH jinja dirs so every
|
|
331
|
+
// runtime path (prebuilt dist reads <distDir>/jinja; dev/source reads
|
|
332
|
+
// .brust/jinja) picks up the same artifact. Runs unconditionally: even a
|
|
333
|
+
// React-only app with zero native/md routes needs the artifact so the React
|
|
334
|
+
// stream injector and X-Powered-By thread share the same decision.
|
|
335
|
+
// The .brust/jinja write is defense-in-depth: the 4.1 mirror cp below
|
|
336
|
+
// overwrites it with identical content, but this write must NOT be removed —
|
|
337
|
+
// it keeps the artifact correct even if that cp ever becomes conditional.
|
|
338
|
+
{
|
|
339
|
+
const { generatorStrings, writeGeneratorArtifact } = await import('../generator.ts')
|
|
340
|
+
const gen = generatorStrings(parsed.generatorVersion)
|
|
341
|
+
writeGeneratorArtifact(jinjaDir, gen)
|
|
342
|
+
writeGeneratorArtifact(path.join(process.cwd(), '.brust', 'jinja'), gen)
|
|
343
|
+
}
|
|
344
|
+
|
|
325
345
|
let mdIslands = new Map<string, string>()
|
|
326
346
|
if (existsSync(routesFile) && loadedRoutes !== undefined) {
|
|
327
347
|
const { emitMdArtifacts } = await import('../md/emit.ts')
|
package/runtime/cli/dev.ts
CHANGED
|
@@ -10,11 +10,13 @@ const REPO_ROOT = path.resolve(import.meta.dir, '..', '..')
|
|
|
10
10
|
interface ParsedArgs {
|
|
11
11
|
entry: string
|
|
12
12
|
port: number | undefined
|
|
13
|
+
generatorVersion: boolean
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
function parseArgs(args: string[]): ParsedArgs {
|
|
16
17
|
let entry: string | undefined
|
|
17
18
|
let port: number | undefined
|
|
19
|
+
let generatorVersion = true
|
|
18
20
|
for (let i = 0; i < args.length; i++) {
|
|
19
21
|
const a = args[i]
|
|
20
22
|
if (a === '--port') {
|
|
@@ -28,6 +30,8 @@ function parseArgs(args: string[]): ParsedArgs {
|
|
|
28
30
|
console.error(`brust dev: invalid port ${v}`)
|
|
29
31
|
process.exit(1)
|
|
30
32
|
}
|
|
33
|
+
} else if (a === '--no-generator-version') {
|
|
34
|
+
generatorVersion = false
|
|
31
35
|
} else if (a.startsWith('--port=')) {
|
|
32
36
|
port = parseInt(a.slice('--port='.length), 10)
|
|
33
37
|
} else if (a.startsWith('-')) {
|
|
@@ -50,14 +54,22 @@ function parseArgs(args: string[]): ParsedArgs {
|
|
|
50
54
|
console.error(`brust dev: no entry file at ${entryPath}; pass a path or create ./index.ts`)
|
|
51
55
|
process.exit(1)
|
|
52
56
|
}
|
|
53
|
-
return { entry: entryPath, port }
|
|
57
|
+
return { entry: entryPath, port, generatorVersion }
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
export async function runDev(args: string[]): Promise<void> {
|
|
57
|
-
const { entry, port } = parseArgs(args)
|
|
61
|
+
const { entry, port, generatorVersion } = parseArgs(args)
|
|
58
62
|
process.env.BRUST_DEV = '1'
|
|
59
63
|
if (port !== undefined) process.env.BRUST_PORT = String(port)
|
|
60
64
|
|
|
65
|
+
// Bake the generator decision BEFORE the first emit — emitters and the boot
|
|
66
|
+
// re-emit paths all resolve <cwd>/.brust/jinja/generator.json internally.
|
|
67
|
+
const { generatorStrings, writeGeneratorArtifact } = await import('../generator.ts')
|
|
68
|
+
writeGeneratorArtifact(
|
|
69
|
+
path.join(process.cwd(), '.brust', 'jinja'),
|
|
70
|
+
generatorStrings(generatorVersion),
|
|
71
|
+
)
|
|
72
|
+
|
|
61
73
|
// Sub-project J — emit .brust/jinja/<Name>.jinja templates BEFORE handing
|
|
62
74
|
// off to the user's entry. The runtime loads these on boot. Dev-mode HMR
|
|
63
75
|
// on .tsx edit is deferred per spec S12 (restart-to-reload for v2).
|
package/runtime/cli/help.ts
CHANGED
|
@@ -61,6 +61,10 @@ export const COMMANDS: CommandDef[] = [
|
|
|
61
61
|
flag: '--ssg-out <dir>',
|
|
62
62
|
desc: 'Output directory for prerendered HTML (default <out-dir>/static)',
|
|
63
63
|
},
|
|
64
|
+
{
|
|
65
|
+
flag: '--no-generator-version',
|
|
66
|
+
desc: 'Drop the version from the generator meta tag + X-Powered-By header (the name stays)',
|
|
67
|
+
},
|
|
64
68
|
],
|
|
65
69
|
notes: [
|
|
66
70
|
'Markdown pages: routes mounted with mdRoutes(<contentDir>) compile to native',
|
|
@@ -75,6 +79,10 @@ export const COMMANDS: CommandDef[] = [
|
|
|
75
79
|
flags: [
|
|
76
80
|
{ flag: '[entry]', desc: 'Entry file (default ./index.ts)' },
|
|
77
81
|
{ flag: '--port <n>', desc: 'Port to listen on' },
|
|
82
|
+
{
|
|
83
|
+
flag: '--no-generator-version',
|
|
84
|
+
desc: 'Drop the version from the generator meta tag + X-Powered-By header (the name stays)',
|
|
85
|
+
},
|
|
78
86
|
],
|
|
79
87
|
},
|
|
80
88
|
{
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node
|
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import { dirname, relative, resolve } from 'node:path'
|
|
4
4
|
import { buildDevClientTag } from '../dev/client.ts'
|
|
5
|
+
import { insertGeneratorMeta, resolveGenerator } from '../generator.ts'
|
|
5
6
|
import { islandChunkBasename } from '../islands/chunk-id.ts'
|
|
6
7
|
import { DIRECTIVES_BOOTSTRAP, ISLANDS_IMPORTMAP_AND_BOOTSTRAP } from '../islands/importmap.ts'
|
|
7
8
|
|
|
@@ -527,6 +528,12 @@ export async function emitNativeTemplates(opts: NativeRouteEmitOpts): Promise<vo
|
|
|
527
528
|
nativeRoutes.length > 0 &&
|
|
528
529
|
(await import('../native/build.ts')).scanDirectiveComponents(opts.entryFile).size > 0
|
|
529
530
|
|
|
531
|
+
// Generator meta: resolved INTERNALLY from the out dir's artifact (NOT a
|
|
532
|
+
// caller param) — emit re-runs from five call sites (build, dev, boot
|
|
533
|
+
// staleness, md boot re-emit, dev HMR) and a param would silently drop the
|
|
534
|
+
// tag on re-emit. Fallback (no artifact) = version-on defaults.
|
|
535
|
+
const generatorMeta = resolveGenerator(opts.outDir).meta
|
|
536
|
+
|
|
530
537
|
const built: string[] = []
|
|
531
538
|
for (const r of nativeRoutes) {
|
|
532
539
|
const name = r.nativeTemplate!
|
|
@@ -632,8 +639,9 @@ export async function emitNativeTemplates(opts: NativeRouteEmitOpts): Promise<vo
|
|
|
632
639
|
// injection, so splice the /_brust/dev WS script in here. reEmitJinja() runs
|
|
633
640
|
// this on every hot reload, so the script is always present in dev.
|
|
634
641
|
const withDirectives = bakeDirectivesIfUsed(compiled.template, hasDirectives)
|
|
642
|
+
const withGenerator = insertGeneratorMeta(withDirectives, generatorMeta)
|
|
635
643
|
const template =
|
|
636
|
-
process.env.BRUST_DEV === '1' ? injectDevClientIntoTemplate(
|
|
644
|
+
process.env.BRUST_DEV === '1' ? injectDevClientIntoTemplate(withGenerator) : withGenerator
|
|
637
645
|
writeFileSync(outPath, template)
|
|
638
646
|
built.push(name)
|
|
639
647
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Generator-tag decision module. ONE resolved decision { meta, header } made at
|
|
2
|
+
// build time (brust build / brust dev write generator.json into every jinja out
|
|
3
|
+
// dir); consumed by the jinja emitters (bake), the React stream injector, and
|
|
4
|
+
// the X-Powered-By napi thread. The name "brust" is mandatory; only the version
|
|
5
|
+
// substring is optional (--no-generator-version). Spec:
|
|
6
|
+
// docs/superpowers/specs/2026-06-12-generator-tag-design.md
|
|
7
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { readVersion } from './cli/help.ts'
|
|
10
|
+
|
|
11
|
+
export interface GeneratorStrings {
|
|
12
|
+
/** Full meta tag, e.g. `<meta name="generator" content="brust 0.1.48-alpha"/>` */
|
|
13
|
+
meta: string
|
|
14
|
+
/** X-Powered-By value, e.g. `brust/0.1.48-alpha` */
|
|
15
|
+
header: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Build the resolved strings. Version comes from the brustjs package.json
|
|
19
|
+
* (readVersion never throws — "unknown" degrades to name-only, never a crash).
|
|
20
|
+
* The version is sanitized to attr/header-safe bytes; semver chars only. */
|
|
21
|
+
export function generatorStrings(versionOn: boolean): GeneratorStrings {
|
|
22
|
+
const raw = readVersion()
|
|
23
|
+
const v = raw === 'unknown' ? '' : raw.replace(/[^0-9A-Za-z.+-]/g, '')
|
|
24
|
+
const withVersion = versionOn && v.length > 0
|
|
25
|
+
return {
|
|
26
|
+
meta: `<meta name="generator" content="brust${withVersion ? ` ${v}` : ''}"/>`,
|
|
27
|
+
header: withVersion ? `brust/${v}` : 'brust',
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The exact head literal the Rust compiler emits for every Document template
|
|
32
|
+
* (crates/jsx-rust-compiler/src/emit_jinja.rs:110). Compiler-owned and stable. */
|
|
33
|
+
const VIEWPORT_ANCHOR = '<meta name="viewport" content="width=device-width, initial-scale=1"/>'
|
|
34
|
+
|
|
35
|
+
/** Insert the generator meta immediately after the compiler-emitted viewport
|
|
36
|
+
* meta. Anchor missing (non-document template) → no-op, never an error.
|
|
37
|
+
* CALLER CONTRACT: pass fresh compiler output — this function does not check
|
|
38
|
+
* for an existing tag, so calling it twice on the same string duplicates.
|
|
39
|
+
* Every emit path recompiles from source each run, which keeps this safe. */
|
|
40
|
+
export function insertGeneratorMeta(jinja: string, metaTag: string): string {
|
|
41
|
+
const at = jinja.indexOf(VIEWPORT_ANCHOR)
|
|
42
|
+
if (at === -1) return jinja
|
|
43
|
+
const end = at + VIEWPORT_ANCHOR.length
|
|
44
|
+
return jinja.slice(0, end) + metaTag + jinja.slice(end)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Write the decision artifact into `dir` (a jinja out dir), creating it. */
|
|
48
|
+
export function writeGeneratorArtifact(dir: string, strings: GeneratorStrings): void {
|
|
49
|
+
mkdirSync(dir, { recursive: true })
|
|
50
|
+
writeFileSync(path.join(dir, 'generator.json'), JSON.stringify(strings))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Read the artifact; null on missing/malformed (caller decides the fallback). */
|
|
54
|
+
export function readGeneratorArtifact(dir: string): GeneratorStrings | null {
|
|
55
|
+
try {
|
|
56
|
+
const raw = readFileSync(path.join(dir, 'generator.json'), 'utf8')
|
|
57
|
+
// unknown + explicit narrowing (not a cast): a JSON `null`/non-object body
|
|
58
|
+
// must reach the `return null` below by DESIGN, not by riding the catch.
|
|
59
|
+
const p: unknown = JSON.parse(raw)
|
|
60
|
+
if (typeof p === 'object' && p !== null) {
|
|
61
|
+
const { meta, header } = p as Record<string, unknown>
|
|
62
|
+
if (typeof meta === 'string' && typeof header === 'string') {
|
|
63
|
+
return { meta, header }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null
|
|
67
|
+
} catch {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Artifact if present, else version-on defaults — the spec's fallback policy
|
|
73
|
+
* (an old dist with no artifact behaves as default = version on). */
|
|
74
|
+
export function resolveGenerator(dir: string): GeneratorStrings {
|
|
75
|
+
return readGeneratorArtifact(dir) ?? generatorStrings(true)
|
|
76
|
+
}
|
package/runtime/index.d.ts
CHANGED
|
@@ -279,6 +279,12 @@ export interface ServeOptions {
|
|
|
279
279
|
tuning?: ServeTuning
|
|
280
280
|
/** Optional action prefix override. Defaults to `/_brust/action`. */
|
|
281
281
|
actionPrefix?: string
|
|
282
|
+
/**
|
|
283
|
+
* `X-Powered-By` header value (e.g. `brust/0.1.48-alpha`). Single-line
|
|
284
|
+
* ASCII; omit to skip the header. The TS runtime always passes it (name
|
|
285
|
+
* mandatory, version per the build's generator.json).
|
|
286
|
+
*/
|
|
287
|
+
generator?: string
|
|
282
288
|
/**
|
|
283
289
|
* Optional in-process TLS: PEM certificate (chain) path. When BOTH this and
|
|
284
290
|
* `tls_key_path` are present, the server terminates TLS itself (ALPN
|
package/runtime/index.js
CHANGED
|
@@ -77,8 +77,8 @@ function requireNative() {
|
|
|
77
77
|
try {
|
|
78
78
|
const binding = require('brustjs-android-arm64')
|
|
79
79
|
const bindingPackageVersion = require('brustjs-android-arm64/package.json').version
|
|
80
|
-
if (bindingPackageVersion !== '0.1.
|
|
81
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
80
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
81
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
82
82
|
}
|
|
83
83
|
return binding
|
|
84
84
|
} catch (e) {
|
|
@@ -93,8 +93,8 @@ function requireNative() {
|
|
|
93
93
|
try {
|
|
94
94
|
const binding = require('brustjs-android-arm-eabi')
|
|
95
95
|
const bindingPackageVersion = require('brustjs-android-arm-eabi/package.json').version
|
|
96
|
-
if (bindingPackageVersion !== '0.1.
|
|
97
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
96
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
97
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
98
98
|
}
|
|
99
99
|
return binding
|
|
100
100
|
} catch (e) {
|
|
@@ -114,8 +114,8 @@ function requireNative() {
|
|
|
114
114
|
try {
|
|
115
115
|
const binding = require('brustjs-win32-x64-gnu')
|
|
116
116
|
const bindingPackageVersion = require('brustjs-win32-x64-gnu/package.json').version
|
|
117
|
-
if (bindingPackageVersion !== '0.1.
|
|
118
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
117
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
118
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
119
119
|
}
|
|
120
120
|
return binding
|
|
121
121
|
} catch (e) {
|
|
@@ -130,8 +130,8 @@ function requireNative() {
|
|
|
130
130
|
try {
|
|
131
131
|
const binding = require('brustjs-win32-x64-msvc')
|
|
132
132
|
const bindingPackageVersion = require('brustjs-win32-x64-msvc/package.json').version
|
|
133
|
-
if (bindingPackageVersion !== '0.1.
|
|
134
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
133
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
134
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
135
135
|
}
|
|
136
136
|
return binding
|
|
137
137
|
} catch (e) {
|
|
@@ -147,8 +147,8 @@ function requireNative() {
|
|
|
147
147
|
try {
|
|
148
148
|
const binding = require('brustjs-win32-ia32-msvc')
|
|
149
149
|
const bindingPackageVersion = require('brustjs-win32-ia32-msvc/package.json').version
|
|
150
|
-
if (bindingPackageVersion !== '0.1.
|
|
151
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
150
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
151
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
152
152
|
}
|
|
153
153
|
return binding
|
|
154
154
|
} catch (e) {
|
|
@@ -163,8 +163,8 @@ function requireNative() {
|
|
|
163
163
|
try {
|
|
164
164
|
const binding = require('brustjs-win32-arm64-msvc')
|
|
165
165
|
const bindingPackageVersion = require('brustjs-win32-arm64-msvc/package.json').version
|
|
166
|
-
if (bindingPackageVersion !== '0.1.
|
|
167
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
166
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
167
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
168
168
|
}
|
|
169
169
|
return binding
|
|
170
170
|
} catch (e) {
|
|
@@ -182,8 +182,8 @@ function requireNative() {
|
|
|
182
182
|
try {
|
|
183
183
|
const binding = require('brustjs-darwin-universal')
|
|
184
184
|
const bindingPackageVersion = require('brustjs-darwin-universal/package.json').version
|
|
185
|
-
if (bindingPackageVersion !== '0.1.
|
|
186
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
185
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
186
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
187
187
|
}
|
|
188
188
|
return binding
|
|
189
189
|
} catch (e) {
|
|
@@ -198,8 +198,8 @@ function requireNative() {
|
|
|
198
198
|
try {
|
|
199
199
|
const binding = require('brustjs-darwin-x64')
|
|
200
200
|
const bindingPackageVersion = require('brustjs-darwin-x64/package.json').version
|
|
201
|
-
if (bindingPackageVersion !== '0.1.
|
|
202
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
201
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
202
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
203
203
|
}
|
|
204
204
|
return binding
|
|
205
205
|
} catch (e) {
|
|
@@ -214,8 +214,8 @@ function requireNative() {
|
|
|
214
214
|
try {
|
|
215
215
|
const binding = require('brustjs-darwin-arm64')
|
|
216
216
|
const bindingPackageVersion = require('brustjs-darwin-arm64/package.json').version
|
|
217
|
-
if (bindingPackageVersion !== '0.1.
|
|
218
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
217
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
218
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
219
219
|
}
|
|
220
220
|
return binding
|
|
221
221
|
} catch (e) {
|
|
@@ -234,8 +234,8 @@ function requireNative() {
|
|
|
234
234
|
try {
|
|
235
235
|
const binding = require('brustjs-freebsd-x64')
|
|
236
236
|
const bindingPackageVersion = require('brustjs-freebsd-x64/package.json').version
|
|
237
|
-
if (bindingPackageVersion !== '0.1.
|
|
238
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
237
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
238
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
239
239
|
}
|
|
240
240
|
return binding
|
|
241
241
|
} catch (e) {
|
|
@@ -250,8 +250,8 @@ function requireNative() {
|
|
|
250
250
|
try {
|
|
251
251
|
const binding = require('brustjs-freebsd-arm64')
|
|
252
252
|
const bindingPackageVersion = require('brustjs-freebsd-arm64/package.json').version
|
|
253
|
-
if (bindingPackageVersion !== '0.1.
|
|
254
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
253
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
254
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
255
255
|
}
|
|
256
256
|
return binding
|
|
257
257
|
} catch (e) {
|
|
@@ -271,8 +271,8 @@ function requireNative() {
|
|
|
271
271
|
try {
|
|
272
272
|
const binding = require('brustjs-linux-x64-musl')
|
|
273
273
|
const bindingPackageVersion = require('brustjs-linux-x64-musl/package.json').version
|
|
274
|
-
if (bindingPackageVersion !== '0.1.
|
|
275
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
274
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
275
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
276
276
|
}
|
|
277
277
|
return binding
|
|
278
278
|
} catch (e) {
|
|
@@ -287,8 +287,8 @@ function requireNative() {
|
|
|
287
287
|
try {
|
|
288
288
|
const binding = require('brustjs-linux-x64-gnu')
|
|
289
289
|
const bindingPackageVersion = require('brustjs-linux-x64-gnu/package.json').version
|
|
290
|
-
if (bindingPackageVersion !== '0.1.
|
|
291
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
290
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
291
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
292
292
|
}
|
|
293
293
|
return binding
|
|
294
294
|
} catch (e) {
|
|
@@ -305,8 +305,8 @@ function requireNative() {
|
|
|
305
305
|
try {
|
|
306
306
|
const binding = require('brustjs-linux-arm64-musl')
|
|
307
307
|
const bindingPackageVersion = require('brustjs-linux-arm64-musl/package.json').version
|
|
308
|
-
if (bindingPackageVersion !== '0.1.
|
|
309
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
308
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
309
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
310
310
|
}
|
|
311
311
|
return binding
|
|
312
312
|
} catch (e) {
|
|
@@ -321,8 +321,8 @@ function requireNative() {
|
|
|
321
321
|
try {
|
|
322
322
|
const binding = require('brustjs-linux-arm64-gnu')
|
|
323
323
|
const bindingPackageVersion = require('brustjs-linux-arm64-gnu/package.json').version
|
|
324
|
-
if (bindingPackageVersion !== '0.1.
|
|
325
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
324
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
325
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
326
326
|
}
|
|
327
327
|
return binding
|
|
328
328
|
} catch (e) {
|
|
@@ -339,8 +339,8 @@ function requireNative() {
|
|
|
339
339
|
try {
|
|
340
340
|
const binding = require('brustjs-linux-arm-musleabihf')
|
|
341
341
|
const bindingPackageVersion = require('brustjs-linux-arm-musleabihf/package.json').version
|
|
342
|
-
if (bindingPackageVersion !== '0.1.
|
|
343
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
342
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
343
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
344
344
|
}
|
|
345
345
|
return binding
|
|
346
346
|
} catch (e) {
|
|
@@ -355,8 +355,8 @@ function requireNative() {
|
|
|
355
355
|
try {
|
|
356
356
|
const binding = require('brustjs-linux-arm-gnueabihf')
|
|
357
357
|
const bindingPackageVersion = require('brustjs-linux-arm-gnueabihf/package.json').version
|
|
358
|
-
if (bindingPackageVersion !== '0.1.
|
|
359
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
358
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
359
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
360
360
|
}
|
|
361
361
|
return binding
|
|
362
362
|
} catch (e) {
|
|
@@ -373,8 +373,8 @@ function requireNative() {
|
|
|
373
373
|
try {
|
|
374
374
|
const binding = require('brustjs-linux-loong64-musl')
|
|
375
375
|
const bindingPackageVersion = require('brustjs-linux-loong64-musl/package.json').version
|
|
376
|
-
if (bindingPackageVersion !== '0.1.
|
|
377
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
376
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
377
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
378
378
|
}
|
|
379
379
|
return binding
|
|
380
380
|
} catch (e) {
|
|
@@ -389,8 +389,8 @@ function requireNative() {
|
|
|
389
389
|
try {
|
|
390
390
|
const binding = require('brustjs-linux-loong64-gnu')
|
|
391
391
|
const bindingPackageVersion = require('brustjs-linux-loong64-gnu/package.json').version
|
|
392
|
-
if (bindingPackageVersion !== '0.1.
|
|
393
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
392
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
393
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
394
394
|
}
|
|
395
395
|
return binding
|
|
396
396
|
} catch (e) {
|
|
@@ -407,8 +407,8 @@ function requireNative() {
|
|
|
407
407
|
try {
|
|
408
408
|
const binding = require('brustjs-linux-riscv64-musl')
|
|
409
409
|
const bindingPackageVersion = require('brustjs-linux-riscv64-musl/package.json').version
|
|
410
|
-
if (bindingPackageVersion !== '0.1.
|
|
411
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
410
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
411
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
412
412
|
}
|
|
413
413
|
return binding
|
|
414
414
|
} catch (e) {
|
|
@@ -423,8 +423,8 @@ function requireNative() {
|
|
|
423
423
|
try {
|
|
424
424
|
const binding = require('brustjs-linux-riscv64-gnu')
|
|
425
425
|
const bindingPackageVersion = require('brustjs-linux-riscv64-gnu/package.json').version
|
|
426
|
-
if (bindingPackageVersion !== '0.1.
|
|
427
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
426
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
427
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
428
428
|
}
|
|
429
429
|
return binding
|
|
430
430
|
} catch (e) {
|
|
@@ -440,8 +440,8 @@ function requireNative() {
|
|
|
440
440
|
try {
|
|
441
441
|
const binding = require('brustjs-linux-ppc64-gnu')
|
|
442
442
|
const bindingPackageVersion = require('brustjs-linux-ppc64-gnu/package.json').version
|
|
443
|
-
if (bindingPackageVersion !== '0.1.
|
|
444
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
443
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
444
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
445
445
|
}
|
|
446
446
|
return binding
|
|
447
447
|
} catch (e) {
|
|
@@ -456,8 +456,8 @@ function requireNative() {
|
|
|
456
456
|
try {
|
|
457
457
|
const binding = require('brustjs-linux-s390x-gnu')
|
|
458
458
|
const bindingPackageVersion = require('brustjs-linux-s390x-gnu/package.json').version
|
|
459
|
-
if (bindingPackageVersion !== '0.1.
|
|
460
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
459
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
460
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
461
461
|
}
|
|
462
462
|
return binding
|
|
463
463
|
} catch (e) {
|
|
@@ -476,8 +476,8 @@ function requireNative() {
|
|
|
476
476
|
try {
|
|
477
477
|
const binding = require('brustjs-openharmony-arm64')
|
|
478
478
|
const bindingPackageVersion = require('brustjs-openharmony-arm64/package.json').version
|
|
479
|
-
if (bindingPackageVersion !== '0.1.
|
|
480
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
479
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
480
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
481
481
|
}
|
|
482
482
|
return binding
|
|
483
483
|
} catch (e) {
|
|
@@ -492,8 +492,8 @@ function requireNative() {
|
|
|
492
492
|
try {
|
|
493
493
|
const binding = require('brustjs-openharmony-x64')
|
|
494
494
|
const bindingPackageVersion = require('brustjs-openharmony-x64/package.json').version
|
|
495
|
-
if (bindingPackageVersion !== '0.1.
|
|
496
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
495
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
496
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
497
497
|
}
|
|
498
498
|
return binding
|
|
499
499
|
} catch (e) {
|
|
@@ -508,8 +508,8 @@ function requireNative() {
|
|
|
508
508
|
try {
|
|
509
509
|
const binding = require('brustjs-openharmony-arm')
|
|
510
510
|
const bindingPackageVersion = require('brustjs-openharmony-arm/package.json').version
|
|
511
|
-
if (bindingPackageVersion !== '0.1.
|
|
512
|
-
throw new Error(`Native binding package version mismatch, expected 0.1.
|
|
511
|
+
if (bindingPackageVersion !== '0.1.49-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
512
|
+
throw new Error(`Native binding package version mismatch, expected 0.1.49-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
513
513
|
}
|
|
514
514
|
return binding
|
|
515
515
|
} catch (e) {
|
package/runtime/index.ts
CHANGED
|
@@ -115,6 +115,13 @@ function loadJinjaOnce(dir: string): void {
|
|
|
115
115
|
_jinjaLoaded = true
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// Resolved generator strings, set by the main-isolate view boot (which runs
|
|
119
|
+
// before serve() binds the listener). serve() falls back to resolving the
|
|
120
|
+
// artifact itself when boot hasn't stashed one (defensive — ordering holds in
|
|
121
|
+
// every real entry: view registration precedes serve).
|
|
122
|
+
// Main-isolate-only: the worker path resolves the artifact itself.
|
|
123
|
+
let resolvedGeneratorStrings: import('./generator.ts').GeneratorStrings | null = null
|
|
124
|
+
|
|
118
125
|
function registerActionsInternal(endpoints: Array<{ method: string; path: string }>): number {
|
|
119
126
|
return (native as any).registerActions(endpoints.map((e) => ({ method: e.method, path: e.path })))
|
|
120
127
|
}
|
|
@@ -146,6 +153,10 @@ export const brust = {
|
|
|
146
153
|
// registration index; the worker dispatches on that same index string.
|
|
147
154
|
registerActionsInternal(opts.actions.endpoints)
|
|
148
155
|
}
|
|
156
|
+
const { resolveGenerator } = await import('./generator.ts')
|
|
157
|
+
const path = await import('node:path')
|
|
158
|
+
const gen =
|
|
159
|
+
resolvedGeneratorStrings ?? resolveGenerator(path.resolve(process.cwd(), '.brust/jinja'))
|
|
149
160
|
;(native as any).beginServe({
|
|
150
161
|
host: opts.host,
|
|
151
162
|
port: opts.port,
|
|
@@ -156,6 +167,10 @@ export const brust = {
|
|
|
156
167
|
// `actionPrefix` (not snake_case). A snake_case key is silently dropped,
|
|
157
168
|
// leaving the prefix at its default — which broke custom-prefix routing.
|
|
158
169
|
actionPrefix: opts.actionPrefix,
|
|
170
|
+
// X-Powered-By value from the build's generator.json (stashed by view
|
|
171
|
+
// boot; artifact fallback covers any ordering edge). Single word — no
|
|
172
|
+
// napi case-mapping trap possible.
|
|
173
|
+
generator: gen.header,
|
|
159
174
|
})
|
|
160
175
|
// Render slots per worker. Propagated to each worker via env (Bun Workers
|
|
161
176
|
// share the OS process, so the worker reads it from process.env at
|
|
@@ -632,6 +647,13 @@ export const brust = {
|
|
|
632
647
|
}
|
|
633
648
|
|
|
634
649
|
configureJinjaDir(jinjaDir)
|
|
650
|
+
{
|
|
651
|
+
const { resolveGenerator } = await import('./generator.ts')
|
|
652
|
+
const gen = resolveGenerator(jinjaDir)
|
|
653
|
+
resolvedGeneratorStrings = gen
|
|
654
|
+
const { configureGeneratorMeta } = await import('./render/inject-generator.ts')
|
|
655
|
+
configureGeneratorMeta(gen.meta)
|
|
656
|
+
}
|
|
635
657
|
loadJinjaOnce(jinjaDir)
|
|
636
658
|
if (prebuilt && existsSync(jinjaDir)) {
|
|
637
659
|
console.log(`[brust] main: using pre-built jinja at ${jinjaDir}`)
|
|
@@ -971,6 +993,11 @@ export const brust = {
|
|
|
971
993
|
? path.join(distDir!, 'jinja')
|
|
972
994
|
: path.resolve(process.cwd(), '.brust/jinja')
|
|
973
995
|
configureJinjaDir(workerJinjaDir)
|
|
996
|
+
{
|
|
997
|
+
const { resolveGenerator } = await import('./generator.ts')
|
|
998
|
+
const { configureGeneratorMeta } = await import('./render/inject-generator.ts')
|
|
999
|
+
configureGeneratorMeta(resolveGenerator(workerJinjaDir).meta)
|
|
1000
|
+
}
|
|
974
1001
|
|
|
975
1002
|
const { makeRenderer: make } = await import('./routes.ts')
|
|
976
1003
|
let wid: number | null = null
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
takeover,
|
|
39
39
|
unmountFallbackRootsIn,
|
|
40
40
|
} from './fallback.ts'
|
|
41
|
+
import { withViewTransition } from './view-transition.ts'
|
|
41
42
|
|
|
42
43
|
// Track React roots created by hydrateOne so we can unmount them before
|
|
43
44
|
// removing their DOM in swapMainContent. Without this, removing the DOM
|
|
@@ -321,16 +322,21 @@ async function attemptClientFallback(
|
|
|
321
322
|
if (!main) return false
|
|
322
323
|
// From here down: the SAME post-swap sequence navigate()'s normal path runs.
|
|
323
324
|
scrollPositions.set(currentPageKey, window.scrollY)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
325
|
+
await withViewTransition(document, () => {
|
|
326
|
+
unmountIslandsIn(main as HTMLElement)
|
|
327
|
+
swapMainContent(main as HTMLElement, html)
|
|
328
|
+
if (title) document.title = title
|
|
329
|
+
// History BEFORE takeover: takeover derives params from location.pathname,
|
|
330
|
+
// so the URL bar must already show the destination.
|
|
331
|
+
if (mode === 'push') history.pushState({}, '', url.href)
|
|
332
|
+
else if (mode === 'replace') history.replaceState({}, '', url.href)
|
|
333
|
+
if (mode === 'none') window.scrollTo(0, scrollPositions.get(pageCacheKey(url)) ?? 0)
|
|
334
|
+
else window.scrollTo(0, 0)
|
|
335
|
+
currentPageKey = pageCacheKey(url)
|
|
336
|
+
})
|
|
337
|
+
// Superseded across the view-transition await → the newer navigation owns
|
|
338
|
+
// the DOM; don't run takeover/hydrate/commit on a soon-to-be-replaced shell.
|
|
339
|
+
if (signal?.aborted) return true
|
|
334
340
|
const container = main.querySelector<HTMLElement>('[data-brust-fallback-root]')
|
|
335
341
|
if (!container) {
|
|
336
342
|
// A fallback payload without its marker is a build bug — log it, but
|
|
@@ -423,19 +429,28 @@ export async function navigate(url: URL, mode: 'push' | 'replace' | 'none'): Pro
|
|
|
423
429
|
}
|
|
424
430
|
const main = document.querySelector('main')
|
|
425
431
|
if (!main) throw new Error('navigation: no <main> element')
|
|
432
|
+
// scrollY of the LEAVING page is read before the transition (it must see
|
|
433
|
+
// the old page's position under the old key).
|
|
426
434
|
scrollPositions.set(currentPageKey, window.scrollY)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
435
|
+
await withViewTransition(document, () => {
|
|
436
|
+
unmountIslandsIn(main as HTMLElement)
|
|
437
|
+
swapMainContent(main as HTMLElement, html)
|
|
438
|
+
// Only a FRESH payload re-applies the server store snapshot: replaying a
|
|
439
|
+
// cached (stale) snapshot would roll back live client store state the
|
|
440
|
+
// user changed since the page was first fetched.
|
|
441
|
+
if (!cached && store) applyStoreSnapshot(store)
|
|
442
|
+
if (title) document.title = title
|
|
443
|
+
if (mode === 'push') history.pushState({}, '', url.href)
|
|
444
|
+
else if (mode === 'replace') history.replaceState({}, '', url.href)
|
|
445
|
+
if (mode === 'none') window.scrollTo(0, scrollPositions.get(key) ?? 0)
|
|
446
|
+
else window.scrollTo(0, 0)
|
|
447
|
+
hydrateMarkersIn(main as HTMLElement)
|
|
448
|
+
})
|
|
449
|
+
// The view-transition await is a new suspension point: a newer navigation
|
|
450
|
+
// could have aborted us between the swap and here. The DOM is already
|
|
451
|
+
// committed (irreversible), but skip the bookkeeping so we don't advance
|
|
452
|
+
// the nav store to this stale destination — the newer navigation owns it.
|
|
453
|
+
if (ac.signal.aborted) return
|
|
439
454
|
currentPageKey = key
|
|
440
455
|
__navCommit(url.pathname, url.search)
|
|
441
456
|
} catch (err) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Opt-in View Transitions for SPA navigation. The framework default is an
|
|
2
|
+
// INSTANT swap; an app opts in by putting `data-brust-view-transitions` on
|
|
3
|
+
// <html> and shipping the `::view-transition-*(root)` CSS itself. The helper is
|
|
4
|
+
// pure (takes `doc`) so it unit-tests without a real browser. Spec:
|
|
5
|
+
// docs/superpowers/specs/2026-06-12-view-transitions-design.md
|
|
6
|
+
|
|
7
|
+
/** True iff this navigation should animate: the browser supports the View
|
|
8
|
+
* Transitions API AND the app opted in with the <html> marker. */
|
|
9
|
+
export function viewTransitionsEnabled(doc: Document): boolean {
|
|
10
|
+
return (
|
|
11
|
+
typeof (doc as { startViewTransition?: unknown }).startViewTransition === 'function' &&
|
|
12
|
+
doc.documentElement.hasAttribute('data-brust-view-transitions')
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Run the synchronous navigation `commit` inside a view transition when
|
|
17
|
+
* enabled, else directly. Resolves once the DOM is committed (NOT when the
|
|
18
|
+
* animation finishes) so caller ordering is preserved. `commit` runs EXACTLY
|
|
19
|
+
* once on every path:
|
|
20
|
+
* - disabled/unsupported → direct call
|
|
21
|
+
* - startViewTransition throws synchronously (before the callback) → direct
|
|
22
|
+
* call (the swap never happened — losing it would blank the page, B2)
|
|
23
|
+
* - updateCallbackDone rejects (the callback ran-and-threw) → NOT re-run; the
|
|
24
|
+
* rejection PROPAGATES so the caller can run its full-reload recovery. */
|
|
25
|
+
export async function withViewTransition(doc: Document, commit: () => void): Promise<void> {
|
|
26
|
+
if (!viewTransitionsEnabled(doc)) {
|
|
27
|
+
commit()
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
const start = (
|
|
31
|
+
doc as Document & {
|
|
32
|
+
startViewTransition: (cb: () => void) => { updateCallbackDone: Promise<void> }
|
|
33
|
+
}
|
|
34
|
+
).startViewTransition
|
|
35
|
+
let tr: { updateCallbackDone: Promise<void> }
|
|
36
|
+
try {
|
|
37
|
+
tr = start.call(doc, commit)
|
|
38
|
+
} catch {
|
|
39
|
+
commit()
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
// `updateCallbackDone` rejects ONLY when the update callback (`commit`) threw
|
|
43
|
+
// — animation failures surface on `.finished`, which we never await. So a
|
|
44
|
+
// rejection means the DOM may be half-committed: propagate it (do NOT re-run
|
|
45
|
+
// `commit`) so the caller's catch runs its error path (`__navError` +
|
|
46
|
+
// full-reload), exactly as the synchronous direct-commit branch does. A
|
|
47
|
+
// transition that is merely SKIPPED (a newer startViewTransition) still
|
|
48
|
+
// resolves `updateCallbackDone`, so this never throws on supersession.
|
|
49
|
+
await tr.updateCallbackDone
|
|
50
|
+
}
|
package/runtime/md/emit.ts
CHANGED
|
@@ -12,6 +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
16
|
import {
|
|
16
17
|
bakeDirectivesIfUsed,
|
|
17
18
|
buildChainWrapperSource,
|
|
@@ -177,6 +178,12 @@ export async function emitMdTemplates(opts: MdEmitOpts): Promise<{
|
|
|
177
178
|
return files.get(src.relPath)
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
// Generator meta: resolved INTERNALLY from the out dir's artifact (NOT a
|
|
182
|
+
// caller param) — emit re-runs from five call sites (build, dev, boot
|
|
183
|
+
// staleness, md boot re-emit, dev HMR) and a param would silently drop the
|
|
184
|
+
// tag on re-emit. Fallback (no artifact) = version-on defaults.
|
|
185
|
+
const generatorMeta = resolveGenerator(opts.outDir).meta
|
|
186
|
+
|
|
180
187
|
// Tag-name → classification, shared across pages (one readFileSync per name).
|
|
181
188
|
const resolutionCache = new Map<string, { res: MdComponentResolution; absPath: string }>()
|
|
182
189
|
|
|
@@ -328,7 +335,10 @@ export async function emitMdTemplates(opts: MdEmitOpts): Promise<{
|
|
|
328
335
|
mdHtml = mdHtml.slice(0, at) + inlined + mdHtml.slice(at + use.marker.length)
|
|
329
336
|
}
|
|
330
337
|
|
|
331
|
-
const template =
|
|
338
|
+
const template = insertGeneratorMeta(
|
|
339
|
+
spliceMdSlot(compiled.template, name, mdHtml),
|
|
340
|
+
generatorMeta,
|
|
341
|
+
)
|
|
332
342
|
if (countMainTags(template) > 1) {
|
|
333
343
|
process.stderr.write(
|
|
334
344
|
`brust: md route "${name}" has more than one <main> after splice — SPA navigation extracts only the first <main>…</main>.\n`,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Render-time generator-meta injection for React-streamed HTML. The tag value
|
|
2
|
+
// comes from the build's generator.json (configured at boot by BOTH the main
|
|
3
|
+
// and worker isolates — module state is per-isolate, same trap as
|
|
4
|
+
// configureJinjaDir). Buffered branch: splice before </head> with a duplicate
|
|
5
|
+
// guard (a hand-authored generator meta wins). Streaming branch (stream.ts)
|
|
6
|
+
// prepends the raw tag with the other first-chunk tags instead — no guard
|
|
7
|
+
// possible there (head bytes arrive in later chunks); documented limitation.
|
|
8
|
+
const ENC = new TextEncoder()
|
|
9
|
+
|
|
10
|
+
let configured: string | null = null
|
|
11
|
+
|
|
12
|
+
/** Seed from generator.json at boot (main + worker). null → no injection. */
|
|
13
|
+
export function configureGeneratorMeta(meta: string | null): void {
|
|
14
|
+
configured = meta
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getGeneratorMeta(): string | null {
|
|
18
|
+
return configured
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const GUARD = ENC.encode('name="generator"')
|
|
22
|
+
|
|
23
|
+
/** Splice `metaTag` immediately before the first `</head>` (case-insensitive).
|
|
24
|
+
* No </head> in the chunk, empty tag, or an existing generator meta → body
|
|
25
|
+
* returned untouched. Byte-level (no decode) — safe with multibyte content. */
|
|
26
|
+
export function injectGeneratorMeta(body: Uint8Array, metaTag: string | null): Uint8Array {
|
|
27
|
+
if (!metaTag) return body
|
|
28
|
+
const pos = findHeadCloseTag(body)
|
|
29
|
+
if (pos < 0) return body
|
|
30
|
+
if (bytesInclude(body, GUARD, pos)) return body
|
|
31
|
+
const tagBytes = ENC.encode(metaTag)
|
|
32
|
+
const out = new Uint8Array(body.length + tagBytes.length)
|
|
33
|
+
out.set(body.subarray(0, pos), 0)
|
|
34
|
+
out.set(tagBytes, pos)
|
|
35
|
+
out.set(body.subarray(pos), pos + tagBytes.length)
|
|
36
|
+
return out
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True if `needle` occurs in `hay[0..limit)`. Naive scan — head is small. */
|
|
40
|
+
function bytesInclude(hay: Uint8Array, needle: Uint8Array, limit: number): boolean {
|
|
41
|
+
const max = Math.min(limit, hay.length) - needle.length
|
|
42
|
+
outer: for (let i = 0; i <= max; i++) {
|
|
43
|
+
for (let j = 0; j < needle.length; j++) {
|
|
44
|
+
if (hay[i + j] !== needle[j]) continue outer
|
|
45
|
+
}
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Byte scan for `</head>` (letters case-insensitive) — same approach as
|
|
52
|
+
* inject-css-link.ts. Returns offset of `<` or -1. */
|
|
53
|
+
function findHeadCloseTag(body: Uint8Array): number {
|
|
54
|
+
const LT = 0x3c
|
|
55
|
+
const SL = 0x2f
|
|
56
|
+
const GT = 0x3e
|
|
57
|
+
for (let i = 0, max = body.length - 6; i < max; i++) {
|
|
58
|
+
if (body[i] !== LT || body[i + 1] !== SL) continue
|
|
59
|
+
if (!isLetter(body[i + 2], 0x48)) continue // H
|
|
60
|
+
if (!isLetter(body[i + 3], 0x45)) continue // E
|
|
61
|
+
if (!isLetter(body[i + 4], 0x41)) continue // A
|
|
62
|
+
if (!isLetter(body[i + 5], 0x44)) continue // D
|
|
63
|
+
if (body[i + 6] !== GT) continue
|
|
64
|
+
return i
|
|
65
|
+
}
|
|
66
|
+
return -1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isLetter(b: number, u: number): boolean {
|
|
70
|
+
return b === u || b === (u | 0x20)
|
|
71
|
+
}
|
package/runtime/render/stream.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { injectDevClient } from './inject-dev-client.ts'
|
|
|
12
12
|
import { injectActionPrefix, getActionPrefixSnippet } from './inject-action-prefix.ts'
|
|
13
13
|
import { injectBrustStore, buildStoreScripts } from './inject-store.ts'
|
|
14
14
|
import { getDevClientSnippet } from '../dev/inject.ts'
|
|
15
|
+
import { getGeneratorMeta, injectGeneratorMeta } from './inject-generator.ts'
|
|
15
16
|
|
|
16
17
|
export interface RenderBranchStreamingArgs {
|
|
17
18
|
element: ReactNode
|
|
@@ -175,6 +176,7 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
|
|
|
175
176
|
let body = concatBuffers(buffer, islandsUsed)
|
|
176
177
|
const perRouteHrefs = args.routePath ? getCssHrefsForRoute(args.routePath) : []
|
|
177
178
|
body = injectCssLink(body, [...getCssHrefs(), ...perRouteHrefs])
|
|
179
|
+
body = injectGeneratorMeta(body, getGeneratorMeta())
|
|
178
180
|
body = injectDevClient(body, getDevClientSnippet())
|
|
179
181
|
body = injectActionPrefix(body, getActionPrefixSnippet())
|
|
180
182
|
body = injectBrustStore(body, args.storeSnapshot ?? null)
|
|
@@ -240,13 +242,15 @@ export function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<
|
|
|
240
242
|
const devTag = getDevClientSnippet() ?? ''
|
|
241
243
|
const prefixTag = getActionPrefixSnippet() ?? ''
|
|
242
244
|
const storeTag = buildStoreScripts(args.storeSnapshot ?? null)
|
|
245
|
+
const genTag = getGeneratorMeta() ?? ''
|
|
243
246
|
if (
|
|
244
247
|
linkTagsStr.length > 0 ||
|
|
245
248
|
devTag.length > 0 ||
|
|
246
249
|
prefixTag.length > 0 ||
|
|
247
|
-
storeTag.length > 0
|
|
250
|
+
storeTag.length > 0 ||
|
|
251
|
+
genTag.length > 0
|
|
248
252
|
) {
|
|
249
|
-
const prepend = encoder.encode(linkTagsStr + prefixTag + devTag + storeTag)
|
|
253
|
+
const prepend = encoder.encode(genTag + linkTagsStr + prefixTag + devTag + storeTag)
|
|
250
254
|
const out = new Uint8Array(flushed.length + prepend.length)
|
|
251
255
|
out.set(flushed, 0)
|
|
252
256
|
out.set(prepend, flushed.length)
|