@abide/abide 0.101.0 → 0.103.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 +26 -0
- package/bin/abide.ts +226 -164
- package/docs/REGISTRY.md +730 -0
- package/docs/RULEBOOK.md +2768 -0
- package/docs/abide.md +13127 -0
- package/package.json +6 -3
- package/scaffold/AGENTS.md +38 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +341 -71
- package/src/compiler/appMap.ts +96 -0
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +53 -8
- package/src/compiler/checkedShadow.ts +73 -6
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +598 -200
- package/src/compiler/compiledEntry.ts +48 -40
- package/src/compiler/componentGraph.ts +57 -71
- package/src/compiler/declaredExports.ts +26 -9
- package/src/compiler/emitClient.ts +142 -72
- package/src/compiler/emitComponent.ts +41 -12
- package/src/compiler/emitSource.ts +102 -13
- package/src/compiler/handlersUnder.ts +164 -76
- package/src/compiler/hooksCalled.ts +35 -0
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +150 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +250 -19
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +62 -8
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +37 -15
- package/src/compiler/serveLanguage.ts +101 -34
- package/src/compiler/sharedModuleWrites.ts +2 -2
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +29 -2
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/MODULE_QUERIES.ts +27 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +457 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +241 -107
- package/src/server/answerRpc.ts +107 -33
- package/src/server/answerSocket.ts +65 -20
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +238 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +38 -37
- package/src/server/componentModules.d.ts +2 -1
- package/src/server/compressAnswer.ts +4 -4
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +77 -79
- package/src/server/devWorker.ts +118 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +57 -32
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +137 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +12 -7
- package/src/server/installSourceMaps.ts +93 -0
- package/src/server/launcherOptions.ts +33 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +6 -6
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/preload.ts +13 -0
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +48 -28
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +79 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +20 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +10 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/LAUNCHER_FILES.ts +9 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +2 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/{ui → shared}/menuBridge.ts +2 -2
- package/src/shared/menuItem.ts +4 -4
- package/src/shared/onMenu.ts +58 -0
- package/src/shared/reactive/Keyed.ts +2 -2
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +43 -43
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +1 -1
- package/src/shared/reactive/watch.ts +3 -3
- package/src/shared/refuse.ts +1 -1
- package/src/{ui → shared}/reportMenu.ts +8 -10
- package/src/shared/routePattern.ts +122 -48
- package/src/shared/rpc.ts +37 -8
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +37 -36
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → index.abide} +0 -0
- /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
|
@@ -1,28 +1,59 @@
|
|
|
1
1
|
import { existsSync, readFileSync, realpathSync } from 'node:fs' // bun exposes no sync existence check, read or realpath of its own
|
|
2
|
-
import { dirname, posix,
|
|
3
|
-
|
|
2
|
+
import { dirname, posix, relative as relativeTo } from 'node:path' // bun exposes no path helpers of its own
|
|
3
|
+
import {
|
|
4
|
+
type Compilation,
|
|
5
|
+
type CompiledFile,
|
|
6
|
+
resolvedFile,
|
|
7
|
+
} from '#server/abideModules.ts'
|
|
4
8
|
import type { Mount } from '#server/answerRpc.ts'
|
|
5
|
-
import {
|
|
6
|
-
import { CLIENT_ENTRY
|
|
7
|
-
import {
|
|
9
|
+
import { abideUnder, appFiles } from '#server/appTree.ts'
|
|
10
|
+
import { CLIENT_ENTRY } from '#server/CLIENT_BUILD.ts'
|
|
11
|
+
import { COMPONENT_QUERY } from '#server/MODULE_QUERIES.ts'
|
|
12
|
+
import { ROUTES_TABLE, SERVER_TABLE } from '#server/TABLES.ts'
|
|
13
|
+
import { VIRTUAL_DIRECTORY } from '#server/VIRTUAL_DIRECTORY.ts'
|
|
8
14
|
import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
9
|
-
import {
|
|
15
|
+
import { LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
|
|
16
|
+
import {
|
|
17
|
+
coversRoute,
|
|
18
|
+
parseRoutePattern,
|
|
19
|
+
placeOf,
|
|
20
|
+
routeOf,
|
|
21
|
+
type Segment,
|
|
22
|
+
} from '#shared/routePattern.ts'
|
|
23
|
+
import {
|
|
24
|
+
ROUTING_SPECIFIER,
|
|
25
|
+
SHARED_SPECIFIER,
|
|
26
|
+
WIRE_SPECIFIER,
|
|
27
|
+
} from '#shared/SPECIFIERS.ts'
|
|
28
|
+
import { abideImportsOf, appGraph } from './componentGraph.ts'
|
|
10
29
|
import { emitSource } from './emitSource.ts'
|
|
11
30
|
import { formatDiagnosis } from './formatDiagnosis.ts'
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
31
|
+
import {
|
|
32
|
+
type HandlerFile,
|
|
33
|
+
handlersUnder,
|
|
34
|
+
type Refuse,
|
|
35
|
+
THROW,
|
|
36
|
+
} from './handlersUnder.ts'
|
|
37
|
+
import { hooksCalled } from './hooksCalled.ts'
|
|
38
|
+
import { mountsOf } from './mountsOf.ts'
|
|
39
|
+
import { nearest } from './nearest.ts'
|
|
40
|
+
import type { AbideFile } from './parseAbide.ts'
|
|
41
|
+
import {
|
|
42
|
+
type ParsedSource,
|
|
43
|
+
parseSources,
|
|
44
|
+
type ReadSource,
|
|
45
|
+
readSources,
|
|
46
|
+
refusal,
|
|
47
|
+
} from './readSources.ts'
|
|
48
|
+
import { respecified } from './splitImports.ts'
|
|
15
49
|
import { type Diagnosis, TypeSession } from './TypeSession.ts'
|
|
16
50
|
import { writeDefaultProject } from './writeDefaultProject.ts'
|
|
17
51
|
|
|
18
|
-
// AN APP'S `.abide` SOURCES, COMPILED — every
|
|
19
|
-
//
|
|
20
|
-
// `abide dev` keeps it in memory (38.
|
|
21
|
-
// same sources is exactly what one compile in two sinks
|
|
22
|
-
//
|
|
23
|
-
// THE KEYS ARE THE OUTPUT LAYOUT, which mirrors the source tree so a module's neighbours are where
|
|
24
|
-
// its imports say they are — `appGraph` lands a `.abide` import on its module whatever the
|
|
25
|
-
// spelling, and every other relative import is pointed back at the source it named (34.14).
|
|
52
|
+
// AN APP'S `.abide` SOURCES, COMPILED — every file keyed by its path below the root, and loaded
|
|
53
|
+
// there through its `?component` face (`abideModules`). Nothing here touches the output directory:
|
|
54
|
+
// `abide build` writes what this hands back and `abide dev` keeps it in memory (38.16), and the two
|
|
55
|
+
// commands serving different markup from the same sources is exactly what one compile in two sinks
|
|
56
|
+
// makes impossible.
|
|
26
57
|
|
|
27
58
|
type AppRoute = {
|
|
28
59
|
/** The address, in the bracket spelling `routePattern` parses. */
|
|
@@ -38,21 +69,17 @@ type AppRoute = {
|
|
|
38
69
|
layouts: string[]
|
|
39
70
|
}
|
|
40
71
|
|
|
41
|
-
/** An app's compiled output: every
|
|
42
|
-
export type CompiledApp = {
|
|
43
|
-
/**
|
|
44
|
-
* Module text by path below the output directory.
|
|
45
|
-
*
|
|
46
|
-
* `ui/pages/page.ts`, its `.client.ts`, and what the build writes rather than an author
|
|
47
|
-
* (`routes.ts`, `server.ts`, `entry.ts`, `rpc.ts` and a wrapper per handler module under
|
|
48
|
-
* `WIRE_DIRECTORY`).
|
|
49
|
-
*/
|
|
50
|
-
modules: Map<string, string>
|
|
72
|
+
/** An app's compiled output: every file and table (`Compilation`), its routes, and its warnings. */
|
|
73
|
+
export type CompiledApp = Compilation & {
|
|
51
74
|
routes: AppRoute[]
|
|
75
|
+
/** Every handler, where it is mounted. 16.2. */
|
|
76
|
+
mounts: Mount[]
|
|
52
77
|
/** How many `.abide` files were read, which is what the commands report. */
|
|
53
78
|
sources: number
|
|
54
79
|
/** What the compiler has to say that does not stop the build, on the `.abide` line. */
|
|
55
80
|
warnings: Diagnosis[]
|
|
81
|
+
/** Every public file's source path to its address, as `appFiles` read them. 36.11. */
|
|
82
|
+
published: Map<string, string>
|
|
56
83
|
}
|
|
57
84
|
|
|
58
85
|
/**
|
|
@@ -64,12 +91,19 @@ export type CompiledApp = {
|
|
|
64
91
|
*/
|
|
65
92
|
export type AppChecker = {
|
|
66
93
|
session: TypeSession
|
|
94
|
+
/** The app's root, real — every path a checker is asked about is spelled under it. */
|
|
95
|
+
root: string
|
|
67
96
|
/**
|
|
68
97
|
* Records a path a watcher saw move, or `undefined` for a move it could not place.
|
|
69
98
|
*
|
|
70
99
|
* Held until `current`.
|
|
71
100
|
*/
|
|
72
101
|
saw: (path: string | undefined) => void
|
|
102
|
+
/**
|
|
103
|
+
* The app's own `.abide`, read (`sourcesUnder`) — kept until `saw` hears anything move, so a
|
|
104
|
+
* check asked again and again while an editor types re-reads nothing on disk.
|
|
105
|
+
*/
|
|
106
|
+
sources: () => ReturnType<typeof sourcesUnder>
|
|
73
107
|
/**
|
|
74
108
|
* Whether the project it was opened on is still the app's; if so, flushes what `saw` held.
|
|
75
109
|
*
|
|
@@ -82,9 +116,15 @@ export type AppChecker = {
|
|
|
82
116
|
close: () => Promise<void>
|
|
83
117
|
}
|
|
84
118
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Opens the checker for the app at `appRoot`, on a default tsconfig where it has none (38.30).
|
|
121
|
+
*
|
|
122
|
+
* AT ITS REAL PATH, which is the one bun resolves every import to: an editor holding the app
|
|
123
|
+
* through a link (`/tmp` is one on macOS) is spelled back at its own edge (`serveLanguage`).
|
|
124
|
+
*/
|
|
125
|
+
export async function openChecker(appRoot: string): Promise<AppChecker> {
|
|
126
|
+
const root = realpathSync(appRoot)
|
|
127
|
+
// Where the app has no tsconfig it is typed against a default one (38.30), so every app is
|
|
88
128
|
// read by the same compiler.
|
|
89
129
|
const found = nearest(root, 'tsconfig.json')
|
|
90
130
|
const opened = projectFiles(root)
|
|
@@ -93,14 +133,25 @@ export async function openChecker(root: string): Promise<AppChecker> {
|
|
|
93
133
|
? await writeDefaultProject(root)
|
|
94
134
|
: { tsconfig: found, remove: async (): Promise<void> => {} }
|
|
95
135
|
const session = await TypeSession.open(project.tsconfig)
|
|
136
|
+
session.root = root
|
|
96
137
|
const moved = new Set<string>()
|
|
97
138
|
let movedSomewhere = false
|
|
139
|
+
let sources: ReturnType<typeof sourcesUnder> | undefined
|
|
98
140
|
return {
|
|
99
141
|
session,
|
|
142
|
+
root,
|
|
100
143
|
saw: (path) => {
|
|
144
|
+
sources = undefined
|
|
101
145
|
if (path === undefined) movedSomewhere = true
|
|
102
146
|
else moved.add(path)
|
|
103
147
|
},
|
|
148
|
+
sources: () => {
|
|
149
|
+
sources ??= sourcesUnder(root).catch((thrown: unknown) => {
|
|
150
|
+
sources = undefined
|
|
151
|
+
throw thrown
|
|
152
|
+
})
|
|
153
|
+
return sources
|
|
154
|
+
},
|
|
104
155
|
current: () => {
|
|
105
156
|
if (projectFiles(root) !== opened) return false
|
|
106
157
|
if (movedSomewhere || moved.size > 0)
|
|
@@ -146,7 +197,7 @@ export async function withChecker<Result>(
|
|
|
146
197
|
}
|
|
147
198
|
|
|
148
199
|
/**
|
|
149
|
-
* Every `.abide`
|
|
200
|
+
* Every `.abide` of the app's own, read, in path order — and the root as bun resolves it.
|
|
150
201
|
*
|
|
151
202
|
* `Bun.resolveSync` answers a symlink-free path (`/tmp` is `/private/tmp` on macOS), so a root
|
|
152
203
|
* spelled through a link would have every import resolve to a file this walk never listed, and
|
|
@@ -157,36 +208,175 @@ export async function sourcesUnder(
|
|
|
157
208
|
appRoot: string,
|
|
158
209
|
): Promise<{ root: string; sources: { path: string; text: string }[] }> {
|
|
159
210
|
const root = realpathSync(appRoot)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
211
|
+
return { root, sources: await sourcesAt(root, abideUnder(root)) }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The `.abide` at each of `relatives`, below `root`, read. */
|
|
215
|
+
async function sourcesAt(
|
|
216
|
+
root: string,
|
|
217
|
+
relatives: readonly string[],
|
|
218
|
+
): Promise<{ path: string; text: string }[]> {
|
|
219
|
+
const paths: string[] = []
|
|
220
|
+
for (const path of relatives) paths.push(`${root}/${path}`)
|
|
163
221
|
const texts = await Promise.all(paths.map((path) => Bun.file(path).text()))
|
|
164
222
|
const sources: { path: string; text: string }[] = []
|
|
165
223
|
for (let at = 0; at < paths.length; at += 1)
|
|
166
224
|
sources.push({ path: paths[at] as string, text: texts[at] as string })
|
|
167
|
-
return
|
|
225
|
+
return sources
|
|
168
226
|
}
|
|
169
227
|
|
|
170
228
|
/**
|
|
171
|
-
*
|
|
229
|
+
* `parsed` and every `.abide` outside them that one of them imports — a dependency's, or a linked
|
|
230
|
+
* package's — parsed, and every one those import in turn; and every `.abide` under a directory one
|
|
231
|
+
* of them mounts, at the position the mount gives it (23.19). With them, the app's own handler files
|
|
232
|
+
* — `own`, below `root`, as its walk found them (`appFiles`) — and every one a mounted directory
|
|
233
|
+
* holds, each at the position it answers by.
|
|
172
234
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
235
|
+
* 34.23 — COMPILED WITH THE APP, by the app's checker, so a component a package ships as source is
|
|
236
|
+
* one of the app's in every way but its address: it is never routed (`placeOf`) unless mounted.
|
|
237
|
+
*/
|
|
238
|
+
export async function withDependencies(
|
|
239
|
+
root: string,
|
|
240
|
+
parsed: readonly ParsedSource[],
|
|
241
|
+
own: readonly string[],
|
|
242
|
+
resolve: (specifier: string, from: string) => string | undefined,
|
|
243
|
+
refused?: Diagnosis[],
|
|
244
|
+
): Promise<{
|
|
245
|
+
sources: ParsedSource[]
|
|
246
|
+
handlers: HandlerFile[]
|
|
247
|
+
mounted: Compilation['mounted']
|
|
248
|
+
}> {
|
|
249
|
+
const all = [...parsed]
|
|
250
|
+
const handlers: HandlerFile[] = []
|
|
251
|
+
for (const relative of own)
|
|
252
|
+
handlers.push({
|
|
253
|
+
path: `${root}/${relative}`,
|
|
254
|
+
relative,
|
|
255
|
+
position: relative,
|
|
256
|
+
})
|
|
257
|
+
// The mounted handler files placed so far; the app's own is never mounted (23.20).
|
|
258
|
+
const handled = new Set<string>()
|
|
259
|
+
const mounted: Compilation['mounted'] = {}
|
|
260
|
+
const byPath = new Map<string, ParsedSource>()
|
|
261
|
+
for (const source of all) byPath.set(source.path, source)
|
|
262
|
+
// 23.20 — a directory mounts once, and by whom.
|
|
263
|
+
const mountedBy = new Map<string, string>()
|
|
264
|
+
const refuse = (path: string, message: string): void => {
|
|
265
|
+
if (refused === undefined) throw new Error(message)
|
|
266
|
+
refused.push(refusal(path, new Error(message)))
|
|
267
|
+
}
|
|
268
|
+
const add = async (found: {
|
|
269
|
+
path: string
|
|
270
|
+
position?: string
|
|
271
|
+
}): Promise<void> => {
|
|
272
|
+
const text = await Bun.file(found.path).text()
|
|
273
|
+
for (const one of parseSources([{ ...found, text }], refused)) {
|
|
274
|
+
byPath.set(one.path, one)
|
|
275
|
+
all.push(one)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (let at = 0; at < all.length; at += 1) {
|
|
279
|
+
const source = all[at] as ParsedSource
|
|
280
|
+
const position = source.position ?? relativeTo(root, source.path)
|
|
281
|
+
const mounts = mountsOf(root, source.path, position, source.file)
|
|
282
|
+
if (typeof mounts === 'string') {
|
|
283
|
+
refuse(source.path, mounts)
|
|
284
|
+
continue
|
|
285
|
+
}
|
|
286
|
+
for (const mount of mounts) {
|
|
287
|
+
const other = mountedBy.get(mount.directory)
|
|
288
|
+
if (other !== undefined) {
|
|
289
|
+
refuse(
|
|
290
|
+
source.path,
|
|
291
|
+
`abide: ${other} and ${position} both mount ${mount.specifier}, and a directory answers at one place. RULEBOOK 23.20.`,
|
|
292
|
+
)
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
mountedBy.set(mount.directory, position)
|
|
296
|
+
mounted[relativeTo(root, mount.directory)] = mount.at
|
|
297
|
+
for (const placed of mount.handlers) {
|
|
298
|
+
// Two mounts can overlap, one directory inside the other; a file is placed once.
|
|
299
|
+
if (handled.has(placed.path)) continue
|
|
300
|
+
handled.add(placed.path)
|
|
301
|
+
handlers.push({
|
|
302
|
+
path: placed.path,
|
|
303
|
+
relative: relativeTo(root, placed.path),
|
|
304
|
+
position: placed.position,
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
for (const { path, position: placed } of mount.files) {
|
|
308
|
+
// A file a component import reached first takes its mount's position all the same.
|
|
309
|
+
const known = byPath.get(path)
|
|
310
|
+
if (known !== undefined) {
|
|
311
|
+
known.position = placed
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
await add({ path, position: placed })
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const target of abideImportsOf(
|
|
318
|
+
source.path,
|
|
319
|
+
source.file,
|
|
320
|
+
resolve,
|
|
321
|
+
).values()) {
|
|
322
|
+
if (!byPath.has(target)) await add({ path: target })
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return { sources: all, handlers, mounted }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The `Compilation` alone out of `app`, which is what is written to disk and served: the rest of a
|
|
330
|
+
* compiled app is the commands' to read.
|
|
331
|
+
*/
|
|
332
|
+
export function compilationOf(app: CompiledApp): Compilation {
|
|
333
|
+
return {
|
|
334
|
+
files: app.files,
|
|
335
|
+
handlers: app.handlers,
|
|
336
|
+
tables: app.tables,
|
|
337
|
+
mounted: app.mounted,
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Compiles every `.abide` source and handler of the app at `appRoot` (`Compilation`).
|
|
343
|
+
*
|
|
344
|
+
* `kept` is a checker the caller holds across compiles and closes itself.
|
|
175
345
|
*/
|
|
176
346
|
export async function compileApp(
|
|
177
347
|
appRoot: string,
|
|
178
|
-
out = buildDirectory(appRoot),
|
|
179
348
|
kept?: AppChecker,
|
|
180
349
|
): Promise<CompiledApp> {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
350
|
+
const root = realpathSync(appRoot)
|
|
351
|
+
// ONE WALK for the sources, the handler files and the public files, refusing a tree 23.16-23.18
|
|
352
|
+
// forbids before anything is read.
|
|
353
|
+
const { abide, handlers, published } = appFiles(root)
|
|
354
|
+
const sources = await sourcesAt(root, abide)
|
|
355
|
+
// 16.2 — an app of handler files alone is still an app.
|
|
356
|
+
if (sources.length === 0 && handlers.length === 0)
|
|
357
|
+
return {
|
|
358
|
+
files: {},
|
|
359
|
+
tables: {},
|
|
360
|
+
handlers: {},
|
|
361
|
+
mounted: {},
|
|
362
|
+
routes: [],
|
|
363
|
+
mounts: [],
|
|
364
|
+
sources: 0,
|
|
365
|
+
warnings: [],
|
|
366
|
+
published,
|
|
367
|
+
}
|
|
184
368
|
|
|
185
369
|
// One session for the whole build: opening it spawns the checker, and the cost is paid once
|
|
186
370
|
// rather than per file.
|
|
187
371
|
return withChecker(root, kept, async (session) => {
|
|
188
372
|
// A BUILD STOPS AT THE FIRST REFUSAL, so neither step is handed a list to report it in.
|
|
189
|
-
const
|
|
373
|
+
const own = parseSources(sources)
|
|
374
|
+
const resolve = resolverIn()
|
|
375
|
+
const {
|
|
376
|
+
sources: parsed,
|
|
377
|
+
handlers: handlerFiles,
|
|
378
|
+
mounted,
|
|
379
|
+
} = await withDependencies(root, own, handlers, resolve)
|
|
190
380
|
const { read, warnings } = await readSources(session, parsed)
|
|
191
381
|
const { ambient, unkeyed } = await session.lints(root, parsed)
|
|
192
382
|
// 11.37 — a build error, every read named, rather than a first one and a second run.
|
|
@@ -200,10 +390,13 @@ export async function compileApp(
|
|
|
200
390
|
root,
|
|
201
391
|
session,
|
|
202
392
|
read,
|
|
203
|
-
|
|
393
|
+
resolve,
|
|
394
|
+
handlerFiles,
|
|
204
395
|
)
|
|
205
396
|
return {
|
|
206
397
|
...emitted,
|
|
398
|
+
mounted,
|
|
399
|
+
published,
|
|
207
400
|
sources: sources.length,
|
|
208
401
|
// 11.30 — warned on, and the build goes on.
|
|
209
402
|
warnings: [...warnings, ...unkeyed, ...emitted.warnings],
|
|
@@ -216,137 +409,340 @@ export async function compileApp(
|
|
|
216
409
|
*
|
|
217
410
|
* `abide check` has done both by then, file by file, and asks this for only what the whole app can
|
|
218
411
|
* refuse: two handlers at one address, or a template the emit cannot lower.
|
|
219
|
-
*
|
|
220
|
-
* `output` is where the modules are written, relative to `root`.
|
|
221
412
|
*/
|
|
222
413
|
export async function emitApp(
|
|
223
414
|
root: string,
|
|
224
415
|
session: TypeSession,
|
|
225
416
|
sources: readonly ReadSource[],
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
417
|
+
/** The resolver the sources' dependencies were found with, whose answers it keeps. */
|
|
418
|
+
resolve: (specifier: string, from: string) => string | undefined,
|
|
419
|
+
/** The handler files, the app's own and each mounted one (`withDependencies`). */
|
|
420
|
+
handlerFiles: readonly HandlerFile[],
|
|
421
|
+
/** Told of each refusal at its file; the build's throws the first. */
|
|
422
|
+
refuse: Refuse = THROW,
|
|
423
|
+
): Promise<
|
|
424
|
+
Pick<
|
|
425
|
+
CompiledApp,
|
|
426
|
+
'files' | 'tables' | 'handlers' | 'routes' | 'mounts' | 'warnings'
|
|
427
|
+
>
|
|
428
|
+
> {
|
|
429
|
+
const files: Record<string, CompiledFile> = {}
|
|
430
|
+
const tables: Record<string, string> = {}
|
|
229
431
|
const routes: AppRoute[] = []
|
|
230
432
|
// 32.17 AND 34.14 — WHICH FILES ARE INVOKED AS COMPONENTS, and where each import lands. A
|
|
231
433
|
// caller's client module imports its child's, so a child with nothing to hydrate still
|
|
232
434
|
// needs one to exist (`emitClient`'s no-op), and an invoked file carries its own markup
|
|
233
435
|
// (`emitClient`). Built before the emit loop because a page may invoke a component the
|
|
234
436
|
// walk reaches later.
|
|
235
|
-
const graph = appGraph(root, sources,
|
|
437
|
+
const graph = appGraph(root, sources, resolve)
|
|
236
438
|
const parsed = graph.nodes
|
|
237
439
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
440
|
+
// 35.1, 35.12 — every `layout.abide` by the route it covers, one to a route; 35.13 — and one
|
|
441
|
+
// with no template wraps nothing, being the route's hooks alone.
|
|
442
|
+
const layouts: AppLayout[] = []
|
|
443
|
+
const layoutAt = new Map<string, string>()
|
|
444
|
+
const errorAt = new Map<string, string>()
|
|
445
|
+
for (const entry of parsed) {
|
|
446
|
+
if (entry.kind !== 'layout' && entry.kind !== 'error') continue
|
|
447
|
+
const route = routeOf(entry.position)
|
|
448
|
+
const held = entry.kind === 'layout' ? layoutAt : errorAt
|
|
449
|
+
const other = held.get(shapeOf(route))
|
|
450
|
+
if (other !== undefined) {
|
|
451
|
+
refuse(
|
|
452
|
+
entry.relative,
|
|
453
|
+
new Error(
|
|
454
|
+
`abide: ${other} and ${entry.relative} are both the ${entry.kind}.abide at ${route}. RULEBOOK 35.12.`,
|
|
455
|
+
),
|
|
456
|
+
)
|
|
457
|
+
continue
|
|
458
|
+
}
|
|
459
|
+
held.set(shapeOf(route), entry.relative)
|
|
460
|
+
if (entry.kind === 'layout' && entry.file.template.length > 0)
|
|
461
|
+
layouts.push({
|
|
462
|
+
source: entry.relative,
|
|
463
|
+
segments: parseRoutePattern(route),
|
|
464
|
+
})
|
|
465
|
+
}
|
|
466
|
+
// 35.2 — every `error.abide`, with the route it answers for and the layouts around it.
|
|
242
467
|
const errors: AppRoute[] = []
|
|
243
468
|
for (const entry of parsed) {
|
|
244
469
|
if (entry.kind !== 'page' && entry.kind !== 'error') continue
|
|
470
|
+
// 23.3, 35.14 — a page or an `error.abide` with nothing to paint is its route's hooks
|
|
471
|
+
// alone.
|
|
472
|
+
if (!paints(entry.file)) continue
|
|
245
473
|
;(entry.kind === 'page' ? routes : errors).push({
|
|
246
474
|
pattern: entry.route as string,
|
|
247
475
|
source: entry.relative,
|
|
248
|
-
layouts: layoutsAround(entry.
|
|
476
|
+
layouts: layoutsAround(entry.route as string, layouts),
|
|
249
477
|
})
|
|
250
478
|
}
|
|
251
479
|
|
|
480
|
+
// 16.2 AND 16.28 — THE HANDLERS, read before a module is emitted: the server mounts each one,
|
|
481
|
+
// and the browser holds its wrapper in its place — a handler file's as that file, a private
|
|
482
|
+
// handler's in the server block's place in its own file's module. Written whether or not there
|
|
483
|
+
// is a page: a handler is answered over http to a caller that has none.
|
|
484
|
+
const { mounts, warnings } = await handlersUnder(
|
|
485
|
+
session,
|
|
486
|
+
parsed,
|
|
487
|
+
handlerFiles,
|
|
488
|
+
refuse,
|
|
489
|
+
)
|
|
490
|
+
refuseSharedRoutes(routes, mounts, refuse)
|
|
491
|
+
refuseStrayLauncher(parsed, handlerFiles, refuse)
|
|
492
|
+
const standIns = standInsOf(mounts)
|
|
493
|
+
// 37.1 — every module a server block or a handler file runs in, for the boot to import before
|
|
494
|
+
// `onStart`.
|
|
495
|
+
const servers: string[] = []
|
|
496
|
+
for (const file of handlerFiles) servers.push(file.relative)
|
|
252
497
|
let transitions = false
|
|
253
498
|
for (const entry of parsed) {
|
|
499
|
+
// 35.16 — of the files with an address, the root layout alone is outermost in every render.
|
|
500
|
+
if (
|
|
501
|
+
entry.file.document !== undefined &&
|
|
502
|
+
entry.kind !== 'component' &&
|
|
503
|
+
!(entry.kind === 'layout' && routeOf(entry.position) === '/')
|
|
504
|
+
) {
|
|
505
|
+
refuse(
|
|
506
|
+
entry.relative,
|
|
507
|
+
new Error(
|
|
508
|
+
`abide: ${entry.relative} writes <html>, and of the pages, layouts and error pages only the root layout.abide writes the document. RULEBOOK 35.16.`,
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
continue
|
|
512
|
+
}
|
|
254
513
|
const emitted = emitSource(
|
|
255
514
|
entry.relative,
|
|
256
515
|
entry.file,
|
|
257
516
|
entry.answer,
|
|
258
517
|
graph.invoked.has(entry.path),
|
|
259
|
-
|
|
518
|
+
standIns.get(entry.relative),
|
|
519
|
+
entry.text,
|
|
520
|
+
entry.position,
|
|
260
521
|
)
|
|
261
|
-
|
|
262
|
-
if (emitted.sheet !== undefined)
|
|
263
|
-
modules.set(`${stem}.style.css`, emitted.sheet)
|
|
522
|
+
if (entry.file.servers.length > 0) servers.push(entry.relative)
|
|
264
523
|
transitions ||= emitted.transitions
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
524
|
+
// 34.26 — a dependency's module runs on the app's runtime, whatever the package carries.
|
|
525
|
+
const onAppRuntime =
|
|
526
|
+
placeOf(entry.relative) === 'dependency'
|
|
527
|
+
? (text: string) => onAbideOf(root, entry.path, text)
|
|
528
|
+
: (text: string) => text
|
|
529
|
+
files[entry.relative] = {
|
|
530
|
+
server: onAppRuntime(emitted.server),
|
|
531
|
+
client: onAppRuntime(emitted.client),
|
|
532
|
+
sheet: emitted.sheet,
|
|
533
|
+
// A map placing nothing — a file of markup alone — is left out.
|
|
534
|
+
serverMappings: placing(emitted.serverMappings),
|
|
535
|
+
clientMappings: placing(emitted.clientMappings),
|
|
536
|
+
}
|
|
268
537
|
}
|
|
269
538
|
// THE ROUTE TABLE, as the build's own knowledge rather than a list an app maintains. Which
|
|
270
|
-
// addresses exist is a fact about the file tree (
|
|
271
|
-
// so a route that is not a
|
|
272
|
-
//
|
|
539
|
+
// addresses exist is a fact about the file tree (a page is an address, 23.3),
|
|
540
|
+
// so a route that is not a file cannot be written and a file that is one cannot be
|
|
541
|
+
// forgotten. The loaders are dynamic imports, which is what keeps a destination's
|
|
273
542
|
// markup and bindings off the entry page's bundle until somebody navigates there.
|
|
274
543
|
//
|
|
275
|
-
// TWO TABLES OFF ONE WALK
|
|
276
|
-
//
|
|
277
|
-
//
|
|
544
|
+
// TWO TABLES OFF ONE WALK, each importing a file's `?component` face — the same addresses
|
|
545
|
+
// either way, which is what keeps the pair from being two walks that can disagree. The
|
|
546
|
+
// server's is written with no
|
|
278
547
|
// page too — an app of handlers alone still answers — and the client's, with the entry
|
|
279
548
|
// that boots it, only where there is a page to boot.
|
|
280
|
-
|
|
549
|
+
tables[SERVER_TABLE] = serverTable(routes, errors, servers, mounts)
|
|
550
|
+
// 16.74 — what the browser holds of each handler file, and the route its hooks are filed at:
|
|
551
|
+
// read by the module's load, which runs before the server table is imported.
|
|
552
|
+
const handlers: Compilation['handlers'] = {}
|
|
553
|
+
for (const mount of mounts)
|
|
554
|
+
if (!mount.private)
|
|
555
|
+
handlers[mount.source] = {
|
|
556
|
+
scope: mount.scope,
|
|
557
|
+
client: standIns.get(mount.source) ?? '',
|
|
558
|
+
}
|
|
281
559
|
if (routes.length > 0) {
|
|
282
|
-
|
|
283
|
-
const shellHead = keyedHead(await shellUnder(root))
|
|
284
|
-
modules.set('routes.ts', clientTable(routes, transitions, shellHead))
|
|
560
|
+
tables[ROUTES_TABLE] = clientTable(routes, transitions)
|
|
285
561
|
// The entry's SOURCE carries the name the bundle is written under — `entry-<hash>.js`
|
|
286
|
-
// from a build and `entry.js` in dev — and its key in the manifest (38.
|
|
287
|
-
|
|
562
|
+
// from a build and `entry.js` in dev — and its key in the manifest (38.22, 38.5).
|
|
563
|
+
tables[`${CLIENT_ENTRY}.ts`] = ENTRY_MODULE
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
files,
|
|
567
|
+
tables,
|
|
568
|
+
handlers,
|
|
569
|
+
routes,
|
|
570
|
+
mounts,
|
|
571
|
+
warnings,
|
|
288
572
|
}
|
|
289
|
-
// 16.2 AND 16.28 — THE HANDLERS, TWICE OFF ONE READ: the table the server mounts them
|
|
290
|
-
// from, and the module a browser is handed in place of each one's source. Written whether
|
|
291
|
-
// or not there is a page: a handler is answered over http to a caller that has none.
|
|
292
|
-
const { mounts, warnings } = await handlersUnder(root, session)
|
|
293
|
-
modules.set('rpc.ts', mountTable(mounts))
|
|
294
|
-
for (const [source, wrapper] of wrappersOf(mounts))
|
|
295
|
-
modules.set(`${WIRE_DIRECTORY}/${source}`, wrapper)
|
|
296
|
-
return { modules, routes, warnings }
|
|
297
573
|
}
|
|
298
574
|
|
|
575
|
+
/**
|
|
576
|
+
* `text`, a module compiled from the dependency's `.abide` at `path`, with each of abide's own
|
|
577
|
+
* specifiers written as the path to the file the app's root resolves it to.
|
|
578
|
+
*
|
|
579
|
+
* 34.26 — RELATIVE, so the compilation still serves once the app and its `node_modules` are moved
|
|
580
|
+
* together, and THE FILE the app's own code loads, so the two are one module instance. A path and
|
|
581
|
+
* not a specifier a plugin answers: bun's runtime hands a plugin no bare specifier that names no
|
|
582
|
+
* file, so a mark on `@abide/abide/server` was never read.
|
|
583
|
+
*/
|
|
584
|
+
function onAbideOf(root: string, path: string, text: string): string {
|
|
585
|
+
const from = dirname(realpathSync(path))
|
|
586
|
+
return respecified(text, (specifier) => {
|
|
587
|
+
if (
|
|
588
|
+
specifier !== SHARED_SPECIFIER &&
|
|
589
|
+
!specifier.startsWith(`${SHARED_SPECIFIER}/`)
|
|
590
|
+
)
|
|
591
|
+
return undefined
|
|
592
|
+
let target: string
|
|
593
|
+
try {
|
|
594
|
+
target = resolvedFile(specifier, root)
|
|
595
|
+
} catch {
|
|
596
|
+
return undefined
|
|
597
|
+
}
|
|
598
|
+
const relativePath = posix.relative(from, target)
|
|
599
|
+
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
|
|
600
|
+
})
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// `mappings` that place something, or `undefined` for a module of markup alone.
|
|
604
|
+
function placing(mappings: string | undefined): string | undefined {
|
|
605
|
+
return mappings !== undefined && /[^;]/.test(mappings)
|
|
606
|
+
? mappings
|
|
607
|
+
: undefined
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* How a table imports the `.abide` at `source`: from `__abide`, one directory below the root, as
|
|
612
|
+
* the module the file compiles to.
|
|
613
|
+
*/
|
|
614
|
+
function tableImport(source: string): string {
|
|
615
|
+
return JSON.stringify(
|
|
616
|
+
source.endsWith('.abide')
|
|
617
|
+
? `../${source}${COMPONENT_QUERY}`
|
|
618
|
+
: `../${source}`,
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// A layout and the route it covers, parsed.
|
|
623
|
+
type AppLayout = { source: string; segments: Segment[] }
|
|
624
|
+
|
|
299
625
|
/**
|
|
300
626
|
* The layouts a page renders inside, outermost first.
|
|
301
627
|
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
628
|
+
* 35.1 — every `layout.abide` whose route lies ON THE WAY DOWN to the page's wraps it and no other
|
|
629
|
+
* does: `src/layout.abide` wraps the page `src/media/index.abide` serves at `/media`, and so does
|
|
630
|
+
* `src/[section]/layout.abide`. So the test is a prefix of the route, kind by kind, and the nesting
|
|
631
|
+
* order is the route's depth.
|
|
305
632
|
*/
|
|
306
|
-
function layoutsAround(
|
|
307
|
-
const
|
|
308
|
-
const chain:
|
|
309
|
-
for (const layout of layouts)
|
|
310
|
-
|
|
311
|
-
|
|
633
|
+
function layoutsAround(route: string, layouts: AppLayout[]): string[] {
|
|
634
|
+
const page = parseRoutePattern(route)
|
|
635
|
+
const chain: AppLayout[] = []
|
|
636
|
+
for (const layout of layouts)
|
|
637
|
+
if (coversRoute(layout.segments, page)) chain.push(layout)
|
|
638
|
+
// A shorter route is an outer one: they are prefixes of each other by construction.
|
|
639
|
+
chain.sort((one, two) => one.segments.length - two.segments.length)
|
|
640
|
+
const sources: string[] = []
|
|
641
|
+
for (const layout of chain) sources.push(layout.source)
|
|
642
|
+
return sources
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* A route with its parameters' names erased: what two routes answering one address share.
|
|
647
|
+
*
|
|
648
|
+
* 19.1 — `/media/[id]` and `/media/[slug]` are one route.
|
|
649
|
+
*/
|
|
650
|
+
function shapeOf(route: string): string {
|
|
651
|
+
let shape = ''
|
|
652
|
+
for (const segment of parseRoutePattern(route))
|
|
653
|
+
shape += `/${segment.kind === 'static' ? segment.name : `[${segment.kind}]`}`
|
|
654
|
+
return shape === '' ? '/' : shape
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Refuses, naming both, where two files answer one method at one route — at the second.
|
|
659
|
+
*
|
|
660
|
+
* 19.1 — a page answers `GET` at its own, and a handler its method at its address.
|
|
661
|
+
*/
|
|
662
|
+
function refuseSharedRoutes(
|
|
663
|
+
routes: AppRoute[],
|
|
664
|
+
mounts: Mount[],
|
|
665
|
+
refuse: Refuse,
|
|
666
|
+
): void {
|
|
667
|
+
const owners = new Map<string, string>()
|
|
668
|
+
const claim = (method: string, route: string, source: string): void => {
|
|
669
|
+
const key = `${method} ${shapeOf(route)}`
|
|
670
|
+
const owner = owners.get(key)
|
|
671
|
+
if (owner === undefined) owners.set(key, source)
|
|
672
|
+
else
|
|
673
|
+
refuse(
|
|
674
|
+
source,
|
|
675
|
+
new Error(
|
|
676
|
+
`abide: ${owner} and ${source} both answer ${method} at ${route}. RULEBOOK 19.1.`,
|
|
677
|
+
),
|
|
678
|
+
)
|
|
312
679
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return chain
|
|
680
|
+
for (const route of routes) claim('GET', route.pattern, route.source)
|
|
681
|
+
for (const mount of mounts) claim(mount.method, mount.address, mount.source)
|
|
316
682
|
}
|
|
317
683
|
|
|
318
684
|
/**
|
|
319
|
-
*
|
|
320
|
-
* does not resolve.
|
|
685
|
+
* Refuses, naming the file, where anything but the root's `launcher.abide` calls `onLauncher`.
|
|
321
686
|
*
|
|
322
|
-
*
|
|
323
|
-
|
|
687
|
+
* 38.107 — the launch loads that file alone, so a call anywhere else is never read.
|
|
688
|
+
*/
|
|
689
|
+
function refuseStrayLauncher(
|
|
690
|
+
sources: readonly { relative: string; position: string; file: AbideFile }[],
|
|
691
|
+
handlerFiles: readonly HandlerFile[],
|
|
692
|
+
refuse: Refuse,
|
|
693
|
+
): void {
|
|
694
|
+
const stray = (relative: string): void =>
|
|
695
|
+
refuse(
|
|
696
|
+
relative,
|
|
697
|
+
new Error(
|
|
698
|
+
`abide: ${relative} calls onLauncher, which is read from src/${LAUNCHER_PAGE} alone. Move the call there. RULEBOOK 38.107.`,
|
|
699
|
+
),
|
|
700
|
+
)
|
|
701
|
+
for (const source of sources)
|
|
702
|
+
if (
|
|
703
|
+
source.position !== LAUNCHER_PAGE &&
|
|
704
|
+
source.file.servers.some(callsLauncher)
|
|
705
|
+
)
|
|
706
|
+
stray(source.relative)
|
|
707
|
+
for (const file of handlerFiles)
|
|
708
|
+
if (callsLauncher(readFileSync(file.path, 'utf8'))) stray(file.relative)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function callsLauncher(source: string): boolean {
|
|
712
|
+
return (
|
|
713
|
+
source.includes('onLauncher') &&
|
|
714
|
+
hooksCalled(source).includes('onLauncher')
|
|
715
|
+
)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Resolves a specifier from a directory as bun does, or `undefined` where it does not resolve.
|
|
324
720
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* real path is spelled back under `root`; a build hands `sourcesUnder`'s real root, and nothing
|
|
329
|
-
* is spelled back.
|
|
721
|
+
* A COMPONENT IMPORT RESOLVES THE WAY THE APP'S OWN IMPORTS DO — its import map, from the file
|
|
722
|
+
* that wrote it. One that does not resolve is left as written, and the bundle names it. THE
|
|
723
|
+
* ANSWER IS A REAL PATH, as every root handed to the compiler is.
|
|
330
724
|
*/
|
|
331
|
-
export function resolverIn(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
725
|
+
export function resolverIn(): (
|
|
726
|
+
specifier: string,
|
|
727
|
+
from: string,
|
|
728
|
+
) => string | undefined {
|
|
729
|
+
// ONE ANSWER PER SPECIFIER AND DIRECTORY for as long as the resolver lives: a compile resolves
|
|
730
|
+
// every import once for its dependencies and again for its graph, and hands both one resolver.
|
|
731
|
+
const answered = new Map<string, string | undefined>()
|
|
335
732
|
return (specifier, from) => {
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
: resolved
|
|
733
|
+
const key = `${from}\0${specifier}`
|
|
734
|
+
if (answered.has(key)) return answered.get(key)
|
|
735
|
+
const found = resolveFrom(specifier, from)
|
|
736
|
+
answered.set(key, found)
|
|
737
|
+
return found
|
|
342
738
|
}
|
|
343
739
|
}
|
|
344
740
|
|
|
345
741
|
function resolveFrom(specifier: string, from: string): string | undefined {
|
|
346
742
|
try {
|
|
347
|
-
return
|
|
743
|
+
return resolvedFile(specifier, from)
|
|
348
744
|
} catch {
|
|
349
|
-
// 38.
|
|
745
|
+
// 38.65 — BUN KEEPS A DIRECTORY'S LISTING FOR THE LIFE OF THE PROCESS, and looks a `#` specifier up
|
|
350
746
|
// in it with no second look — where a relative one that misses reads the directory again.
|
|
351
747
|
// `abide dev` compiles in one process for its whole session, so a component created since
|
|
352
748
|
// its directory was listed never resolved, and was read as uninvoked. The `imports` map is
|
|
@@ -392,29 +788,21 @@ function throughImports(specifier: string, from: string): string | undefined {
|
|
|
392
788
|
return undefined
|
|
393
789
|
}
|
|
394
790
|
|
|
395
|
-
/**
|
|
396
|
-
* The rows of either route table, one per route.
|
|
397
|
-
*
|
|
398
|
-
* The two differ only in which module a pattern loads, so a row is written once and the extension
|
|
399
|
-
* is the argument.
|
|
400
|
-
*/
|
|
791
|
+
/** The rows of either route table, one per route, `column` appending the fields only one carries. */
|
|
401
792
|
function tableOf(
|
|
402
793
|
routes: AppRoute[],
|
|
403
|
-
extension: string,
|
|
404
794
|
column: (route: AppRoute) => string = () => '',
|
|
405
795
|
): string {
|
|
406
796
|
const rows: string[] = []
|
|
407
797
|
for (const route of routes) {
|
|
408
|
-
const module = `./${route.source.replace(/\.abide$/, extension)}`
|
|
409
798
|
// 35.1 — THE CHAIN IS PART OF THE ADDRESS, so it is in the table rather than resolved at
|
|
410
799
|
// run time: which layouts wrap a page is a fact about the file tree, and the server and
|
|
411
800
|
// the client read it off the same walk for the reason the addresses are (30.8).
|
|
412
801
|
const wrapping = route.layouts.map(
|
|
413
|
-
(layout) =>
|
|
414
|
-
`() => import(${JSON.stringify(`./${layout.replace(/\.abide$/, extension)}`)})`,
|
|
802
|
+
(layout) => `() => import(${tableImport(layout)})`,
|
|
415
803
|
)
|
|
416
804
|
rows.push(
|
|
417
|
-
` { pattern: ${JSON.stringify(route.pattern)}, load: () => import(${
|
|
805
|
+
` { pattern: ${JSON.stringify(route.pattern)}, load: () => import(${tableImport(route.source)}), layouts: [${wrapping.join(', ')}]${column(route)} },`,
|
|
418
806
|
)
|
|
419
807
|
}
|
|
420
808
|
return rows.join('\n')
|
|
@@ -427,23 +815,19 @@ function tableOf(
|
|
|
427
815
|
* tsconfig includes its build output typechecks it — and a bare `host` is an implicit `any` that
|
|
428
816
|
* fails `tsc` under the `strict` every abide app is written against.
|
|
429
817
|
*/
|
|
430
|
-
function clientTable(
|
|
431
|
-
routes: AppRoute[],
|
|
432
|
-
transitions: boolean,
|
|
433
|
-
shellHead: string,
|
|
434
|
-
): string {
|
|
818
|
+
function clientTable(routes: AppRoute[], transitions: boolean): string {
|
|
435
819
|
return `// Written by \`abide build\`. The addresses this app answers, and what to load for each.
|
|
436
820
|
import { listenForRoutes, startRouter } from '${ROUTING_SPECIFIER}'
|
|
437
821
|
|
|
438
822
|
export const routes = [
|
|
439
|
-
${tableOf(routes
|
|
823
|
+
${tableOf(routes)}
|
|
440
824
|
]
|
|
441
825
|
|
|
442
826
|
// The router this document gets, the page it arrived on, and the two places a reader navigates
|
|
443
827
|
// from (30.7). Hydrating here rather than leaving it to the app is what makes the page the server
|
|
444
828
|
// painted live without anybody wiring it.
|
|
445
829
|
export function start(host: Element) {
|
|
446
|
-
const router = startRouter(routes, host, ${transitions}
|
|
830
|
+
const router = startRouter(routes, host, ${transitions})
|
|
447
831
|
listenForRoutes(router)
|
|
448
832
|
const ready = router.hydrate()
|
|
449
833
|
return { router, ready }
|
|
@@ -452,108 +836,122 @@ export function start(host: Element) {
|
|
|
452
836
|
}
|
|
453
837
|
|
|
454
838
|
/**
|
|
455
|
-
* The source of `server.ts`: the same addresses, loading the module that PAINTS each page
|
|
839
|
+
* The source of `server.ts`: the same addresses, loading the module that PAINTS each page, the
|
|
840
|
+
* error pages, the modules a server block runs in, and every handler.
|
|
456
841
|
*
|
|
457
842
|
* No `start` here and nothing imported: the server holds the matching and the ranking already,
|
|
458
843
|
* and a server table that carried either would be a second copy of both.
|
|
459
844
|
*/
|
|
460
|
-
function serverTable(
|
|
845
|
+
function serverTable(
|
|
846
|
+
routes: AppRoute[],
|
|
847
|
+
errors: AppRoute[],
|
|
848
|
+
servers: string[],
|
|
849
|
+
mounts: Mount[],
|
|
850
|
+
): string {
|
|
851
|
+
let loaders = ''
|
|
852
|
+
for (const source of servers)
|
|
853
|
+
loaders += ` () => import(${tableImport(source)}),\n`
|
|
854
|
+
let handlers = ''
|
|
855
|
+
for (const mount of mounts) {
|
|
856
|
+
const fields = JSON.stringify(mount)
|
|
857
|
+
// The module the handler is declared in — the handler file, or the `.abide`'s server
|
|
858
|
+
// module, the route table's own.
|
|
859
|
+
handlers += ` { load: () => import(${tableImport(mount.source)}), ${fields.slice(1, -1)} },\n`
|
|
860
|
+
}
|
|
461
861
|
return `// Written by \`abide build\`. The addresses this app answers, and what paints each.
|
|
462
862
|
export const routes = [
|
|
463
|
-
${tableOf(routes,
|
|
863
|
+
${tableOf(routes, clientModules)}
|
|
464
864
|
]
|
|
465
865
|
|
|
466
866
|
// 35.2 — the error pages, by the directory each answers for.
|
|
467
867
|
export const errors = [
|
|
468
|
-
${tableOf(errors
|
|
868
|
+
${tableOf(errors)}
|
|
469
869
|
]
|
|
870
|
+
|
|
871
|
+
// 37.1 — every module a \`<script server>\` runs in, imported before the app boots.
|
|
872
|
+
export const servers = [
|
|
873
|
+
${loaders}]
|
|
874
|
+
|
|
875
|
+
// 16.2 — every handler this app answers over http, and where each is.
|
|
876
|
+
export const handlers = [
|
|
877
|
+
${handlers}]
|
|
470
878
|
`
|
|
471
879
|
}
|
|
472
880
|
|
|
473
881
|
/**
|
|
474
882
|
* The server row's `clientModules` column: what the browser will load at this address.
|
|
475
883
|
*
|
|
476
|
-
* 38.
|
|
884
|
+
* 38.28 — named on the SERVER's row: the server writes the document, and the preloads for the page
|
|
477
885
|
* and its chain have to be in it before the client has run at all. Keys into the manifest
|
|
478
886
|
* `manifestOf` reads off the bundle.
|
|
479
887
|
*/
|
|
480
888
|
function clientModules(route: AppRoute): string {
|
|
481
|
-
|
|
482
|
-
for (const source of [route.source, ...route.layouts])
|
|
483
|
-
modules.push(source.replace(/\.abide$/, '.client.ts'))
|
|
484
|
-
return `, clientModules: ${JSON.stringify(modules)}`
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/** The source of `rpc.ts`: the handler rows `mountHandlers` answers from. */
|
|
488
|
-
function mountTable(mounts: Mount[]): string {
|
|
489
|
-
return `// Written by \`abide build\`. Every handler this app answers over http, and where each is.
|
|
490
|
-
export const handlers = ${JSON.stringify(mounts, null, 4)}
|
|
491
|
-
`
|
|
889
|
+
return `, clientModules: ${JSON.stringify([route.source, ...route.layouts])}`
|
|
492
890
|
}
|
|
493
891
|
|
|
494
892
|
/**
|
|
495
|
-
* The browser
|
|
893
|
+
* The wrappers the browser holds in each handler's place, by source.
|
|
496
894
|
*
|
|
497
|
-
* 16.28 — WHAT A BROWSER
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
* own handlers rather than the root's.
|
|
895
|
+
* 16.28 — WHAT A BROWSER HOLDS WHERE THE APP WROTE A PRIVATE HANDLER: the same name, the same call
|
|
896
|
+
* over the wire, bound in its `.abide`'s server block's place. 16.74 — a handler file's is its own
|
|
897
|
+
* module, its default, answered in the file's place.
|
|
501
898
|
*/
|
|
502
|
-
function
|
|
503
|
-
const
|
|
899
|
+
function standInsOf(mounts: Mount[]): Map<string, string> {
|
|
900
|
+
const lines = new Map<string, string[]>()
|
|
504
901
|
for (const mount of mounts) {
|
|
505
|
-
// 20.4 — withheld from the browser: no wrapper, so
|
|
902
|
+
// 20.4 — withheld from the browser: no wrapper, so a call of it there names nothing.
|
|
506
903
|
if (!mount.ui) continue
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
: mount.copy === undefined
|
|
514
|
-
? ''
|
|
515
|
-
: `, ${JSON.stringify(mount.copy)}`
|
|
516
|
-
const call = `wire(${JSON.stringify(mount.method)}, new URL(${at}, import.meta.url).pathname${copy})`
|
|
517
|
-
const line =
|
|
518
|
-
mount.name === 'default'
|
|
519
|
-
? `export default ${call}\n`
|
|
520
|
-
: `export const ${mount.name} = ${call}\n`
|
|
521
|
-
modules.set(mount.source, (modules.get(mount.source) ?? '') + line)
|
|
522
|
-
}
|
|
523
|
-
const wrappers = new Map<string, string>()
|
|
524
|
-
for (const [source, body] of modules)
|
|
525
|
-
wrappers.set(
|
|
526
|
-
source,
|
|
527
|
-
`// Written by \`abide build\`. What a browser holds in place of ${source} (16.28).
|
|
528
|
-
import { wire } from '${WIRE_SPECIFIER}'
|
|
529
|
-
|
|
530
|
-
${body}`,
|
|
904
|
+
const held = lines.get(mount.source) ?? []
|
|
905
|
+
held.push(
|
|
906
|
+
wireLine(
|
|
907
|
+
mount,
|
|
908
|
+
mount.private ? `const ${mount.name} =` : 'export default',
|
|
909
|
+
),
|
|
531
910
|
)
|
|
532
|
-
|
|
911
|
+
lines.set(mount.source, held)
|
|
912
|
+
}
|
|
913
|
+
const standIns = new Map<string, string>()
|
|
914
|
+
for (const [source, held] of lines) standIns.set(source, wireModule(held))
|
|
915
|
+
return standIns
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* One handler's wrapper, bound as `binding` writes it.
|
|
920
|
+
*
|
|
921
|
+
* The address is written RELATIVE to the bundle's own directory and resolved against the chunk at
|
|
922
|
+
* run time (16.10), so an app mounted under a sub-path calls its own handlers rather than the
|
|
923
|
+
* root's.
|
|
924
|
+
*/
|
|
925
|
+
function wireLine(mount: Mount, binding: string): string {
|
|
926
|
+
const at = JSON.stringify(posix.relative(CLIENT_ADDRESS, mount.address))
|
|
927
|
+
// 18.4 — what the handler's value declares that the browser's copy behaves by.
|
|
928
|
+
// 11.29 — and the defaults its parameter writes, which the copy keys by.
|
|
929
|
+
const copy =
|
|
930
|
+
mount.defaults !== undefined
|
|
931
|
+
? `, ${JSON.stringify(mount.copy)}, ${JSON.stringify(mount.defaults)}`
|
|
932
|
+
: mount.copy === undefined
|
|
933
|
+
? ''
|
|
934
|
+
: `, ${JSON.stringify(mount.copy)}`
|
|
935
|
+
return `${binding} __wire(${JSON.stringify(mount.method)}, new URL(${at}, import.meta.url).pathname${copy})\n`
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/** A module of wrappers: the import they share, then each line. */
|
|
939
|
+
function wireModule(lines: string[]): string {
|
|
940
|
+
return `// 16.28 — what a browser holds in place of a handler.
|
|
941
|
+
import { wire as __wire } from '${WIRE_SPECIFIER}'
|
|
942
|
+
${lines.join('')}`
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
|
|
946
|
+
export function paints(file: AbideFile): boolean {
|
|
947
|
+
return file.template.length > 0 || file.head.length > 0
|
|
533
948
|
}
|
|
534
949
|
|
|
535
950
|
// THE ENTRY. An app does not write it: which module boots the client is the build's knowledge,
|
|
536
951
|
// exactly as the route table is, and a hand-written entry is one more file to keep in step with a
|
|
537
952
|
// layout that moves.
|
|
538
953
|
const ENTRY_MODULE = `// Written by \`abide build\`. What boots this app in a browser.
|
|
539
|
-
import { start } from '
|
|
954
|
+
import { start } from '../${VIRTUAL_DIRECTORY}/${ROUTES_TABLE}'
|
|
540
955
|
|
|
541
956
|
start(document.body)
|
|
542
957
|
`
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* The nearest file of this name at or above `from`, or `undefined`.
|
|
546
|
-
*
|
|
547
|
-
* The compiler needs a project for 31.3, and an app's own tsconfig is the one whose paths and lib
|
|
548
|
-
* its pages are written against.
|
|
549
|
-
*/
|
|
550
|
-
function nearest(from: string, name: string): string | undefined {
|
|
551
|
-
let at = from
|
|
552
|
-
while (true) {
|
|
553
|
-
const candidate = `${at}/${name}`
|
|
554
|
-
if (existsSync(candidate)) return candidate
|
|
555
|
-
const up = dirname(at)
|
|
556
|
-
if (up === at) return undefined
|
|
557
|
-
at = up
|
|
558
|
-
}
|
|
559
|
-
}
|