@brust/core 2.0.0-alpha.35 → 2.0.0-alpha.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/native/index.d.ts +7 -0
- package/native/index.js +54 -54
- package/package.json +10 -10
- package/src/build/bundle.ts +41 -9
- package/src/build/cf-workerd.ts +63 -0
- package/src/build/cloudflare.ts +19 -2
- package/src/build/config.ts +16 -5
- package/src/build/css.ts +3 -3
- package/src/build/index.ts +11 -5
- package/src/build/manifest.ts +2 -0
- package/src/build/static.ts +4 -0
- package/src/build/tailwind.ts +6 -4
- package/src/config.ts +63 -2
- package/src/launcher-watch.ts +54 -1
- package/src/native.ts +4 -0
- package/src/run.ts +25 -2
package/README.md
CHANGED
|
@@ -131,7 +131,8 @@ A loader or job call that has not settled within `BRUST_CALL_TIMEOUT_MS` answers
|
|
|
131
131
|
slot stays claimed until the call settles (the late result is discarded), so a stuck call costs one
|
|
132
132
|
slot, never a frozen worker. Waiting for a free slot is the separate 503 "all workers busy".
|
|
133
133
|
`GET /_brust/cache/stats` reports L1/job cache counters plus `loader_calls` / `job_calls` /
|
|
134
|
-
`timed_out_calls`.
|
|
134
|
+
`timed_out_calls`. It is off by default (404): set `[server] stats` to `"public"` or `"token"` (`stats_token = "env:NAME"`)
|
|
135
|
+
to read it; `brust dev` serves it.
|
|
135
136
|
|
|
136
137
|
## Worker wire (contract 7, `crates/brust-server/src/protocol.rs`)
|
|
137
138
|
|
package/native/index.d.ts
CHANGED
|
@@ -263,6 +263,13 @@ export interface StartOptions {
|
|
|
263
263
|
l2?: NapiL2Options
|
|
264
264
|
/** `brust dev` only (L14): a request carrying this token in `x-brust-dev-token` is traced. Absent in production. */
|
|
265
265
|
devToken?: string
|
|
266
|
+
/** F507: who may read `/_brust/cache/stats`: `"off"` (the default when absent), `"public"` or `"token"`. */
|
|
267
|
+
stats?: string
|
|
268
|
+
/**
|
|
269
|
+
* F507: the secret `stats = "token"` compares `Authorization: Bearer` with (resolved from its env reference by the
|
|
270
|
+
* JS side); never logged. Boot error `stats: …` for a mode and token that do not go together.
|
|
271
|
+
*/
|
|
272
|
+
statsToken?: string
|
|
266
273
|
}
|
|
267
274
|
|
|
268
275
|
/**
|
package/native/index.js
CHANGED
|
@@ -89,8 +89,8 @@ function requireNative() {
|
|
|
89
89
|
try {
|
|
90
90
|
const binding = require('@brust/native-android-arm64')
|
|
91
91
|
const bindingPackageVersion = require('@brust/native-android-arm64/package.json').version
|
|
92
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
93
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
92
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
93
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
94
94
|
}
|
|
95
95
|
return binding
|
|
96
96
|
} catch (e) {
|
|
@@ -105,8 +105,8 @@ function requireNative() {
|
|
|
105
105
|
try {
|
|
106
106
|
const binding = require('@brust/native-android-arm-eabi')
|
|
107
107
|
const bindingPackageVersion = require('@brust/native-android-arm-eabi/package.json').version
|
|
108
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
109
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
108
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
109
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
110
110
|
}
|
|
111
111
|
return binding
|
|
112
112
|
} catch (e) {
|
|
@@ -126,8 +126,8 @@ function requireNative() {
|
|
|
126
126
|
try {
|
|
127
127
|
const binding = require('@brust/native-win32-x64-gnu')
|
|
128
128
|
const bindingPackageVersion = require('@brust/native-win32-x64-gnu/package.json').version
|
|
129
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
130
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
129
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
130
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
131
131
|
}
|
|
132
132
|
return binding
|
|
133
133
|
} catch (e) {
|
|
@@ -142,8 +142,8 @@ function requireNative() {
|
|
|
142
142
|
try {
|
|
143
143
|
const binding = require('@brust/native-win32-x64-msvc')
|
|
144
144
|
const bindingPackageVersion = require('@brust/native-win32-x64-msvc/package.json').version
|
|
145
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
146
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
145
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
146
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
147
147
|
}
|
|
148
148
|
return binding
|
|
149
149
|
} catch (e) {
|
|
@@ -159,8 +159,8 @@ function requireNative() {
|
|
|
159
159
|
try {
|
|
160
160
|
const binding = require('@brust/native-win32-ia32-msvc')
|
|
161
161
|
const bindingPackageVersion = require('@brust/native-win32-ia32-msvc/package.json').version
|
|
162
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
163
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
162
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
163
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
164
164
|
}
|
|
165
165
|
return binding
|
|
166
166
|
} catch (e) {
|
|
@@ -175,8 +175,8 @@ function requireNative() {
|
|
|
175
175
|
try {
|
|
176
176
|
const binding = require('@brust/native-win32-arm64-msvc')
|
|
177
177
|
const bindingPackageVersion = require('@brust/native-win32-arm64-msvc/package.json').version
|
|
178
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
179
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
178
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
179
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
180
180
|
}
|
|
181
181
|
return binding
|
|
182
182
|
} catch (e) {
|
|
@@ -194,8 +194,8 @@ function requireNative() {
|
|
|
194
194
|
try {
|
|
195
195
|
const binding = require('@brust/native-darwin-universal')
|
|
196
196
|
const bindingPackageVersion = require('@brust/native-darwin-universal/package.json').version
|
|
197
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
198
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
197
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
198
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
199
199
|
}
|
|
200
200
|
return binding
|
|
201
201
|
} catch (e) {
|
|
@@ -210,8 +210,8 @@ function requireNative() {
|
|
|
210
210
|
try {
|
|
211
211
|
const binding = require('@brust/native-darwin-x64')
|
|
212
212
|
const bindingPackageVersion = require('@brust/native-darwin-x64/package.json').version
|
|
213
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
214
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
213
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
214
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
215
215
|
}
|
|
216
216
|
return binding
|
|
217
217
|
} catch (e) {
|
|
@@ -226,8 +226,8 @@ function requireNative() {
|
|
|
226
226
|
try {
|
|
227
227
|
const binding = require('@brust/native-darwin-arm64')
|
|
228
228
|
const bindingPackageVersion = require('@brust/native-darwin-arm64/package.json').version
|
|
229
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
230
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
229
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
230
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
231
231
|
}
|
|
232
232
|
return binding
|
|
233
233
|
} catch (e) {
|
|
@@ -246,8 +246,8 @@ function requireNative() {
|
|
|
246
246
|
try {
|
|
247
247
|
const binding = require('@brust/native-freebsd-x64')
|
|
248
248
|
const bindingPackageVersion = require('@brust/native-freebsd-x64/package.json').version
|
|
249
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
250
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
249
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
250
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
251
251
|
}
|
|
252
252
|
return binding
|
|
253
253
|
} catch (e) {
|
|
@@ -262,8 +262,8 @@ function requireNative() {
|
|
|
262
262
|
try {
|
|
263
263
|
const binding = require('@brust/native-freebsd-arm64')
|
|
264
264
|
const bindingPackageVersion = require('@brust/native-freebsd-arm64/package.json').version
|
|
265
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
266
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
265
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
266
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
267
267
|
}
|
|
268
268
|
return binding
|
|
269
269
|
} catch (e) {
|
|
@@ -283,8 +283,8 @@ function requireNative() {
|
|
|
283
283
|
try {
|
|
284
284
|
const binding = require('@brust/native-linux-x64-musl')
|
|
285
285
|
const bindingPackageVersion = require('@brust/native-linux-x64-musl/package.json').version
|
|
286
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
287
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
286
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
287
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
288
288
|
}
|
|
289
289
|
return binding
|
|
290
290
|
} catch (e) {
|
|
@@ -299,8 +299,8 @@ function requireNative() {
|
|
|
299
299
|
try {
|
|
300
300
|
const binding = require('@brust/native-linux-x64-gnu')
|
|
301
301
|
const bindingPackageVersion = require('@brust/native-linux-x64-gnu/package.json').version
|
|
302
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
303
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
302
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
303
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
304
304
|
}
|
|
305
305
|
return binding
|
|
306
306
|
} catch (e) {
|
|
@@ -317,8 +317,8 @@ function requireNative() {
|
|
|
317
317
|
try {
|
|
318
318
|
const binding = require('@brust/native-linux-arm64-musl')
|
|
319
319
|
const bindingPackageVersion = require('@brust/native-linux-arm64-musl/package.json').version
|
|
320
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
321
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
320
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
321
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
322
322
|
}
|
|
323
323
|
return binding
|
|
324
324
|
} catch (e) {
|
|
@@ -333,8 +333,8 @@ function requireNative() {
|
|
|
333
333
|
try {
|
|
334
334
|
const binding = require('@brust/native-linux-arm64-gnu')
|
|
335
335
|
const bindingPackageVersion = require('@brust/native-linux-arm64-gnu/package.json').version
|
|
336
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
337
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
336
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
337
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
338
338
|
}
|
|
339
339
|
return binding
|
|
340
340
|
} catch (e) {
|
|
@@ -351,8 +351,8 @@ function requireNative() {
|
|
|
351
351
|
try {
|
|
352
352
|
const binding = require('@brust/native-linux-arm-musleabihf')
|
|
353
353
|
const bindingPackageVersion = require('@brust/native-linux-arm-musleabihf/package.json').version
|
|
354
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
355
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
354
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
355
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
356
356
|
}
|
|
357
357
|
return binding
|
|
358
358
|
} catch (e) {
|
|
@@ -367,8 +367,8 @@ function requireNative() {
|
|
|
367
367
|
try {
|
|
368
368
|
const binding = require('@brust/native-linux-arm-gnueabihf')
|
|
369
369
|
const bindingPackageVersion = require('@brust/native-linux-arm-gnueabihf/package.json').version
|
|
370
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
371
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
370
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
371
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
372
372
|
}
|
|
373
373
|
return binding
|
|
374
374
|
} catch (e) {
|
|
@@ -385,8 +385,8 @@ function requireNative() {
|
|
|
385
385
|
try {
|
|
386
386
|
const binding = require('@brust/native-linux-loong64-musl')
|
|
387
387
|
const bindingPackageVersion = require('@brust/native-linux-loong64-musl/package.json').version
|
|
388
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
389
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
388
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
389
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
390
390
|
}
|
|
391
391
|
return binding
|
|
392
392
|
} catch (e) {
|
|
@@ -401,8 +401,8 @@ function requireNative() {
|
|
|
401
401
|
try {
|
|
402
402
|
const binding = require('@brust/native-linux-loong64-gnu')
|
|
403
403
|
const bindingPackageVersion = require('@brust/native-linux-loong64-gnu/package.json').version
|
|
404
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
405
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
404
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
405
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
406
406
|
}
|
|
407
407
|
return binding
|
|
408
408
|
} catch (e) {
|
|
@@ -419,8 +419,8 @@ function requireNative() {
|
|
|
419
419
|
try {
|
|
420
420
|
const binding = require('@brust/native-linux-riscv64-musl')
|
|
421
421
|
const bindingPackageVersion = require('@brust/native-linux-riscv64-musl/package.json').version
|
|
422
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
423
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
422
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
423
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
424
424
|
}
|
|
425
425
|
return binding
|
|
426
426
|
} catch (e) {
|
|
@@ -435,8 +435,8 @@ function requireNative() {
|
|
|
435
435
|
try {
|
|
436
436
|
const binding = require('@brust/native-linux-riscv64-gnu')
|
|
437
437
|
const bindingPackageVersion = require('@brust/native-linux-riscv64-gnu/package.json').version
|
|
438
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
439
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
438
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
439
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
440
440
|
}
|
|
441
441
|
return binding
|
|
442
442
|
} catch (e) {
|
|
@@ -452,8 +452,8 @@ function requireNative() {
|
|
|
452
452
|
try {
|
|
453
453
|
const binding = require('@brust/native-linux-ppc64-gnu')
|
|
454
454
|
const bindingPackageVersion = require('@brust/native-linux-ppc64-gnu/package.json').version
|
|
455
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
456
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
455
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
456
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
457
457
|
}
|
|
458
458
|
return binding
|
|
459
459
|
} catch (e) {
|
|
@@ -468,8 +468,8 @@ function requireNative() {
|
|
|
468
468
|
try {
|
|
469
469
|
const binding = require('@brust/native-linux-s390x-gnu')
|
|
470
470
|
const bindingPackageVersion = require('@brust/native-linux-s390x-gnu/package.json').version
|
|
471
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
472
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
471
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
472
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
473
473
|
}
|
|
474
474
|
return binding
|
|
475
475
|
} catch (e) {
|
|
@@ -488,8 +488,8 @@ function requireNative() {
|
|
|
488
488
|
try {
|
|
489
489
|
const binding = require('@brust/native-openharmony-arm64')
|
|
490
490
|
const bindingPackageVersion = require('@brust/native-openharmony-arm64/package.json').version
|
|
491
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
492
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
491
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
492
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
493
493
|
}
|
|
494
494
|
return binding
|
|
495
495
|
} catch (e) {
|
|
@@ -504,8 +504,8 @@ function requireNative() {
|
|
|
504
504
|
try {
|
|
505
505
|
const binding = require('@brust/native-openharmony-x64')
|
|
506
506
|
const bindingPackageVersion = require('@brust/native-openharmony-x64/package.json').version
|
|
507
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
508
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
507
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
508
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
509
509
|
}
|
|
510
510
|
return binding
|
|
511
511
|
} catch (e) {
|
|
@@ -520,8 +520,8 @@ function requireNative() {
|
|
|
520
520
|
try {
|
|
521
521
|
const binding = require('@brust/native-openharmony-arm')
|
|
522
522
|
const bindingPackageVersion = require('@brust/native-openharmony-arm/package.json').version
|
|
523
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
524
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
523
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
524
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
525
525
|
}
|
|
526
526
|
return binding
|
|
527
527
|
} catch (e) {
|
|
@@ -661,8 +661,8 @@ if (!nativeBinding || forceWasi) {
|
|
|
661
661
|
if (!candidateFailed) {
|
|
662
662
|
if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
663
663
|
const bindingPackageVersion = require('@brust/native-wasm32-wasi/package.json').version
|
|
664
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
665
|
-
throw new Error(`WASI binding package version mismatch, expected 2.0.0-alpha.
|
|
664
|
+
if (bindingPackageVersion !== '2.0.0-alpha.36') {
|
|
665
|
+
throw new Error(`WASI binding package version mismatch, expected 2.0.0-alpha.36 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
666
666
|
}
|
|
667
667
|
}
|
|
668
668
|
wasiBinding = require('@brust/native-wasm32-wasi')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brust/core",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.36",
|
|
4
4
|
"description": "brust v2 — React components compiled to Rust-rendered templates",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://brust.dev",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
]
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@brust/runtime-dom": "2.0.0-alpha.
|
|
60
|
+
"@brust/runtime-dom": "2.0.0-alpha.36",
|
|
61
61
|
"tsconfig-paths": "4.2.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
@@ -101,13 +101,13 @@
|
|
|
101
101
|
"access": "public"
|
|
102
102
|
},
|
|
103
103
|
"optionalDependencies": {
|
|
104
|
-
"@brust/native-darwin-x64": "2.0.0-alpha.
|
|
105
|
-
"@brust/native-darwin-arm64": "2.0.0-alpha.
|
|
106
|
-
"@brust/native-linux-x64-gnu": "2.0.0-alpha.
|
|
107
|
-
"@brust/native-linux-arm64-gnu": "2.0.0-alpha.
|
|
108
|
-
"@brust/native-linux-x64-musl": "2.0.0-alpha.
|
|
109
|
-
"@brust/native-linux-arm64-musl": "2.0.0-alpha.
|
|
110
|
-
"@brust/native-win32-x64-msvc": "2.0.0-alpha.
|
|
111
|
-
"@brust/core-cf": "2.0.0-alpha.
|
|
104
|
+
"@brust/native-darwin-x64": "2.0.0-alpha.36",
|
|
105
|
+
"@brust/native-darwin-arm64": "2.0.0-alpha.36",
|
|
106
|
+
"@brust/native-linux-x64-gnu": "2.0.0-alpha.36",
|
|
107
|
+
"@brust/native-linux-arm64-gnu": "2.0.0-alpha.36",
|
|
108
|
+
"@brust/native-linux-x64-musl": "2.0.0-alpha.36",
|
|
109
|
+
"@brust/native-linux-arm64-musl": "2.0.0-alpha.36",
|
|
110
|
+
"@brust/native-win32-x64-msvc": "2.0.0-alpha.36",
|
|
111
|
+
"@brust/core-cf": "2.0.0-alpha.36"
|
|
112
112
|
}
|
|
113
113
|
}
|
package/src/build/bundle.ts
CHANGED
|
@@ -54,11 +54,32 @@ export interface BrowserCtx {
|
|
|
54
54
|
clientDir?: string
|
|
55
55
|
/** F480: load-time writers shipped into an island chunk (F230), by real path: why each is there. A server-only import
|
|
56
56
|
* reached through one names that chain in `server-only-in-client`. */
|
|
57
|
-
loadWriters?: Map<string,
|
|
57
|
+
loadWriters?: Map<string, LoadWriter>
|
|
58
58
|
/** F495 `[server] base_path`: every `/_brust/…` URL a bundle names carries it (`/portal/_brust/…`); absent or `''` = the root. */
|
|
59
59
|
basePath?: string
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** F480/F481: a load writer shipped into island chunks (F230): its path, every island whose chunk carries it, and the modules it is judged to write. */
|
|
63
|
+
export interface LoadWriter {
|
|
64
|
+
rel: string
|
|
65
|
+
islands: string[]
|
|
66
|
+
writes: string[]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** F481: `a, b, c and 2 more`: at most `cap` names, in sorted order, then the rest as a count. */
|
|
70
|
+
export function namedCapped(names: Iterable<string>, cap = 3): string {
|
|
71
|
+
const all = [...new Set(names)].sort()
|
|
72
|
+
return all.length <= cap ? all.join(', ') : `${all.slice(0, cap).join(', ')} and ${all.length - cap} more`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The sentence a server-only refusal carries for the load writers it reached: every writer and every island, capped. */
|
|
76
|
+
export function loadWriterReason(writers: LoadWriter[]): string {
|
|
77
|
+
const one = (w: LoadWriter) => `${w.rel} is loaded into the React ${w.islands.length > 1 ? 'islands' : 'island'} ${namedCapped(w.islands)} because it is judged to write ${namedCapped(w.writes, 99)} when it loads: F230 load writes`
|
|
78
|
+
const sorted = [...writers].sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0))
|
|
79
|
+
const shown = sorted.slice(0, 3).map(one).join('; ')
|
|
80
|
+
return sorted.length > 3 ? `${shown}; and ${sorted.length - 3} more load writers` : shown
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
/** F495: brust's own URL prefix for a build, `/_brust` or `<base>/_brust`. */
|
|
63
84
|
export const brustRoot = (ctx: { basePath?: string }): string => `${ctx.basePath ?? ''}/_brust`
|
|
64
85
|
|
|
@@ -186,9 +207,9 @@ function isServerOnly(spec: string, importer: string, ctx: BrowserCtx): boolean
|
|
|
186
207
|
* offending import is stubbed so the bundler gets as far as it can; `browserBuild` then throws
|
|
187
208
|
* `server-only-in-client` naming every `<importer> imports <spec>`. */
|
|
188
209
|
function serverOnlyPlugin(ctx: BrowserCtx, violations: string[]): BunPlugin {
|
|
189
|
-
const writers = ctx.loadWriters ?? new Map<string,
|
|
190
|
-
// F480: who
|
|
191
|
-
const
|
|
210
|
+
const writers = ctx.loadWriters ?? new Map<string, LoadWriter>()
|
|
211
|
+
// F480/F481: who imports each file (real paths; every importer, not the first), kept only while a load writer ships, to name the writer chain.
|
|
212
|
+
const importersOf = new Map<string, Set<string>>()
|
|
192
213
|
const real = (f: string) => {
|
|
193
214
|
try {
|
|
194
215
|
return realpathSync(f)
|
|
@@ -200,17 +221,28 @@ function serverOnlyPlugin(ctx: BrowserCtx, violations: string[]): BunPlugin {
|
|
|
200
221
|
const from = real(ctx.sourceOf.get(importer) ?? importer)
|
|
201
222
|
try {
|
|
202
223
|
const file = real(isAbsolute(spec) ? spec : Bun.resolveSync(spec, dirname(from)))
|
|
203
|
-
|
|
224
|
+
let set = importersOf.get(file)
|
|
225
|
+
if (!set) importersOf.set(file, (set = new Set()))
|
|
226
|
+
set.add(from)
|
|
204
227
|
} catch {}
|
|
205
228
|
}
|
|
229
|
+
/** Every load writer a module is reached through (a walk up all its importers, stopping at a writer), as the refusal's reason. */
|
|
206
230
|
const writerChain = (importer: string): string => {
|
|
207
231
|
const seen = new Set<string>()
|
|
208
|
-
|
|
232
|
+
const found: LoadWriter[] = []
|
|
233
|
+
const stack = [real(ctx.sourceOf.get(importer) ?? importer)]
|
|
234
|
+
while (stack.length > 0) {
|
|
235
|
+
const f = stack.pop()!
|
|
236
|
+
if (seen.has(f)) continue
|
|
209
237
|
seen.add(f)
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
238
|
+
const w = writers.get(f)
|
|
239
|
+
if (w !== undefined) {
|
|
240
|
+
found.push(w)
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
stack.push(...(importersOf.get(f) ?? []))
|
|
212
244
|
}
|
|
213
|
-
return ''
|
|
245
|
+
return found.length > 0 ? ` (${loadWriterReason(found)})` : ''
|
|
214
246
|
}
|
|
215
247
|
return {
|
|
216
248
|
name: 'brust-server-only',
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// F482: the newest `compatibility_date` the app's installed workerd runs. `wrangler dev` (and so `brust dev --target
|
|
2
|
+
// cloudflare`) refuses a date newer than that, while the default date is the @brust/core-cf build date, which can be newer.
|
|
3
|
+
// workerd's npm package carries its own answer: `lib/main.js` exports `compatibilityDate` (the date part of its version,
|
|
4
|
+
// `1.YYYYMMDD.n`). Read statically (the package is never imported).
|
|
5
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
6
|
+
import { dirname, join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
export interface WorkerdDate {
|
|
9
|
+
/** `YYYY-MM-DD`: the newest compatibility date this workerd supports. */
|
|
10
|
+
date: string
|
|
11
|
+
version: string
|
|
12
|
+
/** The package directory it was read from. */
|
|
13
|
+
dir: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The directory of package `name` as Node resolves it from `from`: the nearest `node_modules/<name>` up the tree
|
|
17
|
+
* (package managers that nest, like pnpm, keep a package's own dependencies beside its real directory). */
|
|
18
|
+
function findPackage(name: string, from: string): string | null {
|
|
19
|
+
for (let dir = from; ; dir = dirname(dir)) {
|
|
20
|
+
const p = join(dir, 'node_modules', name)
|
|
21
|
+
if (existsSync(join(p, 'package.json'))) return realpathSync(p)
|
|
22
|
+
if (dirname(dir) === dir) return null
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const readJson = (file: string): { version?: unknown } | null => {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(file, 'utf8')) as { version?: unknown }
|
|
29
|
+
} catch {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The compatibility date of the workerd in package directory `dir`, or null when it cannot be told. */
|
|
35
|
+
function dateOf(dir: string): WorkerdDate | null {
|
|
36
|
+
const version = String(readJson(join(dir, 'package.json'))?.version ?? '')
|
|
37
|
+
let main = ''
|
|
38
|
+
try {
|
|
39
|
+
const f = join(dir, 'lib', 'main.js')
|
|
40
|
+
if (statSync(f).isFile()) main = readFileSync(f, 'utf8')
|
|
41
|
+
} catch {}
|
|
42
|
+
const exported = /\bcompatibilityDate\s*=\s*"(\d{4}-\d{2}-\d{2})"/.exec(main)?.[1]
|
|
43
|
+
const v = /^1\.(\d{4})(\d{2})(\d{2})\./.exec(version)
|
|
44
|
+
const date = exported ?? (v ? `${v[1]}-${v[2]}-${v[3]}` : undefined)
|
|
45
|
+
return date ? { date, version, dir } : null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The workerd the app's wrangler runs: wrangler → miniflare → workerd, each resolved from the real directory of the one
|
|
49
|
+
* before it (so a nested install is the right copy); else a `workerd` the app resolves itself; else null. */
|
|
50
|
+
export function installedWorkerd(appRoot: string): WorkerdDate | null {
|
|
51
|
+
let from = appRoot
|
|
52
|
+
for (const name of ['wrangler', 'miniflare']) {
|
|
53
|
+
const dir = findPackage(name, from)
|
|
54
|
+
if (!dir) {
|
|
55
|
+
from = ''
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
from = dir
|
|
59
|
+
}
|
|
60
|
+
const viaWrangler = from ? findPackage('workerd', from) : null
|
|
61
|
+
const dir = viaWrangler ?? findPackage('workerd', appRoot)
|
|
62
|
+
return dir ? dateOf(dir) : null
|
|
63
|
+
}
|
package/src/build/cloudflare.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { type CfConfigJson, cfEntrySource } from './cf-entry'
|
|
|
14
14
|
import { type CoreCf, resolveCoreCf } from './cf-core'
|
|
15
15
|
import { type CacheNeed, mergeWrangler, readUserWrangler, renderWrangler, type UserWrangler, verifyCacheBindings } from './cf-wrangler'
|
|
16
16
|
import { type CloudflareBuildConfig, CF_MIN_COMPAT_DATE } from './config'
|
|
17
|
+
import { installedWorkerd } from './cf-workerd'
|
|
17
18
|
import { MANIFEST_VERSION } from './manifest'
|
|
18
19
|
import { exposePlugin, PRODUCTION, type SourceOf, sourceDirPlugin } from './bundle'
|
|
19
20
|
import type { Compiled } from './compile'
|
|
@@ -42,9 +43,25 @@ const DEFAULT_CFG: CloudflareBuildConfig = { name: undefined, compatibilityDate:
|
|
|
42
43
|
|
|
43
44
|
/** Everything that can fail on the app's own input or install, before a byte is written (index.ts rule). */
|
|
44
45
|
export async function prepareCloudflare(appRoot: string, cfg: CloudflareBuildConfig | null, flags: CfFlags): Promise<CfPrepared> {
|
|
45
|
-
const
|
|
46
|
+
const resolved = resolveCoreCf({ appRoot, fromSource: flags.fromSource, env: flags.env })
|
|
46
47
|
const user = await readUserWrangler(appRoot)
|
|
47
|
-
const warnings = [...
|
|
48
|
+
const warnings = [...resolved.warnings]
|
|
49
|
+
// F482: the default date is the core's build date, bounded by what the installed workerd runs (wrangler dev refuses a newer
|
|
50
|
+
// one). An explicit date is used as written, with a warning when it outruns workerd. Not resolvable: today's behaviour.
|
|
51
|
+
const workerd = installedWorkerd(appRoot)
|
|
52
|
+
let core = resolved
|
|
53
|
+
if (cfg?.compatibilityDate !== undefined) {
|
|
54
|
+
if (workerd && cfg.compatibilityDate > workerd.date)
|
|
55
|
+
warnings.push(
|
|
56
|
+
`[brust] warning cf-compat-date compatibility_date ${cfg.compatibilityDate} in brust.toml is newer than ${workerd.date}, the newest the installed workerd ${workerd.version} runs: \`wrangler dev\` and \`brust dev --target cloudflare\` will not start; lower it or upgrade wrangler`,
|
|
57
|
+
)
|
|
58
|
+
} else if (workerd && workerd.date < resolved.meta.compatibilityDate) {
|
|
59
|
+
if (workerd.date >= CF_MIN_COMPAT_DATE) core = { ...resolved, meta: { ...resolved.meta, compatibilityDate: workerd.date } }
|
|
60
|
+
else
|
|
61
|
+
warnings.push(
|
|
62
|
+
`[brust] warning cf-compat-date the installed workerd ${workerd.version} runs dates only up to ${workerd.date}, older than ${CF_MIN_COMPAT_DATE}, the oldest brust supports: the build keeps ${resolved.meta.compatibilityDate}, but \`wrangler dev\` will not start; upgrade wrangler`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
48
65
|
try {
|
|
49
66
|
const toml = Bun.file(join(appRoot, 'brust.toml'))
|
|
50
67
|
if (await toml.exists()) {
|
package/src/build/config.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
streamEntryError,
|
|
23
23
|
} from '../actions/prefix'
|
|
24
24
|
import { ADAPTER_NAMES, BUILTINS, installHint } from '../cache-adapter/load'
|
|
25
|
-
import { normalizeBasePath } from '../config'
|
|
25
|
+
import { checkTrailingSlash, normalizeBasePath, type TrailingSlash } from '../config'
|
|
26
26
|
import { BuildError } from './errors'
|
|
27
27
|
import { posixOf } from './posix'
|
|
28
28
|
|
|
@@ -111,6 +111,8 @@ export interface BuildConfig {
|
|
|
111
111
|
sourceRoots: string[]
|
|
112
112
|
/** `[server] base_path` (F495), normalised (`/portal`); `''` = the app is mounted at the root. Baked into every URL brust emits. */
|
|
113
113
|
basePath: string
|
|
114
|
+
/** `[server] trailing_slash` (F506), baked into the manifest; `strict` (the default) = the path is matched as written. */
|
|
115
|
+
trailingSlash: TrailingSlash
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
const isTable = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
@@ -414,10 +416,18 @@ function parseBasePath(file: string, server: unknown): string {
|
|
|
414
416
|
return r.value
|
|
415
417
|
}
|
|
416
418
|
|
|
419
|
+
/** `[server] trailing_slash` (F506): the one validator is config.ts's; a bad value is a build error here too. */
|
|
420
|
+
function parseTrailingSlash(file: string, server: unknown): TrailingSlash {
|
|
421
|
+
if (!isTable(server) || server.trailing_slash === undefined) return 'strict'
|
|
422
|
+
const r = checkTrailingSlash(server.trailing_slash)
|
|
423
|
+
if (!r.ok) throw new BuildError('config', `${file}: server.trailing_slash ${r.why}`)
|
|
424
|
+
return r.value
|
|
425
|
+
}
|
|
426
|
+
|
|
417
427
|
export async function loadBuildConfig(appRoot: string, o: { actionsOnly?: boolean } = {}): Promise<BuildConfig> {
|
|
418
428
|
const file = join(appRoot, 'brust.toml')
|
|
419
429
|
const f = Bun.file(file)
|
|
420
|
-
if (!(await f.exists())) return { serverOnly: [], actions: null, cloudflare: null, cache: null, cssGlobal: [], sourceRoots: [], basePath: '' }
|
|
430
|
+
if (!(await f.exists())) return { serverOnly: [], actions: null, cloudflare: null, cache: null, cssGlobal: [], sourceRoots: [], basePath: '', trailingSlash: 'strict' }
|
|
421
431
|
let parsed: unknown
|
|
422
432
|
try {
|
|
423
433
|
parsed = Bun.TOML.parse(await f.text())
|
|
@@ -426,17 +436,18 @@ export async function loadBuildConfig(appRoot: string, o: { actionsOnly?: boolea
|
|
|
426
436
|
}
|
|
427
437
|
const root = (parsed ?? {}) as Record<string, unknown>
|
|
428
438
|
const basePath = parseBasePath(file, root.server)
|
|
439
|
+
const trailingSlash = parseTrailingSlash(file, root.server)
|
|
429
440
|
const actions = parseActions(file, root.actions, o.actionsOnly === true, basePath)
|
|
430
441
|
const cloudflare = parseCloudflare(file, appRoot, root.cloudflare)
|
|
431
442
|
const cache = await parseCache(file, appRoot, root.cache)
|
|
432
443
|
const cssGlobal = parseCss(file, root.css)
|
|
433
444
|
const build = root.build
|
|
434
|
-
if (build === undefined) return { serverOnly: [], actions, cloudflare, cache, cssGlobal, sourceRoots: [], basePath }
|
|
445
|
+
if (build === undefined) return { serverOnly: [], actions, cloudflare, cache, cssGlobal, sourceRoots: [], basePath, trailingSlash }
|
|
435
446
|
if (build === null || typeof build !== 'object' || Array.isArray(build)) throw new BuildError('config', `${file}: [build] must be a table`)
|
|
436
447
|
const sourceRoots = parseSourceRoots(file, appRoot, (build as Record<string, unknown>).source_roots)
|
|
437
448
|
const so = (build as Record<string, unknown>).server_only
|
|
438
|
-
if (so === undefined) return { serverOnly: [], actions, cloudflare, cache, cssGlobal, sourceRoots, basePath }
|
|
449
|
+
if (so === undefined) return { serverOnly: [], actions, cloudflare, cache, cssGlobal, sourceRoots, basePath, trailingSlash }
|
|
439
450
|
if (!Array.isArray(so) || so.some((p) => typeof p !== 'string' || p.trim() === ''))
|
|
440
451
|
throw new BuildError('config', `${file}: build.server_only must be an array of non-empty strings`)
|
|
441
|
-
return { serverOnly: so.map((p: string) => posixOf(p.trim()).replace(/^\.\//, '')), actions, cloudflare, cache, cssGlobal, sourceRoots, basePath }
|
|
452
|
+
return { serverOnly: so.map((p: string) => posixOf(p.trim()).replace(/^\.\//, '')), actions, cloudflare, cache, cssGlobal, sourceRoots, basePath, trailingSlash }
|
|
442
453
|
}
|
package/src/build/css.ts
CHANGED
|
@@ -224,9 +224,9 @@ export async function buildCss(
|
|
|
224
224
|
if (hit === undefined) via.set(k, (hit = tailwindUse([k]).using.length > 0))
|
|
225
225
|
return hit
|
|
226
226
|
}
|
|
227
|
-
// F501:
|
|
228
|
-
const own = (o.ownOutput ?? []).map(
|
|
229
|
-
plugins.push(await loadTailwind(appRoot, tw.using[0]!, uses, o.log,
|
|
227
|
+
// F501/F505: brust's own output dirs, as real absolute paths: each is the base of its own negation in the scan.
|
|
228
|
+
const own = [...new Set((o.ownOutput ?? []).map(real))]
|
|
229
|
+
plugins.push(await loadTailwind(appRoot, tw.using[0]!, uses, o.log, own))
|
|
230
230
|
if (tw.imports.length > 1)
|
|
231
231
|
o.log?.(
|
|
232
232
|
`[brust] warning tailwind @import "tailwindcss" is in ${tw.imports.length} files (${tw.imports.map((f) => relative(appRoot, f)).join(', ')}): each one ships the whole of Tailwind; import it once (e.g. app.css) and use @reference elsewhere`,
|
package/src/build/index.ts
CHANGED
|
@@ -130,7 +130,7 @@ export async function runBuild(opts: {
|
|
|
130
130
|
process.env.BRUST_BASE_PATH = built.basePath
|
|
131
131
|
if (opts.actionsOnly)
|
|
132
132
|
return runActionsOnly({ appRoot, dist, config: built, target: opts.target, wasmFromSource: opts.wasmFromSource === true, statOk, assertSafeOutDir, log: opts.log })
|
|
133
|
-
const { serverOnly, cloudflare, cache, cssGlobal, sourceRoots, basePath } = built
|
|
133
|
+
const { serverOnly, cloudflare, cache, cssGlobal, sourceRoots, basePath, trailingSlash } = built
|
|
134
134
|
// L16 R16.4b: `[actions]` with a `base_url` and no actions file (the default name, not one written in the toml)
|
|
135
135
|
// is a page-only build: the app is deployed elsewhere, so nothing is bundled or mounted here.
|
|
136
136
|
const remote =
|
|
@@ -198,7 +198,7 @@ export async function runBuild(opts: {
|
|
|
198
198
|
rmSync(tmp, { recursive: true, force: true })
|
|
199
199
|
mkdirSync(tmp, { recursive: true })
|
|
200
200
|
try {
|
|
201
|
-
await buildInto(tmp, { dev: opts.dev === true, appRoot, entry, dist, leaves, componentFile, componentIssue, appModules, appGraph, serverOnly, actions, actionsEntry, cache, cacheModule, cf, hybrid, cssGlobal, sourceRoots, basePath, actionsRemote: remote?.prefix ?? null, log: opts.log })
|
|
201
|
+
await buildInto(tmp, { dev: opts.dev === true, appRoot, entry, dist, leaves, componentFile, componentIssue, appModules, appGraph, serverOnly, actions, actionsEntry, cache, cacheModule, cf, hybrid, cssGlobal, sourceRoots, basePath, trailingSlash, actionsRemote: remote?.prefix ?? null, log: opts.log })
|
|
202
202
|
if (ssgPlan) {
|
|
203
203
|
// Rendered from the finished temp dist, so a failed render leaves the previous dist untouched.
|
|
204
204
|
const version: string = (await Bun.file(join(import.meta.dir, '../../package.json')).json()).version
|
|
@@ -243,6 +243,8 @@ async function buildInto(
|
|
|
243
243
|
sourceRoots: string[]
|
|
244
244
|
/** `[server] base_path` (F495), `''` = the root. */
|
|
245
245
|
basePath: string
|
|
246
|
+
/** `[server] trailing_slash` (F506); `strict` = the default (not written). */
|
|
247
|
+
trailingSlash: import('../config').TrailingSlash
|
|
246
248
|
/** L16 R16.4b: the prefix of an actions app deployed elsewhere (a page-only build); absent = a local app or none. */
|
|
247
249
|
actionsRemote: string | null
|
|
248
250
|
/** `--target static` with `[actions]`: the worker is built without the cache (static pages are never cached). */
|
|
@@ -258,7 +260,7 @@ async function buildInto(
|
|
|
258
260
|
const serverCss = [o.actionsEntry, o.cacheModule].flatMap((f) => (f ? graph.of(f) : []))
|
|
259
261
|
mkdirSync(join(out, 'client'), { recursive: true })
|
|
260
262
|
// Review N3: `brust dev` builds with BRUST_BUILD_MODE=development (as the JS bundles): readable, unminified CSS.
|
|
261
|
-
const css = await buildCss(out, [...new Set([...[...routeCss.values()].flat(), ...serverCss])], process.env.BRUST_BUILD_MODE !== 'development', { appRoot, log: o.log, basePath: o.basePath, ownOutput: [o.dist, join(appRoot, '.brust')] })
|
|
263
|
+
const css = await buildCss(out, [...new Set([...[...routeCss.values()].flat(), ...serverCss])], process.env.BRUST_BUILD_MODE !== 'development', { appRoot, log: o.log, basePath: o.basePath, ownOutput: [o.dist, join(appRoot, 'dist'), join(appRoot, '.brust')] })
|
|
262
264
|
for (const c of directCss(entry))
|
|
263
265
|
o.log(`[brust] warning css ${relative(appRoot, entry)} imports ${c}: the route entry belongs to no route, so it is linked nowhere; import it from a layout, or list it in [css] global`)
|
|
264
266
|
const ctx: BrowserCtx = { sourceOf: new Map(), appRoot, serverOnly: o.serverOnly, serverFiles: o.actionsEntry ? [realpathSync(o.actionsEntry)] : [], cssMaps: css.maps, clientDir: join(out, 'client'), basePath: o.basePath }
|
|
@@ -297,8 +299,10 @@ async function buildInto(
|
|
|
297
299
|
o.log(`[brust] warning load-writer-in-client ${relPosix(appRoot, w.file)} is imported into the browser chunk of the React island ${c.id}: it changes ${mods} when it loads, and the island reads that module (F230). Its code ships to the browser: keep secrets and server-only code out of it, or write the value in the module that declares it`)
|
|
298
300
|
// F480: a server-only import reached through this writer names the chain in `server-only-in-client`.
|
|
299
301
|
ctx.loadWriters ??= new Map()
|
|
300
|
-
|
|
301
|
-
|
|
302
|
+
const lw = ctx.loadWriters.get(w.file) ?? { rel: relPosix(appRoot, w.file), islands: [], writes: [] }
|
|
303
|
+
if (!lw.islands.includes(c.id)) lw.islands.push(c.id)
|
|
304
|
+
for (const m of mods.split(', ')) if (!lw.writes.includes(m)) lw.writes.push(m)
|
|
305
|
+
ctx.loadWriters.set(w.file, lw)
|
|
302
306
|
}
|
|
303
307
|
return [...new Set(out.map((w) => w.file))]
|
|
304
308
|
}
|
|
@@ -359,6 +363,8 @@ async function buildInto(
|
|
|
359
363
|
await buildJobs(out, compiled, ctx.sourceOf, appRoot, storeModules(manifest), css.maps, routeRoots, reachedJobs(manifest), writers, o.basePath)
|
|
360
364
|
// F495: absent without a base path, so an app at the root writes the manifest it always did.
|
|
361
365
|
if (o.basePath !== '') manifest.base_path = o.basePath
|
|
366
|
+
// F506: absent when strict (the default), so an app that sets nothing writes the manifest it always did.
|
|
367
|
+
if (o.trailingSlash !== 'strict') manifest.trailing_slash = o.trailingSlash
|
|
362
368
|
manifest.actions =
|
|
363
369
|
o.actions && o.actionsEntry
|
|
364
370
|
? { entry: await buildActions(out, o.actionsEntry, css.maps, o.basePath), prefix: o.actions.prefix, maxBodyBytes: o.actions.maxBodyBytes, maxResponseBytes: o.actions.maxResponseBytes, crossSite: o.actions.crossSite, ...o.actions.stream }
|
package/src/build/manifest.ts
CHANGED
|
@@ -102,6 +102,8 @@ export interface ManifestJson {
|
|
|
102
102
|
actions_only?: true
|
|
103
103
|
/** F495 `[server] base_path`: set only for an app mounted under a path. The server answers under it alone and every URL brust owns lies below it. */
|
|
104
104
|
base_path?: string
|
|
105
|
+
/** F506 `[server] trailing_slash`: set only when it is not `strict` (`redirect` | `ignore`). */
|
|
106
|
+
trailing_slash?: 'redirect' | 'ignore'
|
|
105
107
|
/** L16 R16.4b: a page-only build: no local actions app, `base_url` points at the deployment that has it. */
|
|
106
108
|
actions_remote?: { prefix: string }
|
|
107
109
|
/** `[cache]` (cache adapters); absent without one (the manifest is then byte-identical). */
|
package/src/build/static.ts
CHANGED
|
@@ -177,6 +177,10 @@ export async function writeStatic(
|
|
|
177
177
|
o.log(w)
|
|
178
178
|
}
|
|
179
179
|
for (const w of assets.warnings) o.log(w)
|
|
180
|
+
// F506: a static host serves the files as it likes; brust's trailing-slash policy applies to `brust start` only.
|
|
181
|
+
const ts = (JSON.parse(readFileSync(join(dist, 'manifest.json'), 'utf8')) as { trailing_slash?: string }).trailing_slash
|
|
182
|
+
if (ts !== undefined && ts !== 'strict')
|
|
183
|
+
warn(`warning trailing-slash-static server.trailing_slash = ${JSON.stringify(ts)} does not apply to --target static: the static host decides how a path ending in '/' is served`)
|
|
180
184
|
|
|
181
185
|
// R9.4: [cache] does not apply to static files, so the render must not need its adapter (a Redis
|
|
182
186
|
// that is not running at build time): the render reads a manifest without `cache`, and the dist
|
package/src/build/tailwind.ts
CHANGED
|
@@ -68,8 +68,7 @@ const addCommand = (v: string) => `bun add -d --exact ${TAILWIND_PACKAGES.map((p
|
|
|
68
68
|
/** The Tailwind plugin of the CSS build, resolved from the app. Missing packages → `tailwind-missing` with the
|
|
69
69
|
* exact install command; versions that differ → one warning through `log` with the command that aligns them.
|
|
70
70
|
* `uses(file)` says whether a stylesheet needs the compiler (it, or a relative `@import` of it, uses Tailwind);
|
|
71
|
-
* other stylesheets stay Bun's. `ownOutput`:
|
|
72
|
-
* scan may read (F501). */
|
|
71
|
+
* other stylesheets stay Bun's. `ownOutput`: absolute directories of brust's own output that no scan may read (F501, F505). */
|
|
73
72
|
export async function loadTailwind(
|
|
74
73
|
appRoot: string,
|
|
75
74
|
usedBy: string,
|
|
@@ -112,9 +111,12 @@ export async function loadTailwind(
|
|
|
112
111
|
const compiler = await node.compile(css, { base: dirname(a.path), from: a.path, shouldRewriteUrls: true, onDependency: () => {} })
|
|
113
112
|
const root: SourceEntry[] =
|
|
114
113
|
compiler.root === 'none' ? [] : compiler.root === null ? [{ base: appRoot, pattern: '**/*', negated: false }] : [{ ...compiler.root, negated: false }]
|
|
115
|
-
// F501: brust's own output (the dist
|
|
114
|
+
// F501: brust's own output (the dist dirs, `.brust`) is never a source, whatever the ignore files say and whatever
|
|
116
115
|
// the stylesheet's `source(...)` and `@source` lines add: last, so a stale build can never feed the next one.
|
|
117
|
-
|
|
116
|
+
// F505: each directory is the BASE of its own negation (verified: oxide honours a negated entry whose base differs
|
|
117
|
+
// from the scanned root, a base outside the root, a base that does not exist yet, and a name with glob characters
|
|
118
|
+
// such as `dist[1]`, none of which a pattern relative to the app root would survive).
|
|
119
|
+
const own: SourceEntry[] = ownOutput.map((dir) => ({ base: dir, pattern: '**/*', negated: true }))
|
|
118
120
|
const scanner = new oxide.Scanner({ sources: [...root, ...compiler.sources, ...own] })
|
|
119
121
|
let out = compiler.build(scanner.scan())
|
|
120
122
|
// `@reference "./app.css"` pulls in Tailwind's license banner too; only the file that imports Tailwind ships it
|
package/src/config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Runtime configuration (spec §8). Precedence, high → low:
|
|
2
2
|
// env (BRUST_ADDR / BRUST_PORT / BRUST_WORKERS / BRUST_RENDER_SLOTS / BRUST_DRAIN_TIMEOUT_MS /
|
|
3
|
-
// BRUST_BOOT_TIMEOUT_MS / BRUST_CALL_TIMEOUT_MS / BRUST_SLOT_BYTES / BRUST_MAX_LOADER_RESPONSE_BYTES)
|
|
3
|
+
// BRUST_BOOT_TIMEOUT_MS / BRUST_CALL_TIMEOUT_MS / BRUST_SLOT_BYTES / BRUST_MAX_LOADER_RESPONSE_BYTES / BRUST_STATS)
|
|
4
4
|
// > CLI flags (`brust start --port/--workers`) and `run({ config })`
|
|
5
|
-
// > brust.toml ([server] address / port / base_path / max_loader_response_bytes, [workers] count / slot_bytes, [cors], [actions] base_url / credentials)
|
|
5
|
+
// > brust.toml ([server] address / port / base_path / stats / stats_token / max_loader_response_bytes, [workers] count / slot_bytes, [cors], [actions] base_url / credentials)
|
|
6
6
|
// > defaults (localhost, 1337, availableParallelism(), 1, 10000, 30000, 30000, 262144, 8388608).
|
|
7
7
|
// L16 R16.1: `actionsBaseUrl` follows env (BRUST_ACTIONS_BASE_URL) > run({ config }) > toml `[actions] base_url`.
|
|
8
8
|
// `cors` has no env layer; `run({ config: { cors } })` replaces a toml `[cors]` as a whole (never merged), and
|
|
@@ -24,6 +24,20 @@ export interface CorsConfig {
|
|
|
24
24
|
maxAgeSeconds?: number
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** F507 `[server] stats`: who may read `/_brust/cache/stats`. */
|
|
28
|
+
export const STATS_MODES = ['off', 'public', 'token'] as const
|
|
29
|
+
export type StatsMode = (typeof STATS_MODES)[number]
|
|
30
|
+
/** F507 `[server] stats_token`: an env reference only, never the secret itself (like `[cache.redis] url`). */
|
|
31
|
+
const STATS_TOKEN_REF = /^env:[A-Za-z_][A-Za-z0-9_]*$/
|
|
32
|
+
|
|
33
|
+
/** Why `[server] stats` / `stats_token` (or `run({ config })`) are wrong, or `undefined`. The secret never appears in the message. */
|
|
34
|
+
function statsError(mode: unknown, token: unknown, where: string): string | undefined {
|
|
35
|
+
if (mode !== undefined && !STATS_MODES.includes(mode as StatsMode)) return `${where}stats must be one of ${STATS_MODES.join(', ')} (got ${JSON.stringify(mode)})`
|
|
36
|
+
if (token !== undefined && (typeof token !== 'string' || !STATS_TOKEN_REF.test(token)))
|
|
37
|
+
return `${where}stats_token must be "env:NAME" (a literal token is refused: it would sit in a file)`
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
export interface BrustConfig {
|
|
28
42
|
host: string
|
|
29
43
|
port: number
|
|
@@ -47,6 +61,15 @@ export interface BrustConfig {
|
|
|
47
61
|
* root), absent when nothing set it. A build setting too (`brust build` bakes it into the manifest): `brust start` refuses a manifest
|
|
48
62
|
* built for another one, and with none set serves the one the manifest names. */
|
|
49
63
|
basePath?: string
|
|
64
|
+
/** F506 `[server] trailing_slash` / `run({ config: { trailingSlash } })`: what a page request whose path ends in `/` gets — `strict` (matched
|
|
65
|
+
* as written, the default), `redirect` (GET/HEAD answer 308 to the path without it, query kept) or `ignore` (the slash is dropped before
|
|
66
|
+
* matching). A build setting too (baked into the manifest): `brust start` refuses a manifest built with another one. */
|
|
67
|
+
trailingSlash?: TrailingSlash
|
|
68
|
+
/** F507 `[server] stats` / `run({ config: { stats } })`: who may read `/_brust/cache/stats` (`off` = 404, `public`, `token` = a bearer
|
|
69
|
+
* token). Absent = `off` for `brust start`, `public` under `brust dev`. */
|
|
70
|
+
stats?: StatsMode
|
|
71
|
+
/** F507 `[server] stats_token` / `run({ config: { statsToken } })`: `"env:NAME"`, the variable that holds the bearer token (`stats = "token"` only). */
|
|
72
|
+
statsToken?: string
|
|
50
73
|
/** L16 R16.1: the origin the browser `client()` calls (page script `__BRUST_ACTION_BASE__`); absent = same origin. */
|
|
51
74
|
actionsBaseUrl?: string
|
|
52
75
|
/** L16 R16.2: the client's default `RequestInit.credentials` (`__BRUST_ACTION_CREDENTIALS__`); absent = `same-origin`. */
|
|
@@ -56,6 +79,16 @@ export interface BrustConfig {
|
|
|
56
79
|
/** What `run({ config })` / `loadConfig`'s `cli` accepts: any setting, and `cors: null` to turn a toml `[cors]` off. */
|
|
57
80
|
export type ConfigOverrides = Omit<Partial<BrustConfig>, 'cors'> & { cors?: (Omit<CorsConfig, 'credentials'> & { credentials?: boolean }) | null }
|
|
58
81
|
|
|
82
|
+
/** F506: the `[server] trailing_slash` values. */
|
|
83
|
+
export const TRAILING_SLASH = ['strict', 'redirect', 'ignore'] as const
|
|
84
|
+
export type TrailingSlash = (typeof TRAILING_SLASH)[number]
|
|
85
|
+
|
|
86
|
+
/** F506: `[server] trailing_slash`, validated; the reason it is refused otherwise. */
|
|
87
|
+
export function checkTrailingSlash(raw: unknown): { ok: true; value: TrailingSlash } | { ok: false; why: string } {
|
|
88
|
+
if (typeof raw === 'string' && (TRAILING_SLASH as readonly string[]).includes(raw)) return { ok: true, value: raw as TrailingSlash }
|
|
89
|
+
return { ok: false, why: `must be one of ${TRAILING_SLASH.map((v) => JSON.stringify(v)).join(', ')} (got ${JSON.stringify(raw)})` }
|
|
90
|
+
}
|
|
91
|
+
|
|
59
92
|
/** F495: `[server] base_path`, validated: `/seg[/seg…]`, no trailing `/`, each segment `[A-Za-z0-9._~-]+` (not `.`/`..`), the
|
|
60
93
|
* same alphabet as the actions prefix, so it needs no percent-encoding anywhere it is printed. `""` and `"/"` mean the
|
|
61
94
|
* root (`""`). Returns the value, or the reason it is refused. */
|
|
@@ -116,6 +149,11 @@ export async function loadConfig(cwd: string = process.cwd(), cli: ConfigOverrid
|
|
|
116
149
|
checkCli(cli)
|
|
117
150
|
// cli (validated, normalised) over toml as a whole object (R10.2); `null` = off.
|
|
118
151
|
const cors = cli.cors === undefined ? fromToml.cors : cli.cors === null ? undefined : checkCors(cli.cors, 'config.cors', null, false)
|
|
152
|
+
// F507: the two keys belong together, wherever each one came from.
|
|
153
|
+
const stats = env.stats ?? cli.stats ?? fromToml.stats
|
|
154
|
+
const statsToken = cli.statsToken ?? fromToml.statsToken
|
|
155
|
+
if (stats === 'token' && statsToken === undefined) throw new BrustConfigError('stats = "token" needs stats_token = "env:NAME" (the variable that holds the bearer token)', null)
|
|
156
|
+
if (statsToken !== undefined && stats !== 'token') throw new BrustConfigError('stats_token is used only with stats = "token"', null)
|
|
119
157
|
return {
|
|
120
158
|
host: env.host ?? cli.host ?? fromToml.host ?? DEFAULTS.host,
|
|
121
159
|
port: env.port ?? cli.port ?? fromToml.port ?? DEFAULTS.port,
|
|
@@ -130,6 +168,9 @@ export async function loadConfig(cwd: string = process.cwd(), cli: ConfigOverrid
|
|
|
130
168
|
ioThreads: env.ioThreads ?? cli.ioThreads,
|
|
131
169
|
...(cors ? { cors } : {}),
|
|
132
170
|
...pick('basePath', cli.basePath ?? fromToml.basePath),
|
|
171
|
+
...pick('trailingSlash', cli.trailingSlash ?? fromToml.trailingSlash),
|
|
172
|
+
...pick('stats', stats),
|
|
173
|
+
...pick('statsToken', statsToken),
|
|
133
174
|
...pick('actionsBaseUrl', env.actionsBaseUrl ?? cli.actionsBaseUrl ?? fromToml.actionsBaseUrl),
|
|
134
175
|
...pick('actionsCredentials', cli.actionsCredentials ?? fromToml.actionsCredentials),
|
|
135
176
|
}
|
|
@@ -173,6 +214,12 @@ function checkCli(cli: ConfigOverrides): void {
|
|
|
173
214
|
if (!r.ok) throw new BrustConfigError(`config.basePath ${r.why}`, null)
|
|
174
215
|
cli.basePath = r.value
|
|
175
216
|
}
|
|
217
|
+
if (cli.trailingSlash !== undefined) {
|
|
218
|
+
const r = checkTrailingSlash(cli.trailingSlash)
|
|
219
|
+
if (!r.ok) throw new BrustConfigError(`config.trailingSlash ${r.why}`, null)
|
|
220
|
+
}
|
|
221
|
+
const badStats = statsError(cli.stats, cli.statsToken, 'config.')
|
|
222
|
+
if (badStats) throw new BrustConfigError(badStats, null)
|
|
176
223
|
if (cli.actionsBaseUrl !== undefined) {
|
|
177
224
|
const why = baseUrlError(cli.actionsBaseUrl)
|
|
178
225
|
if (why) throw new BrustConfigError(`config.actionsBaseUrl ${why}`, null)
|
|
@@ -204,6 +251,15 @@ function fromTomlTable(parsed: unknown, file: string): Partial<BrustConfig> {
|
|
|
204
251
|
if (!r.ok) throw new BrustConfigError(`${file}: server.base_path ${r.why}`, file)
|
|
205
252
|
out.basePath = r.value
|
|
206
253
|
}
|
|
254
|
+
if (server.trailing_slash !== undefined) {
|
|
255
|
+
const r = checkTrailingSlash(server.trailing_slash)
|
|
256
|
+
if (!r.ok) throw new BrustConfigError(`${file}: server.trailing_slash ${r.why}`, file)
|
|
257
|
+
out.trailingSlash = r.value
|
|
258
|
+
}
|
|
259
|
+
const bad = statsError(server.stats, server.stats_token, `${file}: server.`)
|
|
260
|
+
if (bad) throw new BrustConfigError(bad, file)
|
|
261
|
+
if (server.stats !== undefined) out.stats = server.stats as StatsMode
|
|
262
|
+
if (server.stats_token !== undefined) out.statsToken = server.stats_token as string
|
|
207
263
|
const cap = tomlInt(server.max_loader_response_bytes, 'server.max_loader_response_bytes', MIN_LOADER_RESPONSE_BYTES, MAX_LOADER_RESPONSE_BYTES, file)
|
|
208
264
|
if (cap !== undefined) out.maxLoaderResponseBytes = cap
|
|
209
265
|
if (port !== undefined) {
|
|
@@ -320,6 +376,11 @@ function fromEnv(): Partial<BrustConfig> {
|
|
|
320
376
|
out.slotBytes = envInt('BRUST_SLOT_BYTES', MIN_SLOT_BYTES, MAX_SLOT_BYTES, `an integer in ${MIN_SLOT_BYTES}..${MAX_SLOT_BYTES}`)
|
|
321
377
|
out.maxLoaderResponseBytes = envInt('BRUST_MAX_LOADER_RESPONSE_BYTES', MIN_LOADER_RESPONSE_BYTES, MAX_LOADER_RESPONSE_BYTES, `an integer in ${MIN_LOADER_RESPONSE_BYTES}..${MAX_LOADER_RESPONSE_BYTES}`)
|
|
322
378
|
out.ioThreads = envInt('BRUST_WORKER_THREADS', 1, MAX_IO_THREADS, `an integer in 1..${MAX_IO_THREADS}`)
|
|
379
|
+
const stats = process.env.BRUST_STATS
|
|
380
|
+
if (stats !== undefined && stats !== '') {
|
|
381
|
+
if (!STATS_MODES.includes(stats as StatsMode)) throw new BrustConfigError(`BRUST_STATS must be one of ${STATS_MODES.join(', ')} (got ${JSON.stringify(stats)})`, null)
|
|
382
|
+
out.stats = stats as StatsMode
|
|
383
|
+
}
|
|
323
384
|
const base = process.env.BRUST_ACTIONS_BASE_URL
|
|
324
385
|
if (base !== undefined && base !== '') {
|
|
325
386
|
const why = baseUrlError(base)
|
package/src/launcher-watch.ts
CHANGED
|
@@ -15,12 +15,63 @@ export function pidAlive(pid: number): boolean {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** F492 (PID reuse): the identity of a process that outlives a PID: its creation time (a FILETIME, 100 ns ticks since 1601), read once
|
|
19
|
+
* through kernel32 `GetProcessTimes` (`OpenProcess` with the query-limited right: no elevation, no process table, no PowerShell). A PID
|
|
20
|
+
* that is reused by another process has another creation time. `undefined` when it cannot be read (not Windows, the process is gone, no
|
|
21
|
+
* access, `bun:ffi` missing): the watch then keeps its pid-only behaviour. Synchronous, a few microseconds, nothing awaited. */
|
|
22
|
+
export function processIdentity(pid: number): string | undefined {
|
|
23
|
+
if (process.platform !== 'win32') return undefined
|
|
24
|
+
const k = kernel32()
|
|
25
|
+
if (!k) return undefined
|
|
26
|
+
try {
|
|
27
|
+
const h = k.OpenProcess(0x1000 /* PROCESS_QUERY_LIMITED_INFORMATION */, 0, pid)
|
|
28
|
+
if (!h) return undefined
|
|
29
|
+
try {
|
|
30
|
+
const t = new BigUint64Array(4) // creation, exit, kernel, user
|
|
31
|
+
const base = k.ptr(t)
|
|
32
|
+
if (!k.GetProcessTimes(h, base, k.ptr(t, 8), k.ptr(t, 16), k.ptr(t, 24))) return undefined
|
|
33
|
+
return t[0]!.toString()
|
|
34
|
+
} finally {
|
|
35
|
+
k.CloseHandle(h)
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Kernel32 = {
|
|
43
|
+
OpenProcess(access: number, inherit: number, pid: number): unknown
|
|
44
|
+
GetProcessTimes(h: unknown, a: unknown, b: unknown, c: unknown, d: unknown): number
|
|
45
|
+
CloseHandle(h: unknown): number
|
|
46
|
+
ptr: (view: ArrayBufferView, offset?: number) => never
|
|
47
|
+
}
|
|
48
|
+
let k32: Kernel32 | null | undefined
|
|
49
|
+
/** The three kernel32 bindings, opened ONCE for the life of the process: `dlopen` compiles a binding per symbol and holds it until
|
|
50
|
+
* the library is closed, and the watch reads the identity every tick of a long-running server. `null` = unavailable (kept). */
|
|
51
|
+
function kernel32(): Kernel32 | null {
|
|
52
|
+
if (k32 !== undefined) return k32
|
|
53
|
+
try {
|
|
54
|
+
const { dlopen, FFIType, ptr } = require('bun:ffi') as typeof import('bun:ffi')
|
|
55
|
+
const s = dlopen('kernel32.dll', {
|
|
56
|
+
OpenProcess: { args: [FFIType.u32, FFIType.i32, FFIType.u32], returns: FFIType.ptr },
|
|
57
|
+
GetProcessTimes: { args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
|
|
58
|
+
CloseHandle: { args: [FFIType.ptr], returns: FFIType.i32 },
|
|
59
|
+
}).symbols
|
|
60
|
+
k32 = { ...(s as unknown as Omit<Kernel32, 'ptr'>), ptr: ptr as Kernel32['ptr'] }
|
|
61
|
+
} catch {
|
|
62
|
+
k32 = null
|
|
63
|
+
}
|
|
64
|
+
return k32
|
|
65
|
+
}
|
|
66
|
+
|
|
18
67
|
export interface LauncherWatchOptions {
|
|
19
68
|
/** Runs once, when the parent is gone. */
|
|
20
69
|
onGone: (pid: number) => void
|
|
21
70
|
platform?: NodeJS.Platform
|
|
22
71
|
ppid?: number
|
|
23
72
|
alive?: (pid: number) => boolean
|
|
73
|
+
/** The parent's identity (default: its creation time, win32). Read once at boot and again on each tick while the pid answers alive: a different value means the pid was reused, so the launcher is gone. */
|
|
74
|
+
identity?: (pid: number) => string | undefined
|
|
24
75
|
intervalMs?: number
|
|
25
76
|
log?: (line: string) => void
|
|
26
77
|
}
|
|
@@ -35,8 +86,10 @@ export function watchLauncher(opts: LauncherWatchOptions): { pid: number | undef
|
|
|
35
86
|
;(opts.log ?? console.error)(`[brust] launcher (pid ${ppid}) is already gone at boot: not watching it`)
|
|
36
87
|
return none
|
|
37
88
|
}
|
|
89
|
+
const identity = opts.identity ?? processIdentity
|
|
90
|
+
const born = identity(ppid)
|
|
38
91
|
const timer = setInterval(() => {
|
|
39
|
-
if (alive(ppid)) return
|
|
92
|
+
if (alive(ppid) && (born === undefined || identity(ppid) === born)) return
|
|
40
93
|
clearInterval(timer)
|
|
41
94
|
opts.onGone(ppid)
|
|
42
95
|
}, opts.intervalMs ?? 1000)
|
package/src/native.ts
CHANGED
|
@@ -46,6 +46,10 @@ export interface StartOptions {
|
|
|
46
46
|
l2?: L2Options
|
|
47
47
|
/** `brust dev` only (L14): a request carrying this token (`x-brust-dev-token`) is traced; absent in production. */
|
|
48
48
|
devToken?: string
|
|
49
|
+
/** F507: who may read `/_brust/cache/stats`; absent = `off` (404). Boot error `stats: …` for a mode and token that do not go together. */
|
|
50
|
+
stats?: 'off' | 'public' | 'token'
|
|
51
|
+
/** F507: the secret `stats: 'token'` compares `Authorization: Bearer` with, resolved from its env reference; never logged. */
|
|
52
|
+
statsToken?: string
|
|
49
53
|
}
|
|
50
54
|
/** napi `NapiL2Options`; defaults 50 ms, 1048576 bytes. */
|
|
51
55
|
export interface L2Options {
|
package/src/run.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface RunManifest {
|
|
|
37
37
|
cache?: { adapter: string; getTimeoutMs: number; maxBodyBytes: number; namespace: string; buildId: string } | null
|
|
38
38
|
/** F495: the `[server] base_path` the build baked in; absent = the app is at the root. */
|
|
39
39
|
base_path?: string
|
|
40
|
+
/** F506: the `[server] trailing_slash` the build baked in; absent = `strict`. */
|
|
41
|
+
trailing_slash?: string
|
|
40
42
|
/** F165: `false` = no route can call a worker; absent (older manifests) = the pool is needed. */
|
|
41
43
|
needs_workers?: boolean
|
|
42
44
|
}
|
|
@@ -57,7 +59,9 @@ export function workerCount(cfg: BrustConfig, m: RunManifest): number {
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/** Pure: the napi `StartOptions`. No actions/cors/cache → exactly the pre-actions keys. */
|
|
60
|
-
export function startOptions(cfg: BrustConfig, distDir: string, m: RunManifest, version: string, devToken?: string): StartOptions {
|
|
62
|
+
export function startOptions(cfg: BrustConfig, distDir: string, m: RunManifest, version: string, devToken?: string, env: Record<string, string | undefined> = process.env): StartOptions {
|
|
63
|
+
// F507: `/_brust/cache/stats` is off unless set; `brust dev` (its server carries a dev token) defaults to public.
|
|
64
|
+
const stats = cfg.stats ?? (devToken ? 'public' : 'off')
|
|
61
65
|
return {
|
|
62
66
|
host: cfg.host,
|
|
63
67
|
port: cfg.port,
|
|
@@ -71,10 +75,19 @@ export function startOptions(cfg: BrustConfig, distDir: string, m: RunManifest,
|
|
|
71
75
|
...(actionsClient(cfg, m) ? { actionsClient: actionsClient(cfg, m) } : {}),
|
|
72
76
|
...(cfg.cors ? { cors: cfg.cors } : {}),
|
|
73
77
|
...(devToken ? { devToken } : {}),
|
|
78
|
+
...(stats !== 'off' ? { stats, ...(stats === 'token' ? { statsToken: statsTokenOf(cfg.statsToken, env) } : {}) } : {}),
|
|
74
79
|
...(m.cache ? { l2: { adapter: m.cache.adapter, getTimeoutMs: m.cache.getTimeoutMs, maxBodyBytes: m.cache.maxBodyBytes, namespace: m.cache.namespace, buildId: m.cache.buildId } } : {}),
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
/** F507: the bearer token `[server] stats_token = "env:NAME"` names; a boot error naming the variable (never a value) when it is unset or empty. */
|
|
84
|
+
function statsTokenOf(ref: string | undefined, env: Record<string, string | undefined>): string {
|
|
85
|
+
const name = (ref ?? '').replace(/^env:/, '')
|
|
86
|
+
const v = name ? env[name] : undefined
|
|
87
|
+
if (!v) throw new Error(`stats: [server] stats_token is "env:${name}" but ${name} is not set (or empty)`)
|
|
88
|
+
return v
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
/** L16: what the page tells the browser `client()` (R16.1/R16.2); absent while it would only repeat the prefix script. */
|
|
79
92
|
function actionsClient(cfg: BrustConfig, m: RunManifest): NonNullable<StartOptions['actionsClient']> | undefined {
|
|
80
93
|
const credentials = cfg.actionsCredentials !== undefined && cfg.actionsCredentials !== 'same-origin' ? cfg.actionsCredentials : undefined
|
|
@@ -92,6 +105,14 @@ export function basePathMismatch(cfg: Pick<BrustConfig, 'basePath'>, m: Pick<Run
|
|
|
92
105
|
return `base-path-mismatch: server.base_path is ${show(cfg.basePath)} but this build is for ${show(built)}; the base path is baked in at build time, so rebuild (brust build)`
|
|
93
106
|
}
|
|
94
107
|
|
|
108
|
+
/** F506: the message when the config names a trailing-slash policy other than the one the manifest was built with
|
|
109
|
+
* (`trailing-slash-mismatch`), else null. A config that sets none serves the manifest's. */
|
|
110
|
+
export function trailingSlashMismatch(cfg: Pick<BrustConfig, 'trailingSlash'>, m: Pick<RunManifest, 'trailing_slash'>): string | null {
|
|
111
|
+
const built = m.trailing_slash ?? 'strict'
|
|
112
|
+
if (cfg.trailingSlash === undefined || cfg.trailingSlash === built) return null
|
|
113
|
+
return `trailing-slash-mismatch: server.trailing_slash is ${JSON.stringify(cfg.trailingSlash)} but this build is for ${JSON.stringify(built)}; it is baked in at build time, so rebuild (brust build)`
|
|
114
|
+
}
|
|
115
|
+
|
|
95
116
|
/** Every worker's response buffer, kept reachable until the process exits (see run()). */
|
|
96
117
|
const workerBuffers: SharedArrayBuffer[] = []
|
|
97
118
|
|
|
@@ -122,7 +143,7 @@ export async function run(opts: RunOptions = {}): Promise<void> {
|
|
|
122
143
|
fail((e as Error).message)
|
|
123
144
|
}
|
|
124
145
|
|
|
125
|
-
const mismatch = basePathMismatch(cfg, m)
|
|
146
|
+
const mismatch = basePathMismatch(cfg, m) ?? trailingSlashMismatch(cfg, m)
|
|
126
147
|
if (mismatch) fail(mismatch)
|
|
127
148
|
// `basePath` of `@brust/core` in the app's modules (the workers inherit the env).
|
|
128
149
|
process.env.BRUST_BASE_PATH = m.base_path ?? ''
|
|
@@ -210,6 +231,8 @@ export async function run(opts: RunOptions = {}): Promise<void> {
|
|
|
210
231
|
// F485 P12: on Windows Ctrl+C (SIGINT) drains; SIGTERM handlers rarely fire and a kill ends the process without a drain (POSIX-only).
|
|
211
232
|
process.on('SIGINT', () => onSignal(130))
|
|
212
233
|
process.on('SIGTERM', () => onSignal(143))
|
|
234
|
+
// F492: a console Ctrl+Break (and closing the console) reaches Bun as SIGBREAK on Windows: drain like Ctrl+C.
|
|
235
|
+
if (process.platform === 'win32') process.on('SIGBREAK', () => onSignal(149))
|
|
213
236
|
|
|
214
237
|
// F488 (Windows only): a killed `bun run start` wrapper takes the shim (our parent) down but leaves this process running; drain when the
|
|
215
238
|
// parent is gone (same path as Ctrl+C). Synchronous, nothing awaited. Not for a one-shot (`opts.onReady`: a static render that exits itself).
|