brustjs 0.1.50-alpha → 0.1.52-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/package.json +39 -15
  2. package/runtime/cache-sync.ts +291 -0
  3. package/runtime/cache.ts +4 -0
  4. package/runtime/cli/dev.ts +7 -0
  5. package/runtime/cli/native-routes-emit.ts +147 -1
  6. package/runtime/config.ts +42 -0
  7. package/runtime/index.d.ts +63 -0
  8. package/runtime/index.js +57 -52
  9. package/runtime/index.ts +108 -9
  10. package/runtime/native/runtime.ts +220 -7
  11. package/runtime/render/fragment.ts +87 -0
  12. package/runtime/routes.ts +225 -48
  13. package/runtime/templates.ts +47 -0
  14. package/runtime/treaty.ts +24 -1
  15. package/types/action-error.d.ts +18 -0
  16. package/types/cache-sync.d.ts +42 -0
  17. package/types/cache.d.ts +20 -0
  18. package/types/cli/help.d.ts +28 -0
  19. package/types/cli/jinja-staleness.d.ts +14 -0
  20. package/types/cli/native-routes-emit.d.ts +217 -0
  21. package/types/cli/new.d.ts +30 -0
  22. package/types/cli/templates.d.ts +39 -0
  23. package/types/client/index.d.ts +14 -0
  24. package/types/config.d.ts +42 -0
  25. package/types/cookies.d.ts +25 -0
  26. package/types/create.d.ts +1 -0
  27. package/types/css/build.d.ts +11 -0
  28. package/types/css/component-build.d.ts +17 -0
  29. package/types/css/component-loader.d.ts +8 -0
  30. package/types/css/manifest.d.ts +21 -0
  31. package/types/css/process-modules.d.ts +31 -0
  32. package/types/css/route-deps.d.ts +20 -0
  33. package/types/css/scan-imports.d.ts +13 -0
  34. package/types/css.d.ts +16 -0
  35. package/types/define-actions.d.ts +133 -0
  36. package/types/dev/client.d.ts +8 -0
  37. package/types/dev/coordinator.d.ts +33 -0
  38. package/types/dev/inject.d.ts +6 -0
  39. package/types/dev/jinja-reload.d.ts +7 -0
  40. package/types/dev/tui.d.ts +35 -0
  41. package/types/dev/watcher.d.ts +34 -0
  42. package/types/dev/worker-registry.d.ts +17 -0
  43. package/types/dev/ws-channel.d.ts +39 -0
  44. package/types/generator.d.ts +23 -0
  45. package/types/index.d.ts +222 -0
  46. package/types/islands/brust-page.d.ts +74 -0
  47. package/types/islands/build.d.ts +49 -0
  48. package/types/islands/chunk-id.d.ts +10 -0
  49. package/types/islands/importmap.d.ts +2 -0
  50. package/types/islands/island.d.ts +65 -0
  51. package/types/islands/isr-jsx.d.ts +31 -0
  52. package/types/islands/native-render.d.ts +89 -0
  53. package/types/loader-cache.d.ts +18 -0
  54. package/types/mcp/extractor.d.ts +14 -0
  55. package/types/mcp/manifest.d.ts +23 -0
  56. package/types/mcp/schema.d.ts +19 -0
  57. package/types/mcp/server.d.ts +15 -0
  58. package/types/md/emit.d.ts +72 -0
  59. package/types/md/render.d.ts +80 -0
  60. package/types/md/routes.d.ts +119 -0
  61. package/types/md/scan.d.ts +34 -0
  62. package/types/md/slug.d.ts +1 -0
  63. package/types/native/build.d.ts +30 -0
  64. package/types/native/index.d.ts +2 -0
  65. package/types/native/runtime.d.ts +52 -0
  66. package/types/navigation/active-nav.d.ts +2 -0
  67. package/types/navigation/index.d.ts +5 -0
  68. package/types/navigation/navigate.d.ts +14 -0
  69. package/types/navigation/react.d.ts +15 -0
  70. package/types/navigation/store.d.ts +44 -0
  71. package/types/render/fragment.d.ts +20 -0
  72. package/types/render/inject-action-prefix.d.ts +9 -0
  73. package/types/render/inject-css-link.d.ts +8 -0
  74. package/types/render/inject-dev-client.d.ts +6 -0
  75. package/types/render/inject-generator.d.ts +7 -0
  76. package/types/render/inject-store.d.ts +9 -0
  77. package/types/render/stream.d.ts +45 -0
  78. package/types/request-context.d.ts +16 -0
  79. package/types/routes.d.ts +506 -0
  80. package/types/sse/handler.d.ts +22 -0
  81. package/types/standard-schema.d.ts +31 -0
  82. package/types/store/define-store.d.ts +31 -0
  83. package/types/store/index.d.ts +5 -0
  84. package/types/store/react.d.ts +2 -0
  85. package/types/store/serialize.d.ts +5 -0
  86. package/types/store/server-context.d.ts +4 -0
  87. package/types/store/signal.d.ts +18 -0
  88. package/types/templates.d.ts +18 -0
  89. package/types/treaty.d.ts +70 -0
  90. package/types/ws/handler.d.ts +26 -0
