@abide/abide 0.103.0 → 0.105.0
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/CHANGELOG.md +29 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +27 -27
- package/docs/RULEBOOK.md +68 -24
- package/docs/abide.md +31 -46
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +46 -89
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -25
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +12 -10
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +183 -478
- package/src/compiler/componentGraph.ts +2 -71
- package/src/compiler/declaredExports.ts +79 -60
- package/src/compiler/emitClient.ts +11 -13
- package/src/compiler/emitComponent.ts +21 -6
- package/src/compiler/emitSource.ts +20 -20
- package/src/compiler/handlersUnder.ts +46 -36
- package/src/compiler/hookCalls.ts +152 -0
- package/src/compiler/mountsOf.ts +31 -54
- package/src/compiler/projectShadow.ts +1 -1
- package/src/compiler/readSources.ts +27 -42
- package/src/compiler/readsIn.ts +5 -16
- package/src/compiler/resolverIn.ts +77 -0
- package/src/server/abideModules.ts +152 -146
- package/src/server/answerRequest.ts +18 -4
- package/src/server/answerSocket.ts +136 -76
- package/src/server/answerValue.ts +28 -15
- package/src/server/appTree.ts +59 -73
- package/src/server/claudeApi.ts +2 -5
- package/src/server/clientBundle.ts +198 -0
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +93 -57
- package/src/server/createApp.ts +90 -16
- package/src/server/deferredBuilds.ts +175 -0
- package/src/server/devWorker.ts +107 -319
- package/src/server/faceOf.ts +120 -0
- package/src/server/fenceServer.ts +10 -18
- package/src/server/generatedSurfaces.ts +12 -53
- package/src/server/index.ts +3 -0
- package/src/server/placement.ts +167 -0
- package/src/server/remote.ts +14 -42
- package/src/server/render.ts +25 -17
- package/src/server/renderComponent.ts +9 -5
- package/src/server/requestScope.ts +10 -4
- package/src/server/responseRelay.ts +389 -0
- package/src/server/runCompiled.ts +9 -12
- package/src/server/runDesktop.ts +6 -9
- package/src/server/serveBuild.ts +26 -73
- package/src/server/serveDev.ts +19 -176
- package/src/server/streamEvent.ts +29 -0
- package/src/server/versionOf.ts +1 -2
- package/src/server/withAbideSources.ts +2 -1
- package/src/shared/HandlerRoute.ts +68 -0
- package/src/shared/REFUSAL_KEY.ts +5 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +6 -0
- package/src/shared/answerOf.ts +105 -78
- package/src/shared/filledPath.ts +1 -9
- package/src/shared/framing.ts +127 -128
- package/src/shared/queryText.ts +15 -7
- package/src/shared/reactive/Cursor.ts +69 -124
- package/src/shared/reactive/Reactive.ts +296 -162
- package/src/shared/reactive/callLoad.ts +4 -18
- package/src/shared/reactive/index.ts +11 -8
- package/src/shared/reactive/pairReactive.ts +1 -1
- package/src/shared/reactive/scoped.ts +4 -3
- package/src/shared/reactive/share.ts +11 -1
- package/src/shared/routePattern.ts +0 -110
- package/src/shared/rpc.ts +17 -24
- package/src/shared/rpcRequest.ts +18 -47
- package/src/shared/takeEarlyCall.ts +12 -37
- package/src/shared/url.ts +1 -1
- package/src/ui/Level.ts +276 -0
- package/src/ui/List.ts +17 -17
- package/src/ui/Router.ts +20 -87
- package/src/ui/applyHead.ts +92 -79
- package/src/ui/index.ts +4 -2
- package/src/ui/reconcile.ts +4 -110
- package/src/ui/rows.ts +123 -2
- package/src/ui/wire.ts +12 -27
- package/src/compiler/hooksCalled.ts +0 -35
- package/src/server/clientBuildOptions.ts +0 -54
- package/src/server/refuseAddressCollision.ts +0 -37
- package/src/shared/reactive/chunksOfStream.ts +0 -57
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
package/src/server/appTree.ts
CHANGED
|
@@ -5,22 +5,23 @@
|
|
|
5
5
|
// So listing a directory lists its routes.
|
|
6
6
|
//
|
|
7
7
|
// ONE WALK for the sources, the handler files, the server and lib modules, and the public files
|
|
8
|
-
// `abide build` copies and `abide dev` answers, and the refusals (23.16, 23.17, 23.18, 23.22) —
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// `abide build` copies and `abide dev` answers, and the refusals (23.16, 23.17, 23.18, 23.22) — each
|
|
9
|
+
// file what `placeOf` says of its path, so the walk and every reader of one path agree, and ONE SKIP
|
|
10
|
+
// RULE, `skipped`, for every walk of the tree.
|
|
11
11
|
//
|
|
12
12
|
// A WALK RATHER THAN A GLOB: a glob descends `node_modules` and the build output before a filter
|
|
13
13
|
// can drop them.
|
|
14
14
|
|
|
15
15
|
import { type Dirent, existsSync, readdirSync } from 'node:fs' // bun has no directory listing or sync existence check of its own
|
|
16
|
+
import type { Refusals } from '#compiler/Refusals.ts'
|
|
16
17
|
import { ABIDE_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
17
18
|
import {
|
|
18
|
-
|
|
19
|
+
HOLDS_SERVER,
|
|
19
20
|
PUBLIC_DIRECTORY,
|
|
20
21
|
RESERVED_DIRECTORIES,
|
|
21
22
|
SERVER_DIRECTORY,
|
|
22
23
|
} from '#shared/RESERVED_DIRECTORIES.ts'
|
|
23
|
-
import { DIRECTORY_FILES, routeOf, skipped } from '
|
|
24
|
+
import { DIRECTORY_FILES, placeOf, routeOf, skipped } from './placement.ts'
|
|
24
25
|
|
|
25
26
|
/** What one walk of the app finds, every path relative to the root. */
|
|
26
27
|
type AppFiles = {
|
|
@@ -28,29 +29,19 @@ type AppFiles = {
|
|
|
28
29
|
abide: string[]
|
|
29
30
|
/** Every handler file, in path order. */
|
|
30
31
|
handlers: string[]
|
|
31
|
-
/** Every module under a `server` or a `lib` directory, in path order —
|
|
32
|
+
/** Every module under a `server` or a `lib` directory, in path order — read for 37.13. */
|
|
32
33
|
modules: string[]
|
|
33
34
|
/** Every file under a `public` directory, to the address it is answered at below the mount. */
|
|
34
35
|
published: Map<string, string>
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
/** Told of each thing the tree refuses, at the path relative to the root. */
|
|
38
|
-
type Refuse = (path: string, message: string) => void
|
|
39
|
-
|
|
40
|
-
const THROW: Refuse = (_path, message) => {
|
|
41
|
-
throw new Error(message)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
38
|
/**
|
|
45
39
|
* The app's `.abide` sources, handler files, server and lib modules and public files, refusing a
|
|
46
|
-
* tree 23.16, 23.17, 23.18 or 23.22 forbids.
|
|
47
|
-
*
|
|
48
|
-
* `refuse` throws by default, which is the build's answer; `abide check` collects instead (38.33),
|
|
49
|
-
* and the checker's route table ignores them, since a refused tree is the build's to report.
|
|
40
|
+
* tree 23.16, 23.17, 23.18 or 23.22 forbids into `refusals`, each at its path below the root.
|
|
50
41
|
*
|
|
51
42
|
* A missing root is an app with nothing in it, not a fault.
|
|
52
43
|
*/
|
|
53
|
-
export function appFiles(root: string,
|
|
44
|
+
export function appFiles(root: string, refusals: Refusals): AppFiles {
|
|
54
45
|
const files: AppFiles = {
|
|
55
46
|
abide: [],
|
|
56
47
|
handlers: [],
|
|
@@ -60,7 +51,7 @@ export function appFiles(root: string, refuse: Refuse = THROW): AppFiles {
|
|
|
60
51
|
// 23.22, 38.25 — a root that is its package's directory holds the package's own files beside its
|
|
61
52
|
// pages.
|
|
62
53
|
const packaged = existsSync(`${root}/package.json`)
|
|
63
|
-
walk(root, '', undefined, files,
|
|
54
|
+
walk(root, '', undefined, files, refusals, packaged)
|
|
64
55
|
files.abide.sort()
|
|
65
56
|
files.handlers.sort()
|
|
66
57
|
files.modules.sort()
|
|
@@ -68,13 +59,14 @@ export function appFiles(root: string, refuse: Refuse = THROW): AppFiles {
|
|
|
68
59
|
}
|
|
69
60
|
|
|
70
61
|
// `reserved` is the nearest reserved directory above `relative` — its path and its name — or
|
|
71
|
-
// `undefined` where none is.
|
|
62
|
+
// `undefined` where none is: what 23.16 refuses is the nesting of two, which is a directory's shape
|
|
63
|
+
// rather than a file's place.
|
|
72
64
|
function walk(
|
|
73
65
|
directory: string,
|
|
74
66
|
relative: string,
|
|
75
67
|
reserved: { path: string; name: string } | undefined,
|
|
76
68
|
files: AppFiles,
|
|
77
|
-
|
|
69
|
+
refusals: Refusals,
|
|
78
70
|
packaged = false,
|
|
79
71
|
): void {
|
|
80
72
|
let entries: Dirent[]
|
|
@@ -90,10 +82,19 @@ function walk(
|
|
|
90
82
|
if (entry.isDirectory()) {
|
|
91
83
|
let within = reserved
|
|
92
84
|
if (RESERVED_DIRECTORIES.has(entry.name)) {
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
// 23.16 — `server` nests in the fences that hold code.
|
|
86
|
+
if (
|
|
87
|
+
reserved !== undefined &&
|
|
88
|
+
!(
|
|
89
|
+
entry.name === SERVER_DIRECTORY &&
|
|
90
|
+
HOLDS_SERVER.has(reserved.name)
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
refusals.at(
|
|
95
94
|
path,
|
|
96
|
-
|
|
95
|
+
new Error(
|
|
96
|
+
`abide: ${path} is a ${entry.name} directory under the ${reserved.name} directory ${reserved.path}. RULEBOOK 23.16.`,
|
|
97
|
+
),
|
|
97
98
|
)
|
|
98
99
|
within = { path, name: entry.name }
|
|
99
100
|
}
|
|
@@ -102,37 +103,43 @@ function walk(
|
|
|
102
103
|
`${path}/`,
|
|
103
104
|
within,
|
|
104
105
|
files,
|
|
105
|
-
|
|
106
|
+
refusals,
|
|
106
107
|
)
|
|
107
108
|
continue
|
|
108
109
|
}
|
|
109
110
|
const abide = path.endsWith('.abide')
|
|
110
111
|
if (abide) files.abide.push(path)
|
|
111
|
-
|
|
112
|
+
const place = placeOf(path)
|
|
113
|
+
if (place === 'address') {
|
|
112
114
|
if (!abide && !isModule(path)) {
|
|
113
115
|
if (packaged && relative === '' && !path.endsWith('.ts'))
|
|
114
116
|
continue
|
|
115
|
-
|
|
117
|
+
refusals.at(
|
|
116
118
|
path,
|
|
117
|
-
|
|
119
|
+
new Error(
|
|
120
|
+
`abide: ${path} is directly in an address directory, which holds pages, layouts, error pages and handler files alone. Move it into a lib directory beside it. RULEBOOK 23.22.`,
|
|
121
|
+
),
|
|
118
122
|
)
|
|
119
123
|
continue
|
|
120
124
|
}
|
|
121
125
|
if (!abide) files.handlers.push(path)
|
|
122
126
|
// 23.17 — anything that could answer here would answer in abide's own namespace.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
const framework = frameworkRouteRefusal(routeOf(path), path)
|
|
128
|
+
if (framework !== undefined) refusals.at(path, framework)
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
// Any other place is a fence's, and the walk passed through it.
|
|
132
|
+
const fence = reserved as { path: string; name: string }
|
|
133
|
+
if (DIRECTORY_FILES.has(entry.name))
|
|
134
|
+
refusals.at(
|
|
126
135
|
path,
|
|
127
|
-
|
|
136
|
+
new Error(
|
|
137
|
+
`abide: ${path} is under the ${fence.name} directory ${fence.path}, where nothing has an address, and ${entry.name} is a route's name. RULEBOOK 23.18.`,
|
|
138
|
+
),
|
|
128
139
|
)
|
|
129
|
-
else if (
|
|
130
|
-
files.published.set(path, publicAddress(path,
|
|
131
|
-
else if (
|
|
132
|
-
(reserved.name === SERVER_DIRECTORY ||
|
|
133
|
-
reserved.name === LIB_DIRECTORY) &&
|
|
134
|
-
isModule(path)
|
|
135
|
-
)
|
|
140
|
+
else if (place === 'public')
|
|
141
|
+
files.published.set(path, publicAddress(path, fence.path))
|
|
142
|
+
else if ((place === 'server' || place === 'lib') && isModule(path))
|
|
136
143
|
files.modules.push(path)
|
|
137
144
|
}
|
|
138
145
|
}
|
|
@@ -149,17 +156,16 @@ function isModule(path: string): boolean {
|
|
|
149
156
|
)
|
|
150
157
|
}
|
|
151
158
|
|
|
152
|
-
/**
|
|
153
|
-
export function
|
|
159
|
+
/** The refusal of `source` where `route` lies in the framework's own namespace, or `undefined`. 23.17. */
|
|
160
|
+
export function frameworkRouteRefusal(
|
|
154
161
|
route: string,
|
|
155
162
|
source: string,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
163
|
+
): Error | undefined {
|
|
164
|
+
return route === ABIDE_ADDRESS || route.startsWith(`${ABIDE_ADDRESS}/`)
|
|
165
|
+
? new Error(
|
|
166
|
+
`abide: ${source} answers at ${route}, which is under ${ABIDE_ADDRESS}, where abide's own addresses are. RULEBOOK 23.17.`,
|
|
167
|
+
)
|
|
168
|
+
: undefined
|
|
163
169
|
}
|
|
164
170
|
|
|
165
171
|
// 36.11 — the directories above `public`, then the file's own path below it.
|
|
@@ -183,19 +189,6 @@ export function publishedUnder(
|
|
|
183
189
|
return paths
|
|
184
190
|
}
|
|
185
191
|
|
|
186
|
-
/**
|
|
187
|
-
* Every `.abide` under `root`, relative to it, in path order — the app's own, and none a
|
|
188
|
-
* dependency ships.
|
|
189
|
-
*
|
|
190
|
-
* Refuses nothing: the language server lists them on every check, and a tree 23.16 forbids is the
|
|
191
|
-
* build's to refuse (`appFiles`).
|
|
192
|
-
*/
|
|
193
|
-
export function abideUnder(root: string): string[] {
|
|
194
|
-
const found: string[] = []
|
|
195
|
-
for (const path of abideFilesUnder(root, '')) found.push(path)
|
|
196
|
-
return found.sort()
|
|
197
|
-
}
|
|
198
|
-
|
|
199
192
|
/**
|
|
200
193
|
* Whether an app lies under `root`: a `.abide` anywhere or a handler file, stopping at the first.
|
|
201
194
|
*
|
|
@@ -203,15 +196,13 @@ export function abideUnder(root: string): string[] {
|
|
|
203
196
|
* is as cheap as the tree allows.
|
|
204
197
|
*/
|
|
205
198
|
export function holdsApp(root: string): boolean {
|
|
206
|
-
return !
|
|
199
|
+
return !appSourcesUnder(root, '').next().done
|
|
207
200
|
}
|
|
208
201
|
|
|
209
|
-
// Every `.abide` under `directory
|
|
210
|
-
function*
|
|
202
|
+
// Every `.abide` under `directory`, and every handler file — a module whose place is an address.
|
|
203
|
+
function* appSourcesUnder(
|
|
211
204
|
directory: string,
|
|
212
205
|
relative: string,
|
|
213
|
-
handlers = false,
|
|
214
|
-
fenced = false,
|
|
215
206
|
): Generator<string> {
|
|
216
207
|
let entries: Dirent[]
|
|
217
208
|
try {
|
|
@@ -223,15 +214,10 @@ function* abideFilesUnder(
|
|
|
223
214
|
if (skipped(entry.name)) continue
|
|
224
215
|
const path = `${relative}${entry.name}`
|
|
225
216
|
if (entry.isDirectory())
|
|
226
|
-
yield*
|
|
227
|
-
`${directory}/${entry.name}`,
|
|
228
|
-
`${path}/`,
|
|
229
|
-
handlers,
|
|
230
|
-
fenced || RESERVED_DIRECTORIES.has(entry.name),
|
|
231
|
-
)
|
|
217
|
+
yield* appSourcesUnder(`${directory}/${entry.name}`, `${path}/`)
|
|
232
218
|
else if (
|
|
233
219
|
path.endsWith('.abide') ||
|
|
234
|
-
(
|
|
220
|
+
(isModule(path) && placeOf(path) === 'address')
|
|
235
221
|
)
|
|
236
222
|
yield path
|
|
237
223
|
}
|
package/src/server/claudeApi.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Every block of an assistant turn goes back as it came — a `thinking` block with its signature
|
|
7
7
|
// included, which a turn continuing after a tool call is refused without.
|
|
8
8
|
|
|
9
|
-
import { framedStream
|
|
9
|
+
import { framedStream } from '#shared/framing.ts'
|
|
10
10
|
import { refuse } from '#shared/refuse.ts'
|
|
11
11
|
import type { AssistantEvent, Provider } from './assistant.ts'
|
|
12
12
|
|
|
@@ -112,10 +112,7 @@ export function claudeApi(config: ClaudeApiConfig): Provider {
|
|
|
112
112
|
const partials: string[] = []
|
|
113
113
|
const called: Block[] = []
|
|
114
114
|
let stop = 'end_turn'
|
|
115
|
-
for await (const chunk of framedStream(
|
|
116
|
-
answered.body,
|
|
117
|
-
framingPrefix('text/event-stream') as string,
|
|
118
|
-
)) {
|
|
115
|
+
for await (const chunk of framedStream(answered.body, 'sse')) {
|
|
119
116
|
const event = chunk as ApiEvent
|
|
120
117
|
switch (event.type) {
|
|
121
118
|
case 'message_start':
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// 38.5, 38.16 — THE CLIENT BUNDLE, for both lanes that build one: `abide build`, which writes it to
|
|
2
|
+
// disk for `abide start`, and `abide dev`'s worker, which holds it in memory. What decides what the
|
|
3
|
+
// bundle IS — its plugins and their order, whether there is a page to boot, how a stylesheet's
|
|
4
|
+
// addresses are written, the manifest — is here once, so dev resolves an address the way the
|
|
5
|
+
// built app does by construction; a lane picks only a `mode`. And both answer it at
|
|
6
|
+
// `/__abide/client/<name>` through `bundleServer`, differing in where a byte is read from and how
|
|
7
|
+
// long a browser may keep it.
|
|
8
|
+
|
|
9
|
+
import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
10
|
+
import { abideModules, type Compilation, tablePath } from './abideModules.ts'
|
|
11
|
+
import { appPlugins } from './appPlugins.ts'
|
|
12
|
+
import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
|
|
13
|
+
import { fenceServer } from './fenceServer.ts'
|
|
14
|
+
import { type Manifest, manifestOf, NO_MANIFEST } from './manifestOf.ts'
|
|
15
|
+
import { type Encoding, negotiateEncoding } from './negotiateEncoding.ts'
|
|
16
|
+
|
|
17
|
+
/** One file the bundle produced: its bytes — a stylesheet's already rewritten — and its kind. */
|
|
18
|
+
type BundledFile = {
|
|
19
|
+
bytes: Uint8Array<ArrayBuffer>
|
|
20
|
+
type: string
|
|
21
|
+
kind: Bun.BuildArtifact['kind']
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A client bundle: every file it produced, by its name below `/__abide/client`, and its manifest. */
|
|
25
|
+
export type ClientBundle = {
|
|
26
|
+
files: Map<string, BundledFile>
|
|
27
|
+
manifest: Manifest
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The client bundle of the app at `root`, out of `compilation`, built for `mode`.
|
|
32
|
+
*
|
|
33
|
+
* `published` is each public file's source path to its address. `'build'` is 38.5's — minified and
|
|
34
|
+
* every name hashed; `'dev'` is 38.16's — unminified, the entry named by its source, with linked
|
|
35
|
+
* maps. A bundle that fails throws, its logs the error's.
|
|
36
|
+
*
|
|
37
|
+
* AN APP OF HANDLERS ALONE has no page to boot, and is bundled as nothing with no manifest.
|
|
38
|
+
*/
|
|
39
|
+
export async function clientBundle(
|
|
40
|
+
root: string,
|
|
41
|
+
compilation: Compilation,
|
|
42
|
+
published: Map<string, string>,
|
|
43
|
+
mode: 'build' | 'dev',
|
|
44
|
+
): Promise<ClientBundle> {
|
|
45
|
+
const bundle: ClientBundle = { files: new Map(), manifest: NO_MANIFEST }
|
|
46
|
+
// A page among what compiled: the compiler writes the entry only where there is one to boot.
|
|
47
|
+
if (compilation.tables[`${CLIENT_ENTRY}.ts`] === undefined) return bundle
|
|
48
|
+
const built = await Bun.build({
|
|
49
|
+
// The entry is a table the plugin serves (`abideModules`), below the root.
|
|
50
|
+
entrypoints: [tablePath(root, `${CLIENT_ENTRY}.ts`)],
|
|
51
|
+
// The app's own root, because `Bun.build` opens the entry's directory otherwise, and the
|
|
52
|
+
// tables' is not on disk to open.
|
|
53
|
+
root,
|
|
54
|
+
// 16.28, 16.29 — the browser's view of the server: a `<script server>` is its stand-ins and a
|
|
55
|
+
// `server/` module refuses — FENCED FIRST, before the plugin that would serve the module. 38.43
|
|
56
|
+
// — then the app's own, as `bunfig.toml` declares them.
|
|
57
|
+
plugins: [
|
|
58
|
+
fenceServer(root, compilation.mounted),
|
|
59
|
+
abideModules(root, compilation),
|
|
60
|
+
...(await appPlugins(root)),
|
|
61
|
+
],
|
|
62
|
+
target: 'browser',
|
|
63
|
+
format: 'esm',
|
|
64
|
+
// So a destination's markup and bindings stay in the chunk its own route loads (30.11)
|
|
65
|
+
// rather than landing in the entry every page pays for.
|
|
66
|
+
splitting: true,
|
|
67
|
+
metafile: true,
|
|
68
|
+
// 36.15 — a stylesheet's `url('/fonts/…')` is the public file's address, not a path here,
|
|
69
|
+
// and `Bun.build` reads a leading `/` as a path on this disk and fails on it not being
|
|
70
|
+
// there. The list is the files that exist rather than every `/…`: a URL naming nothing the
|
|
71
|
+
// app serves is still worth a build error.
|
|
72
|
+
external: [...published.values()],
|
|
73
|
+
throw: false,
|
|
74
|
+
...(mode === 'build'
|
|
75
|
+
? {
|
|
76
|
+
// 38.5 — MINIFIED, and EVERY NAME HASHED, the entry's included: chunks and assets are
|
|
77
|
+
// already `[name]-[hash]` by default and the entry is not, which left the one file
|
|
78
|
+
// every page loads served for a year under a name the next deploy writes different
|
|
79
|
+
// bytes to. The manifest is how the document learns what the entry was called.
|
|
80
|
+
//
|
|
81
|
+
// NAMES KEPT, because a class's name is what an error reads to say what it was
|
|
82
|
+
// handed (`canonicalKey`), and a production error naming `a e` is one nobody can
|
|
83
|
+
// act on. It is free where it is paid: the JS the docs app's `/values` sends is
|
|
84
|
+
// 25 600 bytes brotli with the names and without, against 31 900 unminified.
|
|
85
|
+
minify: {
|
|
86
|
+
whitespace: true,
|
|
87
|
+
syntax: true,
|
|
88
|
+
identifiers: true,
|
|
89
|
+
keepNames: true,
|
|
90
|
+
},
|
|
91
|
+
naming: { entry: '[name]-[hash].[ext]' },
|
|
92
|
+
}
|
|
93
|
+
: {
|
|
94
|
+
// 38.97 — a debugger in the browser reads the `.abide` (`withAbideSources`).
|
|
95
|
+
sourcemap: 'linked' as const,
|
|
96
|
+
// 38.16 — the entry named by its source, so what a browser loads and what a stack
|
|
97
|
+
// trace names are the module the author wrote. A dev chunk that changed has to be
|
|
98
|
+
// fetched again, and a name that changes with it is a cache the reload cannot clear.
|
|
99
|
+
naming: {
|
|
100
|
+
entry: '[name].[ext]',
|
|
101
|
+
chunk: '[name]-[hash].[ext]',
|
|
102
|
+
asset: '[name]-[hash].[ext]',
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
})
|
|
106
|
+
// THROWN RATHER THAN LOGGED: a build that emitted server modules and no bundle is an app that
|
|
107
|
+
// serves every page dead — the markup right and nothing wired — which is the one failure a
|
|
108
|
+
// reader cannot see. Dev catches it (38.17); the build stops on it.
|
|
109
|
+
if (!built.success)
|
|
110
|
+
throw new AggregateError(
|
|
111
|
+
built.logs,
|
|
112
|
+
`abide ${mode}: the client bundle failed`,
|
|
113
|
+
)
|
|
114
|
+
// The outputs are independent, so they are read at once rather than one after another.
|
|
115
|
+
const read = await Promise.all(
|
|
116
|
+
built.outputs.map((output) => output.arrayBuffer()),
|
|
117
|
+
)
|
|
118
|
+
for (let index = 0; index < built.outputs.length; index += 1) {
|
|
119
|
+
const output = built.outputs[index] as Bun.BuildArtifact
|
|
120
|
+
const name = output.path.slice(output.path.lastIndexOf('/') + 1)
|
|
121
|
+
let bytes = new Uint8Array(read[index] as ArrayBuffer)
|
|
122
|
+
if (name.endsWith('.css'))
|
|
123
|
+
bytes = new TextEncoder().encode(
|
|
124
|
+
stylesheetUnderMount(new TextDecoder().decode(bytes)),
|
|
125
|
+
)
|
|
126
|
+
bundle.files.set(name, { bytes, type: output.type, kind: output.kind })
|
|
127
|
+
}
|
|
128
|
+
bundle.manifest = manifestOf(built.metafile as Bun.BuildMetafile, root)
|
|
129
|
+
return bundle
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// From the bundle's own directory up to the app's root: `../../` below `/__abide/client`.
|
|
133
|
+
const UP_TO_ROOT = '../'.repeat(CLIENT_ADDRESS.split('/').length - 1)
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A built stylesheet with every root-absolute `url()` written relative to the bundle's directory.
|
|
137
|
+
*
|
|
138
|
+
* 16.10, 36.15 — THE SAME MECHANISM AS THE HANDLER WRAPPERS: the address is written relative to
|
|
139
|
+
* where the bundle is served and the browser resolves it against the sheet, so one build serves
|
|
140
|
+
* at the root and below a mount. What survives the build as `url(/…)` is a public file's address
|
|
141
|
+
* (the `external` list above), and the browser would resolve it against the origin. `//` is
|
|
142
|
+
* another host's.
|
|
143
|
+
*/
|
|
144
|
+
function stylesheetUnderMount(css: string): string {
|
|
145
|
+
return css.replace(
|
|
146
|
+
/url\((["']?)\/(?!\/)/g,
|
|
147
|
+
(_whole, quote: string) => `url(${quote}${UP_TO_ROOT}`,
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A bundle file as it is answered: its type, its bytes, and each precompressed copy of them. */
|
|
152
|
+
export type ServedFile = {
|
|
153
|
+
type: string
|
|
154
|
+
body: () => BodyInit
|
|
155
|
+
encoded: Partial<Record<Encoding, () => BodyInit>>
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Answers `/__abide/client/<name>` out of `files`, each with `cacheControl`; `undefined` for any
|
|
160
|
+
* other address, and for a name the bundle does not hold.
|
|
161
|
+
*
|
|
162
|
+
* THE BUNDLE, ENUMERATED RATHER THAN RESOLVED. A request names a file in this set or it names
|
|
163
|
+
* nothing — there is no path to normalize, so `..` is not a case that has to be got right, and what
|
|
164
|
+
* is servable is exactly what the bundle produced.
|
|
165
|
+
*/
|
|
166
|
+
export function bundleServer(
|
|
167
|
+
files: Map<string, ServedFile>,
|
|
168
|
+
cacheControl: string,
|
|
169
|
+
): (pathname: string, request: Request) => Response | undefined {
|
|
170
|
+
// Each file's encodings listed once, rather than on every request for it.
|
|
171
|
+
const offeredOf = new Map<string, Encoding[]>()
|
|
172
|
+
for (const [name, file] of files)
|
|
173
|
+
offeredOf.set(name, Object.keys(file.encoded) as Encoding[])
|
|
174
|
+
return (pathname, request) => {
|
|
175
|
+
if (!pathname.startsWith(`${CLIENT_ADDRESS}/`)) return undefined
|
|
176
|
+
const name = pathname.slice(CLIENT_ADDRESS.length + 1)
|
|
177
|
+
const file = files.get(name)
|
|
178
|
+
if (file === undefined) return undefined
|
|
179
|
+
const headers: Record<string, string> = {
|
|
180
|
+
'cache-control': cacheControl,
|
|
181
|
+
'content-type': file.type,
|
|
182
|
+
}
|
|
183
|
+
const offered = offeredOf.get(name) as Encoding[]
|
|
184
|
+
if (offered.length === 0) return new Response(file.body(), { headers })
|
|
185
|
+
headers.vary = 'accept-encoding'
|
|
186
|
+
// 38.29 — the precompressed copy where the request takes its encoding.
|
|
187
|
+
const encoding = negotiateEncoding(
|
|
188
|
+
request.headers.get('accept-encoding'),
|
|
189
|
+
offered,
|
|
190
|
+
)
|
|
191
|
+
const encoded =
|
|
192
|
+
encoding === undefined ? undefined : file.encoded[encoding]
|
|
193
|
+
if (encoding === undefined || encoded === undefined)
|
|
194
|
+
return new Response(file.body(), { headers })
|
|
195
|
+
headers['content-encoding'] = encoding
|
|
196
|
+
return new Response(encoded(), { headers })
|
|
197
|
+
}
|
|
198
|
+
}
|
package/src/server/coerceArgs.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// has declared no types, and its args stay the text they arrived as.
|
|
8
8
|
|
|
9
9
|
import { numberFromText } from '#shared/numberFromText.ts'
|
|
10
|
+
import { bareName } from '#shared/queryText.ts'
|
|
10
11
|
import { type Gated, gateOf } from '#shared/reactive/argsContract.ts'
|
|
11
|
-
import { bareName } from '#shared/rpcRequest.ts'
|
|
12
12
|
|
|
13
13
|
export type Document = {
|
|
14
14
|
type?: string | string[]
|