@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
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brust/core",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.30",
|
|
4
4
|
"description": "brust v2 — React components compiled to Rust-rendered templates",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"homepage": "https://brust.dev",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/AssetsArt/brust.git"
|
|
9
|
+
"url": "git+https://github.com/AssetsArt/brust.git",
|
|
10
|
+
"directory": "packages/brust"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/AssetsArt/brust/issues"
|
|
9
14
|
},
|
|
10
15
|
"type": "module",
|
|
11
16
|
"bin": {
|
|
@@ -17,6 +22,7 @@
|
|
|
17
22
|
"browser": "./src/browser.ts",
|
|
18
23
|
"default": "./src/index.ts"
|
|
19
24
|
},
|
|
25
|
+
"./build-time": "./src/build-time.ts",
|
|
20
26
|
"./routes": "./src/routes.ts",
|
|
21
27
|
"./server": "./src/server.ts",
|
|
22
28
|
"./server-handlers": "./src/server-handlers.ts",
|
|
@@ -25,6 +31,7 @@
|
|
|
25
31
|
"./cache-adapter/conformance": "./src/cache-adapter/conformance.ts",
|
|
26
32
|
"./store": "./src/store/index.ts",
|
|
27
33
|
"./store/react": "./src/store/react.ts",
|
|
34
|
+
"./navigation": "./src/navigation.ts",
|
|
28
35
|
"./elysia": "./src/actions/elysia.ts",
|
|
29
36
|
"./client": "./src/actions/client.ts",
|
|
30
37
|
"./native": "./native/index.js"
|
|
@@ -49,13 +56,14 @@
|
|
|
49
56
|
]
|
|
50
57
|
},
|
|
51
58
|
"dependencies": {
|
|
52
|
-
"@brust/runtime-dom": "2.0.0-alpha.
|
|
59
|
+
"@brust/runtime-dom": "2.0.0-alpha.30",
|
|
60
|
+
"tsconfig-paths": "4.2.0"
|
|
53
61
|
},
|
|
54
62
|
"peerDependencies": {
|
|
55
63
|
"react": "^19.0.0",
|
|
56
64
|
"react-dom": "^19.0.0",
|
|
57
|
-
"elysia": "2.0.0-
|
|
58
|
-
"@elysia/eden": "2.0.0-
|
|
65
|
+
"elysia": "^2.0.0-rc.0",
|
|
66
|
+
"@elysia/eden": "^2.0.0-rc.0"
|
|
59
67
|
},
|
|
60
68
|
"peerDependenciesMeta": {
|
|
61
69
|
"elysia": {
|
|
@@ -66,11 +74,17 @@
|
|
|
66
74
|
}
|
|
67
75
|
},
|
|
68
76
|
"devDependencies": {
|
|
69
|
-
"@elysia/eden": "2.0.0-
|
|
77
|
+
"@elysia/eden": "2.0.0-rc.0",
|
|
70
78
|
"@napi-rs/cli": "^3.0.0",
|
|
71
|
-
"@
|
|
79
|
+
"@radix-ui/react-collection": "1.2.0",
|
|
80
|
+
"@radix-ui/react-id": "1.1.4",
|
|
81
|
+
"@radix-ui/react-use-layout-effect": "1.1.4",
|
|
82
|
+
"@radix-ui/react-presence": "1.1.11",
|
|
83
|
+
"@radix-ui/react-slot": "1.5.0",
|
|
84
|
+
"@types/bun": "^1.4.4",
|
|
72
85
|
"@types/react": "^19.2.0",
|
|
73
|
-
"elysia": "2.0.0-
|
|
86
|
+
"elysia": "2.0.0-rc.0",
|
|
87
|
+
"lucide-react": "1.55.0",
|
|
74
88
|
"react": "^19.2.0",
|
|
75
89
|
"react-dom": "^19.2.0",
|
|
76
90
|
"typescript": "^7.0.2"
|
|
@@ -86,12 +100,12 @@
|
|
|
86
100
|
"access": "public"
|
|
87
101
|
},
|
|
88
102
|
"optionalDependencies": {
|
|
89
|
-
"@brust/native-darwin-x64": "2.0.0-alpha.
|
|
90
|
-
"@brust/native-darwin-arm64": "2.0.0-alpha.
|
|
91
|
-
"@brust/native-linux-x64-gnu": "2.0.0-alpha.
|
|
92
|
-
"@brust/native-linux-arm64-gnu": "2.0.0-alpha.
|
|
93
|
-
"@brust/native-linux-x64-musl": "2.0.0-alpha.
|
|
94
|
-
"@brust/native-linux-arm64-musl": "2.0.0-alpha.
|
|
95
|
-
"@brust/core-cf": "2.0.0-alpha.
|
|
103
|
+
"@brust/native-darwin-x64": "2.0.0-alpha.30",
|
|
104
|
+
"@brust/native-darwin-arm64": "2.0.0-alpha.30",
|
|
105
|
+
"@brust/native-linux-x64-gnu": "2.0.0-alpha.30",
|
|
106
|
+
"@brust/native-linux-arm64-gnu": "2.0.0-alpha.30",
|
|
107
|
+
"@brust/native-linux-x64-musl": "2.0.0-alpha.30",
|
|
108
|
+
"@brust/native-linux-arm64-musl": "2.0.0-alpha.30",
|
|
109
|
+
"@brust/core-cf": "2.0.0-alpha.30"
|
|
96
110
|
}
|
|
97
111
|
}
|
package/src/actions/client.ts
CHANGED
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
// elysia is type-only here. Differences from 0.1.x runtime/treaty.ts: docs/actions.md "The client".
|
|
3
3
|
import { type Treaty, treaty } from '@elysia/eden/treaty2'
|
|
4
4
|
import type { AnyElysia } from 'elysia'
|
|
5
|
-
import {
|
|
5
|
+
import { clearNavigationCache } from '../navigate-browser'
|
|
6
|
+
import { type ActionCredentials, DEFAULT_ACTION_PREFIX } from './prefix'
|
|
6
7
|
|
|
7
8
|
export { DEFAULT_ACTION_PREFIX }
|
|
8
9
|
export type { Treaty }
|
|
9
10
|
|
|
11
|
+
/** One link of the request chain (F471). `next` sends `req` (or a request you build from it) to the next link, or to
|
|
12
|
+
* the transport after the last one. Call it zero times (answer yourself), once, or many times (renew a token, then
|
|
13
|
+
* send the same request again). Every `next(r)` sends its own copy of `r`: `r` itself is never consumed, so a
|
|
14
|
+
* retry sends the same body again. Read a body through `req.clone()`. */
|
|
15
|
+
export type ActionMiddleware = (
|
|
16
|
+
req: Request,
|
|
17
|
+
next: (req: Request) => Promise<Response>,
|
|
18
|
+
) => Promise<Response> | Response
|
|
19
|
+
|
|
10
20
|
export interface ClientOptions<Head extends Record<string, unknown> = {}>
|
|
11
21
|
extends Omit<Treaty.Config<Head>, 'keepDomain' | 'fetcher' | 'fetch'> {
|
|
12
22
|
/** Mount point of the actions app. Default: see resolvePrefix. */
|
|
@@ -17,9 +27,22 @@ export interface ClientOptions<Head extends Record<string, unknown> = {}>
|
|
|
17
27
|
fetch?: typeof fetch
|
|
18
28
|
/** RequestInit defaults for every call (Eden's `fetch` option), e.g. `{ credentials: 'include' }`. */
|
|
19
29
|
init?: Treaty.Config<Head>['fetch']
|
|
30
|
+
/** F471: links between Eden and the transport; `middleware[0]` is the outermost. They see the request Eden built
|
|
31
|
+
* (URL, body, `headers`, `onRequest`) and return the Response Eden parses. A throw is a network failure (503). */
|
|
32
|
+
middleware?: ActionMiddleware[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface PageGlobals {
|
|
36
|
+
__BRUST_ACTION_PREFIX__?: string
|
|
37
|
+
/** L16 R16.1: injected from `[actions] base_url` / BRUST_ACTIONS_BASE_URL / run({ config: { actionsBaseUrl } }). */
|
|
38
|
+
__BRUST_ACTION_BASE__?: string
|
|
39
|
+
/** L16 R16.2: injected from `[actions] credentials`. */
|
|
40
|
+
__BRUST_ACTION_CREDENTIALS__?: ActionCredentials
|
|
20
41
|
}
|
|
42
|
+
const page = () => globalThis as PageGlobals
|
|
21
43
|
|
|
22
|
-
/** 0.1.x runtime/treaty.ts:86-93, verbatim order.
|
|
44
|
+
/** 0.1.x runtime/treaty.ts:86-93, verbatim order. A per-call `baseUrl` keeps the default prefix; a base that comes
|
|
45
|
+
* from the page global (L16) pairs with the page's injected prefix, both being the same deployment's config. */
|
|
23
46
|
export function resolvePrefix(opts?: { prefix?: string; baseUrl?: string }): string {
|
|
24
47
|
if (opts?.prefix) return opts.prefix
|
|
25
48
|
if (opts?.baseUrl !== undefined) return DEFAULT_ACTION_PREFIX
|
|
@@ -31,7 +54,8 @@ export function resolvePrefix(opts?: { prefix?: string; baseUrl?: string }): str
|
|
|
31
54
|
|
|
32
55
|
/** 0.1.x runtime/treaty.ts:95-106; message names `client`. */
|
|
33
56
|
export function resolveBaseUrl(opts?: { baseUrl?: string }): string {
|
|
34
|
-
|
|
57
|
+
// R16.1 order: opts.baseUrl, the page's __BRUST_ACTION_BASE__, relative.
|
|
58
|
+
const b = opts?.baseUrl ?? page().__BRUST_ACTION_BASE__
|
|
35
59
|
if (b === undefined) return ''
|
|
36
60
|
if (!/^https?:\/\//.test(b))
|
|
37
61
|
throw new Error(
|
|
@@ -43,12 +67,34 @@ export function resolveBaseUrl(opts?: { baseUrl?: string }): string {
|
|
|
43
67
|
export function client<const App extends AnyElysia, Head extends Record<string, unknown> = {}>(
|
|
44
68
|
opts: ClientOptions<Head> = {},
|
|
45
69
|
): Treaty.Instance<App, Head> {
|
|
46
|
-
const { prefix: _p, baseUrl: _b, fetch: fetcher, init, ...rest } = opts
|
|
70
|
+
const { prefix: _p, baseUrl: _b, fetch: fetcher, init, middleware, ...rest } = opts
|
|
71
|
+
// R16.2: the page's credentials default (`opts.init` wins); only when it differs from the browser default.
|
|
72
|
+
const creds = page().__BRUST_ACTION_CREDENTIALS__
|
|
73
|
+
const defaults = creds !== undefined && creds !== 'same-origin' && typeof init !== 'function' ? { credentials: creds } : undefined
|
|
47
74
|
// keepDomain: Eden would rewrite a relative base to `https:///…` (probe kd.ts).
|
|
48
|
-
|
|
75
|
+
// The root prefix (an actions-only deployment) adds nothing to the base: `https://api.example.com/team`, not `//team`.
|
|
76
|
+
const base = resolveBaseUrl(opts)
|
|
77
|
+
const prefix = resolvePrefix(opts)
|
|
78
|
+
// SPA navigation design §10.1: a mutation clears the page cache, so no page read after it is served from before it
|
|
79
|
+
const transport = async (url: RequestInfo | URL, req?: RequestInit) => {
|
|
80
|
+
const res = await (fetcher ?? fetch)(url, req)
|
|
81
|
+
if (!/^(GET|HEAD)$/i.test(req?.method ?? (url instanceof Request ? url.method : 'GET'))) clearNavigationCache()
|
|
82
|
+
return res
|
|
83
|
+
}
|
|
84
|
+
// F471: Eden calls `send(url, init)`; the chain works on a Request. `next` clones what it is given, so the caller's
|
|
85
|
+
// request stays unread and can be sent again.
|
|
86
|
+
const chain = middleware?.length ? middleware : undefined
|
|
87
|
+
const send = (chain
|
|
88
|
+
? async (url: RequestInfo | URL, init?: RequestInit) => {
|
|
89
|
+
const run = (i: number, req: Request): Promise<Response> =>
|
|
90
|
+
i < chain.length ? Promise.resolve(chain[i]!(req, (r) => run(i + 1, r.clone()))) : transport(req)
|
|
91
|
+
return run(0, new Request(url, init))
|
|
92
|
+
}
|
|
93
|
+
: transport) as typeof fetch
|
|
94
|
+
return treaty<App, Head>(base !== '' && prefix === '/' ? base : base + prefix, {
|
|
49
95
|
...rest,
|
|
50
96
|
keepDomain: true,
|
|
51
|
-
|
|
52
|
-
...(init ? { fetch: init } : {}),
|
|
97
|
+
fetcher: send,
|
|
98
|
+
...(init || defaults ? { fetch: typeof init === 'function' ? init : { ...defaults, ...init } } : {}),
|
|
53
99
|
})
|
|
54
100
|
}
|
package/src/actions/elysia.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `@brust/core/elysia` (M3-F E2, ruling R2): the adapter an actions app is built with. Rust owns
|
|
2
|
-
// HTTP; the worker drives the app with `app.fetch(request)`. Elysia 2.0.0-
|
|
2
|
+
// HTTP; the worker drives the app with `app.fetch(request)`. Elysia 2.0.0-rc.0's adapter is
|
|
3
3
|
// `{ listen?, parse, response, setup? }` (elysia/adapter/types.d.ts); `name` is brust's own tag.
|
|
4
4
|
import type { ElysiaAdapter } from 'elysia/adapter'
|
|
5
5
|
import { WebStandardAdapter } from 'elysia/adapter/web-standard'
|
package/src/actions/load.ts
CHANGED
|
@@ -14,9 +14,32 @@ export function mountActions(app: unknown, prefix: string, what = 'actions entry
|
|
|
14
14
|
throw new Error(
|
|
15
15
|
`${what}: build the app with new Elysia({ adapter: brust() }) — import { brust } from '@brust/core/elysia'`,
|
|
16
16
|
)
|
|
17
|
-
|
|
17
|
+
// L16 R16.4: the root prefix of an actions-only build is no prefix at all (`prefix: '/'` would double the slash).
|
|
18
|
+
return new Elysia({ adapter: brust(), ...(prefix === '/' ? {} : { prefix }) }).use(app)
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export async function loadActionsApp(file: string, prefix: string): Promise<AnyElysia> {
|
|
21
22
|
return mountActions((await import(file)).default, prefix, file)
|
|
22
23
|
}
|
|
24
|
+
|
|
25
|
+
/** One path segment of each side matches the other: equal, or either is a parameter (`:x`, `{x}`, `*`). */
|
|
26
|
+
const segMatch = (a: string, b: string) => a === b || /^(:|\{|\*)/.test(a) || /^(:|\{|\*)/.test(b)
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* F470: the GET actions a page or loader route under the prefix shadows (a GET there is the route's), as
|
|
30
|
+
* `GET <action path> (route <pattern>)`. Best effort, for a boot warning: segments compared, a parameter matches any.
|
|
31
|
+
*/
|
|
32
|
+
export function shadowedGetActions(app: AnyElysia, patterns: string[], prefix: string): string[] {
|
|
33
|
+
const under = patterns.filter((p) => p.startsWith(`${prefix}/`))
|
|
34
|
+
const out: string[] = []
|
|
35
|
+
for (const r of app.routes) {
|
|
36
|
+
if (r.method !== 'GET') continue
|
|
37
|
+
const a = r.path.split('/')
|
|
38
|
+
const hit = under.find((p) => {
|
|
39
|
+
const b = p.split('/')
|
|
40
|
+
return a.length === b.length && a.every((s, i) => segMatch(s, b[i]!))
|
|
41
|
+
})
|
|
42
|
+
if (hit) out.push(`GET ${r.path} (route ${hit})`)
|
|
43
|
+
}
|
|
44
|
+
return out
|
|
45
|
+
}
|
package/src/actions/prefix.ts
CHANGED
|
@@ -5,8 +5,10 @@ export const DEFAULT_MAX_RESPONSE_BYTES = 8_388_608
|
|
|
5
5
|
export const MAX_ACTION_BYTES = 67_108_864
|
|
6
6
|
const SEG = /^[A-Za-z0-9._~-]+$/
|
|
7
7
|
|
|
8
|
-
/** Why `p` cannot be the actions prefix, or null. Segment-safe like the server's match.
|
|
9
|
-
|
|
8
|
+
/** Why `p` cannot be the actions prefix, or null. Segment-safe like the server's match. `root`: an actions-only
|
|
9
|
+
* build (L16 R16.4) may mount at `/`; a normal build never may (its page routes would be unreachable). */
|
|
10
|
+
export function prefixError(p: unknown, root = false): string | null {
|
|
11
|
+
if (root && p === '/') return null
|
|
10
12
|
if (typeof p !== 'string' || !p.startsWith('/') || p.length < 2 || p.endsWith('/'))
|
|
11
13
|
return `must start with '/', be longer than '/' and have no trailing '/' (got ${JSON.stringify(p)})`
|
|
12
14
|
const segs = p.slice(1).split('/')
|
|
@@ -21,3 +23,23 @@ export function prefixError(p: unknown): string | null {
|
|
|
21
23
|
return `${p} is reserved for the server's own /_brust/ paths`
|
|
22
24
|
return null
|
|
23
25
|
}
|
|
26
|
+
|
|
27
|
+
/** L16 R16.1: `base_url` is an absolute http(s) origin: scheme, host, optional port; no path, query, fragment or userinfo. */
|
|
28
|
+
export function baseUrlError(v: unknown): string | null {
|
|
29
|
+
if (typeof v !== 'string' || !/^https?:\/\//.test(v))
|
|
30
|
+
return `must be an absolute http(s) origin such as https://api.example.com (got ${JSON.stringify(v)})`
|
|
31
|
+
let u: URL
|
|
32
|
+
try {
|
|
33
|
+
u = new URL(v)
|
|
34
|
+
} catch {
|
|
35
|
+
return `is not a valid URL (got ${JSON.stringify(v)})`
|
|
36
|
+
}
|
|
37
|
+
const authority = v.replace(/^https?:\/\//, '')
|
|
38
|
+
if (u.search !== '' || u.hash !== '' || u.username !== '' || u.password !== '' || /[/?#@\\\s]/.test(authority))
|
|
39
|
+
return `must be an origin only: no path, query, fragment or credentials (got ${JSON.stringify(v)})`
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** L16 R16.2: the default `RequestInit.credentials` of the browser client. */
|
|
44
|
+
export const ACTION_CREDENTIALS = ['same-origin', 'include', 'omit'] as const
|
|
45
|
+
export type ActionCredentials = (typeof ACTION_CREDENTIALS)[number]
|
package/src/actions-bridge.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface FetchRequest {
|
|
|
22
22
|
method: string
|
|
23
23
|
url: string
|
|
24
24
|
headers: [string, string][]
|
|
25
|
-
/** `{heap: n}` (F82): more than 262144 bytes, fetched with `takeFetchBody(slot)`. */
|
|
25
|
+
/** `{heap: n}` (F82): more than 262144 bytes (`ACTION_INLINE_BODY_BYTES`, independent of the slot size), fetched with `takeFetchBody(slot)`. */
|
|
26
26
|
body: { text: string } | { b64: string } | { heap: number } | null
|
|
27
27
|
req: LoaderReq
|
|
28
28
|
}
|
|
@@ -139,7 +139,7 @@ export const noActions = async (_: Request): Promise<Response> =>
|
|
|
139
139
|
new Response('brust: actions are not configured in this worker', { status: 500, headers: { 'content-type': 'text/plain; charset=utf-8' } })
|
|
140
140
|
|
|
141
141
|
/** The `fetch` call (makeDispatch). Never rejects, never writes JSON: every outcome is a frame. A body of
|
|
142
|
-
* more than 262144 bytes arrives by `io.takeBody` (A1 `{heap}`); a response that does not fit the slot leaves
|
|
142
|
+
* more than 262144 bytes (`ACTION_INLINE_BODY_BYTES`, not the slot size) arrives by `io.takeBody` (A1 `{heap}`); a response that does not fit the slot leaves
|
|
143
143
|
* by `io.putReply` with only status/headers/`heap` in the slot (F82), unless it exceeds `max_response_bytes`. */
|
|
144
144
|
export async function runFetch(requestJson: string, view: Uint8Array, slot: number, slots: number, io: HeapIo = nativeHeapIo): Promise<number> {
|
|
145
145
|
const a = app
|
package/src/assets.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// F411: an import of a font or an image (`SERVED_ASSET_EXT`) is the served URL of the file, `/_brust/client/<name>-<hash>.<ext>`,
|
|
2
|
+
// the same URL a stylesheet's `url()` of it is rewritten to (the build's `cssPlugin`). Pulled in by a `/// <reference>` from the
|
|
3
|
+
// entries apps import, so `import face from './fonts/face.woff2'` typechecks.
|
|
4
|
+
declare module '*.woff2' {
|
|
5
|
+
const url: string
|
|
6
|
+
export default url
|
|
7
|
+
}
|
|
8
|
+
declare module '*.woff' {
|
|
9
|
+
const url: string
|
|
10
|
+
export default url
|
|
11
|
+
}
|
|
12
|
+
declare module '*.ttf' {
|
|
13
|
+
const url: string
|
|
14
|
+
export default url
|
|
15
|
+
}
|
|
16
|
+
declare module '*.otf' {
|
|
17
|
+
const url: string
|
|
18
|
+
export default url
|
|
19
|
+
}
|
|
20
|
+
declare module '*.eot' {
|
|
21
|
+
const url: string
|
|
22
|
+
export default url
|
|
23
|
+
}
|
|
24
|
+
declare module '*.png' {
|
|
25
|
+
const url: string
|
|
26
|
+
export default url
|
|
27
|
+
}
|
|
28
|
+
declare module '*.jpg' {
|
|
29
|
+
const url: string
|
|
30
|
+
export default url
|
|
31
|
+
}
|
|
32
|
+
declare module '*.jpeg' {
|
|
33
|
+
const url: string
|
|
34
|
+
export default url
|
|
35
|
+
}
|
|
36
|
+
declare module '*.gif' {
|
|
37
|
+
const url: string
|
|
38
|
+
export default url
|
|
39
|
+
}
|
|
40
|
+
declare module '*.webp' {
|
|
41
|
+
const url: string
|
|
42
|
+
export default url
|
|
43
|
+
}
|
|
44
|
+
declare module '*.avif' {
|
|
45
|
+
const url: string
|
|
46
|
+
export default url
|
|
47
|
+
}
|
|
48
|
+
declare module '*.svg' {
|
|
49
|
+
const url: string
|
|
50
|
+
export default url
|
|
51
|
+
}
|
|
52
|
+
declare module '*.ico' {
|
|
53
|
+
const url: string
|
|
54
|
+
export default url
|
|
55
|
+
}
|
package/src/browser.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
/// <reference path="./css-modules.d.ts" />
|
|
2
|
+
/// <reference path="./assets.d.ts" />
|
|
1
3
|
// `@brust/core` under the `browser` export condition (and what every browser bundle of
|
|
2
4
|
// `brust build` resolves the package root to): only the side-effect-free pieces. No `run`, no
|
|
3
5
|
// config, no napi addon — a react island may import `cache` from the package root.
|
|
4
6
|
import { cacheComponent } from './cache-core'
|
|
7
|
+
export { buildTime } from './build-time'
|
|
8
|
+
export { clearNavigationCache, navigate } from './navigate-browser'
|
|
9
|
+
export type { NavErrorDetail, NavLoadDetail, NavigateOptions, NavSuccessDetail, NavTrigger } from './navigate'
|
|
5
10
|
import type { InvalidateArgs, InvalidateResult } from './native'
|
|
6
11
|
|
|
7
12
|
function invalidate(_args: InvalidateArgs): InvalidateResult {
|
|
@@ -26,4 +31,4 @@ export {
|
|
|
26
31
|
Outlet,
|
|
27
32
|
redirect,
|
|
28
33
|
} from './routes'
|
|
29
|
-
export type { ComponentType, FlatRoute, HttpErrorOpts, HttpErrorTrigger, LoaderCtx, LoaderReq, Route, RouteCacheConfig, Verdict } from './routes'
|
|
34
|
+
export type { ComponentType, FlatRoute, HttpErrorOpts, HttpErrorTrigger, LoaderCtx, LoaderReq, Route, RouteCacheConfig, RouteSsgConfig, Verdict } from './routes'
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// `brust build --actions-only` (gaps round L16, R16.3/R16.4): a dist with NO page routes: the actions app behind
|
|
2
|
+
// the same Rust admission (method gate, body caps, `Content-Encoding`, the cross-site guard, CORS), every other
|
|
3
|
+
// path a 404 problem+json. With `--target cloudflare` it also writes `<out>/cf/`: a Worker serving only actions
|
|
4
|
+
// (no Static Assets). The routes entry is neither read nor required.
|
|
5
|
+
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
7
|
+
import { buildActions } from './bundle'
|
|
8
|
+
import { type CfPrepared, buildCloudflare, prepareCloudflare } from './cloudflare'
|
|
9
|
+
import type { ActionsBuildConfig, BuildConfig } from './config'
|
|
10
|
+
import { BuildError } from './errors'
|
|
11
|
+
import { MANIFEST_VERSION, type ManifestJson } from './manifest'
|
|
12
|
+
|
|
13
|
+
const MARKER = '.brust'
|
|
14
|
+
|
|
15
|
+
export async function runActionsOnly(o: {
|
|
16
|
+
appRoot: string
|
|
17
|
+
dist: string
|
|
18
|
+
config: BuildConfig
|
|
19
|
+
target: 'bun' | 'cloudflare' | 'static' | undefined
|
|
20
|
+
wasmFromSource: boolean
|
|
21
|
+
statOk: (p: string) => boolean
|
|
22
|
+
assertSafeOutDir: (dist: string, appRoot: string, keep: string[]) => void
|
|
23
|
+
log: (s: string) => void
|
|
24
|
+
}): Promise<void> {
|
|
25
|
+
const { appRoot, dist, config } = o
|
|
26
|
+
const actions: ActionsBuildConfig | null = config.actions
|
|
27
|
+
if (!actions) throw new BuildError('actions-only', '--actions-only needs an [actions] table in brust.toml')
|
|
28
|
+
if (o.target === 'static') throw new BuildError('actions-only', '--actions-only builds no pages: it cannot be combined with --target static')
|
|
29
|
+
const entry = resolve(appRoot, actions.entry)
|
|
30
|
+
const rel = relative(appRoot, entry)
|
|
31
|
+
if (isAbsolute(actions.entry) || rel === '..' || rel.startsWith('../') || isAbsolute(rel) || !o.statOk(entry))
|
|
32
|
+
throw new BuildError('actions-entry', `actions.entry ${actions.entry} is not a file inside the app`)
|
|
33
|
+
o.assertSafeOutDir(dist, appRoot, [entry])
|
|
34
|
+
if (config.cache) o.log('[brust] actions-only: [cache] and route options do not apply (ignored)')
|
|
35
|
+
if (actions.baseUrl !== undefined) o.log('[brust] actions-only: [actions] base_url is for the pages that call these actions (ignored here)')
|
|
36
|
+
const cf: CfPrepared | null = o.target === 'cloudflare' ? await prepareCloudflare(appRoot, config.cloudflare, { fromSource: o.wasmFromSource, env: process.env }) : null
|
|
37
|
+
|
|
38
|
+
const tmp = join(dirname(dist), `.${basename(dist)}.brust-tmp`)
|
|
39
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
40
|
+
mkdirSync(tmp, { recursive: true })
|
|
41
|
+
try {
|
|
42
|
+
const manifest: ManifestJson = {
|
|
43
|
+
version: MANIFEST_VERSION,
|
|
44
|
+
routes: [],
|
|
45
|
+
components: {},
|
|
46
|
+
assets: { runtime: '', features: {} },
|
|
47
|
+
jobs_module: 'jobs.js',
|
|
48
|
+
actions: { entry: await buildActions(tmp, entry), prefix: actions.prefix, maxBodyBytes: actions.maxBodyBytes, maxResponseBytes: actions.maxResponseBytes, crossSite: actions.crossSite },
|
|
49
|
+
actions_only: true,
|
|
50
|
+
}
|
|
51
|
+
mkdirSync(join(tmp, 'jinja'), { recursive: true })
|
|
52
|
+
writeFileSync(join(tmp, 'jobs.js'), '// generated by `brust build --actions-only`: no pages, so no jobs\nexport default {}\n')
|
|
53
|
+
writeFileSync(join(tmp, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
|
54
|
+
writeFileSync(join(tmp, MARKER), 'generated by `brust build`: this directory is replaced on every build\n')
|
|
55
|
+
writeFileSync(
|
|
56
|
+
join(tmp, 'index.js'),
|
|
57
|
+
[
|
|
58
|
+
'// generated by `brust build --actions-only` — `bun dist/index.js` serves the actions (do not edit)',
|
|
59
|
+
"process.env.BRUST_PREBUILT = '1'",
|
|
60
|
+
'process.env.BRUST_DIST_DIR = import.meta.dir',
|
|
61
|
+
"const { run } = await import('@brust/core')",
|
|
62
|
+
'await run()',
|
|
63
|
+
'',
|
|
64
|
+
].join('\n'),
|
|
65
|
+
)
|
|
66
|
+
if (cf) {
|
|
67
|
+
const warnings = await buildCloudflare(
|
|
68
|
+
tmp,
|
|
69
|
+
{ appRoot, entry, dist, compiled: new Map(), manifest, sourceOf: new Map(), actionsEntry: entry, actionsOnly: true, log: o.log },
|
|
70
|
+
cf,
|
|
71
|
+
)
|
|
72
|
+
for (const w of warnings) o.log(w)
|
|
73
|
+
}
|
|
74
|
+
rmSync(join(tmp, '.stage'), { recursive: true, force: true })
|
|
75
|
+
if (existsSync(dist)) {
|
|
76
|
+
const old = join(dirname(dist), `.${basename(dist)}.brust-old`)
|
|
77
|
+
rmSync(old, { recursive: true, force: true })
|
|
78
|
+
renameSync(dist, old)
|
|
79
|
+
renameSync(tmp, dist)
|
|
80
|
+
rmSync(old, { recursive: true, force: true })
|
|
81
|
+
} else renameSync(tmp, dist)
|
|
82
|
+
} catch (e) {
|
|
83
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
84
|
+
throw e
|
|
85
|
+
}
|
|
86
|
+
}
|