@@ -0,0 +1,217 @@
1
+ /** Gather transitive component sources starting from a page source file.
2
+ *
3
+ * BFS/DFS over local imports reachable from `pageSourcePath`:
4
+ * - Reads each file's source text and adds it to `sources[ident]`.
5
+ * - Recursively follows local imports in each visited file.
6
+ * - Deduplicates by resolved path to handle cycles.
7
+ * - `mergedImports` is the union of every visited file's `scanImports`, with
8
+ * the page's own imports taking precedence on a conflicting ident.
9
+ *
10
+ * Throws when the same ident resolves to two different absolute paths (across
11
+ * two different importing files) — that would be ambiguous for the Rust
12
+ * compiler. */
13
+ export declare function gatherComponentSources(pageSourcePath: string): {
14
+ sources: Record<string, string>;
15
+ mergedImports: Map<string, ResolvedImport>;
16
+ };
17
+ /** T2 / B1 fix — gather component sources for an ENTIRE native chain.
18
+ *
19
+ * The leaf of a composed chain is a bare fragment that no longer imports its
20
+ * ancestors, so seeding `gatherComponentSources` from the leaf alone would
21
+ * leave every ancestor's source ABSENT — `<AppLayout native>` would then
22
+ * silently soft-fall to an SsrComponent (React render) and break native. This
23
+ * unions `gatherComponentSources` over EVERY chain component's resolved source
24
+ * file (resolved name→file via the entry's `importMap`), then injects each
25
+ * chain component's OWN source keyed by its ident.
26
+ *
27
+ * Returns the merged `sources` map and `mergedImports` (ident→absolute path).
28
+ * Post-condition: `sources` has a key for every chain component name. Throws if
29
+ * a chain component name can't be resolved to a source file via `importMap`. */
30
+ export declare function gatherChainSources(chainNames: string[], importMap: Map<string, string>): {
31
+ sources: Record<string, string>;
32
+ mergedImports: Map<string, ResolvedImport>;
33
+ };
34
+ /** T2 — build the synthetic wrapper SOURCE STRING for a native route chain.
35
+ *
36
+ * Given the component identifiers parent→leaf (e.g. `['AppLayout', 'Leaf']`),
37
+ * emit a default-exported function whose body nests every component, leaf
38
+ * innermost, with the `native` attribute on EVERY tag:
39
+ *
40
+ * export default function Leaf__chain() { return <AppLayout native><Leaf native/></AppLayout>; }
41
+ *
42
+ * Load-bearing details:
43
+ * - `export default function` is required — the Rust compiler's
44
+ * `find_default_export` only matches that exact form.
45
+ * - `native` on every tag — without it a nested component lowers to an
46
+ * SsrComponent (React render) instead of being inlined into the chain.
47
+ * - The leaf tag is self-closing; each ancestor wraps the next via `<Outlet/>`
48
+ * inside the ancestor's own source (the compiler substitutes the children
49
+ * slot for `<Outlet/>`).
50
+ *
51
+ * The wrapper function name is `${leafName}__chain` — purely cosmetic (the
52
+ * compiler keys off the default export, not the name). */
53
+ export declare function buildChainWrapperSource(chainNames: string[]): string;
54
+ /** Count opening `<main>` tags in a compiled template. SPA navigation extracts
55
+ * the FIRST `<main>…</main>` block (routes.ts), so a composed native template
56
+ * must contain exactly one `<main>` — the layout owns it and leaf fragments must
57
+ * not add their own. More than one silently truncates the SPA-nav payload. */
58
+ export declare function countMainTags(template: string): number;
59
+ /** Dev-only: splice the /_brust/dev WS client `<script>` into a compiled native
60
+ * template so `native: true` (jinja) routes auto-reload like React-SSR routes.
61
+ *
62
+ * Native routes render Rust-side on the fast lane, bypassing the React renderer
63
+ * that injects the dev client (runtime/render/stream.ts) — so without this the
64
+ * browser on a native page never opens the dev WS and can never auto-reload.
65
+ *
66
+ * Inserted before the first `</head>` when the page has one (a `<BrustPage>`
67
+ * shell); otherwise appended (bare-fragment pages like a plain `<div>`). The
68
+ * browser executes the module script in either position. Gated on `BRUST_DEV`
69
+ * so `brust build` never bakes it into production templates.
70
+ *
71
+ * Exported for the md emit step (runtime/md/emit.ts), which bakes the same tag
72
+ * under its `withDevClient` option — md pages render Rust-side too, so without
73
+ * it they never auto-reload in dev. */
74
+ export declare function injectDevClientIntoTemplate(template: string): string;
75
+ /** Bake the directive runtime loader into a native template iff it uses any
76
+ * x-data directive. Idempotent. Wrapped in {% raw %} for symmetry with the islands
77
+ * bootstrap bake (the tag has no {{ }} but the wrap is harmless + consistent). */
78
+ export declare function bakeDirectivesIfUsed(template: string, force?: boolean): string;
79
+ /** Sub-project J — build pass that turns user's `pages/<Name>.tsx` files into
80
+ * `.brust/jinja/<Name>.jinja` templates. Invoked from `brust build` and
81
+ * `brust dev` after the user's routes are flattened.
82
+ *
83
+ * Limitations (spec S7 + S13.10):
84
+ * - Regex-based import scanner — handles `import Name from './path'` only.
85
+ * Full swc AST + re-export chain support deferred to v2.x.
86
+ * - Dev mode does NOT hot-reload templates on .tsx edit. Boot-only; restart
87
+ * required. Deferred per spec S12.
88
+ */
89
+ export interface NativeRouteEmitOpts {
90
+ /** User's routes entry file (absolute path). Scanned for ImportDeclarations
91
+ * to resolve each native: true route's Component to its source .tsx. */
92
+ entryFile: string;
93
+ /** Flat routes array; only entries with `nativeTemplate` are emitted. The
94
+ * runtime objects are full FlatRoutes — `chain` (parent→leaf route nodes,
95
+ * each carrying its `Component`) drives T2 native-chain composition. */
96
+ flatRoutes: {
97
+ nativeTemplate?: string;
98
+ /** Chain nodes parent→leaf. A leaf carrying `__mdSource` is a markdown
99
+ * page (runtime/md/routes.ts) — emitted by `emitMdTemplates`, NOT here. */
100
+ chain?: Array<{
101
+ Component?: {
102
+ name?: string;
103
+ };
104
+ __mdSource?: unknown;
105
+ }>;
106
+ }[];
107
+ /** `.brust/jinja` absolute output dir. Created if missing. */
108
+ outDir: string;
109
+ /** Repo root. Retained for call-site compatibility; native compilation now
110
+ * goes through the napi addon's `compileJsx`, not a target/ binary. */
111
+ repoRoot: string;
112
+ /** Dev-loop incremental compile (R14). When true, each route's resolved
113
+ * compileJsx inputs (route source + every transitively imported local source
114
+ * + the lucide/directive/component-source env) are content-hashed and memoized
115
+ * for the lifetime of the process; an unchanged route SKIPS compileJsx and the
116
+ * sidecar rewrites (the previous emit's outputs are already on disk) but still
117
+ * appears in the returned manifest. ANY error in hashing falls back to a full
118
+ * compile — correctness over speed. Set only by `brust dev`'s emit calls;
119
+ * `brust build` stays full-fidelity (default false → no memo read OR write,
120
+ * and any stale memo entry for the route is dropped). */
121
+ incremental?: boolean;
122
+ /** TEST SEAM — replaces the canonical-input hasher so tests can prove the
123
+ * hash-failure → compile-all fallback. Never set outside tests. */
124
+ hashInputsForTest?: (canonicalInputs: string) => string;
125
+ }
126
+ /** Per-route emit outcome counts for `emitNativeTemplates` — testability seam
127
+ * for the dev-loop incremental memo (R14). `compiled + skipped` = routes
128
+ * emitted (routes dropped for a missing import count in neither). */
129
+ export interface NativeEmitStats {
130
+ compiled: number;
131
+ skipped: number;
132
+ }
133
+ /** Clear the incremental memo (test isolation). */
134
+ export declare function resetNativeEmitMemo(): void;
135
+ /** Write `<Name>.components.json` and `<Name>.factory.ts` for a native route
136
+ * that has SSR components. Also scans each SSR component's source for Island
137
+ * `component={X}` references and returns those identifiers so the build step
138
+ * can ensure their JS chunks are built.
139
+ *
140
+ * Both artifacts use PROJECT-RELATIVE paths, never the build machine's absolute
141
+ * path: `components.json` stores `sourcePath` relative to the project root
142
+ * (cwd); `.factory.ts` imports relative to its own location (`.brust/jinja/`).
143
+ * `enriched` keeps the ABSOLUTE path internally for the build-time island scan
144
+ * below (`readFileSync`).
145
+ *
146
+ * Exported for the md emit step (runtime/md/emit.ts), whose chained wrappers
147
+ * can carry layout SSR components and need the same sidecar emission. */
148
+ export declare function emitComponentArtifacts(jinjaPath: string, componentsJsonStr: string, pageImports: Map<string, ResolvedImport>, routeName: string): {
149
+ islandIdsFromComponents: string[];
150
+ };
151
+ export declare function emitNativeTemplates(opts: NativeRouteEmitOpts): Promise<NativeEmitStats>;
152
+ /** A resolved import reference, capturing the import FORM so the SSR factory can
153
+ * regenerate the correct `import` statement. Used only by the SSR-component path
154
+ * (`gatherComponentSources`/`gatherChainSources` → `emitComponentArtifacts` /
155
+ * `reconcileIslandManifest`), NOT by `scanImports` (which stays local-default
156
+ * string-valued for the two external callers in islands/native build). */
157
+ export interface ResolvedImport {
158
+ /** Module specifier: an ABSOLUTE file path for local imports, or the verbatim
159
+ * bare specifier for package imports. */
160
+ spec: string;
161
+ /** true ⇒ `spec` is a package/bare specifier (keep verbatim; do not
162
+ * readFileSync/relativize/recurse). */
163
+ bare: boolean;
164
+ /** How the symbol was imported, so the factory regenerates the right import. */
165
+ kind: 'default' | 'named' | 'namespace';
166
+ /** For `named`, the exported name (may differ from the local alias). */
167
+ imported?: string;
168
+ }
169
+ /** Scan ALL import forms in `file` and resolve each local-name binding to a
170
+ * {@link ResolvedImport}. Unlike {@link scanImports} (default-local only, kept
171
+ * stable for external callers), this:
172
+ * - parses default / `* as ns` / `{ a, b as c }` / mixed `d, { a }` forms;
173
+ * - keeps package/bare specifiers verbatim (`bare:true`) instead of skipping
174
+ * them — the SSR path needs them to regenerate `import` lines and to SSR
175
+ * third-party components (e.g. lucide-react icons).
176
+ *
177
+ * Returns `Map<localName, ResolvedImport>` (localName = the in-source identifier
178
+ * used in JSX). Namespace imports are recorded parse-only (the Rust compiler
179
+ * rejects member-expression elements, so `<Ns.Member/>` isn't renderable yet). */
180
+ export declare function scanImportRefs(file: string): Map<string, ResolvedImport>;
181
+ /** Extract static SVG node data for every `lucide-react` icon imported by
182
+ * `file`, keyed by its in-source local name.
183
+ *
184
+ * For each lucide import (`entry.bare && entry.spec === 'lucide-react'`), the
185
+ * icon's exported name (`imported` for named/aliased, the local name for a
186
+ * default import) is kebab-cased to locate `lucide-react/dist/esm/icons/
187
+ * <kebab>.mjs`, whose `__iconNode` array is reshaped into the JSON the Rust
188
+ * compiler deserializes: `{ cls, node: [[tag, [[attr,val],…]], …] }`. The
189
+ * lucide-internal `key` attr is stripped and all attr values are coerced to
190
+ * strings. An unresolvable icon name is silently omitted. */
191
+ export declare function extractLucideIcons(file: string): Promise<Record<string, string>>;
192
+ /** Scan the entry file's `import Name from './path'` declarations and build a
193
+ * map of localName -> resolved absolute path (DEFAULT-LOCAL only — package
194
+ * specifiers skipped). Extension resolution tries `.tsx`, `.ts`, `/index.tsx`,
195
+ * `/index.ts` in order. Kept string-valued + default-local for the external
196
+ * callers in islands/build.ts + native/build.ts; the SSR-component path uses the
197
+ * richer {@link scanImportRefs} instead. */
198
+ export declare function scanImports(entryFile: string): Map<string, string>;
199
+ /** Reconcile the raw `<Name>.islands.json` jsx-rustc emitted against the page's
200
+ * own imports, then bake the importmap+bootstrap into the `.jinja`.
201
+ *
202
+ * Pure-ish & synchronous (fs only) so it unit-tests deterministically:
203
+ * 1. If `islandsJsonPath` is absent → no-op (the route has no islands; the
204
+ * `.jinja` stays byte-identical).
205
+ * 2. Resolve every entry's `sourcePath` from the page's `import <component>
206
+ * from "..."` (else throw).
207
+ * 3. Enrich each entry with that absolute `sourcePath` and rewrite the
208
+ * `.islands.json`.
209
+ * 4. Append `{% raw %}…{% endraw %}`-wrapped bootstrap to the `.jinja`. The raw
210
+ * block keeps the importmap's literal `}}`/`{{` inert through minijinja's
211
+ * boot-time compile. The md emit step passes `bakeBootstrap: false` and bakes
212
+ * once itself at the END of its pipeline (the append here has no `includes()`
213
+ * guard, so a second pass over the same template would double-bake).
214
+ */
215
+ export declare function reconcileIslandManifest(jinjaPath: string, islandsJsonPath: string, pageImports: Map<string, ResolvedImport>, routeName: string, options?: {
216
+ bakeBootstrap?: boolean;
217
+ }): void;
@@ -0,0 +1,30 @@
1
+ import { type EmittedFile, type TemplateDef } from './templates.ts';
2
+ export interface ParsedNewArgs {
3
+ projectName: string;
4
+ targetDir: string;
5
+ template?: string;
6
+ yes: boolean;
7
+ }
8
+ export declare function parseArgs(args: string[]): ParsedNewArgs;
9
+ export interface BrustRef {
10
+ kind: 'file' | 'version';
11
+ spec: string;
12
+ }
13
+ export declare function resolveBrustRef(startDir?: string): BrustRef;
14
+ export interface CopyTemplateOpts {
15
+ templateDir: string;
16
+ targetDir: string;
17
+ substitutions: Record<string, string>;
18
+ exclude?: Set<string>;
19
+ extraFiles?: EmittedFile[];
20
+ }
21
+ export declare function copyTemplate(opts: CopyTemplateOpts): Promise<void>;
22
+ export interface SelectTemplateOpts {
23
+ explicit?: string;
24
+ yes: boolean;
25
+ isTTY: boolean;
26
+ read?: (label: string) => string | null;
27
+ print?: (s: string) => void;
28
+ }
29
+ export declare function selectTemplate(opts: SelectTemplateOpts): TemplateDef;
30
+ export declare function runNew(args: string[]): Promise<void>;
@@ -0,0 +1,39 @@
1
+ export interface ScaffoldCtx {
2
+ projectName: string;
3
+ /** Raw brust dep spec, e.g. "file:/abs/path" or "^0.1.16-alpha". */
4
+ brustSpec: string;
5
+ }
6
+ export interface EmittedFile {
7
+ /** POSIX-relative destination path under the project root. */
8
+ relPath: string;
9
+ content: string;
10
+ }
11
+ export interface TemplateDef {
12
+ name: string;
13
+ title: string;
14
+ description: string;
15
+ sourceDir: string;
16
+ /**
17
+ * POSIX-relative paths (from sourceDir) to skip when copying. Matching uses
18
+ * the SOURCE entry name (pre-`renameForEmit`), so to exclude an emitted
19
+ * `.gitignore` you list its source name `_gitignore`, not `.gitignore`.
20
+ */
21
+ exclude: Set<string>;
22
+ /** Files the source tree lacks, generated at scaffold time. */
23
+ extraFiles?: (ctx: ScaffoldCtx) => EmittedFile[];
24
+ }
25
+ export declare const DEFAULT_TEMPLATE = "minimal";
26
+ /**
27
+ * Walk up from `startDir` to the directory of the nearest package.json whose
28
+ * `name === 'brustjs'`. Single source of truth for locating the brust package
29
+ * root (consumed by both `resolveBrustRef` and pokedex template resolution).
30
+ * Works in the source tree (repo root) and a published install
31
+ * (`node_modules/brustjs`). Throws if no such package.json is found.
32
+ */
33
+ export declare function findBrustPackageRoot(startDir?: string): string;
34
+ /**
35
+ * Build the template registry fresh per call so a missing/corrupt install
36
+ * surfaces as a thrown error at invocation time (not import time).
37
+ */
38
+ export declare function listTemplates(): TemplateDef[];
39
+ export declare function getTemplate(name: string): TemplateDef | undefined;
@@ -0,0 +1,14 @@
1
+ /** Browser-only client helpers. This module is loaded by hydrated island
2
+ * bundles. It intentionally does NOT import from runtime/routes.ts or
3
+ * runtime/index.ts — those pull in React and server-side surface that the
4
+ * client doesn't need.
5
+ */
6
+ export declare class BrustActionError extends Error {
7
+ readonly status: number;
8
+ readonly payload: unknown;
9
+ constructor(message: string, status: number, payload: unknown);
10
+ }
11
+ export { client } from '../treaty.ts';
12
+ export type { TreatyResponse, ClientOptions } from '../treaty.ts';
13
+ export { useStore } from '../store/react.ts';
14
+ export { useNav } from '../navigation/react.ts';
@@ -0,0 +1,42 @@
1
+ export interface BrustConfig {
2
+ /** Host/address to bind on. Default `localhost`. Accepts a hostname
3
+ * (resolved Rust-side) or a literal IP such as `0.0.0.0` / `127.0.0.1`. */
4
+ host: string;
5
+ /** TCP port to bind on. Default 1337. */
6
+ port: number;
7
+ /** Bun Worker count for render dispatch. Default `availableParallelism()`. */
8
+ workers: number;
9
+ /** L1 response-cache capacity (entries). Undefined → Rust default of 1000. */
10
+ cacheMaxEntries?: number;
11
+ /** L2 page-cache capacity (entries). Undefined → Rust default of 1000. */
12
+ cachePageMaxEntries?: number;
13
+ /** R9 cross-process cache invalidation: redis/dragonfly URL. Absent →
14
+ * feature disabled (current single-process behavior). */
15
+ cacheSyncUrl?: string;
16
+ /** Pub/sub channel for cache invalidation. Undefined → module default
17
+ * (`brust:cache:invalidate`). */
18
+ cacheSyncChannel?: string;
19
+ }
20
+ /** Caller-supplied fallbacks (e.g. `brust.run({ address, port })`) applied
21
+ * BELOW env + TOML but ABOVE the framework defaults. */
22
+ export interface ConfigDefaults {
23
+ host?: string;
24
+ port?: number;
25
+ }
26
+ export declare class BrustConfigError extends Error {
27
+ readonly file: string | null;
28
+ constructor(message: string, file: string | null);
29
+ }
30
+ /**
31
+ * Resolve Brust configuration. Precedence (low → high):
32
+ * framework defaults < caller `defaults` < TOML < env.
33
+ *
34
+ * - Framework defaults: { host: 'localhost', port: 1337, workers: availableParallelism() }
35
+ * - Caller `defaults`: e.g. `brust.run({ address, port })` — app-level fallbacks
36
+ * that fill in only when neither TOML nor env specify the value (so a
37
+ * `brust dev --port` / BRUST_PORT still wins over code).
38
+ * - TOML: brust.toml at `cwd` (missing file is fine — only a present file with
39
+ * wrong shape is an error). `[server] address` / `port`.
40
+ * - Env: BRUST_ADDR, BRUST_PORT, BRUST_WORKERS override either source.
41
+ */
42
+ export declare function loadConfig(cwd?: string, defaults?: ConfigDefaults): Promise<BrustConfig>;
@@ -0,0 +1,25 @@
1
+ export interface CookieOptions {
2
+ maxAge?: number;
3
+ expires?: Date;
4
+ path?: string;
5
+ domain?: string;
6
+ secure?: boolean;
7
+ httpOnly?: boolean;
8
+ sameSite?: 'Strict' | 'Lax' | 'None';
9
+ }
10
+ /** Serialize a single Set-Cookie value. The value is URL-encoded; attributes
11
+ * are appended in a stable order. Mirrors the standard cookie attribute names.
12
+ *
13
+ * Hardening: the name is validated as an RFC 6265 token, and the final line is
14
+ * asserted CRLF-free — so a stray `\r\n` in a name/path/domain (which are NOT
15
+ * URL-encoded, unlike the value) can't smuggle an extra response header. */
16
+ export declare function serializeCookie(name: string, value: string, opts?: CookieOptions): string;
17
+ /** Per-request cookie helper. `get` reads the incoming request cookies; `set`
18
+ * and `delete` stage a Set-Cookie onto the active request scope, flushed onto
19
+ * the response by routes.ts. Outside a request scope, `set`/`delete` are no-ops
20
+ * (dev-warn under BRUST_DEV). */
21
+ export declare const cookies: {
22
+ get(name: string): string | undefined;
23
+ set(name: string, value: string, opts?: CookieOptions): void;
24
+ delete(name: string, opts?: Pick<CookieOptions, "path" | "domain">): void;
25
+ };
@@ -0,0 +1 @@
1
+ export { runNew } from './cli/new.ts';
@@ -0,0 +1,11 @@
1
+ export interface BuildCssOptions {
2
+ /** Absolute path to the entry CSS file (typically <scanRoot>/app.css). */
3
+ entry: string;
4
+ /** Absolute path to the output directory. Created if missing. */
5
+ outDir: string;
6
+ }
7
+ export interface CssBuildResult {
8
+ outDir: string;
9
+ files: string[];
10
+ }
11
+ export declare function buildCss(opts: BuildCssOptions): Promise<CssBuildResult>;
@@ -0,0 +1,17 @@
1
+ import { processCssFile } from './process-modules.ts';
2
+ import { type RouteForCss } from './route-deps.ts';
3
+ import { type ComponentCssManifest } from './manifest.ts';
4
+ export interface BuildComponentCssOptions {
5
+ /** Absolute path to the user's app dir (where Home.tsx lives). */
6
+ scanRoot: string;
7
+ /** Absolute path under which chunk files + manifest are written. */
8
+ outDir: string;
9
+ /** Optional Tailwind compiler (for @apply resolution). */
10
+ tailwindCompile: Parameters<typeof processCssFile>[0]['tailwindCompile'];
11
+ /** Routes to map to CSS chunks. */
12
+ routes: RouteForCss[];
13
+ }
14
+ /** Run the full component CSS pipeline: scan → process → emit chunks +
15
+ * .d.ts + manifest. Returns the in-memory manifest for callers (brust.run)
16
+ * that need to register the Bun.plugin immediately. */
17
+ export declare function buildComponentCss(opts: BuildComponentCssOptions): Promise<ComponentCssManifest>;
@@ -0,0 +1,8 @@
1
+ import type { BunPlugin } from 'bun';
2
+ import type { ComponentCssManifest } from './manifest.ts';
3
+ /** Build a Bun.plugin that resolves component CSS imports.
4
+ * - .module.css → `export default <name-map>` (JS, baked from manifest)
5
+ * - .css → empty JS (side-effect; real CSS already on disk)
6
+ *
7
+ * Register once per isolate (brust.run main + each worker). */
8
+ export declare function cssLoaderPlugin(manifest: ComponentCssManifest): BunPlugin;
@@ -0,0 +1,21 @@
1
+ /** Per-CSS-file entry. Side-effect imports (.css) have exports:null;
2
+ * CSS Modules (.module.css) have a flat name→hashed-name map. */
3
+ export interface ComponentCssModuleEntry {
4
+ /** Absolute URL path served by Rust (e.g. /_brust/css/components/<sha>.css). */
5
+ chunk: string;
6
+ /** Original class name → hashed class name. null for non-module .css. */
7
+ exports: Record<string, string> | null;
8
+ }
9
+ export interface ComponentCssManifest {
10
+ version: 1;
11
+ /** Absolute filesystem path of the source .css file → entry. */
12
+ modules: Record<string, ComponentCssModuleEntry>;
13
+ /** Route.fullPath → ordered, deduplicated chunk href list. */
14
+ routeChunks: Record<string, string[]>;
15
+ }
16
+ /** Read + validate. Returns null when the file doesn't exist (project has
17
+ * no component CSS — treated as a no-op everywhere). Throws on malformed
18
+ * JSON or version mismatch. */
19
+ export declare function readComponentCssManifest(absolutePath: string): Promise<ComponentCssManifest | null>;
20
+ /** Write to disk. Creates the parent directory if needed. */
21
+ export declare function writeComponentCssManifest(absolutePath: string, manifest: ComponentCssManifest): Promise<void>;
@@ -0,0 +1,31 @@
1
+ export interface ProcessCssOptions {
2
+ /** Absolute path; Lightning CSS uses this to derive [hash] in cssModules pattern. */
3
+ filename: string;
4
+ /** Source CSS text. */
5
+ source: string;
6
+ /** True iff this file is a .module.css. */
7
+ isModule: boolean;
8
+ /** Optional Tailwind compiler. When set, source is piped through it FIRST
9
+ * so @apply directives resolve before module class rewriting. Null when
10
+ * Tailwind isn't available. */
11
+ tailwindCompile: {
12
+ build(candidates: string[]): string;
13
+ sources: {
14
+ base: string;
15
+ pattern: string;
16
+ negated: boolean;
17
+ }[];
18
+ } | null;
19
+ }
20
+ export interface ProcessCssResult {
21
+ /** Compiled CSS bytes. */
22
+ code: Uint8Array;
23
+ /** null for non-module files; original→hashed name map for .module.css. */
24
+ exports: Record<string, string> | null;
25
+ }
26
+ /** Pipe a CSS file through Tailwind (if available) then Lightning CSS.
27
+ * For .module.css, class names are hashed via Lightning's default
28
+ * pattern `[local]_[hash]` where [hash] is file-derived (~6 chars).
29
+ * Two classes in the same file share the suffix; two files with the same
30
+ * class name get different hashes. */
31
+ export declare function processCssFile(opts: ProcessCssOptions): Promise<ProcessCssResult>;
@@ -0,0 +1,20 @@
1
+ import type { CssDep } from './scan-imports.ts';
2
+ export interface RouteForCss {
3
+ /** Route.fullPath (e.g. '/' or '/blog/{slug}'). */
4
+ fullPath: string;
5
+ /** Absolute source files of the route's component CHAIN (root layout → leaf).
6
+ * computeRouteChunks walks the local import graph from each and unions the
7
+ * CSS deps it finds — so a layout's co-located `.module.css` links to every
8
+ * route under it, and a leaf's links only to that route. */
9
+ componentSources: string[];
10
+ }
11
+ /** Build the route → CSS chunk hrefs map.
12
+ *
13
+ * For each route, BFS the LOCAL import graph rooted at the route's component
14
+ * chain (via {@link scanImports}, same default-import walk islands use) and
15
+ * collect the CSS deps of every reachable file. This means a `.module.css`
16
+ * co-located with the component that imports it is enough — no need to also
17
+ * import it in routes.tsx. Chunks are deduplicated and sorted per route. */
18
+ export declare function computeRouteChunks(routes: RouteForCss[], scan: Map<string, CssDep[]>, modules: Record<string, {
19
+ chunk: string;
20
+ }>): Record<string, string[]>;
@@ -0,0 +1,13 @@
1
+ export interface CssDep {
2
+ /** Absolute path to the .css or .module.css file. */
3
+ path: string;
4
+ /** True iff the file ends with `.module.css`. */
5
+ isModule: boolean;
6
+ /** Default-import binding name (e.g. `styles` for `import styles from ...`).
7
+ * null for side-effect imports (`import './foo.css'`). */
8
+ importedName: string | null;
9
+ }
10
+ /** Walk `scanRoot` recursively, parse every TS/TSX file with the TypeScript
11
+ * compiler API, return a map of source file → CSS deps. Files outside
12
+ * scanRoot, inside ignored dirs, or test files are skipped. */
13
+ export declare function scanCssImports(scanRoot: string): Promise<Map<string, CssDep[]>>;
package/types/css.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /** Configure the list of stylesheet hrefs that the renderer should
2
+ * inject into the SSR HTML before </head>. Replaces any previous list.
3
+ * Called from brust.run() main and worker branches (both need to know
4
+ * so the per-worker renderer can inject). */
5
+ export declare function configureCssEnabled(hrefs: readonly string[]): void;
6
+ /** Returns the configured hrefs as a defensive copy. */
7
+ export declare function getCssHrefs(): readonly string[];
8
+ /** Set the CSS hrefs to inject for a specific route. Replaces any previous
9
+ * list for that route. Called from brust.run() main after the component
10
+ * manifest loads. Keys are route.fullPath strings (e.g. '/' or '/blog/{slug}'). */
11
+ export declare function configureCssHrefsForRoute(routePath: string, hrefs: readonly string[]): void;
12
+ /** Returns the CSS hrefs for a specific route, or [] when none configured.
13
+ * Defensive copy on the way out. */
14
+ export declare function getCssHrefsForRoute(routePath: string): readonly string[];
15
+ /** @internal — used by the unit test suite to wipe both global and per-route state. */
16
+ export declare function _resetCssForTests(): void;
@@ -0,0 +1,133 @@
1
+ import type { BrustRequest, Middleware } from './routes.ts';
2
+ import type { StandardSchemaV1, InferOutput } from './standard-schema.ts';
3
+ declare const RESPOND: unique symbol;
4
+ export interface ActionResponseSentinel {
5
+ readonly [RESPOND]: true;
6
+ status: number;
7
+ body: unknown;
8
+ headers?: Record<string, string>;
9
+ }
10
+ export declare function isRespondSentinel(v: unknown): v is ActionResponseSentinel;
11
+ export declare function makeRespond(): (body: unknown, init?: {
12
+ status?: number;
13
+ headers?: Record<string, string>;
14
+ }) => ActionResponseSentinel;
15
+ export interface ActionContext<Body = unknown, Params = Record<string, string>, Query = Record<string, string>> {
16
+ req: BrustRequest;
17
+ body: Body;
18
+ params: Params;
19
+ query: Query;
20
+ headers: Record<string, string>;
21
+ respond: (body: unknown, init?: {
22
+ status?: number;
23
+ headers?: Record<string, string>;
24
+ }) => ActionResponseSentinel;
25
+ }
26
+ type Handler<B, P, Q, R> = (ctx: ActionContext<B, P, Q>) => R | Promise<R>;
27
+ export interface EndpointOptions {
28
+ body?: StandardSchemaV1;
29
+ query?: StandardSchemaV1;
30
+ middleware?: Middleware[];
31
+ /** Build-time MCP tool description (read by the manifest extractor). */
32
+ description?: string;
33
+ /** Declared domain errors, keyed by code. Each value is a StandardSchema for
34
+ * the error's `data` payload. TYPE-ONLY: flows into the treaty client's typed
35
+ * error union; the runtime ignores it (handlers throw `ActionError`). */
36
+ errors?: Record<string, StandardSchemaV1>;
37
+ }
38
+ export interface EndpointDef {
39
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';
40
+ path: string;
41
+ handler: (ctx: ActionContext) => unknown;
42
+ body?: StandardSchemaV1;
43
+ query?: StandardSchemaV1;
44
+ middleware: Middleware[];
45
+ }
46
+ type ParamKeys<P extends string> = P extends `${string}{${infer K}}${infer Rest}` ? (K extends `*${infer C}` ? C : K) | ParamKeys<Rest> : never;
47
+ type Params<P extends string> = [ParamKeys<P>] extends [never] ? Record<string, string> : {
48
+ [K in ParamKeys<P>]: string;
49
+ };
50
+ type BodyOf<O> = O extends {
51
+ body: infer S;
52
+ } ? S extends StandardSchemaV1 ? InferOutput<S> : unknown : unknown;
53
+ type QueryOf<O> = O extends {
54
+ query: infer S;
55
+ } ? S extends StandardSchemaV1 ? InferOutput<S> : unknown : Record<string, string>;
56
+ /** Discriminated error union derived from `opts.errors`. Each declared code maps
57
+ * to `{ code; message; data }` where `data` is the schema's inferred output. */
58
+ type ErrorOf<O> = O extends {
59
+ errors: infer E;
60
+ } ? {
61
+ [K in keyof E & string]: {
62
+ code: K;
63
+ message: string;
64
+ data: E[K] extends StandardSchemaV1 ? InferOutput<E[K]> : unknown;
65
+ };
66
+ }[keyof E & string] : never;
67
+ export type EndpointEntry = {
68
+ input: unknown;
69
+ output: unknown;
70
+ error: unknown;
71
+ };
72
+ export type EndpointMap = Record<string, Partial<Record<EndpointDef['method'], EndpointEntry>>>;
73
+ export declare function isValidEndpointPath(p: string): boolean;
74
+ export interface ActionsBuilder<Acc extends EndpointMap = {}> {
75
+ endpoints: EndpointDef[];
76
+ use(mw: Middleware): ActionsBuilder<Acc>;
77
+ get<P extends string, O extends EndpointOptions, R>(path: P, handler: Handler<BodyOf<O>, Params<P>, QueryOf<O>, R>, opts?: O): ActionsBuilder<Acc & {
78
+ [K in P]: {
79
+ GET: {
80
+ input: QueryOf<O>;
81
+ output: Awaited<R>;
82
+ error: ErrorOf<O>;
83
+ };
84
+ };
85
+ }>;
86
+ post<P extends string, O extends EndpointOptions, R>(path: P, handler: Handler<BodyOf<O>, Params<P>, QueryOf<O>, R>, opts?: O): ActionsBuilder<Acc & {
87
+ [K in P]: {
88
+ POST: {
89
+ input: BodyOf<O>;
90
+ output: Awaited<R>;
91
+ error: ErrorOf<O>;
92
+ };
93
+ };
94
+ }>;
95
+ put<P extends string, O extends EndpointOptions, R>(path: P, handler: Handler<BodyOf<O>, Params<P>, QueryOf<O>, R>, opts?: O): ActionsBuilder<Acc & {
96
+ [K in P]: {
97
+ PUT: {
98
+ input: BodyOf<O>;
99
+ output: Awaited<R>;
100
+ error: ErrorOf<O>;
101
+ };
102
+ };
103
+ }>;
104
+ patch<P extends string, O extends EndpointOptions, R>(path: P, handler: Handler<BodyOf<O>, Params<P>, QueryOf<O>, R>, opts?: O): ActionsBuilder<Acc & {
105
+ [K in P]: {
106
+ PATCH: {
107
+ input: BodyOf<O>;
108
+ output: Awaited<R>;
109
+ error: ErrorOf<O>;
110
+ };
111
+ };
112
+ }>;
113
+ delete<P extends string, O extends EndpointOptions, R>(path: P, handler: Handler<BodyOf<O>, Params<P>, QueryOf<O>, R>, opts?: O): ActionsBuilder<Acc & {
114
+ [K in P]: {
115
+ DELETE: {
116
+ input: BodyOf<O>;
117
+ output: Awaited<R>;
118
+ error: ErrorOf<O>;
119
+ };
120
+ };
121
+ }>;
122
+ head<P extends string, O extends EndpointOptions, R>(path: P, handler: Handler<BodyOf<O>, Params<P>, QueryOf<O>, R>, opts?: O): ActionsBuilder<Acc & {
123
+ [K in P]: {
124
+ HEAD: {
125
+ input: QueryOf<O>;
126
+ output: Awaited<R>;
127
+ error: ErrorOf<O>;
128
+ };
129
+ };
130
+ }>;
131
+ }
132
+ export declare function defineActions(): ActionsBuilder;
133
+ export { RESPOND };