brustjs 0.1.49-alpha → 0.1.51-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +39 -15
- package/runtime/cache-sync.ts +291 -0
- package/runtime/cache.ts +4 -0
- package/runtime/cli/build.ts +7 -0
- package/runtime/cli/dev.ts +7 -0
- package/runtime/cli/native-routes-emit.ts +147 -1
- package/runtime/cli/ssg.ts +94 -23
- package/runtime/config.ts +42 -0
- package/runtime/index.d.ts +63 -0
- package/runtime/index.js +57 -52
- package/runtime/index.ts +114 -9
- package/runtime/islands/page-cache.ts +32 -2
- package/runtime/native/runtime.ts +220 -7
- package/runtime/render/fragment.ts +87 -0
- package/runtime/routes.ts +482 -95
- package/runtime/templates.ts +47 -0
- package/runtime/treaty.ts +24 -1
- package/types/action-error.d.ts +18 -0
- package/types/cache-sync.d.ts +42 -0
- package/types/cache.d.ts +20 -0
- package/types/cli/help.d.ts +28 -0
- package/types/cli/jinja-staleness.d.ts +14 -0
- package/types/cli/native-routes-emit.d.ts +217 -0
- package/types/cli/new.d.ts +30 -0
- package/types/cli/templates.d.ts +39 -0
- package/types/client/index.d.ts +14 -0
- package/types/config.d.ts +42 -0
- package/types/cookies.d.ts +25 -0
- package/types/create.d.ts +1 -0
- package/types/css/build.d.ts +11 -0
- package/types/css/component-build.d.ts +17 -0
- package/types/css/component-loader.d.ts +8 -0
- package/types/css/manifest.d.ts +21 -0
- package/types/css/process-modules.d.ts +31 -0
- package/types/css/route-deps.d.ts +20 -0
- package/types/css/scan-imports.d.ts +13 -0
- package/types/css.d.ts +16 -0
- package/types/define-actions.d.ts +133 -0
- package/types/dev/client.d.ts +8 -0
- package/types/dev/coordinator.d.ts +33 -0
- package/types/dev/inject.d.ts +6 -0
- package/types/dev/jinja-reload.d.ts +7 -0
- package/types/dev/tui.d.ts +35 -0
- package/types/dev/watcher.d.ts +34 -0
- package/types/dev/worker-registry.d.ts +17 -0
- package/types/dev/ws-channel.d.ts +39 -0
- package/types/generator.d.ts +23 -0
- package/types/index.d.ts +222 -0
- package/types/islands/brust-page.d.ts +74 -0
- package/types/islands/build.d.ts +49 -0
- package/types/islands/chunk-id.d.ts +10 -0
- package/types/islands/importmap.d.ts +2 -0
- package/types/islands/island.d.ts +65 -0
- package/types/islands/isr-jsx.d.ts +31 -0
- package/types/islands/native-render.d.ts +89 -0
- package/types/loader-cache.d.ts +18 -0
- package/types/mcp/extractor.d.ts +14 -0
- package/types/mcp/manifest.d.ts +23 -0
- package/types/mcp/schema.d.ts +19 -0
- package/types/mcp/server.d.ts +15 -0
- package/types/md/emit.d.ts +72 -0
- package/types/md/render.d.ts +80 -0
- package/types/md/routes.d.ts +119 -0
- package/types/md/scan.d.ts +34 -0
- package/types/md/slug.d.ts +1 -0
- package/types/native/build.d.ts +30 -0
- package/types/native/index.d.ts +2 -0
- package/types/native/runtime.d.ts +52 -0
- package/types/navigation/active-nav.d.ts +2 -0
- package/types/navigation/index.d.ts +5 -0
- package/types/navigation/navigate.d.ts +14 -0
- package/types/navigation/react.d.ts +15 -0
- package/types/navigation/store.d.ts +44 -0
- package/types/render/fragment.d.ts +20 -0
- package/types/render/inject-action-prefix.d.ts +9 -0
- package/types/render/inject-css-link.d.ts +8 -0
- package/types/render/inject-dev-client.d.ts +6 -0
- package/types/render/inject-generator.d.ts +7 -0
- package/types/render/inject-store.d.ts +9 -0
- package/types/render/stream.d.ts +45 -0
- package/types/request-context.d.ts +16 -0
- package/types/routes.d.ts +506 -0
- package/types/sse/handler.d.ts +22 -0
- package/types/standard-schema.d.ts +31 -0
- package/types/store/define-store.d.ts +31 -0
- package/types/store/index.d.ts +5 -0
- package/types/store/react.d.ts +2 -0
- package/types/store/serialize.d.ts +5 -0
- package/types/store/server-context.d.ts +4 -0
- package/types/store/signal.d.ts +18 -0
- package/types/templates.d.ts +18 -0
- package/types/treaty.d.ts +70 -0
- package/types/ws/handler.d.ts +26 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// R1 dynamic template registry — runtime registration of minijinja templates
|
|
2
|
+
// (per-tenant sections etc.). Thin TS over NAPI; the Rust env is process-global
|
|
3
|
+
// so registrations are visible to every worker isolate immediately.
|
|
4
|
+
import * as native from './index.js'
|
|
5
|
+
|
|
6
|
+
export const templates = {
|
|
7
|
+
/** Register (or replace) a runtime template under `name`. Names are opaque
|
|
8
|
+
* keys (`shop/42/section/7@v3` is fine). Throws on jinja syntax errors
|
|
9
|
+
* (message includes line info). Replacement is atomic: concurrent renders
|
|
10
|
+
* see old or new, never missing. */
|
|
11
|
+
register(name: string, jinjaSource: string): void {
|
|
12
|
+
// napi-rs v3 hands back the `Error` as the RETURN VALUE for sync
|
|
13
|
+
// fallible bindings under Bun instead of throwing (verified empirically
|
|
14
|
+
// for both `Result<()>` and `Result<String>`) — normalize to the
|
|
15
|
+
// documented throw.
|
|
16
|
+
const err = (native as any).napiRegisterTemplate(name, jinjaSource)
|
|
17
|
+
if (err instanceof Error) throw err
|
|
18
|
+
},
|
|
19
|
+
/** Remove a runtime-registered template. Returns whether it existed.
|
|
20
|
+
* Boot-tier templates (compiled from routes) are not removable. */
|
|
21
|
+
remove(name: string): boolean {
|
|
22
|
+
return (native as any).napiRemoveTemplate(name)
|
|
23
|
+
},
|
|
24
|
+
/** True when `name` resolves in either tier (dynamic first, then boot). */
|
|
25
|
+
has(name: string): boolean {
|
|
26
|
+
return (native as any).napiHasTemplate(name)
|
|
27
|
+
},
|
|
28
|
+
/** Names of runtime-registered templates (dynamic tier only). */
|
|
29
|
+
list(): string[] {
|
|
30
|
+
return (native as any).napiListDynamicTemplates() ?? []
|
|
31
|
+
},
|
|
32
|
+
/** Render a template (either tier) to an HTML string. Pure (name, data) →
|
|
33
|
+
* html — no request/store context. NOT the request fast lane; intended for
|
|
34
|
+
* handlers/loaders/tooling (draft canvases, section previews). */
|
|
35
|
+
render(name: string, data?: unknown): string {
|
|
36
|
+
// `data == null` (undefined OR explicit null) renders against an empty
|
|
37
|
+
// context — JSON.stringify(null) would hand minijinja a null root where
|
|
38
|
+
// every variable lookup goes Undefined.
|
|
39
|
+
const json = JSON.stringify(data == null ? {} : data)
|
|
40
|
+
// Same napi-rs v3 Bun quirk as register(): the Error comes back as the
|
|
41
|
+
// return value, not a throw — without this guard a consumer would
|
|
42
|
+
// interpolate "Error: ..." into HTML instead of catching.
|
|
43
|
+
const out = (native as any).napiRenderTemplate(name, json)
|
|
44
|
+
if (out instanceof Error) throw out
|
|
45
|
+
return out
|
|
46
|
+
},
|
|
47
|
+
}
|
package/runtime/treaty.ts
CHANGED
|
@@ -11,6 +11,14 @@ export interface TreatyResponse<Data = unknown, Err = unknown> {
|
|
|
11
11
|
}
|
|
12
12
|
export interface ClientOptions {
|
|
13
13
|
prefix?: string
|
|
14
|
+
/** Absolute origin of ANOTHER brust deployment to target, e.g.
|
|
15
|
+
* `https://api.example.com` (a path suffix like `/v2` composes by
|
|
16
|
+
* concatenation). Must match `^https?://`; a trailing slash is stripped.
|
|
17
|
+
* When set, the action prefix is `prefix ?? '/_brust/action'` — the global
|
|
18
|
+
* `__BRUST_ACTION_PREFIX__` belongs to the SERVING app and is never
|
|
19
|
+
* consulted. Cross-origin cookies: pass a `fetch` override that sets
|
|
20
|
+
* `credentials: 'include'` (and configure `cors.credentials` server-side). */
|
|
21
|
+
baseUrl?: string
|
|
14
22
|
headers?: Record<string, string> | (() => Record<string, string>)
|
|
15
23
|
fetch?: typeof fetch
|
|
16
24
|
}
|
|
@@ -77,15 +85,30 @@ export type Treaty<App> =
|
|
|
77
85
|
|
|
78
86
|
function resolvePrefix(opts?: ClientOptions): string {
|
|
79
87
|
if (opts?.prefix) return opts.prefix
|
|
88
|
+
// The global __BRUST_ACTION_PREFIX__ is injected by the SERVING app's pages;
|
|
89
|
+
// it describes THIS origin's mount point, never a cross-origin target's.
|
|
90
|
+
if (opts?.baseUrl !== undefined) return '/_brust/action'
|
|
80
91
|
const g = (globalThis as { __BRUST_ACTION_PREFIX__?: string }).__BRUST_ACTION_PREFIX__
|
|
81
92
|
return g ?? '/_brust/action'
|
|
82
93
|
}
|
|
83
94
|
|
|
95
|
+
/** Validate + normalize `baseUrl`: absolute http(s) origin, trailing slash(es)
|
|
96
|
+
* stripped. Absent → '' (same-origin, byte-identical legacy behavior). */
|
|
97
|
+
function resolveBaseUrl(opts?: ClientOptions): string {
|
|
98
|
+
const b = opts?.baseUrl
|
|
99
|
+
if (b === undefined) return ''
|
|
100
|
+
if (!/^https?:\/\//.test(b)) {
|
|
101
|
+
throw new Error(`treaty baseUrl must be an absolute http(s) origin (got ${JSON.stringify(b)})`)
|
|
102
|
+
}
|
|
103
|
+
return b.replace(/\/+$/, '')
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
/** Build a treaty proxy. Static segments accumulate as a path; a function call
|
|
85
107
|
* with an object fills the next {param}(s) positionally (in insertion order); a
|
|
86
108
|
* terminal method key (.get/.post/…) performs the request. URL is composed from
|
|
87
109
|
* the literal accumulated segments — never from any inferred type. */
|
|
88
110
|
export function client<App = unknown>(opts?: ClientOptions): Treaty<App> {
|
|
111
|
+
const baseUrl = resolveBaseUrl(opts)
|
|
89
112
|
const prefix = resolvePrefix(opts)
|
|
90
113
|
const doFetch = opts?.fetch ?? fetch
|
|
91
114
|
function make(segments: string[]): any {
|
|
@@ -98,7 +121,7 @@ export function client<App = unknown>(opts?: ClientOptions): Treaty<App> {
|
|
|
98
121
|
| { query?: Record<string, string>; headers?: Record<string, string> }
|
|
99
122
|
| undefined
|
|
100
123
|
const body = isBodyless ? undefined : arg1
|
|
101
|
-
let url = prefix + '/' + segments.join('/')
|
|
124
|
+
let url = baseUrl + prefix + '/' + segments.join('/')
|
|
102
125
|
if (options?.query) {
|
|
103
126
|
const qs = new URLSearchParams(options.query as Record<string, string>).toString()
|
|
104
127
|
if (qs) url += '?' + qs
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
declare const ACTION_ERROR: unique symbol;
|
|
2
|
+
export interface ActionErrorBody {
|
|
3
|
+
code: string;
|
|
4
|
+
message: string;
|
|
5
|
+
data?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare class ActionError extends Error {
|
|
8
|
+
readonly [ACTION_ERROR]: true;
|
|
9
|
+
readonly status: number;
|
|
10
|
+
readonly code: string;
|
|
11
|
+
readonly data?: unknown;
|
|
12
|
+
constructor(status: number, code: string, opts?: {
|
|
13
|
+
message?: string;
|
|
14
|
+
data?: unknown;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export declare function isActionError(v: unknown): v is ActionError;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { InvalidateArgs } from './cache.ts';
|
|
2
|
+
/** Public wire contract (documented for external publishers like studio):
|
|
3
|
+
* `{ "v": 1, "sender": "<uuid>", "key": "...", "tags": ["..."],
|
|
4
|
+
* "path": "...", "method": "GET" }` — all invalidation fields optional,
|
|
5
|
+
* `sender` may be omitted (never matches our token, always applied). */
|
|
6
|
+
export interface CacheSyncMessage {
|
|
7
|
+
v: 1;
|
|
8
|
+
sender?: string;
|
|
9
|
+
key?: string;
|
|
10
|
+
tags?: string[];
|
|
11
|
+
path?: string;
|
|
12
|
+
method?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const CHANNEL_DEFAULT = "brust:cache:invalidate";
|
|
15
|
+
/** Injectable publish seam so unit tests never construct a RedisClient. */
|
|
16
|
+
export interface CacheSyncTransport {
|
|
17
|
+
publish(channel: string, message: string): Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
export declare function __setTransportForTest(t: CacheSyncTransport | null): void;
|
|
20
|
+
/** The redis URL may carry credentials (`redis://:pass@host:port/db`). Every
|
|
21
|
+
* log line that mentions the target MUST go through this — host:port only. */
|
|
22
|
+
export declare function redactUrl(url: string): string;
|
|
23
|
+
/** Apply a parsed peer message to the local NAPI caches. Direct fan-out —
|
|
24
|
+
* NEVER via cache.invalidate, so a received message can never re-publish
|
|
25
|
+
* (no loop, even across misconfigured channels). Exported for tests. */
|
|
26
|
+
export declare function applyCacheSyncMessage(msg: CacheSyncMessage): void;
|
|
27
|
+
/** Publish an invalidation to peers. No-op when sync is not configured
|
|
28
|
+
* (BRUST_CACHE_SYNC_URL absent). Fire-and-forget: synchronous from the
|
|
29
|
+
* caller's view, failures warn (throttled) and never propagate — local
|
|
30
|
+
* invalidation must never depend on redis state. */
|
|
31
|
+
export declare function publishCacheSync(args: InvalidateArgs): void;
|
|
32
|
+
/** Start the subscriber (main isolate only — run() guards; workers never call
|
|
33
|
+
* this). Idempotent; never throws. A down redis at boot logs a redacted warn
|
|
34
|
+
* and retries with capped exponential backoff — the server boots regardless. */
|
|
35
|
+
export declare function startCacheSync(opts: {
|
|
36
|
+
url: string;
|
|
37
|
+
channel?: string;
|
|
38
|
+
}): void;
|
|
39
|
+
/** Shutdown/test hook: stop retries, close both clients, reset state so tests
|
|
40
|
+
* can restart. Wired into gracefulExit so backoff timers can't fire between
|
|
41
|
+
* drain and exit. */
|
|
42
|
+
export declare function stopCacheSync(): void;
|
package/types/cache.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as native from './index.js';
|
|
2
|
+
export interface InvalidateArgs {
|
|
3
|
+
key?: string;
|
|
4
|
+
tags?: string[];
|
|
5
|
+
/** L1 response-cache: evict all entries for this request path (any prefix /
|
|
6
|
+
* query variant). Independent of `tags`. */
|
|
7
|
+
path?: string;
|
|
8
|
+
/** HTTP method for the `path` eviction (defaults to GET in Rust). */
|
|
9
|
+
method?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const cache: {
|
|
12
|
+
/** Evict across all three caches: islands, L2 page cache, and L1 response
|
|
13
|
+
* cache. `key`/`tags` hit islands + L2; the L1 response cache is reached by
|
|
14
|
+
* `tags` (the ROUTE must declare static `cache.tags` for its L1 entries to
|
|
15
|
+
* carry them) and by `path` (evicts every L1 entry for that request path,
|
|
16
|
+
* any prefix/query variant). All fields optional; `invalidate({})` is a
|
|
17
|
+
* deliberate no-op. The `?.` guards against a stale addon built before a
|
|
18
|
+
* given binding existed (degrades to no-op). */
|
|
19
|
+
invalidate(args: InvalidateArgs): void;
|
|
20
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Read the brustjs package.json version. `help.ts` lives at
|
|
2
|
+
* <root>/runtime/cli/, so ../../package.json is <root>/package.json in both the
|
|
3
|
+
* source tree and an installed node_modules/brustjs layout. Never throws —
|
|
4
|
+
* returns "unknown" on any failure (version must not crash the CLI). */
|
|
5
|
+
export declare function readVersion(): string;
|
|
6
|
+
export declare const style: {
|
|
7
|
+
bold: (s: string) => string;
|
|
8
|
+
dim: (s: string) => string;
|
|
9
|
+
cyan: (s: string) => string;
|
|
10
|
+
green: (s: string) => string;
|
|
11
|
+
red: (s: string) => string;
|
|
12
|
+
};
|
|
13
|
+
interface CommandDef {
|
|
14
|
+
name: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
usage: string;
|
|
17
|
+
flags: {
|
|
18
|
+
flag: string;
|
|
19
|
+
desc: string;
|
|
20
|
+
}[];
|
|
21
|
+
/** Free-form lines rendered after the Options block (one paragraph per entry). */
|
|
22
|
+
notes?: string[];
|
|
23
|
+
}
|
|
24
|
+
export declare const COMMANDS: CommandDef[];
|
|
25
|
+
export declare function renderVersion(): string;
|
|
26
|
+
export declare function renderRootHelp(): string;
|
|
27
|
+
export declare function renderCommandHelp(name: string): string | null;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** True when the emitted native templates in `jinjaDir` are missing or older
|
|
2
|
+
* than the authored `.tsx` sources under `scanRoot` — i.e. a boot-time
|
|
3
|
+
* recompile is warranted so `bun run <entry>` (source mode) doesn't require a
|
|
4
|
+
* prior `brust build`, and an edited page is picked up without a stale render.
|
|
5
|
+
*
|
|
6
|
+
* Staleness = the build marker (`_manifest.json`, written last by
|
|
7
|
+
* `emitNativeTemplates`) is absent, OR any source `.tsx` is newer than it.
|
|
8
|
+
*
|
|
9
|
+
* `manifestDir` is where `md-manifest.json` lives. It defaults to
|
|
10
|
+
* `dirname(jinjaDir)` — the layout contract is `jinjaDir == <x>/jinja` with
|
|
11
|
+
* the md manifest written next to it in `<x>` (both `emitMdArtifacts` callers
|
|
12
|
+
* pass `manifestDirs: [<x>]`). A caller that already holds the manifest dir
|
|
13
|
+
* should pass it explicitly instead of relying on that positional contract. */
|
|
14
|
+
export declare function isJinjaStale(scanRoot: string, jinjaDir: string, manifestDir?: string): boolean;
|
|
@@ -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;
|