@brust/core 2.0.0-alpha.3 → 2.0.0-alpha.30
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 +66 -6
- package/native/index.d.ts +63 -4
- package/native/index.js +57 -54
- package/package.json +29 -15
- package/src/actions/client.ts +53 -7
- package/src/actions/elysia.ts +1 -1
- package/src/actions/load.ts +24 -1
- package/src/actions/prefix.ts +24 -2
- package/src/actions-bridge.ts +2 -2
- package/src/assets.d.ts +55 -0
- package/src/browser.ts +6 -1
- package/src/build/actions-only.ts +86 -0
- package/src/build/build-eval.ts +332 -0
- package/src/build/build-icons.ts +69 -0
- package/src/build/bundle.ts +466 -54
- package/src/build/cf-assets.ts +4 -2
- package/src/build/cf-entry.ts +7 -3
- package/src/build/cf-wrangler.ts +3 -1
- package/src/build/cloudflare.ts +21 -11
- package/src/build/compile.ts +263 -6
- package/src/build/config.ts +85 -11
- package/src/build/css.ts +353 -0
- package/src/build/esm-resolve.ts +170 -0
- package/src/build/features.ts +119 -0
- package/src/build/index.ts +210 -27
- package/src/build/manifest.ts +88 -7
- package/src/build/pkg-src.ts +514 -0
- package/src/build/recognised.ts +19 -0
- package/src/build/scan.ts +71 -20
- package/src/build/server-only.ts +62 -0
- package/src/build/ssg-render.ts +65 -0
- package/src/build/ssg.ts +146 -0
- package/src/build/static-worker.ts +91 -0
- package/src/build/static.ts +259 -0
- package/src/build/tailwind.ts +117 -0
- package/src/build/tsconfig-paths.ts +67 -0
- package/src/build-time.ts +10 -0
- package/src/cache-adapter/types.ts +1 -1
- package/src/cf.ts +4 -1
- package/src/cli.ts +40 -4
- package/src/config.ts +111 -25
- package/src/css-modules.d.ts +8 -0
- package/src/dev/bun.ts +371 -0
- package/src/dev/client.ts +92 -0
- package/src/dev/devtools-data.ts +23 -0
- package/src/dev/devtools.ts +358 -0
- package/src/dev/diag.ts +30 -0
- package/src/dev/proxy.ts +236 -0
- package/src/dev/tui.ts +144 -0
- package/src/dev/watch.ts +130 -0
- package/src/dev.ts +15 -28
- package/src/handlers.ts +115 -9
- package/src/index.ts +6 -1
- package/src/native.ts +18 -0
- package/src/navigate-browser.ts +19 -0
- package/src/navigate.ts +19 -0
- package/src/navigation.ts +91 -0
- package/src/routes.ts +31 -1
- package/src/run.ts +53 -11
- package/src/shadcn/init.ts +513 -0
- package/src/shadcn/jsonc.ts +129 -0
- package/src/shadcn/registry.ts +149 -0
- package/src/store/builtin.ts +4 -0
- package/src/worker.ts +98 -22
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Tailwind CSS v4 for `brust build` (L32, F135, R33.1-R33.4): brust's own Bun plugin over the app's Tailwind.
|
|
2
|
+
// It does what the official integrations (@tailwindcss/vite, @tailwindcss/postcss) do: `compile()` from
|
|
3
|
+
// `@tailwindcss/node`, a `Scanner` from `@tailwindcss/oxide` over the compiler's sources (the app root unless the
|
|
4
|
+
// stylesheet says `source(...)`, plus every `@source` / `@source not`), then `build(candidates)`. Both packages are
|
|
5
|
+
// resolved from the APP, never bundled into or depended on by brust, so the compiler is the app's pinned
|
|
6
|
+
// `tailwindcss` (`@tailwindcss/node` depends on its own exact `tailwindcss`; the app installs all three at one
|
|
7
|
+
// version, and a mismatch is a loud warning with the command that aligns them).
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
import { dirname, relative } from 'node:path'
|
|
10
|
+
import type { BunPlugin } from 'bun'
|
|
11
|
+
import { BuildError } from './errors'
|
|
12
|
+
|
|
13
|
+
/** The install set (R33.2): one exact version for all three. */
|
|
14
|
+
export const TAILWIND_PACKAGES = ['tailwindcss', '@tailwindcss/node', '@tailwindcss/oxide'] as const
|
|
15
|
+
|
|
16
|
+
interface SourceEntry {
|
|
17
|
+
base: string
|
|
18
|
+
pattern: string
|
|
19
|
+
negated: boolean
|
|
20
|
+
}
|
|
21
|
+
interface Compiler {
|
|
22
|
+
root: 'none' | { base: string; pattern: string } | null
|
|
23
|
+
sources: SourceEntry[]
|
|
24
|
+
build(candidates: string[]): string
|
|
25
|
+
}
|
|
26
|
+
interface TailwindNode {
|
|
27
|
+
compile(css: string, o: { base: string; from?: string; shouldRewriteUrls?: boolean; onDependency: (path: string) => void }): Promise<Compiler>
|
|
28
|
+
}
|
|
29
|
+
interface TailwindOxide {
|
|
30
|
+
Scanner: new (o: { sources: SourceEntry[] }) => { scan(): string[] }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The `version` of `pkg` as resolved from `from` (a directory), or undefined when it does not resolve. */
|
|
34
|
+
function versionFrom(pkg: string, from: string): string | undefined {
|
|
35
|
+
try {
|
|
36
|
+
return (JSON.parse(readFileSync(Bun.resolveSync(`${pkg}/package.json`, from), 'utf8')) as { version?: string }).version
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The versions that decide what compiles (R33.3): the app's `tailwindcss`, the `tailwindcss` that
|
|
43
|
+
* `@tailwindcss/node` imports (the compiler), and the two integration packages. */
|
|
44
|
+
export function tailwindVersions(appRoot: string): { app?: string; compiler?: string; node?: string; oxide?: string } {
|
|
45
|
+
let compiler: string | undefined
|
|
46
|
+
try {
|
|
47
|
+
compiler = versionFrom('tailwindcss', dirname(Bun.resolveSync('@tailwindcss/node/package.json', appRoot)))
|
|
48
|
+
} catch {}
|
|
49
|
+
return { app: versionFrom('tailwindcss', appRoot), compiler, node: versionFrom('@tailwindcss/node', appRoot), oxide: versionFrom('@tailwindcss/oxide', appRoot) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The oldest Tailwind this plugin runs (review F1): 4.0's `compile()` has no `sources` (it takes globs), so its build
|
|
53
|
+
* would crash mid-way; 4.1.0 is the first with the API the official integrations use today. */
|
|
54
|
+
export const TAILWIND_FLOOR = '4.1.0'
|
|
55
|
+
/** The version brust is tested with (create-app pins the same); the command that lifts an app off an old Tailwind names it. */
|
|
56
|
+
export const TAILWIND_TESTED = '4.3.3'
|
|
57
|
+
|
|
58
|
+
/** `a < b` for plain `major.minor.patch` versions (a pre-release suffix counts as its base version). */
|
|
59
|
+
export function versionBelow(a: string, b: string): boolean {
|
|
60
|
+
const n = (v: string) => v.split('-')[0]!.split('.').map((x) => Number(x) || 0)
|
|
61
|
+
const [x, y] = [n(a), n(b)]
|
|
62
|
+
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) < (y[i] ?? 0)
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const addCommand = (v: string) => `bun add -d --exact ${TAILWIND_PACKAGES.map((p) => `${p}@${v}`).join(' ')}`
|
|
67
|
+
|
|
68
|
+
/** The Tailwind plugin of the CSS build, resolved from the app. Missing packages → `tailwind-missing` with the
|
|
69
|
+
* exact install command; versions that differ → one warning through `log` with the command that aligns them.
|
|
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. */
|
|
72
|
+
export async function loadTailwind(appRoot: string, usedBy: string, uses: (file: string) => boolean, log?: (s: string) => void): Promise<BunPlugin> {
|
|
73
|
+
const v = tailwindVersions(appRoot)
|
|
74
|
+
const missing = TAILWIND_PACKAGES.filter((p) => versionFrom(p, appRoot) === undefined)
|
|
75
|
+
if (missing.length > 0)
|
|
76
|
+
throw new BuildError(
|
|
77
|
+
'tailwind-missing',
|
|
78
|
+
`${relative(appRoot, usedBy)} uses Tailwind CSS, but ${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} not installed in the app: run \`${v.app ? addCommand(v.app) : `bun add -d ${TAILWIND_PACKAGES.map((p) => `${p}@4`).join(' ')}`}\``,
|
|
79
|
+
)
|
|
80
|
+
const old = (
|
|
81
|
+
[
|
|
82
|
+
['tailwindcss', v.app],
|
|
83
|
+
['the compiler tailwindcss', v.compiler],
|
|
84
|
+
['@tailwindcss/node', v.node],
|
|
85
|
+
['@tailwindcss/oxide', v.oxide],
|
|
86
|
+
] as const
|
|
87
|
+
).filter(([, ver]) => ver !== undefined && versionBelow(ver, TAILWIND_FLOOR))
|
|
88
|
+
if (old.length > 0)
|
|
89
|
+
throw new BuildError(
|
|
90
|
+
'tailwind-version',
|
|
91
|
+
`${relative(appRoot, usedBy)} uses Tailwind CSS, but ${old.map(([p, ver]) => `${p} ${ver}`).join(', ')} ${old.length > 1 ? 'are' : 'is'} older than ${TAILWIND_FLOOR}, the oldest brust builds with: run \`${addCommand(TAILWIND_TESTED)}\``,
|
|
92
|
+
)
|
|
93
|
+
if (new Set([v.app, v.compiler, v.node, v.oxide]).size > 1)
|
|
94
|
+
log?.(
|
|
95
|
+
`[brust] warning tailwind-version the app has tailwindcss ${v.app}, but the compiler is tailwindcss ${v.compiler} (@tailwindcss/node ${v.node}, @tailwindcss/oxide ${v.oxide}): install one version of all three: ${addCommand(v.app!)}`,
|
|
96
|
+
)
|
|
97
|
+
const node = (await import(Bun.resolveSync('@tailwindcss/node', appRoot))) as TailwindNode
|
|
98
|
+
const oxide = (await import(Bun.resolveSync('@tailwindcss/oxide', appRoot))) as TailwindOxide
|
|
99
|
+
return {
|
|
100
|
+
name: 'brust-tailwind',
|
|
101
|
+
setup(b) {
|
|
102
|
+
b.onLoad({ filter: /\.css$/ }, async (a) => {
|
|
103
|
+
if (!uses(a.path)) return undefined // Bun's own loader (CSS Modules included)
|
|
104
|
+
const css = readFileSync(a.path, 'utf8')
|
|
105
|
+
const compiler = await node.compile(css, { base: dirname(a.path), from: a.path, shouldRewriteUrls: true, onDependency: () => {} })
|
|
106
|
+
const root: SourceEntry[] =
|
|
107
|
+
compiler.root === 'none' ? [] : compiler.root === null ? [{ base: appRoot, pattern: '**/*', negated: false }] : [{ ...compiler.root, negated: false }]
|
|
108
|
+
const scanner = new oxide.Scanner({ sources: [...root, ...compiler.sources] })
|
|
109
|
+
let out = compiler.build(scanner.scan())
|
|
110
|
+
// `@reference "./app.css"` pulls in Tailwind's license banner too; only the file that imports Tailwind ships it
|
|
111
|
+
// (one banner per page, as before)
|
|
112
|
+
if (!/@import\s+(?:url\(\s*)?["']tailwindcss["']/.test(css)) out = out.replace(/\/\*! tailwindcss v[^*]*\*\/\s*/g, '')
|
|
113
|
+
return { contents: out, loader: 'css' }
|
|
114
|
+
})
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// `tsconfig` `paths` aliases (shadcn S1, R34.76): `import … from "@/components/ui/button"` is a file of the app. The compiler
|
|
2
|
+
// resolves the specifiers that match an alias to the relative import of the file they name, from root-relative templates
|
|
3
|
+
// this module reads out of the app's own `tsconfig.json` / `jsconfig.json`. v1 reads that one file: no `extends`, no
|
|
4
|
+
// `references` (a named fallback below); exact and single-`*` patterns only. The bundler resolves the same specifiers
|
|
5
|
+
// natively; `checkAliasAgreement` fails the build if the two ever disagree.
|
|
6
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
7
|
+
import { dirname, isAbsolute, join, normalize, relative, sep } from 'node:path'
|
|
8
|
+
import { BuildError } from './errors'
|
|
9
|
+
|
|
10
|
+
export interface PathAlias {
|
|
11
|
+
pattern: string
|
|
12
|
+
targets: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const FILES = ['tsconfig.json', 'jsconfig.json']
|
|
16
|
+
|
|
17
|
+
/** The aliases of the app's tsconfig, as root-relative `/`-separated templates; `notes` says what was left out and why. */
|
|
18
|
+
export function readPathAliases(appRoot: string): { aliases: PathAlias[]; notes: string[] } {
|
|
19
|
+
const file = FILES.map((f) => join(appRoot, f)).find((f) => existsSync(f))
|
|
20
|
+
if (!file) return { aliases: [], notes: [] }
|
|
21
|
+
let json: { extends?: unknown; references?: unknown; compilerOptions?: { baseUrl?: unknown; paths?: unknown } }
|
|
22
|
+
try {
|
|
23
|
+
json = Bun.JSONC.parse(readFileSync(file, 'utf8')) as typeof json
|
|
24
|
+
} catch {
|
|
25
|
+
return { aliases: [], notes: [`${relative(appRoot, file)} could not be parsed; its \`paths\` aliases are not followed`] }
|
|
26
|
+
}
|
|
27
|
+
const notes: string[] = []
|
|
28
|
+
const paths = json?.compilerOptions?.paths
|
|
29
|
+
if (json?.extends !== undefined || json?.references !== undefined)
|
|
30
|
+
notes.push(
|
|
31
|
+
`${relative(appRoot, file)} uses \`${json.extends !== undefined ? 'extends' : 'references'}\`, which brust does not follow: \`paths\` aliases written in another tsconfig stay package imports (put \`paths\` in ${relative(appRoot, file)})`,
|
|
32
|
+
)
|
|
33
|
+
if (!paths || typeof paths !== 'object') return { aliases: [], notes }
|
|
34
|
+
const base = typeof json.compilerOptions?.baseUrl === 'string' ? json.compilerOptions.baseUrl : '.'
|
|
35
|
+
const aliases: PathAlias[] = []
|
|
36
|
+
for (const [pattern, raw] of Object.entries(paths as Record<string, unknown>)) {
|
|
37
|
+
const stars = pattern.split('*').length - 1
|
|
38
|
+
if (stars > 1 || !Array.isArray(raw)) {
|
|
39
|
+
notes.push(`the \`paths\` pattern "${pattern}" is not an exact or single-\`*\` pattern; it is not followed`)
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
const targets: string[] = []
|
|
43
|
+
for (const t of raw) {
|
|
44
|
+
if (typeof t !== 'string' || (t.split('*').length - 1) > 1 || isAbsolute(t)) continue
|
|
45
|
+
const rel = normalize(join(base, t)).split(sep).join('/')
|
|
46
|
+
// Outside the app sources, or a package: not a file the compiler reads.
|
|
47
|
+
if (rel.startsWith('..') || rel.includes('node_modules/')) continue
|
|
48
|
+
targets.push(rel)
|
|
49
|
+
}
|
|
50
|
+
if (targets.length > 0) aliases.push({ pattern, targets })
|
|
51
|
+
}
|
|
52
|
+
return { aliases, notes }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The compiler's answer for every alias import must be the file the bundler resolves the same specifier to. */
|
|
56
|
+
export function checkAliasAgreement(appRoot: string, imports: { from: string; spec: string; target: string }[]): void {
|
|
57
|
+
for (const { from, spec, target } of imports) {
|
|
58
|
+
let got: string
|
|
59
|
+
try {
|
|
60
|
+
got = realpathSync(Bun.resolveSync(spec, dirname(join(appRoot, from))))
|
|
61
|
+
} catch {
|
|
62
|
+
throw new BuildError('alias-mismatch', `${from}: the compiler resolves "${spec}" to ${target}, but the bundler cannot resolve it (check tsconfig \`paths\`)`)
|
|
63
|
+
}
|
|
64
|
+
if (got !== realpathSync(join(appRoot, target)))
|
|
65
|
+
throw new BuildError('alias-mismatch', `${from}: "${spec}" resolves to ${target} for the compiler but to ${relative(appRoot, got)} for the bundler (tsconfig \`paths\` is read differently); fix the alias or use a relative import`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// `buildTime(() => …)`: the explicit opt-in to evaluate a pure call once, at `brust build` (L2/H1).
|
|
2
|
+
// The compiler recognises the call only when it is imported from `@brust/core`; the closure may be
|
|
3
|
+
// `async` and may use package imports. At runtime (a per-request job, the server) it just runs the
|
|
4
|
+
// closure, so behaviour is identical wherever it is not folded away at build. It is a build error
|
|
5
|
+
// in code that reaches the browser.
|
|
6
|
+
|
|
7
|
+
/** Runs `fn`. At build the compiler evaluates it once and bakes the result into the template. */
|
|
8
|
+
export function buildTime<T>(fn: () => T): T {
|
|
9
|
+
return fn()
|
|
10
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The cache adapter contract (
|
|
1
|
+
// The cache adapter contract (cache adapter design §3.1): a shared second
|
|
2
2
|
// tier behind brust's in-process cache. An adapter has two optional capabilities — a `store` of
|
|
3
3
|
// rendered pages and a `broadcast` of invalidations (push `subscribe` or pull `since`) — so a store
|
|
4
4
|
// without a channel (KV) can be composed with another adapter's broadcast (DO).
|
package/src/cf.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// `@brust/core` under the `workerd` export condition (M4-CF, ruling c30e2633): the app-facing API a Worker can run.
|
|
2
2
|
// Same names as `index.ts` minus what needs a host process: no `run`, no `loadConfig`. `cache.invalidate` goes through
|
|
3
3
|
// the CF bundle's napi stub to the runtime the wasm glue installed at start (`server-handlers` `install`).
|
|
4
|
+
export { buildTime } from './build-time'
|
|
5
|
+
export { navigate } from './navigate'
|
|
6
|
+
export type { NavErrorDetail, NavLoadDetail, NavigateOptions, NavSuccessDetail, NavTrigger } from './navigate'
|
|
4
7
|
export { cache } from './cache'
|
|
5
8
|
export type { ComponentCacheOptions, InvalidateArgs, InvalidateResult } from './cache'
|
|
6
9
|
export {
|
|
@@ -15,4 +18,4 @@ export {
|
|
|
15
18
|
Outlet,
|
|
16
19
|
redirect,
|
|
17
20
|
} from './routes'
|
|
18
|
-
export type { ComponentType, FlatRoute, HttpErrorOpts, HttpErrorTrigger, LoaderCtx, LoaderReq, Route, RouteCacheConfig, Verdict } from './routes'
|
|
21
|
+
export type { ComponentType, FlatRoute, HttpErrorOpts, HttpErrorTrigger, LoaderCtx, LoaderReq, Route, RouteCacheConfig, RouteSsgConfig, Verdict } from './routes'
|
package/src/cli.ts
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
// Exit codes: 0 ok, 1 build/boot error (`error <rule> <message>` / `[brust] …`), 2 usage.
|
|
4
4
|
import { parseArgs } from 'node:util'
|
|
5
5
|
import { BuildError, runBuild } from './build'
|
|
6
|
+
import { runBunDev } from './dev/bun'
|
|
6
7
|
import { runDev } from './dev'
|
|
7
8
|
import { run } from './run'
|
|
8
9
|
|
|
9
10
|
const USAGE = `usage:
|
|
10
|
-
brust build [entry=routes.tsx] [--out-dir dist] [--force] [--target bun|cloudflare] [--wasm-from-source]
|
|
11
|
+
brust build [entry=routes.tsx] [--out-dir dist] [--force] [--target bun|cloudflare|static] [--actions-only] [--strict] [--wasm-from-source] [--dev]
|
|
12
|
+
brust dev [entry=routes.tsx] [--port N] [--workers N] (Bun target: proxy + hot reload + inline TUI)
|
|
11
13
|
brust dev [entry=routes.tsx] --target cloudflare [--out-dir dist] [--port N] [--wasm-from-source] [-- <wrangler args>]
|
|
12
14
|
brust start [--port N] [--workers N] [--dist-dir dist] [--entry routes.tsx]
|
|
15
|
+
brust shadcn init [component ...] [--force] [--css <file>] [--base-color <name>] [--shadcn-version <x.y.z>]
|
|
13
16
|
|
|
14
17
|
Config precedence for start: env (BRUST_ADDR, BRUST_PORT, BRUST_WORKERS, BRUST_RENDER_SLOTS,
|
|
15
18
|
BRUST_DRAIN_TIMEOUT_MS, BRUST_BOOT_TIMEOUT_MS, BRUST_DIST_DIR, BRUST_APP_ENTRY) > flags
|
|
@@ -51,8 +54,14 @@ export async function main(argv: string[]): Promise<void> {
|
|
|
51
54
|
port: { type: 'string' },
|
|
52
55
|
workers: { type: 'string' },
|
|
53
56
|
force: { type: 'boolean' },
|
|
57
|
+
css: { type: 'string' },
|
|
58
|
+
'base-color': { type: 'string' },
|
|
59
|
+
'shadcn-version': { type: 'string' },
|
|
60
|
+
strict: { type: 'boolean' },
|
|
54
61
|
target: { type: 'string' },
|
|
55
62
|
'wasm-from-source': { type: 'boolean' },
|
|
63
|
+
'actions-only': { type: 'boolean' },
|
|
64
|
+
dev: { type: 'boolean' },
|
|
56
65
|
help: { type: 'boolean', short: 'h' },
|
|
57
66
|
},
|
|
58
67
|
})
|
|
@@ -66,7 +75,10 @@ export async function main(argv: string[]): Promise<void> {
|
|
|
66
75
|
}
|
|
67
76
|
const str = (k: string) => values[k] as string | undefined
|
|
68
77
|
const target = str('target')
|
|
69
|
-
if (target !== undefined && target !== 'bun' && target !== 'cloudflare'
|
|
78
|
+
if (target !== undefined && target !== 'bun' && target !== 'cloudflare' && target !== 'static')
|
|
79
|
+
usage(`--target must be bun, cloudflare or static (got ${JSON.stringify(target)})`)
|
|
80
|
+
if (values['actions-only'] === true && cmd !== 'build') usage('--actions-only only applies to brust build')
|
|
81
|
+
if (values.strict === true && (cmd !== 'build' || target !== 'static')) usage('--strict only applies to brust build --target static')
|
|
70
82
|
|
|
71
83
|
if (cmd === 'build') {
|
|
72
84
|
if (positionals.length > 1) usage(`build takes one entry (got ${positionals.join(' ')})`)
|
|
@@ -77,8 +89,11 @@ export async function main(argv: string[]): Promise<void> {
|
|
|
77
89
|
entry: positionals[0] ?? 'routes.tsx',
|
|
78
90
|
outDir: str('out-dir') ?? 'dist',
|
|
79
91
|
force: values.force === true,
|
|
80
|
-
target: target as 'bun' | 'cloudflare' | undefined,
|
|
92
|
+
target: target as 'bun' | 'cloudflare' | 'static' | undefined,
|
|
93
|
+
strict: values.strict === true,
|
|
81
94
|
wasmFromSource: values['wasm-from-source'] === true,
|
|
95
|
+
actionsOnly: values['actions-only'] === true,
|
|
96
|
+
dev: values.dev === true,
|
|
82
97
|
log: (s) => console.log(s),
|
|
83
98
|
})
|
|
84
99
|
} catch (e) {
|
|
@@ -92,8 +107,12 @@ export async function main(argv: string[]): Promise<void> {
|
|
|
92
107
|
}
|
|
93
108
|
if (cmd === 'dev') {
|
|
94
109
|
if (positionals.length > 1) usage(`dev takes one entry (got ${positionals.join(' ')})`)
|
|
95
|
-
if (target !== 'cloudflare') usage('brust dev: only --target cloudflare in this release')
|
|
96
110
|
const port = intFlag('port', str('port'), 0)
|
|
111
|
+
if (target !== 'cloudflare') {
|
|
112
|
+
const workers = intFlag('workers', str('workers'), 1)
|
|
113
|
+
if (passthrough.length > 0) usage('brust dev: arguments after -- are for --target cloudflare (wrangler)')
|
|
114
|
+
process.exit(await runBunDev({ appRoot: process.cwd(), entry: positionals[0] ?? 'routes.tsx', ...(port !== undefined && { port }), ...(workers !== undefined && { workers }) }))
|
|
115
|
+
}
|
|
97
116
|
try {
|
|
98
117
|
process.exit(
|
|
99
118
|
await runDev({
|
|
@@ -112,6 +131,23 @@ export async function main(argv: string[]): Promise<void> {
|
|
|
112
131
|
process.exit(1)
|
|
113
132
|
}
|
|
114
133
|
}
|
|
134
|
+
if (cmd === 'shadcn') {
|
|
135
|
+
const [sub, ...components] = positionals
|
|
136
|
+
if (sub !== 'init') usage(`shadcn takes a subcommand: init (got ${sub === undefined ? 'none' : JSON.stringify(sub)})`)
|
|
137
|
+
// F252: loaded here, so no other command pays for it (it pulls in tsconfig-paths)
|
|
138
|
+
const { shadcnInit, spawnRunner } = await import('./shadcn/init')
|
|
139
|
+
const code = await shadcnInit({
|
|
140
|
+
root: process.cwd(),
|
|
141
|
+
components,
|
|
142
|
+
force: values.force === true,
|
|
143
|
+
...(str('css') !== undefined && { css: str('css') }),
|
|
144
|
+
...(str('base-color') !== undefined && { baseColor: str('base-color') }),
|
|
145
|
+
...(str('shadcn-version') !== undefined && { shadcnVersion: str('shadcn-version') }),
|
|
146
|
+
run: spawnRunner,
|
|
147
|
+
out: (s) => console.log(s),
|
|
148
|
+
})
|
|
149
|
+
process.exit(code)
|
|
150
|
+
}
|
|
115
151
|
if (cmd === 'start') {
|
|
116
152
|
if (positionals.length > 0) usage(`start takes no positional arguments (got ${positionals.join(' ')})`)
|
|
117
153
|
const port = intFlag('port', str('port'), 0)
|
package/src/config.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
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)
|
|
4
|
-
// > CLI flags (`brust start --port/--workers`)
|
|
5
|
-
// > brust.toml ([server] address / port, [workers] count)
|
|
6
|
-
// > defaults (localhost, 1337, availableParallelism(), 1, 10000, 30000, 30000).
|
|
3
|
+
// BRUST_BOOT_TIMEOUT_MS / BRUST_CALL_TIMEOUT_MS / BRUST_SLOT_BYTES / BRUST_MAX_LOADER_RESPONSE_BYTES)
|
|
4
|
+
// > CLI flags (`brust start --port/--workers`) and `run({ config })`
|
|
5
|
+
// > brust.toml ([server] address / port / max_loader_response_bytes, [workers] count / slot_bytes, [cors], [actions] base_url / credentials)
|
|
6
|
+
// > defaults (localhost, 1337, availableParallelism(), 1, 10000, 30000, 30000, 262144, 8388608).
|
|
7
|
+
// L16 R16.1: `actionsBaseUrl` follows env (BRUST_ACTIONS_BASE_URL) > run({ config }) > toml `[actions] base_url`.
|
|
8
|
+
// `cors` has no env layer; `run({ config: { cors } })` replaces a toml `[cors]` as a whole (never merged), and
|
|
9
|
+
// `cors: null` turns a toml `[cors]` off.
|
|
7
10
|
// `brust start`'s BRUST_DIST_DIR / BRUST_APP_ENTRY follow the same env > flag rule (run.ts).
|
|
8
11
|
// A missing brust.toml is fine; a present one with the wrong shape is an error. Messages follow
|
|
9
12
|
// 0.1.x `runtime/config.ts`.
|
|
10
13
|
import { availableParallelism } from 'node:os'
|
|
11
14
|
import { join } from 'node:path'
|
|
15
|
+
import { ACTION_CREDENTIALS, type ActionCredentials, baseUrlError } from './actions/prefix'
|
|
12
16
|
|
|
13
17
|
/** `[cors]` in brust.toml; same shape and validation as the Rust `CorsConfig` (crates/brust-server/src/config.rs). */
|
|
14
18
|
export interface CorsConfig {
|
|
@@ -32,10 +36,22 @@ export interface BrustConfig {
|
|
|
32
36
|
callTimeoutMs: number
|
|
33
37
|
/** tokio threads of the HTTP runtime (renders every page); absent = one per core (server default). */
|
|
34
38
|
ioThreads?: number
|
|
35
|
-
/**
|
|
39
|
+
/** The SAB response slot of one render slot, bytes (`slot_bytes`, env BRUST_SLOT_BYTES); 64 KiB..16 MiB. */
|
|
40
|
+
slotBytes: number
|
|
41
|
+
/** The biggest loader/jobs reply that crosses by heap when it does not fit the slot (`max_loader_response_bytes`,
|
|
42
|
+
* env BRUST_MAX_LOADER_RESPONSE_BYTES); 256 KiB..64 MiB. */
|
|
43
|
+
maxLoaderResponseBytes: number
|
|
44
|
+
/** `run({ config: { cors } })` over brust.toml `[cors]` (a whole-object replace); absent = CORS off. No env layer. */
|
|
36
45
|
cors?: CorsConfig
|
|
46
|
+
/** L16 R16.1: the origin the browser `client()` calls (page script `__BRUST_ACTION_BASE__`); absent = same origin. */
|
|
47
|
+
actionsBaseUrl?: string
|
|
48
|
+
/** L16 R16.2: the client's default `RequestInit.credentials` (`__BRUST_ACTION_CREDENTIALS__`); absent = `same-origin`. */
|
|
49
|
+
actionsCredentials?: ActionCredentials
|
|
37
50
|
}
|
|
38
51
|
|
|
52
|
+
/** What `run({ config })` / `loadConfig`'s `cli` accepts: any setting, and `cors: null` to turn a toml `[cors]` off. */
|
|
53
|
+
export type ConfigOverrides = Omit<Partial<BrustConfig>, 'cors'> & { cors?: (Omit<CorsConfig, 'credentials'> & { credentials?: boolean }) | null }
|
|
54
|
+
|
|
39
55
|
export class BrustConfigError extends Error {
|
|
40
56
|
override name = 'BrustConfigError'
|
|
41
57
|
constructor(
|
|
@@ -46,11 +62,11 @@ export class BrustConfigError extends Error {
|
|
|
46
62
|
}
|
|
47
63
|
}
|
|
48
64
|
|
|
49
|
-
const DEFAULTS = { host: 'localhost', port: 1337, drainTimeoutMs: 10_000, bootTimeoutMs: 30_000, callTimeoutMs: 30_000 }
|
|
65
|
+
const DEFAULTS = { host: 'localhost', port: 1337, drainTimeoutMs: 10_000, bootTimeoutMs: 30_000, callTimeoutMs: 30_000, slotBytes: 256 * 1024, maxLoaderResponseBytes: 8_388_608 }
|
|
50
66
|
|
|
51
67
|
/** Ceilings: ms values cross napi as `u32` (a larger number would wrap), more than 1024 Bun
|
|
52
|
-
* workers is a misconfiguration, not a deployment, and each render slot is a 256 KiB
|
|
53
|
-
* response buffer per worker (more than 64 would only exhaust memory). */
|
|
68
|
+
* workers is a misconfiguration, not a deployment, and each render slot is a `slotBytes` (default 256 KiB)
|
|
69
|
+
* shared response buffer per worker (more than 64 would only exhaust memory). */
|
|
54
70
|
export const MAX_MS = 0xffff_ffff
|
|
55
71
|
/** `max_age_seconds` crosses napi as `u32`, like the ms values. */
|
|
56
72
|
const MAX_U32 = 0xffff_ffff
|
|
@@ -58,8 +74,14 @@ const MAX_U32 = 0xffff_ffff
|
|
|
58
74
|
export const MAX_IO_THREADS = 256
|
|
59
75
|
export const MAX_WORKERS = 1024
|
|
60
76
|
export const MAX_RENDER_SLOTS = 64
|
|
77
|
+
/** `slotBytes` (L10): 64 KiB..16 MiB. */
|
|
78
|
+
export const MIN_SLOT_BYTES = 64 * 1024
|
|
79
|
+
export const MAX_SLOT_BYTES = 16 * 1024 * 1024
|
|
80
|
+
/** `maxLoaderResponseBytes` (L10): 256 KiB..64 MiB, like the actions limits. */
|
|
81
|
+
export const MIN_LOADER_RESPONSE_BYTES = 256 * 1024
|
|
82
|
+
export const MAX_LOADER_RESPONSE_BYTES = 64 * 1024 * 1024
|
|
61
83
|
|
|
62
|
-
export async function loadConfig(cwd: string = process.cwd(), cli:
|
|
84
|
+
export async function loadConfig(cwd: string = process.cwd(), cli: ConfigOverrides = {}): Promise<BrustConfig> {
|
|
63
85
|
const tomlPath = join(cwd, 'brust.toml')
|
|
64
86
|
let fromToml: Partial<BrustConfig> = {}
|
|
65
87
|
const file = Bun.file(tomlPath)
|
|
@@ -75,6 +97,8 @@ export async function loadConfig(cwd: string = process.cwd(), cli: Partial<Brust
|
|
|
75
97
|
}
|
|
76
98
|
const env = fromEnv()
|
|
77
99
|
checkCli(cli)
|
|
100
|
+
// cli (validated, normalised) over toml as a whole object (R10.2); `null` = off.
|
|
101
|
+
const cors = cli.cors === undefined ? fromToml.cors : cli.cors === null ? undefined : checkCors(cli.cors, 'config.cors', null, false)
|
|
78
102
|
return {
|
|
79
103
|
host: env.host ?? cli.host ?? fromToml.host ?? DEFAULTS.host,
|
|
80
104
|
port: env.port ?? cli.port ?? fromToml.port ?? DEFAULTS.port,
|
|
@@ -84,11 +108,17 @@ export async function loadConfig(cwd: string = process.cwd(), cli: Partial<Brust
|
|
|
84
108
|
drainTimeoutMs: env.drainTimeoutMs ?? cli.drainTimeoutMs ?? DEFAULTS.drainTimeoutMs,
|
|
85
109
|
bootTimeoutMs: env.bootTimeoutMs ?? cli.bootTimeoutMs ?? DEFAULTS.bootTimeoutMs,
|
|
86
110
|
callTimeoutMs: env.callTimeoutMs ?? cli.callTimeoutMs ?? DEFAULTS.callTimeoutMs,
|
|
111
|
+
slotBytes: env.slotBytes ?? cli.slotBytes ?? fromToml.slotBytes ?? DEFAULTS.slotBytes,
|
|
112
|
+
maxLoaderResponseBytes: env.maxLoaderResponseBytes ?? cli.maxLoaderResponseBytes ?? fromToml.maxLoaderResponseBytes ?? DEFAULTS.maxLoaderResponseBytes,
|
|
87
113
|
ioThreads: env.ioThreads ?? cli.ioThreads,
|
|
88
|
-
...(
|
|
114
|
+
...(cors ? { cors } : {}),
|
|
115
|
+
...pick('actionsBaseUrl', env.actionsBaseUrl ?? cli.actionsBaseUrl ?? fromToml.actionsBaseUrl),
|
|
116
|
+
...pick('actionsCredentials', cli.actionsCredentials ?? fromToml.actionsCredentials),
|
|
89
117
|
}
|
|
90
118
|
}
|
|
91
119
|
|
|
120
|
+
const pick = <K extends string, V>(k: K, v: V | undefined) => (v === undefined ? {} : ({ [k]: v } as { [P in K]: V }))
|
|
121
|
+
|
|
92
122
|
/** `[cors]` of brust.toml and nothing else (no env, no cli): what the Cloudflare build ships in CONFIG.
|
|
93
123
|
* Throws `BrustConfigError` on a bad table, exactly like `loadConfig`. */
|
|
94
124
|
export async function loadCors(cwd: string): Promise<CorsConfig | undefined> {
|
|
@@ -106,7 +136,7 @@ export async function loadCors(cwd: string): Promise<CorsConfig | undefined> {
|
|
|
106
136
|
}
|
|
107
137
|
|
|
108
138
|
/** CLI / programmatic overrides get the same bounds as env (a CLI `--port 0` = any free port). */
|
|
109
|
-
function checkCli(cli:
|
|
139
|
+
function checkCli(cli: ConfigOverrides): void {
|
|
110
140
|
const check = (name: string, v: number | undefined, min: number, max: number) => {
|
|
111
141
|
if (v !== undefined && (!Number.isInteger(v) || v < min || v > max))
|
|
112
142
|
throw new BrustConfigError(`${name} must be an integer in ${min}..${max} (got ${JSON.stringify(v)})`, null)
|
|
@@ -118,6 +148,14 @@ function checkCli(cli: Partial<BrustConfig>): void {
|
|
|
118
148
|
check('bootTimeoutMs', cli.bootTimeoutMs, 1, MAX_MS)
|
|
119
149
|
check('callTimeoutMs', cli.callTimeoutMs, 1, MAX_MS)
|
|
120
150
|
check('ioThreads', cli.ioThreads, 1, MAX_IO_THREADS)
|
|
151
|
+
check('slotBytes', cli.slotBytes, MIN_SLOT_BYTES, MAX_SLOT_BYTES)
|
|
152
|
+
check('maxLoaderResponseBytes', cli.maxLoaderResponseBytes, MIN_LOADER_RESPONSE_BYTES, MAX_LOADER_RESPONSE_BYTES)
|
|
153
|
+
if (cli.actionsBaseUrl !== undefined) {
|
|
154
|
+
const why = baseUrlError(cli.actionsBaseUrl)
|
|
155
|
+
if (why) throw new BrustConfigError(`config.actionsBaseUrl ${why}`, null)
|
|
156
|
+
}
|
|
157
|
+
if (cli.actionsCredentials !== undefined && !ACTION_CREDENTIALS.includes(cli.actionsCredentials))
|
|
158
|
+
throw new BrustConfigError(`config.actionsCredentials must be one of ${ACTION_CREDENTIALS.join(', ')} (got ${JSON.stringify(cli.actionsCredentials)})`, null)
|
|
121
159
|
}
|
|
122
160
|
|
|
123
161
|
function table(v: unknown, what: string, file: string): Record<string, unknown> {
|
|
@@ -125,12 +163,21 @@ function table(v: unknown, what: string, file: string): Record<string, unknown>
|
|
|
125
163
|
return v as Record<string, unknown>
|
|
126
164
|
}
|
|
127
165
|
|
|
166
|
+
function tomlInt(v: unknown, name: string, min: number, max: number, file: string): number | undefined {
|
|
167
|
+
if (v === undefined) return undefined
|
|
168
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < min || v > max)
|
|
169
|
+
throw new BrustConfigError(`${file}: ${name} must be an integer in ${min}..${max} (got ${JSON.stringify(v)})`, file)
|
|
170
|
+
return v
|
|
171
|
+
}
|
|
172
|
+
|
|
128
173
|
function fromTomlTable(parsed: unknown, file: string): Partial<BrustConfig> {
|
|
129
174
|
const root = table(parsed, 'top level', file)
|
|
130
175
|
const out: Partial<BrustConfig> = {}
|
|
131
176
|
if ('server' in root) {
|
|
132
177
|
const server = table(root.server, '[server]', file)
|
|
133
178
|
const { port, address } = server
|
|
179
|
+
const cap = tomlInt(server.max_loader_response_bytes, 'server.max_loader_response_bytes', MIN_LOADER_RESPONSE_BYTES, MAX_LOADER_RESPONSE_BYTES, file)
|
|
180
|
+
if (cap !== undefined) out.maxLoaderResponseBytes = cap
|
|
134
181
|
if (port !== undefined) {
|
|
135
182
|
if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65535)
|
|
136
183
|
throw new BrustConfigError(`${file}: server.port must be an integer in 1..65535 (got ${JSON.stringify(port)})`, file)
|
|
@@ -143,7 +190,10 @@ function fromTomlTable(parsed: unknown, file: string): Partial<BrustConfig> {
|
|
|
143
190
|
}
|
|
144
191
|
}
|
|
145
192
|
if ('workers' in root) {
|
|
146
|
-
const
|
|
193
|
+
const w = table(root.workers, '[workers]', file)
|
|
194
|
+
const { count } = w
|
|
195
|
+
const slot = tomlInt(w.slot_bytes, 'workers.slot_bytes', MIN_SLOT_BYTES, MAX_SLOT_BYTES, file)
|
|
196
|
+
if (slot !== undefined) out.slotBytes = slot
|
|
147
197
|
if (count !== undefined) {
|
|
148
198
|
if (typeof count !== 'number' || !Number.isInteger(count) || count < 1 || count > MAX_WORKERS)
|
|
149
199
|
throw new BrustConfigError(`${file}: workers.count must be an integer in 1..${MAX_WORKERS} (got ${JSON.stringify(count)})`, file)
|
|
@@ -151,34 +201,62 @@ function fromTomlTable(parsed: unknown, file: string): Partial<BrustConfig> {
|
|
|
151
201
|
}
|
|
152
202
|
}
|
|
153
203
|
if ('cors' in root) out.cors = parseCors(root.cors, file)
|
|
204
|
+
if ('actions' in root) {
|
|
205
|
+
const a = table(root.actions, '[actions]', file)
|
|
206
|
+
if (a.base_url !== undefined) {
|
|
207
|
+
const why = baseUrlError(a.base_url)
|
|
208
|
+
if (why) throw new BrustConfigError(`${file}: actions.base_url ${why}`, file)
|
|
209
|
+
out.actionsBaseUrl = a.base_url as string
|
|
210
|
+
}
|
|
211
|
+
if (a.credentials !== undefined) {
|
|
212
|
+
if (typeof a.credentials !== 'string' || !ACTION_CREDENTIALS.includes(a.credentials as ActionCredentials))
|
|
213
|
+
throw new BrustConfigError(`${file}: actions.credentials must be one of ${ACTION_CREDENTIALS.join(', ')} (got ${JSON.stringify(a.credentials)})`, file)
|
|
214
|
+
out.actionsCredentials = a.credentials as ActionCredentials
|
|
215
|
+
}
|
|
216
|
+
}
|
|
154
217
|
return out
|
|
155
218
|
}
|
|
156
219
|
|
|
157
220
|
function parseCors(v: unknown, file: string): CorsConfig {
|
|
158
|
-
|
|
221
|
+
return checkCors(corsFromToml(table(v, 'cors', file)), `${file}: cors`, file, true)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** brust.toml `[cors]` (snake_case keys) → the camelCase shape [`checkCors`] validates. Unknown toml keys are ignored, as always. */
|
|
225
|
+
function corsFromToml(t: Record<string, unknown>): Record<string, unknown> {
|
|
226
|
+
return { origins: t.origins, methods: t.methods, headers: t.headers, exposeHeaders: t.expose_headers, credentials: t.credentials, maxAgeSeconds: t.max_age_seconds }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const CORS_KEYS = ['origins', 'methods', 'headers', 'exposeHeaders', 'credentials', 'maxAgeSeconds']
|
|
230
|
+
|
|
231
|
+
/** The one `[cors]` validator (Rust `CorsConfig::validate`, verbatim), for toml (`where` = `<file>: cors`, key names in
|
|
232
|
+
* snake_case) and for `run({ config: { cors } })` (`where` = `config.cors`, camelCase; an unknown key is an error). */
|
|
233
|
+
function checkCors(c: unknown, where: string, file: string | null, snake: boolean): CorsConfig {
|
|
234
|
+
const fail = (m: string) => new BrustConfigError(m, file)
|
|
235
|
+
if (c === null || typeof c !== 'object' || Array.isArray(c)) throw fail(`${where} must be ${file === null ? 'an object (or null)' : 'a table'} (got ${JSON.stringify(c)})`)
|
|
236
|
+
const t = c as Record<string, unknown>
|
|
237
|
+
if (!snake) {
|
|
238
|
+
const bad = Object.keys(t).find((k) => !CORS_KEYS.includes(k))
|
|
239
|
+
if (bad !== undefined) throw fail(`${where}.${bad} is not a cors option (expected ${CORS_KEYS.join(', ')})`)
|
|
240
|
+
}
|
|
241
|
+
const name = (k: string) => (snake ? k.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`) : k)
|
|
159
242
|
const strings = (val: unknown, key: string): string[] | undefined => {
|
|
160
243
|
if (val === undefined) return undefined
|
|
161
|
-
if (!Array.isArray(val) || val.some((x) => typeof x !== 'string'))
|
|
162
|
-
throw new BrustConfigError(`${file}: cors.${key} must be an array of strings (got ${JSON.stringify(val)})`, file)
|
|
244
|
+
if (!Array.isArray(val) || val.some((x) => typeof x !== 'string')) throw fail(`${where}.${name(key)} must be an array of strings (got ${JSON.stringify(val)})`)
|
|
163
245
|
return val as string[]
|
|
164
246
|
}
|
|
165
247
|
const origins = strings(t.origins, 'origins') ?? []
|
|
166
248
|
const methods = strings(t.methods, 'methods')
|
|
167
249
|
const headers = strings(t.headers, 'headers')
|
|
168
|
-
const exposeHeaders = strings(t.
|
|
169
|
-
if (t.credentials !== undefined && typeof t.credentials !== 'boolean')
|
|
170
|
-
throw new BrustConfigError(`${file}: cors.credentials must be a boolean (got ${JSON.stringify(t.credentials)})`, file)
|
|
250
|
+
const exposeHeaders = strings(t.exposeHeaders, 'exposeHeaders')
|
|
251
|
+
if (t.credentials !== undefined && typeof t.credentials !== 'boolean') throw fail(`${where}.credentials must be a boolean (got ${JSON.stringify(t.credentials)})`)
|
|
171
252
|
const credentials = t.credentials === true
|
|
172
|
-
const age = t.
|
|
253
|
+
const age = t.maxAgeSeconds
|
|
173
254
|
if (age !== undefined && (typeof age !== 'number' || !Number.isInteger(age) || age < 0 || age > MAX_U32))
|
|
174
|
-
throw
|
|
255
|
+
throw fail(`${where}.${name('maxAgeSeconds')} must be an integer in 0..${MAX_U32} (got ${JSON.stringify(age)})`)
|
|
175
256
|
// Rust CorsConfig::validate, verbatim.
|
|
176
|
-
if (origins.length === 0) throw
|
|
257
|
+
if (origins.length === 0) throw fail(`${where}.origins must be non-empty`)
|
|
177
258
|
if (credentials && origins.includes('*'))
|
|
178
|
-
throw
|
|
179
|
-
`${file}: cors.credentials cannot be combined with a wildcard origin '*' (browsers reject Access-Control-Allow-Origin: * with credentials); list explicit origins instead`,
|
|
180
|
-
file,
|
|
181
|
-
)
|
|
259
|
+
throw fail(`${where}.credentials cannot be combined with a wildcard origin '*' (browsers reject Access-Control-Allow-Origin: * with credentials); list explicit origins instead`)
|
|
182
260
|
return {
|
|
183
261
|
origins,
|
|
184
262
|
...(methods ? { methods } : {}),
|
|
@@ -211,6 +289,14 @@ function fromEnv(): Partial<BrustConfig> {
|
|
|
211
289
|
out.drainTimeoutMs = envInt('BRUST_DRAIN_TIMEOUT_MS', 0, MAX_MS, `an integer in 0..${MAX_MS}`)
|
|
212
290
|
out.bootTimeoutMs = envInt('BRUST_BOOT_TIMEOUT_MS', 1, MAX_MS, `an integer in 1..${MAX_MS}`)
|
|
213
291
|
out.callTimeoutMs = envInt('BRUST_CALL_TIMEOUT_MS', 1, MAX_MS, `an integer in 1..${MAX_MS}`)
|
|
292
|
+
out.slotBytes = envInt('BRUST_SLOT_BYTES', MIN_SLOT_BYTES, MAX_SLOT_BYTES, `an integer in ${MIN_SLOT_BYTES}..${MAX_SLOT_BYTES}`)
|
|
293
|
+
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}`)
|
|
214
294
|
out.ioThreads = envInt('BRUST_WORKER_THREADS', 1, MAX_IO_THREADS, `an integer in 1..${MAX_IO_THREADS}`)
|
|
295
|
+
const base = process.env.BRUST_ACTIONS_BASE_URL
|
|
296
|
+
if (base !== undefined && base !== '') {
|
|
297
|
+
const why = baseUrlError(base)
|
|
298
|
+
if (why) throw new BrustConfigError(`BRUST_ACTIONS_BASE_URL ${why}`, null)
|
|
299
|
+
out.actionsBaseUrl = base
|
|
300
|
+
}
|
|
215
301
|
return out
|
|
216
302
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// L19 (R19.4): a `.module.css` import is its class map (the build's `cssPlugin`); any other `.css`
|
|
2
|
+
// import is a side-effect import the server links (R19.3). Pulled in by a `/// <reference>` from the
|
|
3
|
+
// entries apps import, so both forms typecheck.
|
|
4
|
+
declare module '*.module.css' {
|
|
5
|
+
const classes: { readonly [name: string]: string }
|
|
6
|
+
export default classes
|
|
7
|
+
}
|
|
8
|
+
declare module '*.css' {}
|