brustjs 0.1.8-alpha → 0.1.10-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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brustjs",
3
- "version": "0.1.8-alpha",
3
+ "version": "0.1.10-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": {
@@ -40,12 +40,12 @@
40
40
  "typescript": "^6.0.3"
41
41
  },
42
42
  "optionalDependencies": {
43
- "brustjs-darwin-x64": "0.1.8-alpha",
44
- "brustjs-darwin-arm64": "0.1.8-alpha",
45
- "brustjs-linux-x64-gnu": "0.1.8-alpha",
46
- "brustjs-linux-arm64-gnu": "0.1.8-alpha",
47
- "brustjs-linux-x64-musl": "0.1.8-alpha",
48
- "brustjs-linux-arm64-musl": "0.1.8-alpha"
43
+ "brustjs-darwin-x64": "0.1.10-alpha",
44
+ "brustjs-darwin-arm64": "0.1.10-alpha",
45
+ "brustjs-linux-x64-gnu": "0.1.10-alpha",
46
+ "brustjs-linux-arm64-gnu": "0.1.10-alpha",
47
+ "brustjs-linux-x64-musl": "0.1.10-alpha",
48
+ "brustjs-linux-arm64-musl": "0.1.10-alpha"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "react": "^19.2.6",
@@ -1,8 +1,88 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
- import { dirname, resolve } from 'node:path'
2
+ import { dirname, relative, resolve } from 'node:path'
3
3
  import { buildDevClientTag } from '../dev/client.ts'
4
4
  import { ISLANDS_IMPORTMAP_AND_BOOTSTRAP } from '../islands/importmap.ts'
5
5
 
6
+ /** Gather transitive component sources starting from a page source file.
7
+ *
8
+ * BFS/DFS over local imports reachable from `pageSourcePath`:
9
+ * - Reads each file's source text and adds it to `sources[ident]`.
10
+ * - Recursively follows local imports in each visited file.
11
+ * - Deduplicates by resolved path to handle cycles.
12
+ * - `mergedImports` is the union of every visited file's `scanImports`, with
13
+ * the page's own imports taking precedence on a conflicting ident.
14
+ *
15
+ * Throws when the same ident resolves to two different absolute paths (across
16
+ * two different importing files) — that would be ambiguous for the Rust
17
+ * compiler. */
18
+ export function gatherComponentSources(pageSourcePath: string): {
19
+ sources: Record<string, string>
20
+ mergedImports: Map<string, string>
21
+ } {
22
+ const sources: Record<string, string> = {}
23
+ // mergedImports accumulates ident→resolvedPath across ALL visited files.
24
+ // The page's own imports win on conflict (inserted last below).
25
+ const mergedImports = new Map<string, string>()
26
+ const visited = new Set<string>()
27
+
28
+ // Queue items: { ident, resolvedPath } — ident is how the PARENT imported it.
29
+ // We start with the children of pageSourcePath (not the page itself).
30
+ function visit(filePath: string, ident: string) {
31
+ if (visited.has(filePath)) return
32
+ visited.add(filePath)
33
+
34
+ const sourceText = readFileSync(filePath, 'utf8')
35
+
36
+ // Record source keyed by ident — detect ambiguity.
37
+ if (ident in sources) {
38
+ // Same ident already seen — only an error if it resolves to a different path.
39
+ // We get the previous path from mergedImports (which maps ident → resolvedPath).
40
+ const existingPath = mergedImports.get(ident)
41
+ if (existingPath && existingPath !== filePath) {
42
+ throw new Error(
43
+ `native build: ambiguous component ident "${ident}" resolves to two paths: ${existingPath} and ${filePath}`,
44
+ )
45
+ }
46
+ } else {
47
+ sources[ident] = sourceText
48
+ }
49
+
50
+ // Scan this file's imports and recurse into local ones.
51
+ const childImports = scanImports(filePath)
52
+ for (const [childIdent, childPath] of childImports) {
53
+ // Merge into mergedImports — check for ambiguity.
54
+ const existing = mergedImports.get(childIdent)
55
+ if (existing !== undefined && existing !== childPath) {
56
+ throw new Error(
57
+ `native build: ambiguous component ident "${childIdent}" resolves to two paths: ${existing} and ${childPath}`,
58
+ )
59
+ }
60
+ if (existing === undefined) {
61
+ mergedImports.set(childIdent, childPath)
62
+ }
63
+ // Recurse into local files (skip node_modules / unresolved paths).
64
+ if (!childPath.includes('node_modules')) {
65
+ visit(childPath, childIdent)
66
+ }
67
+ }
68
+ }
69
+
70
+ // Seed: scan the page's own imports and visit each local file.
71
+ const pageImports = scanImports(pageSourcePath)
72
+ for (const [ident, resolvedPath] of pageImports) {
73
+ if (!resolvedPath.includes('node_modules')) {
74
+ visit(resolvedPath, ident)
75
+ }
76
+ }
77
+
78
+ // Page's own imports win on ident conflict — merge them last.
79
+ for (const [ident, resolvedPath] of pageImports) {
80
+ mergedImports.set(ident, resolvedPath)
81
+ }
82
+
83
+ return { sources, mergedImports }
84
+ }
85
+
6
86
  /** Dev-only: splice the /_brust/dev WS client `<script>` into a compiled native
7
87
  * template so `native: true` (jinja) routes auto-reload like React-SSR routes.
8
88
  *
@@ -48,6 +128,29 @@ export interface NativeRouteEmitOpts {
48
128
  repoRoot: string
49
129
  }
50
130
 
131
+ /** Raw component entry from compileJsx's `componentsJson` field. camelCase keys
132
+ * match what Rust's `components_to_json` emits. */
133
+ interface RawComponentEntry {
134
+ component: string
135
+ instance: number
136
+ factoryExpr: string
137
+ referencedComponents: string[]
138
+ usesIsland: boolean
139
+ /** ISR cache fields (present only on components with an `isr` attr). Declared
140
+ * so the `{ ...entry }` / `{ ...e }` enrich spreads below are type-complete —
141
+ * they MUST survive into the enriched `<Name>.components.json`, or runtime ISR
142
+ * caching for SSR components silently never activates. Mirrors RawIslandEntry. */
143
+ keyPath?: string
144
+ tagsPath?: string
145
+ revalidate?: number
146
+ }
147
+
148
+ /** Enriched component entry written to `<Name>.components.json`. */
149
+ interface EnrichedComponentEntry extends RawComponentEntry {
150
+ /** Absolute path to the component's source file (resolved from page imports). */
151
+ sourcePath: string
152
+ }
153
+
51
154
  /** One entry in a `<Name>.islands.json` as emitted by `jsx-rustc` (camelCase,
52
155
  * see crates/jsx-rust-compiler/src/lib.rs). Enriched with `sourcePath`. */
53
156
  interface RawIslandEntry {
@@ -69,6 +172,130 @@ interface EnrichedIslandEntry extends RawIslandEntry {
69
172
  sourcePath: string
70
173
  }
71
174
 
175
+ /** Build a portable ESM specifier (forward slashes, kept `./`/`../`-prefixed)
176
+ * for `to` interpreted against directory `from`. Used for the factory's
177
+ * component imports so they resolve relative to the factory FILE at runtime
178
+ * instead of baking the build machine's absolute path. */
179
+ function toRelativeSpecifier(from: string, to: string): string {
180
+ const rel = relative(from, to).replaceAll('\\', '/')
181
+ return rel.startsWith('.') ? rel : `./${rel}`
182
+ }
183
+
184
+ /** Write `<Name>.components.json` and `<Name>.factory.ts` for a native route
185
+ * that has SSR components. Also scans each SSR component's source for Island
186
+ * `component={X}` references and returns those identifiers so the build step
187
+ * can ensure their JS chunks are built.
188
+ *
189
+ * Both artifacts use PROJECT-RELATIVE paths, never the build machine's absolute
190
+ * path: `components.json` stores `sourcePath` relative to the project root
191
+ * (cwd); `.factory.ts` imports relative to its own location (`.brust/jinja/`).
192
+ * `enriched` keeps the ABSOLUTE path internally for the build-time island scan
193
+ * below (`readFileSync`). */
194
+ function emitComponentArtifacts(
195
+ jinjaPath: string,
196
+ componentsJsonStr: string,
197
+ pageImports: Map<string, string>,
198
+ routeName: string,
199
+ ): { islandIdsFromComponents: string[] } {
200
+ const raw = JSON.parse(componentsJsonStr) as RawComponentEntry[]
201
+ if (raw.length === 0) return { islandIdsFromComponents: [] }
202
+
203
+ const jinjaDir = dirname(jinjaPath)
204
+ const projectRoot = process.cwd()
205
+
206
+ // Enrich with ABSOLUTE source paths resolved from page's own imports — kept
207
+ // absolute for the readFileSync island scan further down.
208
+ const enriched: EnrichedComponentEntry[] = raw.map((entry) => {
209
+ const sourcePath = pageImports.get(entry.component)
210
+ if (!sourcePath) {
211
+ throw new Error(
212
+ `SSR component "${entry.component}" in native route "${routeName}" has no matching import in the page source (expected \`import ${entry.component} from "..."\`)`,
213
+ )
214
+ }
215
+ return { ...entry, sourcePath }
216
+ })
217
+
218
+ // Write <Name>.components.json with PROJECT-RELATIVE sourcePaths. (sourcePath
219
+ // is build-time metadata — resolveComponentContext imports the factory, not
220
+ // these paths — so relative is purely a portability/readability win.)
221
+ const compJsonPath = jinjaPath.replace(/\.jinja$/, '.components.json')
222
+ const compJsonEntries = enriched.map((e) => ({
223
+ ...e,
224
+ sourcePath: relative(projectRoot, e.sourcePath).replaceAll('\\', '/'),
225
+ }))
226
+ writeFileSync(compJsonPath, JSON.stringify(compJsonEntries))
227
+
228
+ // Collect import lines. Deduplicate referenced components.
229
+ const seen = new Set<string>()
230
+ const importLines: string[] = []
231
+ const needsIsland = enriched.some((e) => e.usesIsland)
232
+
233
+ // React createElement is always needed.
234
+ importLines.push("import { createElement as h } from 'react'")
235
+ if (needsIsland) {
236
+ importLines.push("import { Island } from 'brustjs'")
237
+ }
238
+
239
+ // Import each referenced component RELATIVE to the factory file's own dir so
240
+ // `await import(factory)` resolves them at runtime regardless of where the
241
+ // project lives (no absolute build-machine path baked in).
242
+ const allReferenced = [...new Set(enriched.flatMap((e) => e.referencedComponents))]
243
+ for (const compName of allReferenced) {
244
+ if (seen.has(compName)) continue
245
+ seen.add(compName)
246
+ const srcPath = pageImports.get(compName)
247
+ if (srcPath) {
248
+ const spec = toRelativeSpecifier(jinjaDir, srcPath)
249
+ importLines.push(`import ${compName} from ${JSON.stringify(spec)}`)
250
+ }
251
+ }
252
+
253
+ // Scan SSR component sources for <Island component={X}> to discover Island
254
+ // chunk identifiers that don't appear in the page's own .islands.json.
255
+ const islandIdsFromComponents: string[] = []
256
+ const islandAttrRe = /<Island\s[^>]*component=\{(\w+)\}/g
257
+ for (const entry of enriched) {
258
+ try {
259
+ const src = readFileSync(entry.sourcePath, 'utf8')
260
+ islandAttrRe.lastIndex = 0
261
+ for (;;) {
262
+ const m = islandAttrRe.exec(src)
263
+ if (m === null) break
264
+ if (m[1] && !islandIdsFromComponents.includes(m[1]!)) {
265
+ islandIdsFromComponents.push(m[1]!)
266
+ }
267
+ }
268
+ } catch {
269
+ // Unreadable source — skip
270
+ }
271
+ }
272
+
273
+ // Write <Name>.factory.ts
274
+ const factoryPath = jinjaPath.replace(/\.jinja$/, '.factory.ts')
275
+ const factoryLines = [
276
+ '// Auto-generated by brust build — do not edit',
277
+ ...importLines,
278
+ '',
279
+ 'export const factories: Array<(ctx: any) => any> = [',
280
+ ...enriched.map((e, i) => ` // comp_${i}: ${e.component}\n ${e.factoryExpr},`),
281
+ ']',
282
+ ]
283
+ writeFileSync(factoryPath, factoryLines.join('\n') + '\n')
284
+
285
+ // If any SSR component embeds an Island, bake the importmap + bootstrap into
286
+ // the Jinja template — the same injection reconcileIslandManifest would do.
287
+ // Without this the client bootstrap never loads and nested Islands don't hydrate.
288
+ if (needsIsland) {
289
+ const baked = `{% raw %}${ISLANDS_IMPORTMAP_AND_BOOTSTRAP}{% endraw %}`
290
+ const currentJinja = readFileSync(jinjaPath, 'utf8')
291
+ if (!currentJinja.includes(baked)) {
292
+ writeFileSync(jinjaPath, currentJinja + baked)
293
+ }
294
+ }
295
+
296
+ return { islandIdsFromComponents }
297
+ }
298
+
72
299
  export async function emitNativeTemplates(opts: NativeRouteEmitOpts): Promise<void> {
73
300
  mkdirSync(opts.outDir, { recursive: true })
74
301
 
@@ -80,7 +307,11 @@ export async function emitNativeTemplates(opts: NativeRouteEmitOpts): Promise<vo
80
307
  // the addon (`.node`) ships with every platform package, so this path works
81
308
  // for source builds and installed projects alike.
82
309
  let compileJsx:
83
- | ((source: string, path: string) => { template: string; islandsJson: string })
310
+ | ((
311
+ source: string,
312
+ path: string,
313
+ componentSources?: Record<string, string>,
314
+ ) => { template: string; islandsJson: string; warnings?: string[] })
84
315
  | null = null
85
316
  if (nativeRoutes.length > 0) {
86
317
  const native = await import('../index.js')
@@ -107,12 +338,22 @@ export async function emitNativeTemplates(opts: NativeRouteEmitOpts): Promise<vo
107
338
  continue
108
339
  }
109
340
  const outPath = resolve(opts.outDir, `${name}.jinja`)
110
- let compiled: { template: string; islandsJson: string }
341
+
342
+ // Gather transitive component sources for native inlining and build the
343
+ // merged import map that covers nested components (e.g. islands inside an
344
+ // inlined native component that don't appear in the page's own imports).
345
+ const { sources, mergedImports } = gatherComponentSources(sourcePath)
346
+
347
+ let compiled: { template: string; islandsJson: string; warnings?: string[] }
111
348
  try {
112
- compiled = compileJsx!(readFileSync(sourcePath, 'utf8'), sourcePath)
349
+ compiled = compileJsx!(readFileSync(sourcePath, 'utf8'), sourcePath, sources)
113
350
  } catch (e) {
114
351
  throw new Error(`native route "${name}" failed to compile (${sourcePath}):\n${String(e)}`)
115
352
  }
353
+
354
+ // Print non-fatal compiler warnings to stderr.
355
+ for (const w of compiled.warnings ?? []) process.stderr.write(`brust: ${w}\n`)
356
+
116
357
  // Dev-only: native routes don't pass through the React renderer's dev-client
117
358
  // injection, so splice the /_brust/dev WS script in here. reEmitJinja() runs
118
359
  // this on every hot reload, so the script is always present in dev.
@@ -130,12 +371,16 @@ export async function emitNativeTemplates(opts: NativeRouteEmitOpts): Promise<vo
130
371
  const islandsJsonPath = resolve(opts.outDir, `${name}.islands.json`)
131
372
  if (compiled.islandsJson && compiled.islandsJson !== '[]') {
132
373
  writeFileSync(islandsJsonPath, compiled.islandsJson)
133
- // Island source paths resolve from the PAGE file's own imports.
134
- const pageImports = scanImports(sourcePath)
135
- reconcileIslandManifest(outPath, islandsJsonPath, pageImports, name)
374
+ reconcileIslandManifest(outPath, islandsJsonPath, mergedImports, name)
136
375
  } else if (existsSync(islandsJsonPath)) {
137
376
  rmSync(islandsJsonPath, { force: true })
138
377
  }
378
+
379
+ // SSR component artifacts: .components.json + .factory.ts
380
+ const compJsonStr = (compiled as any).componentsJson ?? '[]'
381
+ if (compJsonStr !== '[]') {
382
+ emitComponentArtifacts(outPath, compJsonStr, mergedImports, name)
383
+ }
139
384
  }
140
385
 
141
386
  writeFileSync(
@@ -39,8 +39,18 @@ export class Coordinator {
39
39
  switch (ev.kind) {
40
40
  case 'ts':
41
41
  case 'html':
42
+ case 'islands':
42
43
  // Stale frozen island renders must not survive a source edit.
43
44
  this.deps.clearIslandCache?.()
45
+ // Rebuild island CLIENT chunks. The watcher classifies every `.tsx`
46
+ // (incl. an island's client source) as `ts` — it has no way to tell
47
+ // an island source from a page/component by path alone, and the
48
+ // island set changes as you add/remove `<Island>` usages. So we
49
+ // rebuild island chunks on every render-affecting reload; otherwise
50
+ // editing an island's JS never reaches the browser without a
51
+ // dev-server restart (the chunk on disk stays stale). buildIslands
52
+ // re-scans routes + re-bundles into the served `.brust/islands` dir.
53
+ await this.deps.buildIslands()
44
54
  await this.deps.workers.terminateAll()
45
55
  await this.deps.workers.spawnAll()
46
56
  // Reload native-route templates (process-global minijinja env — does
@@ -48,14 +58,6 @@ export class Coordinator {
48
58
  await this.deps.reEmitJinja()
49
59
  await this.deps.broadcast({ type: 'reload' })
50
60
  break
51
- case 'islands':
52
- this.deps.clearIslandCache?.()
53
- await this.deps.buildIslands()
54
- await this.deps.workers.terminateAll()
55
- await this.deps.workers.spawnAll()
56
- await this.deps.reEmitJinja()
57
- await this.deps.broadcast({ type: 'reload' })
58
- break
59
61
  case 'css':
60
62
  await this.deps.buildCss()
61
63
  await this.deps.broadcast({
@@ -7,18 +7,19 @@ export interface CachedIslandJs {
7
7
  props: string
8
8
  }
9
9
 
10
- /**
11
- * Compile a single native-route source to its jinja template + island
12
- * manifest. `path` is used only in error messages. Mirrors `jsx-rustc`'s
13
- * `compile_full` + `islands_to_json` so the TS emit step can write the same
14
- * `<Name>.jinja` and `<Name>.islands.json` files.
15
- */
16
- export declare function compileJsx(source: string, path: string): NapiCompiledJsx
10
+ export declare function compileJsx(source: string, path: string, componentSources?: Record<string, string> | undefined | null): NapiCompiledJsx
17
11
 
18
12
  export declare function configureCache(maxEntries: number): NapiResult<undefined>
19
13
 
20
14
  export declare function configureCssDir(path: string): NapiResult<undefined>
21
15
 
16
+ /**
17
+ * Enable/disable dev mode. Called by the TS dev coordinator (`brust.run` when
18
+ * `dev` is on). Flips static-asset caching to `no-store` so hot-reloaded island
19
+ * /CSS chunks (unhashed URLs) are never served stale from the browser cache.
20
+ */
21
+ export declare function configureDevMode(enabled: boolean): void
22
+
22
23
  export declare function configureIslandsDir(path: string): NapiResult<undefined>
23
24
 
24
25
  export declare function islandCacheClear(): void
@@ -31,15 +32,24 @@ export declare function islandCacheSet(key: string, tags: Array<string>, ttlMs:
31
32
 
32
33
  export declare function isWorker(): boolean
33
34
 
34
- /** Result of compiling one `pages/<Name>.tsx` source. */
35
35
  export interface NapiCompiledJsx {
36
- /** The emitted jinja template. */
37
36
  template: string
38
37
  /**
39
- * Island manifest as JSON (`"[]"` when the route uses no `<Island>`). The
40
- * camelCase keys match `RawIslandEntry` in native-routes-emit.ts.
38
+ * Island manifest as JSON (`"[]"` when no `<Island>`). camelCase keys match
39
+ * `RawIslandEntry` in native-routes-emit.ts.
41
40
  */
42
41
  islandsJson: string
42
+ /**
43
+ * SSR component manifest as JSON (`"[]"` when no SSR components). camelCase
44
+ * keys: `component`, `instance`, `factoryExpr`, `referencedComponents`,
45
+ * `usesIsland`.
46
+ */
47
+ componentsJson: string
48
+ /**
49
+ * Non-fatal diagnostic messages from native-inline lowering. Empty when no
50
+ * `component_sources` are provided (i.e. the default `None` path).
51
+ */
52
+ warnings: Array<string>
43
53
  }
44
54
 
45
55
  /**
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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
80
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
96
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
117
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
133
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
150
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
166
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
185
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
201
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
217
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
237
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
253
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
274
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
290
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
308
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
324
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
342
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
358
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
376
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
392
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
410
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
426
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
443
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
459
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
479
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
495
+ if (bindingPackageVersion !== '0.1.10-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.10-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.8-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.8-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
511
+ if (bindingPackageVersion !== '0.1.10-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.10-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
513
513
  }
514
514
  return binding
515
515
  } catch (e) {
@@ -580,6 +580,7 @@ module.exports.beginServe = nativeBinding.beginServe
580
580
  module.exports.compileJsx = nativeBinding.compileJsx
581
581
  module.exports.configureCache = nativeBinding.configureCache
582
582
  module.exports.configureCssDir = nativeBinding.configureCssDir
583
+ module.exports.configureDevMode = nativeBinding.configureDevMode
583
584
  module.exports.configureIslandsDir = nativeBinding.configureIslandsDir
584
585
  module.exports.islandCacheClear = nativeBinding.islandCacheClear
585
586
  module.exports.islandCacheGet = nativeBinding.islandCacheGet
package/runtime/index.ts CHANGED
@@ -446,6 +446,11 @@ export const brust = {
446
446
  )
447
447
 
448
448
  if (dev) {
449
+ // Tell Rust we're in dev so static assets (islands/CSS chunks) are
450
+ // served `Cache-Control: no-store` — chunk URLs are unhashed, so a
451
+ // hot-reload rebuild would otherwise be masked by the browser cache.
452
+ // `?.` keeps an older addon (no export) from throwing.
453
+ ;(native as any).configureDevMode?.(true)
449
454
  const { createWatcher } = await import('./dev/watcher.ts')
450
455
  const { Coordinator } = await import('./dev/coordinator.ts')
451
456
  const { broadcast } = await import('./dev/ws-channel.ts')
@@ -678,6 +683,13 @@ export type { BrustConfig } from './config.ts'
678
683
 
679
684
  export { Island } from './islands/island.tsx'
680
685
  export type { IslandProps, HydrateTrigger } from './islands/island.tsx'
686
+ // Side-effect import (NOT type-only) so the `react` JSX augmentation in
687
+ // isr-jsx.ts is pulled into the program of any project that value-imports
688
+ // brustjs — a type-only re-export would be elided and the augmentation lost.
689
+ // The augmentation makes `isr` valid on any component, like `key`. (Compiles to
690
+ // an empty module; harmless at runtime.)
691
+ import './islands/isr-jsx.ts'
692
+ export type { IsrConfig } from './islands/isr-jsx.ts'
681
693
 
682
694
  export { BrustPage } from './islands/brust-page.tsx'
683
695
  export type { BrustPageProps } from './islands/brust-page.tsx'
@@ -0,0 +1,11 @@
1
+ // Test fixture: an SSR component that bumps the shared renderCounter on every
2
+ // server render, so the component-ISR integration test can prove a cache HIT
3
+ // skipped the factory render. Reuses the island fixture's render-counter
4
+ // singleton (Bun module cache → same object across imports).
5
+ import { createElement } from 'react'
6
+ import { renderCounter } from './render-counter.ts'
7
+
8
+ export default ({ n }: { n: number }) => {
9
+ renderCounter.count++
10
+ return createElement('span', null, String(n))
11
+ }
@@ -0,0 +1,7 @@
1
+ // Test fixture for resolveComponentContext. A factory that renders a <p>
2
+ // element containing ctx.greeting. Uses createElement (no JSX) so it
3
+ // transpiles trivially, and imports from the project's react so bare
4
+ // specifiers resolve correctly from within the in-repo fixture directory.
5
+ import { createElement as h } from 'react'
6
+
7
+ export const factories: Array<(ctx: any) => any> = [(ctx: any) => h('p', null, ctx.greeting)]
@@ -35,7 +35,11 @@ export function scanIslandChunks(routesEntryFile: string): Map<string, string> {
35
35
  const source = readFileSync(pagePath, 'utf8')
36
36
  const pageImports = scanImports(pagePath)
37
37
 
38
- const tags = source.match(/<Island\b[\s\S]*?\/>/g) ?? []
38
+ // `[^<]*?` (not `[\s\S]*?`) so a tag cannot bridge across another `<` — this
39
+ // stops a bare `<Island>` mentioned in a comment from lazily matching forward
40
+ // to an unrelated later `/>` (which would then falsely report "no component
41
+ // prop"). A real island tag never contains `<` between `<Island` and `/>`.
42
+ const tags = source.match(/<Island\b[^<]*?\/>/g) ?? []
39
43
  for (const tag of tags) {
40
44
  const compMatch = tag.match(/component=\{\s*(\w+)\s*\}/)
41
45
  if (!compMatch) {
@@ -1,4 +1,5 @@
1
1
  import { createElement, type ComponentType, type ReactNode } from 'react'
2
+ import type { IsrConfig } from './isr-jsx.ts'
2
3
 
3
4
  /** Triggers that activate hydration of an island marker. */
4
5
  export type HydrateTrigger = 'load' | 'idle' | 'visible' | 'interaction'
@@ -37,11 +38,7 @@ export interface IslandProps<P> {
37
38
  *
38
39
  * Example: `isr={{ key: data.cacheKey, tags: ['blog'], revalidate: 60 }}`
39
40
  */
40
- isr?: {
41
- key: string
42
- tags?: string[]
43
- revalidate?: number
44
- }
41
+ isr?: IsrConfig
45
42
  }
46
43
 
47
44
  /** Module-scope flag flipped by every `<Island>` render. `makeRenderer`
@@ -0,0 +1,53 @@
1
+ // ISR config type + a global JSX augmentation that makes the `isr` attribute
2
+ // accept-anywhere, the way React's built-in `key`/`ref` work.
3
+ //
4
+ // `isr` is a brust COMPILER attribute: the JSX compiler reads it at lower time
5
+ // and strips it (it never reaches the component at runtime — the generated
6
+ // factory contains no `isr` prop). So, like `key`, it is not part of any
7
+ // component's own props type. Without this augmentation TypeScript would reject
8
+ // `<MyLayout isr={{ … }}>` because the user's `MyLayout` props don't declare
9
+ // `isr`. Augmenting `React.Attributes` (where `key` lives) makes `isr` valid on
10
+ // every element/component with zero per-component boilerplate.
11
+ //
12
+ // This file must stay in the package's type import graph (it is re-exported
13
+ // from `runtime/index.ts` and consumed by `island.tsx`) so the augmentation is
14
+ // in scope for any project that imports from `brustjs`.
15
+
16
+ /** Shape of the `isr` attribute on an SSR component or `ssr` island on a
17
+ * native-jinja route. `renderToString` runs ONCE per `key`; later same-key
18
+ * requests serve the frozen markup from the Rust-side cache. */
19
+ export interface IsrConfig {
20
+ /** Required unique cache key. A different key is a different cached render.
21
+ * Compute it in the loader and pass it through (e.g. `data.cacheKey`). */
22
+ key: string
23
+ /** Optional groups for bulk invalidation —
24
+ * `import { cache } from 'brustjs'; cache.invalidate({ tags: ['blog'] })`
25
+ * evicts every entry carrying a tag. `cache.invalidate({ key })` evicts one. */
26
+ tags?: string[]
27
+ /** Optional TTL in SECONDS (integer). Omit to cache until invalidated. */
28
+ revalidate?: number
29
+ }
30
+
31
+ // Augment `React.JSX.IntrinsicAttributes` — the interface the `react-jsx`
32
+ // transform consults for props allowed on EVERY element/component (it is where
33
+ // `key` lives, via `IntrinsicAttributes extends React.Attributes`). Targeting it
34
+ // directly is the form that actually merges under React 19's `export = React`
35
+ // types; augmenting `React.Attributes` does not propagate through the export-=
36
+ // module boundary.
37
+ declare module 'react' {
38
+ namespace JSX {
39
+ interface IntrinsicAttributes {
40
+ /** brust ISR cache directive — compiler-consumed (stripped before the
41
+ * component is called), valid on any SSR component or `ssr` island on a
42
+ * native route. See {@link IsrConfig}. */
43
+ isr?: IsrConfig
44
+ /** brust `native` inline directive — compiler-consumed (stripped at lower
45
+ * time). On a native-jinja route, `<Comp native />` expands the component
46
+ * inline at compile time (no JS-worker render) when it is pure
47
+ * (props→JSX, no hooks/side-effects); otherwise it degrades to a normal
48
+ * SSR component with a build warning. Bare boolean, like `ssr` on an
49
+ * island. */
50
+ native?: boolean
51
+ }
52
+ }
53
+ }
@@ -34,6 +34,10 @@ export interface NativeIslandEntry {
34
34
  tagsPath?: string
35
35
  /** Revalidate window in SECONDS; converted to ttlMs on cache.set. */
36
36
  revalidate?: number
37
+ /** Static ISR cache key (string literal in JSX). Takes precedence over keyPath. */
38
+ keyLiteral?: string
39
+ /** Static ISR cache tags (array literal in JSX). Takes precedence over tagsPath. */
40
+ tagsLiteral?: string[]
37
41
  }
38
42
 
39
43
  /** Rust-side ISR cache, injected as a port for testability. A `get` hit
@@ -143,22 +147,27 @@ export async function resolveIslandContext(
143
147
  // stored one) and skips render. A non-string-but-defined key is a manifest
144
148
  // bug — warn and fall through to an uncached render.
145
149
  let key: string | undefined
146
- if (cache && entry.keyPath) {
150
+ if (cache && entry.keyLiteral !== undefined) {
151
+ // Literal key takes precedence over keyPath; it's always a string.
152
+ key = entry.keyLiteral
153
+ } else if (cache && entry.keyPath) {
147
154
  const k = pathInto(data, entry.keyPath)
148
155
  if (typeof k === 'string') {
149
156
  key = k
150
- const hit = cache.get(key)
151
- if (hit) {
152
- out['island_' + entry.instance + '_html'] = hit.html
153
- out['island_' + entry.instance + '_props'] = hit.props
154
- continue
155
- }
156
157
  } else if (k !== undefined) {
157
158
  console.warn(
158
159
  `[brust] ssr island "${entry.component}" ISR keyPath "${entry.keyPath}" resolved to a non-string value; rendering uncached`,
159
160
  )
160
161
  }
161
162
  }
163
+ if (cache && key) {
164
+ const hit = cache.get(key)
165
+ if (hit) {
166
+ out['island_' + entry.instance + '_html'] = hit.html
167
+ out['island_' + entry.instance + '_props'] = hit.props
168
+ continue
169
+ }
170
+ }
162
171
 
163
172
  try {
164
173
  let Component = componentCache.get(entry.sourcePath)
@@ -185,7 +194,9 @@ export async function resolveIslandContext(
185
194
  // Rust as Vec<String>, so a non-array OR an array with a non-string
186
195
  // element degrades to no tags + a warning, never a bad NAPI payload.
187
196
  let tags: string[] = []
188
- if (entry.tagsPath !== undefined) {
197
+ if (entry.tagsLiteral !== undefined) {
198
+ tags = entry.tagsLiteral
199
+ } else if (entry.tagsPath !== undefined) {
189
200
  const tagsValue = pathInto(data, entry.tagsPath)
190
201
  if (Array.isArray(tagsValue) && tagsValue.every((t) => typeof t === 'string')) {
191
202
  tags = tagsValue
@@ -212,3 +223,144 @@ export async function resolveIslandContext(
212
223
  }
213
224
  return out
214
225
  }
226
+
227
+ /** One entry in `<Name>.components.json` as enriched by `emitComponentArtifacts`. */
228
+ export interface NativeComponentEntry {
229
+ component: string
230
+ instance: number
231
+ sourcePath: string
232
+ /** Dotted path into loader data yielding the ISR cache key (string). */
233
+ keyPath?: string
234
+ /** Dotted path into loader data yielding the ISR cache tags (string[]). */
235
+ tagsPath?: string
236
+ /** Revalidate window in SECONDS; converted to ttlMs on cache.set. */
237
+ revalidate?: number
238
+ /** Static ISR cache key (string literal in JSX). Takes precedence over keyPath. */
239
+ keyLiteral?: string
240
+ /** Static ISR cache tags (array literal in JSX). Takes precedence over tagsPath. */
241
+ tagsLiteral?: string[]
242
+ }
243
+
244
+ // Cache component manifests by absolute path (same pattern as island manifests).
245
+ const componentManifestCache = new Map<string, NativeComponentEntry[] | null>()
246
+
247
+ /** Read `<jinjaDir>/<templateName>.components.json` and return the parsed entry
248
+ * array, or `null` if the file doesn't exist. Both hits and misses are cached
249
+ * by absolute path (same invariant as `loadIslandManifest`). */
250
+ export function loadComponentManifest(
251
+ templateName: string,
252
+ jinjaDir?: string,
253
+ ): NativeComponentEntry[] | null {
254
+ const dir = jinjaDir ?? path.resolve(process.cwd(), '.brust/jinja')
255
+ const abs = path.resolve(dir, `${templateName}.components.json`)
256
+ if (componentManifestCache.has(abs)) return componentManifestCache.get(abs)!
257
+ let parsed: NativeComponentEntry[] | null
258
+ try {
259
+ parsed = JSON.parse(readFileSync(abs, 'utf8')) as NativeComponentEntry[]
260
+ } catch {
261
+ componentManifestCache.set(abs, null)
262
+ return null
263
+ }
264
+ componentManifestCache.set(abs, parsed)
265
+ return parsed
266
+ }
267
+
268
+ // Cache factory modules by absolute path.
269
+ const factoryCache = new Map<string, { factories: Array<(ctx: unknown) => unknown> } | null>()
270
+
271
+ /** Build the per-component context additions for a manifest. Each entry
272
+ * contributes `comp_<instance>_html` — the component rendered to HTML by
273
+ * the route's factory function. On `renderToString` failure: degrade to
274
+ * `comp_N_html = ""` and log, mirrors SSR island failure behaviour. */
275
+ export async function resolveComponentContext(
276
+ manifest: NativeComponentEntry[],
277
+ data: unknown,
278
+ templateName: string,
279
+ jinjaDir?: string,
280
+ cache?: IslandCache,
281
+ ): Promise<Record<string, string>> {
282
+ const out: Record<string, string> = {}
283
+ if (!manifest.length) return out
284
+
285
+ const dir = jinjaDir ?? path.resolve(process.cwd(), '.brust/jinja')
286
+ const factoryPath = path.resolve(dir, `${templateName}.factory.ts`)
287
+
288
+ let factoryMod = factoryCache.get(factoryPath)
289
+ if (factoryMod === undefined) {
290
+ try {
291
+ factoryMod = (await import(factoryPath)) as {
292
+ factories: Array<(ctx: unknown) => unknown>
293
+ }
294
+ } catch {
295
+ factoryMod = null
296
+ }
297
+ factoryCache.set(factoryPath, factoryMod)
298
+ }
299
+
300
+ for (let i = 0; i < manifest.length; i++) {
301
+ const entry = manifest[i]!
302
+
303
+ // ISR fast-path: resolve a string cache key out of loader data. A hit
304
+ // serves the FROZEN html and skips the factory. A non-string-but-defined
305
+ // key is a manifest bug — warn and fall through to an uncached render.
306
+ let key: string | undefined
307
+ if (cache && entry.keyLiteral !== undefined) {
308
+ // Literal key takes precedence over keyPath; it's always a string.
309
+ key = entry.keyLiteral
310
+ } else if (cache && entry.keyPath) {
311
+ const k = pathInto(data, entry.keyPath)
312
+ if (typeof k === 'string') {
313
+ key = k
314
+ } else if (k !== undefined) {
315
+ console.warn(
316
+ `[brust] SSR component "${entry.component}" ISR keyPath "${entry.keyPath}" resolved to a non-string value; rendering uncached`,
317
+ )
318
+ }
319
+ }
320
+ if (cache && key) {
321
+ const hit = cache.get(key)
322
+ if (hit) {
323
+ // Components have NO `_props` slot (unlike islands): the cache stores
324
+ // props="" for component entries, so we serve only hit.html and
325
+ // deliberately ignore hit.props. See the write-through below.
326
+ out[`comp_${entry.instance}_html`] = hit.html
327
+ continue
328
+ }
329
+ }
330
+
331
+ try {
332
+ if (!factoryMod?.factories?.[i]) {
333
+ throw new Error(`factory[${i}] not found in ${factoryPath}`)
334
+ }
335
+ const node = factoryMod.factories[i]!(data)
336
+ const html = renderToString(node as React.ReactNode)
337
+ out[`comp_${entry.instance}_html`] = html
338
+ // Write-through: SUCCESS path only (a throwing render must not poison the
339
+ // cache). props is "" — components have no separate hydration props attr.
340
+ if (cache && key) {
341
+ let tags: string[] = []
342
+ if (entry.tagsLiteral !== undefined) {
343
+ tags = entry.tagsLiteral
344
+ } else if (entry.tagsPath !== undefined) {
345
+ const tagsValue = pathInto(data, entry.tagsPath)
346
+ if (Array.isArray(tagsValue) && tagsValue.every((t) => typeof t === 'string')) {
347
+ tags = tagsValue
348
+ } else if (tagsValue !== undefined) {
349
+ console.warn(
350
+ `[brust] SSR component "${entry.component}" ISR tagsPath "${entry.tagsPath}" must resolve to a string[]; using no tags`,
351
+ )
352
+ }
353
+ }
354
+ const ttlMs = entry.revalidate !== undefined ? entry.revalidate * 1000 : undefined
355
+ cache.set(key, tags, ttlMs, html, '')
356
+ }
357
+ } catch (e) {
358
+ console.error(
359
+ `[brust] SSR component "${entry.component}" renderToString failed; degrading to empty:`,
360
+ e,
361
+ )
362
+ out[`comp_${entry.instance}_html`] = ''
363
+ }
364
+ }
365
+ return out
366
+ }
package/runtime/routes.ts CHANGED
@@ -8,10 +8,14 @@ import { createContext, createElement, useContext, type ComponentType, type Reac
8
8
  import { renderToPipeableStream } from 'react-dom/server.node'
9
9
  import { Writable } from 'node:stream'
10
10
  import { Buffer } from 'node:buffer'
11
- // @ts-expect-error - index.js is generated by napi-rs at build time
12
11
  import * as native from './index.js'
13
12
  import { renderBranchStreaming } from './render/stream.ts'
14
- import { loadIslandManifest, resolveIslandContext } from './islands/native-render.ts'
13
+ import {
14
+ loadIslandManifest,
15
+ resolveIslandContext,
16
+ loadComponentManifest,
17
+ resolveComponentContext,
18
+ } from './islands/native-render.ts'
15
19
  import type { IslandCache } from './islands/native-render.ts'
16
20
  import type { ActionDef } from './actions.ts'
17
21
 
@@ -605,19 +609,33 @@ export function makeRenderer(
605
609
  }
606
610
  }
607
611
  const json = JSON.stringify(data ?? {})
608
- // Sub-project J — islands. If this template has an enriched islands
609
- // manifest, merge per-island context vars (island_<id>_props, plus
610
- // island_<id>_html for ssr entries) into the loader data before
611
- // shipping it. resolveIslandContext is async (T9: it awaits the dynamic
612
- // import + renderToString of each ssr island source).
612
+ // Sub-project J — islands + components. If this template has an enriched
613
+ // islands manifest or a components manifest, merge per-island context vars
614
+ // (island_<id>_props, plus island_<id>_html for ssr entries) and per-component
615
+ // context vars (comp_<id>_html for SSR components) into the loader data before
616
+ // shipping it. Both resolvers are async; run them in parallel via Promise.all.
613
617
  const manifest = loadIslandManifest(flat.nativeTemplate)
614
- if (manifest && manifest.length > 0) {
615
- const rt = JSON.parse(json) // roundtrip ONCE; props read from rt
616
- const extra = await resolveIslandContext(manifest, rt, islandCache)
617
- const ctx = { ...rt, ...extra }
618
+ const compManifest = loadComponentManifest(flat.nativeTemplate)
619
+ if ((manifest && manifest.length > 0) || (compManifest && compManifest.length > 0)) {
620
+ const rt = JSON.parse(json) as Record<string, unknown>
621
+ const [islandExtra, componentExtra] = await Promise.all([
622
+ manifest && manifest.length > 0
623
+ ? resolveIslandContext(manifest, rt, islandCache)
624
+ : Promise.resolve({} as Record<string, string>),
625
+ compManifest && compManifest.length > 0
626
+ ? resolveComponentContext(
627
+ compManifest,
628
+ rt,
629
+ flat.nativeTemplate,
630
+ undefined,
631
+ islandCache,
632
+ )
633
+ : Promise.resolve({} as Record<string, string>),
634
+ ])
635
+ const ctx = { ...rt, ...islandExtra, ...componentExtra }
618
636
  const finalBytes = encoder.encode(JSON.stringify(ctx))
619
637
  // The original size check guarded the pre-island bytes; the merged
620
- // context (with island props) can be larger. Re-check on finalBytes.
638
+ // context (with island props + component html) can be larger. Re-check on finalBytes.
621
639
  if (finalBytes.length > view.length) {
622
640
  return packSingleChunkResponse(view, encoder, {
623
641
  status: 413,