@abide/abide 0.101.0 → 0.102.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 +16 -0
- package/bin/abide.ts +223 -164
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +5 -3
- package/scaffold/AGENTS.md +31 -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 +262 -67
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +43 -15
- package/src/compiler/checkedShadow.ts +39 -5
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +433 -193
- package/src/compiler/compiledEntry.ts +43 -39
- package/src/compiler/componentGraph.ts +58 -71
- package/src/compiler/declaredExports.ts +24 -7
- package/src/compiler/emitClient.ts +141 -71
- package/src/compiler/emitComponent.ts +37 -8
- package/src/compiler/emitSource.ts +106 -13
- package/src/compiler/handlersUnder.ts +106 -59
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +138 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +213 -7
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +61 -7
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +30 -11
- package/src/compiler/serveLanguage.ts +125 -34
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/syncTypes.ts +32 -0
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +1 -1
- package/src/compiler/writeTypes.ts +82 -0
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/LAUNCHER_FILES.ts +9 -0
- package/src/server/MODULE_QUERIES.ts +18 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +391 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +235 -106
- package/src/server/answerRpc.ts +102 -30
- 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 +179 -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 +37 -37
- package/src/server/componentModules.d.ts +3 -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 +59 -73
- package/src/server/devWorker.ts +114 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +37 -35
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +124 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +11 -7
- package/src/server/installSourceMaps.ts +88 -0
- package/src/server/launcherOptions.ts +34 -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 +5 -5
- package/src/server/negotiateEncoding.ts +1 -1
- 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 +52 -27
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +73 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +18 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +7 -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/RESERVED_DIRECTORIES.ts +15 -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 +1 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/shared/menuItem.ts +3 -3
- 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/shared/routePattern.ts +82 -46
- 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 +38 -31
- 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/menuBridge.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/reportMenu.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 → page.abide} +0 -0
|
@@ -1,28 +1,53 @@
|
|
|
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 {
|
|
16
|
+
coversRoute,
|
|
17
|
+
parseRoutePattern,
|
|
18
|
+
placeOf,
|
|
19
|
+
routeOf,
|
|
20
|
+
type Segment,
|
|
21
|
+
} from '#shared/routePattern.ts'
|
|
22
|
+
import { abideImportsOf, appGraph } from './componentGraph.ts'
|
|
10
23
|
import { emitSource } from './emitSource.ts'
|
|
11
24
|
import { formatDiagnosis } from './formatDiagnosis.ts'
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
25
|
+
import { handlersUnder } from './handlersUnder.ts'
|
|
26
|
+
import { mountsOf } from './mountsOf.ts'
|
|
27
|
+
import { nearest } from './nearest.ts'
|
|
28
|
+
import type { AbideFile } from './parseAbide.ts'
|
|
29
|
+
import {
|
|
30
|
+
type ParsedSource,
|
|
31
|
+
parseSources,
|
|
32
|
+
type ReadSource,
|
|
33
|
+
readSources,
|
|
34
|
+
refusal,
|
|
35
|
+
} from './readSources.ts'
|
|
36
|
+
import {
|
|
37
|
+
ROUTING_SPECIFIER,
|
|
38
|
+
SHARED_SPECIFIER,
|
|
39
|
+
WIRE_SPECIFIER,
|
|
40
|
+
} from './SPECIFIERS.ts'
|
|
41
|
+
import { respecified } from './splitImports.ts'
|
|
15
42
|
import { type Diagnosis, TypeSession } from './TypeSession.ts'
|
|
16
43
|
import { writeDefaultProject } from './writeDefaultProject.ts'
|
|
44
|
+
import { writeTypes } from './writeTypes.ts'
|
|
17
45
|
|
|
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).
|
|
46
|
+
// AN APP'S `.abide` SOURCES, COMPILED — every file keyed by its path below the root, and loaded
|
|
47
|
+
// there through its `?component` face (`abideModules`). Nothing here touches the output directory:
|
|
48
|
+
// `abide build` writes what this hands back and `abide dev` keeps it in memory (38.16), and the two
|
|
49
|
+
// commands serving different markup from the same sources is exactly what one compile in two sinks
|
|
50
|
+
// makes impossible.
|
|
26
51
|
|
|
27
52
|
type AppRoute = {
|
|
28
53
|
/** The address, in the bracket spelling `routePattern` parses. */
|
|
@@ -38,21 +63,17 @@ type AppRoute = {
|
|
|
38
63
|
layouts: string[]
|
|
39
64
|
}
|
|
40
65
|
|
|
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>
|
|
66
|
+
/** An app's compiled output: every file and table (`Compilation`), its routes, and its warnings. */
|
|
67
|
+
export type CompiledApp = Compilation & {
|
|
51
68
|
routes: AppRoute[]
|
|
69
|
+
/** Every handler, where it is mounted. 16.2. */
|
|
70
|
+
mounts: Mount[]
|
|
52
71
|
/** How many `.abide` files were read, which is what the commands report. */
|
|
53
72
|
sources: number
|
|
54
73
|
/** What the compiler has to say that does not stop the build, on the `.abide` line. */
|
|
55
74
|
warnings: Diagnosis[]
|
|
75
|
+
/** Every public file's source path to its address, as `appFiles` read them. 36.11. */
|
|
76
|
+
published: Map<string, string>
|
|
56
77
|
}
|
|
57
78
|
|
|
58
79
|
/**
|
|
@@ -64,12 +85,19 @@ export type CompiledApp = {
|
|
|
64
85
|
*/
|
|
65
86
|
export type AppChecker = {
|
|
66
87
|
session: TypeSession
|
|
88
|
+
/** The app's root, real — every path a checker is asked about is spelled under it. */
|
|
89
|
+
root: string
|
|
67
90
|
/**
|
|
68
91
|
* Records a path a watcher saw move, or `undefined` for a move it could not place.
|
|
69
92
|
*
|
|
70
93
|
* Held until `current`.
|
|
71
94
|
*/
|
|
72
95
|
saw: (path: string | undefined) => void
|
|
96
|
+
/**
|
|
97
|
+
* The app's own `.abide`, read (`sourcesUnder`) — kept until `saw` hears anything move, so a
|
|
98
|
+
* check asked again and again while an editor types re-reads nothing on disk.
|
|
99
|
+
*/
|
|
100
|
+
sources: () => ReturnType<typeof sourcesUnder>
|
|
73
101
|
/**
|
|
74
102
|
* Whether the project it was opened on is still the app's; if so, flushes what `saw` held.
|
|
75
103
|
*
|
|
@@ -82,9 +110,15 @@ export type AppChecker = {
|
|
|
82
110
|
close: () => Promise<void>
|
|
83
111
|
}
|
|
84
112
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Opens the checker for the app at `appRoot`, on a default tsconfig where it has none (38.30).
|
|
115
|
+
*
|
|
116
|
+
* AT ITS REAL PATH, which is the one bun resolves every import to: an editor holding the app
|
|
117
|
+
* through a link (`/tmp` is one on macOS) is spelled back at its own edge (`serveLanguage`).
|
|
118
|
+
*/
|
|
119
|
+
export async function openChecker(appRoot: string): Promise<AppChecker> {
|
|
120
|
+
const root = realpathSync(appRoot)
|
|
121
|
+
// Where the app has no tsconfig it is typed against a default one (38.30), so every app is
|
|
88
122
|
// read by the same compiler.
|
|
89
123
|
const found = nearest(root, 'tsconfig.json')
|
|
90
124
|
const opened = projectFiles(root)
|
|
@@ -93,14 +127,25 @@ export async function openChecker(root: string): Promise<AppChecker> {
|
|
|
93
127
|
? await writeDefaultProject(root)
|
|
94
128
|
: { tsconfig: found, remove: async (): Promise<void> => {} }
|
|
95
129
|
const session = await TypeSession.open(project.tsconfig)
|
|
130
|
+
session.root = root
|
|
96
131
|
const moved = new Set<string>()
|
|
97
132
|
let movedSomewhere = false
|
|
133
|
+
let sources: ReturnType<typeof sourcesUnder> | undefined
|
|
98
134
|
return {
|
|
99
135
|
session,
|
|
136
|
+
root,
|
|
100
137
|
saw: (path) => {
|
|
138
|
+
sources = undefined
|
|
101
139
|
if (path === undefined) movedSomewhere = true
|
|
102
140
|
else moved.add(path)
|
|
103
141
|
},
|
|
142
|
+
sources: () => {
|
|
143
|
+
sources ??= sourcesUnder(root).catch((thrown: unknown) => {
|
|
144
|
+
sources = undefined
|
|
145
|
+
throw thrown
|
|
146
|
+
})
|
|
147
|
+
return sources
|
|
148
|
+
},
|
|
104
149
|
current: () => {
|
|
105
150
|
if (projectFiles(root) !== opened) return false
|
|
106
151
|
if (movedSomewhere || moved.size > 0)
|
|
@@ -146,7 +191,7 @@ export async function withChecker<Result>(
|
|
|
146
191
|
}
|
|
147
192
|
|
|
148
193
|
/**
|
|
149
|
-
* Every `.abide`
|
|
194
|
+
* Every `.abide` of the app's own, read, in path order — and the root as bun resolves it.
|
|
150
195
|
*
|
|
151
196
|
* `Bun.resolveSync` answers a symlink-free path (`/tmp` is `/private/tmp` on macOS), so a root
|
|
152
197
|
* spelled through a link would have every import resolve to a file this walk never listed, and
|
|
@@ -157,36 +202,128 @@ export async function sourcesUnder(
|
|
|
157
202
|
appRoot: string,
|
|
158
203
|
): Promise<{ root: string; sources: { path: string; text: string }[] }> {
|
|
159
204
|
const root = realpathSync(appRoot)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
205
|
+
return { root, sources: await sourcesAt(root, abideUnder(root)) }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The `.abide` at each of `relatives`, below `root`, read. */
|
|
209
|
+
async function sourcesAt(
|
|
210
|
+
root: string,
|
|
211
|
+
relatives: readonly string[],
|
|
212
|
+
): Promise<{ path: string; text: string }[]> {
|
|
213
|
+
const paths: string[] = []
|
|
214
|
+
for (const path of relatives) paths.push(`${root}/${path}`)
|
|
163
215
|
const texts = await Promise.all(paths.map((path) => Bun.file(path).text()))
|
|
164
216
|
const sources: { path: string; text: string }[] = []
|
|
165
217
|
for (let at = 0; at < paths.length; at += 1)
|
|
166
218
|
sources.push({ path: paths[at] as string, text: texts[at] as string })
|
|
167
|
-
return
|
|
219
|
+
return sources
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* `parsed` and every `.abide` outside them that one of them imports — a dependency's, or a linked
|
|
224
|
+
* package's — parsed, and every one those import in turn; and every `.abide` under a directory one
|
|
225
|
+
* of them mounts, at the position the mount gives it (23.19).
|
|
226
|
+
*
|
|
227
|
+
* 34.27 — COMPILED WITH THE APP, by the app's checker, so a component a package ships as source is
|
|
228
|
+
* one of the app's in every way but its address: it is never routed (`placeOf`) unless mounted.
|
|
229
|
+
*/
|
|
230
|
+
export async function withDependencies(
|
|
231
|
+
root: string,
|
|
232
|
+
parsed: readonly ParsedSource[],
|
|
233
|
+
resolve: (specifier: string, from: string) => string | undefined,
|
|
234
|
+
refused?: Diagnosis[],
|
|
235
|
+
): Promise<ParsedSource[]> {
|
|
236
|
+
const all = [...parsed]
|
|
237
|
+
const byPath = new Map<string, ParsedSource>()
|
|
238
|
+
for (const source of all) byPath.set(source.path, source)
|
|
239
|
+
// 23.20 — a directory mounts once, and by whom.
|
|
240
|
+
const mountedBy = new Map<string, string>()
|
|
241
|
+
const refuse = (path: string, message: string): void => {
|
|
242
|
+
if (refused === undefined) throw new Error(message)
|
|
243
|
+
refused.push(refusal(path, new Error(message)))
|
|
244
|
+
}
|
|
245
|
+
const add = async (found: {
|
|
246
|
+
path: string
|
|
247
|
+
position?: string
|
|
248
|
+
}): Promise<void> => {
|
|
249
|
+
const text = await Bun.file(found.path).text()
|
|
250
|
+
for (const one of parseSources([{ ...found, text }], refused)) {
|
|
251
|
+
byPath.set(one.path, one)
|
|
252
|
+
all.push(one)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (let at = 0; at < all.length; at += 1) {
|
|
256
|
+
const source = all[at] as ParsedSource
|
|
257
|
+
const position = source.position ?? relativeTo(root, source.path)
|
|
258
|
+
const mounts = mountsOf(root, source.path, position, source.file)
|
|
259
|
+
if (typeof mounts === 'string') {
|
|
260
|
+
refuse(source.path, mounts)
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
for (const mount of mounts) {
|
|
264
|
+
const other = mountedBy.get(mount.directory)
|
|
265
|
+
if (other !== undefined) {
|
|
266
|
+
refuse(
|
|
267
|
+
source.path,
|
|
268
|
+
`abide: ${other} and ${position} both mount ${mount.specifier}, and a directory answers at one place. RULEBOOK 23.20.`,
|
|
269
|
+
)
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
mountedBy.set(mount.directory, position)
|
|
273
|
+
for (const { path, position: placed } of mount.files) {
|
|
274
|
+
// A file a component import reached first takes its mount's position all the same.
|
|
275
|
+
const known = byPath.get(path)
|
|
276
|
+
if (known !== undefined) {
|
|
277
|
+
known.position = placed
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
await add({ path, position: placed })
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const target of abideImportsOf(
|
|
284
|
+
source.path,
|
|
285
|
+
source.file,
|
|
286
|
+
resolve,
|
|
287
|
+
).values()) {
|
|
288
|
+
if (!byPath.has(target)) await add({ path: target })
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return all
|
|
168
292
|
}
|
|
169
293
|
|
|
170
294
|
/**
|
|
171
|
-
* Compiles every `.abide` source and handler of the app at `appRoot`
|
|
295
|
+
* Compiles every `.abide` source and handler of the app at `appRoot` (`Compilation`).
|
|
172
296
|
*
|
|
173
|
-
* `
|
|
174
|
-
* source depends on (34.14). `kept` is a checker the caller holds across compiles and closes itself.
|
|
297
|
+
* `kept` is a checker the caller holds across compiles and closes itself.
|
|
175
298
|
*/
|
|
176
299
|
export async function compileApp(
|
|
177
300
|
appRoot: string,
|
|
178
|
-
out = buildDirectory(appRoot),
|
|
179
301
|
kept?: AppChecker,
|
|
180
302
|
): Promise<CompiledApp> {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
303
|
+
const root = realpathSync(appRoot)
|
|
304
|
+
// ONE WALK for the sources and the public files, refusing a tree 23.16-23.18 forbids before
|
|
305
|
+
// anything is read.
|
|
306
|
+
const { abide, published } = appFiles(root)
|
|
307
|
+
const sources = await sourcesAt(root, abide)
|
|
308
|
+
if (sources.length === 0)
|
|
309
|
+
return {
|
|
310
|
+
files: {},
|
|
311
|
+
tables: {},
|
|
312
|
+
routes: [],
|
|
313
|
+
mounts: [],
|
|
314
|
+
sources: 0,
|
|
315
|
+
warnings: [],
|
|
316
|
+
published,
|
|
317
|
+
}
|
|
184
318
|
|
|
185
319
|
// One session for the whole build: opening it spawns the checker, and the cost is paid once
|
|
186
320
|
// rather than per file.
|
|
187
321
|
return withChecker(root, kept, async (session) => {
|
|
188
322
|
// A BUILD STOPS AT THE FIRST REFUSAL, so neither step is handed a list to report it in.
|
|
189
|
-
const
|
|
323
|
+
const own = parseSources(sources)
|
|
324
|
+
await writeTypes(root, own)
|
|
325
|
+
const resolve = resolverIn()
|
|
326
|
+
const parsed = await withDependencies(root, own, resolve)
|
|
190
327
|
const { read, warnings } = await readSources(session, parsed)
|
|
191
328
|
const { ambient, unkeyed } = await session.lints(root, parsed)
|
|
192
329
|
// 11.37 — a build error, every read named, rather than a first one and a second run.
|
|
@@ -196,14 +333,10 @@ export async function compileApp(
|
|
|
196
333
|
lines.push(formatDiagnosis(read, process.cwd()))
|
|
197
334
|
throw new Error(lines.join('\n'))
|
|
198
335
|
}
|
|
199
|
-
const emitted = await emitApp(
|
|
200
|
-
root,
|
|
201
|
-
session,
|
|
202
|
-
read,
|
|
203
|
-
posix.relative(resolve(appRoot), resolve(out)),
|
|
204
|
-
)
|
|
336
|
+
const emitted = await emitApp(root, session, read, resolve)
|
|
205
337
|
return {
|
|
206
338
|
...emitted,
|
|
339
|
+
published,
|
|
207
340
|
sources: sources.length,
|
|
208
341
|
// 11.30 — warned on, and the build goes on.
|
|
209
342
|
warnings: [...warnings, ...unkeyed, ...emitted.warnings],
|
|
@@ -216,137 +349,260 @@ export async function compileApp(
|
|
|
216
349
|
*
|
|
217
350
|
* `abide check` has done both by then, file by file, and asks this for only what the whole app can
|
|
218
351
|
* 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
352
|
*/
|
|
222
353
|
export async function emitApp(
|
|
223
354
|
root: string,
|
|
224
355
|
session: TypeSession,
|
|
225
356
|
sources: readonly ReadSource[],
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
357
|
+
/** The resolver the sources' dependencies were found with, whose answers it keeps. */
|
|
358
|
+
resolve: (specifier: string, from: string) => string | undefined,
|
|
359
|
+
): Promise<
|
|
360
|
+
Pick<CompiledApp, 'files' | 'tables' | 'routes' | 'mounts' | 'warnings'>
|
|
361
|
+
> {
|
|
362
|
+
const files: Record<string, CompiledFile> = {}
|
|
363
|
+
const tables: Record<string, string> = {}
|
|
229
364
|
const routes: AppRoute[] = []
|
|
230
365
|
// 32.17 AND 34.14 — WHICH FILES ARE INVOKED AS COMPONENTS, and where each import lands. A
|
|
231
366
|
// caller's client module imports its child's, so a child with nothing to hydrate still
|
|
232
367
|
// needs one to exist (`emitClient`'s no-op), and an invoked file carries its own markup
|
|
233
368
|
// (`emitClient`). Built before the emit loop because a page may invoke a component the
|
|
234
369
|
// walk reaches later.
|
|
235
|
-
const graph = appGraph(root, sources,
|
|
370
|
+
const graph = appGraph(root, sources, resolve)
|
|
236
371
|
const parsed = graph.nodes
|
|
237
372
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
373
|
+
// 35.1, 35.12 — every `layout.abide` by the route it covers, one to a route; 35.13 — and one
|
|
374
|
+
// with no template wraps nothing, being the route's hooks alone.
|
|
375
|
+
const layouts: AppLayout[] = []
|
|
376
|
+
const layoutAt = new Map<string, string>()
|
|
377
|
+
const errorAt = new Map<string, string>()
|
|
378
|
+
for (const entry of parsed) {
|
|
379
|
+
if (entry.kind !== 'layout' && entry.kind !== 'error') continue
|
|
380
|
+
const route = routeOf(entry.position)
|
|
381
|
+
const held = entry.kind === 'layout' ? layoutAt : errorAt
|
|
382
|
+
const other = held.get(shapeOf(route))
|
|
383
|
+
if (other !== undefined)
|
|
384
|
+
throw new Error(
|
|
385
|
+
`abide: ${other} and ${entry.relative} are both the ${entry.kind}.abide at ${route}. RULEBOOK 35.12.`,
|
|
386
|
+
)
|
|
387
|
+
held.set(shapeOf(route), entry.relative)
|
|
388
|
+
if (entry.kind === 'layout' && entry.file.template.length > 0)
|
|
389
|
+
layouts.push({
|
|
390
|
+
source: entry.relative,
|
|
391
|
+
segments: parseRoutePattern(route),
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
// 35.2 — every `error.abide`, with the route it answers for and the layouts around it.
|
|
242
395
|
const errors: AppRoute[] = []
|
|
243
396
|
for (const entry of parsed) {
|
|
244
397
|
if (entry.kind !== 'page' && entry.kind !== 'error') continue
|
|
398
|
+
// 23.3, 35.14 — a `page.abide` or an `error.abide` with nothing to paint is its route's
|
|
399
|
+
// handlers and hooks alone.
|
|
400
|
+
if (!paints(entry.file)) continue
|
|
245
401
|
;(entry.kind === 'page' ? routes : errors).push({
|
|
246
402
|
pattern: entry.route as string,
|
|
247
403
|
source: entry.relative,
|
|
248
|
-
layouts: layoutsAround(entry.
|
|
404
|
+
layouts: layoutsAround(entry.route as string, layouts),
|
|
249
405
|
})
|
|
250
406
|
}
|
|
251
407
|
|
|
408
|
+
// 16.2 AND 16.28 — THE HANDLERS, read before a module is emitted: the server mounts each one,
|
|
409
|
+
// and the browser's module of the file declaring it carries its wrapper in the server block's
|
|
410
|
+
// place (34.23). Written whether or not there is a page: a handler is answered over http to a
|
|
411
|
+
// caller that has none.
|
|
412
|
+
const { mounts, warnings } = await handlersUnder(session, parsed)
|
|
413
|
+
refuseSharedRoutes(routes, mounts)
|
|
414
|
+
const standIns = standInsOf(mounts)
|
|
415
|
+
// 37.1 — every module a server block runs in, for the boot to import before `onStart`.
|
|
416
|
+
const servers: string[] = []
|
|
252
417
|
let transitions = false
|
|
253
418
|
for (const entry of parsed) {
|
|
419
|
+
// 35.16 — of the files with an address, the root layout alone is outermost in every render.
|
|
420
|
+
if (
|
|
421
|
+
entry.file.document !== undefined &&
|
|
422
|
+
entry.kind !== 'component' &&
|
|
423
|
+
!(entry.kind === 'layout' && routeOf(entry.position) === '/')
|
|
424
|
+
)
|
|
425
|
+
throw new Error(
|
|
426
|
+
`abide: ${entry.relative} writes <html>, and of the pages, layouts and error pages only the root layout.abide writes the document. RULEBOOK 35.16.`,
|
|
427
|
+
)
|
|
254
428
|
const emitted = emitSource(
|
|
255
429
|
entry.relative,
|
|
256
430
|
entry.file,
|
|
257
431
|
entry.answer,
|
|
258
432
|
graph.invoked.has(entry.path),
|
|
259
|
-
|
|
433
|
+
standIns.get(entry.relative),
|
|
434
|
+
entry.text,
|
|
435
|
+
entry.position,
|
|
260
436
|
)
|
|
261
|
-
|
|
262
|
-
if (emitted.sheet !== undefined)
|
|
263
|
-
modules.set(`${stem}.style.css`, emitted.sheet)
|
|
437
|
+
if (entry.file.servers.length > 0) servers.push(entry.relative)
|
|
264
438
|
transitions ||= emitted.transitions
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
439
|
+
// 34.30 — a dependency's module runs on the app's runtime, whatever the package carries.
|
|
440
|
+
const onAppRuntime =
|
|
441
|
+
placeOf(entry.relative) === 'dependency'
|
|
442
|
+
? (text: string) => onAbideOf(root, entry.path, text)
|
|
443
|
+
: (text: string) => text
|
|
444
|
+
files[entry.relative] = {
|
|
445
|
+
server: onAppRuntime(emitted.server),
|
|
446
|
+
client: onAppRuntime(emitted.client),
|
|
447
|
+
sheet: emitted.sheet,
|
|
448
|
+
// A map placing nothing — a file of markup alone — is left out.
|
|
449
|
+
serverMappings: placing(emitted.serverMappings),
|
|
450
|
+
clientMappings: placing(emitted.clientMappings),
|
|
451
|
+
}
|
|
268
452
|
}
|
|
269
453
|
// 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 (`page.abide` is an address,
|
|
271
|
-
// so a route that is not a
|
|
272
|
-
//
|
|
454
|
+
// addresses exist is a fact about the file tree (a `page.abide` is an address, 23.3),
|
|
455
|
+
// so a route that is not a file cannot be written and a file that is one cannot be
|
|
456
|
+
// forgotten. The loaders are dynamic imports, which is what keeps a destination's
|
|
273
457
|
// markup and bindings off the entry page's bundle until somebody navigates there.
|
|
274
458
|
//
|
|
275
|
-
// TWO TABLES OFF ONE WALK
|
|
276
|
-
//
|
|
277
|
-
//
|
|
459
|
+
// TWO TABLES OFF ONE WALK, each importing a file's `?component` face — the same addresses
|
|
460
|
+
// either way, which is what keeps the pair from being two walks that can disagree. The
|
|
461
|
+
// server's is written with no
|
|
278
462
|
// page too — an app of handlers alone still answers — and the client's, with the entry
|
|
279
463
|
// that boots it, only where there is a page to boot.
|
|
280
|
-
|
|
464
|
+
tables[SERVER_TABLE] = serverTable(routes, errors, servers, mounts)
|
|
281
465
|
if (routes.length > 0) {
|
|
282
|
-
|
|
283
|
-
const shellHead = keyedHead(await shellUnder(root))
|
|
284
|
-
modules.set('routes.ts', clientTable(routes, transitions, shellHead))
|
|
466
|
+
tables[ROUTES_TABLE] = clientTable(routes, transitions)
|
|
285
467
|
// 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
|
-
|
|
468
|
+
// from a build and `entry.js` in dev — and its key in the manifest (38.22, 38.5).
|
|
469
|
+
tables[`${CLIENT_ENTRY}.ts`] = ENTRY_MODULE
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
files,
|
|
473
|
+
tables,
|
|
474
|
+
routes,
|
|
475
|
+
mounts,
|
|
476
|
+
warnings,
|
|
288
477
|
}
|
|
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
478
|
}
|
|
298
479
|
|
|
480
|
+
/**
|
|
481
|
+
* `text`, a module compiled from the dependency's `.abide` at `path`, with each of abide's own
|
|
482
|
+
* specifiers written as the path to the file the app's root resolves it to.
|
|
483
|
+
*
|
|
484
|
+
* 34.30 — RELATIVE, so the compilation still serves once the app and its `node_modules` are moved
|
|
485
|
+
* together, and THE FILE the app's own code loads, so the two are one module instance. A path and
|
|
486
|
+
* not a specifier a plugin answers: bun's runtime hands a plugin no bare specifier that names no
|
|
487
|
+
* file, so a mark on `@abide/abide/server` was never read.
|
|
488
|
+
*/
|
|
489
|
+
function onAbideOf(root: string, path: string, text: string): string {
|
|
490
|
+
const from = dirname(realpathSync(path))
|
|
491
|
+
return respecified(text, (specifier) => {
|
|
492
|
+
if (
|
|
493
|
+
specifier !== SHARED_SPECIFIER &&
|
|
494
|
+
!specifier.startsWith(`${SHARED_SPECIFIER}/`)
|
|
495
|
+
)
|
|
496
|
+
return undefined
|
|
497
|
+
let target: string
|
|
498
|
+
try {
|
|
499
|
+
target = resolvedFile(specifier, root)
|
|
500
|
+
} catch {
|
|
501
|
+
return undefined
|
|
502
|
+
}
|
|
503
|
+
const relativePath = posix.relative(from, target)
|
|
504
|
+
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// `mappings` that place something, or `undefined` for a module of markup alone.
|
|
509
|
+
function placing(mappings: string | undefined): string | undefined {
|
|
510
|
+
return mappings !== undefined && /[^;]/.test(mappings)
|
|
511
|
+
? mappings
|
|
512
|
+
: undefined
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* How a table imports the `.abide` at `source`: from `__abide`, one directory below the root, as
|
|
517
|
+
* the module the file compiles to.
|
|
518
|
+
*/
|
|
519
|
+
function tableImport(source: string): string {
|
|
520
|
+
return JSON.stringify(`../${source}${COMPONENT_QUERY}`)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// A layout and the route it covers, parsed.
|
|
524
|
+
type AppLayout = { source: string; segments: Segment[] }
|
|
525
|
+
|
|
299
526
|
/**
|
|
300
527
|
* The layouts a page renders inside, outermost first.
|
|
301
528
|
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
529
|
+
* 35.1 — every `layout.abide` whose route lies ON THE WAY DOWN to the page's wraps it and no other
|
|
530
|
+
* does: `src/layout.abide` wraps the page `src/media/page.abide` serves at `/media`, and so does
|
|
531
|
+
* `src/[section]/layout.abide`. So the test is a prefix of the route, kind by kind, and the nesting
|
|
532
|
+
* order is the route's depth.
|
|
305
533
|
*/
|
|
306
|
-
function layoutsAround(
|
|
307
|
-
const
|
|
308
|
-
const chain:
|
|
309
|
-
for (const layout of layouts)
|
|
310
|
-
|
|
311
|
-
|
|
534
|
+
function layoutsAround(route: string, layouts: AppLayout[]): string[] {
|
|
535
|
+
const page = parseRoutePattern(route)
|
|
536
|
+
const chain: AppLayout[] = []
|
|
537
|
+
for (const layout of layouts)
|
|
538
|
+
if (coversRoute(layout.segments, page)) chain.push(layout)
|
|
539
|
+
// A shorter route is an outer one: they are prefixes of each other by construction.
|
|
540
|
+
chain.sort((one, two) => one.segments.length - two.segments.length)
|
|
541
|
+
const sources: string[] = []
|
|
542
|
+
for (const layout of chain) sources.push(layout.source)
|
|
543
|
+
return sources
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* A route with its parameters' names erased: what two routes answering one address share.
|
|
548
|
+
*
|
|
549
|
+
* 19.1 — `/media/[id]` and `/media/[slug]` are one route.
|
|
550
|
+
*/
|
|
551
|
+
function shapeOf(route: string): string {
|
|
552
|
+
let shape = ''
|
|
553
|
+
for (const segment of parseRoutePattern(route))
|
|
554
|
+
shape += `/${segment.kind === 'static' ? segment.name : `[${segment.kind}]`}`
|
|
555
|
+
return shape === '' ? '/' : shape
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Throws, naming both, where two files answer one method at one route.
|
|
560
|
+
*
|
|
561
|
+
* 19.1 — a page answers `GET` at its own, and a handler its method at its address.
|
|
562
|
+
*/
|
|
563
|
+
function refuseSharedRoutes(routes: AppRoute[], mounts: Mount[]): void {
|
|
564
|
+
const owners = new Map<string, string>()
|
|
565
|
+
const claim = (method: string, route: string, source: string): void => {
|
|
566
|
+
const key = `${method} ${shapeOf(route)}`
|
|
567
|
+
const owner = owners.get(key)
|
|
568
|
+
if (owner !== undefined)
|
|
569
|
+
throw new Error(
|
|
570
|
+
`abide: ${owner} and ${source} both answer ${method} at ${route}. RULEBOOK 19.1.`,
|
|
571
|
+
)
|
|
572
|
+
owners.set(key, source)
|
|
312
573
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return chain
|
|
574
|
+
for (const route of routes) claim('GET', route.pattern, route.source)
|
|
575
|
+
for (const mount of mounts) claim(mount.method, mount.address, mount.source)
|
|
316
576
|
}
|
|
317
577
|
|
|
318
578
|
/**
|
|
319
|
-
* Resolves a specifier from a directory as bun does,
|
|
320
|
-
* does not resolve.
|
|
579
|
+
* Resolves a specifier from a directory as bun does, or `undefined` where it does not resolve.
|
|
321
580
|
*
|
|
322
581
|
* A COMPONENT IMPORT RESOLVES THE WAY THE APP'S OWN IMPORTS DO — its import map, from the file
|
|
323
|
-
* that wrote it. One that does not resolve is left as written, and the bundle names it.
|
|
324
|
-
*
|
|
325
|
-
* THE RESOLVER FOLLOWS SYMLINKS AND `root` MAY NOT: under a linked root — `/tmp` is one on macOS —
|
|
326
|
-
* a component resolves outside the root's own spelling of the app, where an editor's file, its
|
|
327
|
-
* unsaved text and the checked module pointed at it are not found. So an answer under the root's
|
|
328
|
-
* real path is spelled back under `root`; a build hands `sourcesUnder`'s real root, and nothing
|
|
329
|
-
* is spelled back.
|
|
582
|
+
* that wrote it. One that does not resolve is left as written, and the bundle names it. THE
|
|
583
|
+
* ANSWER IS A REAL PATH, as every root handed to the compiler is.
|
|
330
584
|
*/
|
|
331
|
-
export function resolverIn(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
585
|
+
export function resolverIn(): (
|
|
586
|
+
specifier: string,
|
|
587
|
+
from: string,
|
|
588
|
+
) => string | undefined {
|
|
589
|
+
// ONE ANSWER PER SPECIFIER AND DIRECTORY for as long as the resolver lives: a compile resolves
|
|
590
|
+
// every import once for its dependencies and again for its graph, and hands both one resolver.
|
|
591
|
+
const answered = new Map<string, string | undefined>()
|
|
335
592
|
return (specifier, from) => {
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
: resolved
|
|
593
|
+
const key = `${from}\0${specifier}`
|
|
594
|
+
if (answered.has(key)) return answered.get(key)
|
|
595
|
+
const found = resolveFrom(specifier, from)
|
|
596
|
+
answered.set(key, found)
|
|
597
|
+
return found
|
|
342
598
|
}
|
|
343
599
|
}
|
|
344
600
|
|
|
345
601
|
function resolveFrom(specifier: string, from: string): string | undefined {
|
|
346
602
|
try {
|
|
347
|
-
return
|
|
603
|
+
return resolvedFile(specifier, from)
|
|
348
604
|
} catch {
|
|
349
|
-
// 38.
|
|
605
|
+
// 38.65 — BUN KEEPS A DIRECTORY'S LISTING FOR THE LIFE OF THE PROCESS, and looks a `#` specifier up
|
|
350
606
|
// in it with no second look — where a relative one that misses reads the directory again.
|
|
351
607
|
// `abide dev` compiles in one process for its whole session, so a component created since
|
|
352
608
|
// its directory was listed never resolved, and was read as uninvoked. The `imports` map is
|
|
@@ -392,29 +648,21 @@ function throughImports(specifier: string, from: string): string | undefined {
|
|
|
392
648
|
return undefined
|
|
393
649
|
}
|
|
394
650
|
|
|
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
|
-
*/
|
|
651
|
+
/** The rows of either route table, one per route, `column` appending the fields only one carries. */
|
|
401
652
|
function tableOf(
|
|
402
653
|
routes: AppRoute[],
|
|
403
|
-
extension: string,
|
|
404
654
|
column: (route: AppRoute) => string = () => '',
|
|
405
655
|
): string {
|
|
406
656
|
const rows: string[] = []
|
|
407
657
|
for (const route of routes) {
|
|
408
|
-
const module = `./${route.source.replace(/\.abide$/, extension)}`
|
|
409
658
|
// 35.1 — THE CHAIN IS PART OF THE ADDRESS, so it is in the table rather than resolved at
|
|
410
659
|
// run time: which layouts wrap a page is a fact about the file tree, and the server and
|
|
411
660
|
// the client read it off the same walk for the reason the addresses are (30.8).
|
|
412
661
|
const wrapping = route.layouts.map(
|
|
413
|
-
(layout) =>
|
|
414
|
-
`() => import(${JSON.stringify(`./${layout.replace(/\.abide$/, extension)}`)})`,
|
|
662
|
+
(layout) => `() => import(${tableImport(layout)})`,
|
|
415
663
|
)
|
|
416
664
|
rows.push(
|
|
417
|
-
` { pattern: ${JSON.stringify(route.pattern)}, load: () => import(${
|
|
665
|
+
` { pattern: ${JSON.stringify(route.pattern)}, load: () => import(${tableImport(route.source)}), layouts: [${wrapping.join(', ')}]${column(route)} },`,
|
|
418
666
|
)
|
|
419
667
|
}
|
|
420
668
|
return rows.join('\n')
|
|
@@ -427,23 +675,19 @@ function tableOf(
|
|
|
427
675
|
* tsconfig includes its build output typechecks it — and a bare `host` is an implicit `any` that
|
|
428
676
|
* fails `tsc` under the `strict` every abide app is written against.
|
|
429
677
|
*/
|
|
430
|
-
function clientTable(
|
|
431
|
-
routes: AppRoute[],
|
|
432
|
-
transitions: boolean,
|
|
433
|
-
shellHead: string,
|
|
434
|
-
): string {
|
|
678
|
+
function clientTable(routes: AppRoute[], transitions: boolean): string {
|
|
435
679
|
return `// Written by \`abide build\`. The addresses this app answers, and what to load for each.
|
|
436
680
|
import { listenForRoutes, startRouter } from '${ROUTING_SPECIFIER}'
|
|
437
681
|
|
|
438
682
|
export const routes = [
|
|
439
|
-
${tableOf(routes
|
|
683
|
+
${tableOf(routes)}
|
|
440
684
|
]
|
|
441
685
|
|
|
442
686
|
// The router this document gets, the page it arrived on, and the two places a reader navigates
|
|
443
687
|
// from (30.7). Hydrating here rather than leaving it to the app is what makes the page the server
|
|
444
688
|
// painted live without anybody wiring it.
|
|
445
689
|
export function start(host: Element) {
|
|
446
|
-
const router = startRouter(routes, host, ${transitions}
|
|
690
|
+
const router = startRouter(routes, host, ${transitions})
|
|
447
691
|
listenForRoutes(router)
|
|
448
692
|
const ready = router.hydrate()
|
|
449
693
|
return { router, ready }
|
|
@@ -452,57 +696,70 @@ export function start(host: Element) {
|
|
|
452
696
|
}
|
|
453
697
|
|
|
454
698
|
/**
|
|
455
|
-
* The source of `server.ts`: the same addresses, loading the module that PAINTS each page
|
|
699
|
+
* The source of `server.ts`: the same addresses, loading the module that PAINTS each page, the
|
|
700
|
+
* error pages, the modules a server block runs in, and every handler.
|
|
456
701
|
*
|
|
457
702
|
* No `start` here and nothing imported: the server holds the matching and the ranking already,
|
|
458
703
|
* and a server table that carried either would be a second copy of both.
|
|
459
704
|
*/
|
|
460
|
-
function serverTable(
|
|
705
|
+
function serverTable(
|
|
706
|
+
routes: AppRoute[],
|
|
707
|
+
errors: AppRoute[],
|
|
708
|
+
servers: string[],
|
|
709
|
+
mounts: Mount[],
|
|
710
|
+
): string {
|
|
711
|
+
let loaders = ''
|
|
712
|
+
for (const source of servers)
|
|
713
|
+
loaders += ` () => import(${tableImport(source)}),\n`
|
|
714
|
+
let handlers = ''
|
|
715
|
+
for (const mount of mounts) {
|
|
716
|
+
const fields = JSON.stringify(mount)
|
|
717
|
+
// The module the handler is declared in — the file's server module, the route table's own.
|
|
718
|
+
handlers += ` { load: () => import(${tableImport(mount.source)}), ${fields.slice(1, -1)} },\n`
|
|
719
|
+
}
|
|
461
720
|
return `// Written by \`abide build\`. The addresses this app answers, and what paints each.
|
|
462
721
|
export const routes = [
|
|
463
|
-
${tableOf(routes,
|
|
722
|
+
${tableOf(routes, clientModules)}
|
|
464
723
|
]
|
|
465
724
|
|
|
466
725
|
// 35.2 — the error pages, by the directory each answers for.
|
|
467
726
|
export const errors = [
|
|
468
|
-
${tableOf(errors
|
|
727
|
+
${tableOf(errors)}
|
|
469
728
|
]
|
|
729
|
+
|
|
730
|
+
// 37.1 — every module a \`<script server>\` runs in, imported before the app boots.
|
|
731
|
+
export const servers = [
|
|
732
|
+
${loaders}]
|
|
733
|
+
|
|
734
|
+
// 16.2 — every handler this app answers over http, and where each is.
|
|
735
|
+
export const handlers = [
|
|
736
|
+
${handlers}]
|
|
470
737
|
`
|
|
471
738
|
}
|
|
472
739
|
|
|
473
740
|
/**
|
|
474
741
|
* The server row's `clientModules` column: what the browser will load at this address.
|
|
475
742
|
*
|
|
476
|
-
* 38.
|
|
743
|
+
* 38.28 — named on the SERVER's row: the server writes the document, and the preloads for the page
|
|
477
744
|
* and its chain have to be in it before the client has run at all. Keys into the manifest
|
|
478
745
|
* `manifestOf` reads off the bundle.
|
|
479
746
|
*/
|
|
480
747
|
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
|
-
`
|
|
748
|
+
return `, clientModules: ${JSON.stringify([route.source, ...route.layouts])}`
|
|
492
749
|
}
|
|
493
750
|
|
|
494
751
|
/**
|
|
495
|
-
* The browser's
|
|
752
|
+
* The wrappers the browser's module of each file exports in its server block's place, by source.
|
|
496
753
|
*
|
|
497
|
-
* 16.28 — WHAT A BROWSER IMPORTS WHERE THE APP WROTE A HANDLER
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
754
|
+
* 16.28, 34.23 — WHAT A BROWSER IMPORTS WHERE THE APP WROTE A HANDLER: the same export name, the
|
|
755
|
+
* same call over the wire. The address is written RELATIVE to the bundle's own directory and
|
|
756
|
+
* resolved against the chunk at run time (16.10), so an app mounted under a sub-path calls its own
|
|
757
|
+
* handlers rather than the root's.
|
|
501
758
|
*/
|
|
502
|
-
function
|
|
503
|
-
const
|
|
759
|
+
function standInsOf(mounts: Mount[]): Map<string, string> {
|
|
760
|
+
const lines = new Map<string, string>()
|
|
504
761
|
for (const mount of mounts) {
|
|
505
|
-
// 20.4 — withheld from the browser: no wrapper, so an import of it
|
|
762
|
+
// 20.4 — withheld from the browser: no wrapper, so an import of it names nothing there.
|
|
506
763
|
if (!mount.ui) continue
|
|
507
764
|
const at = JSON.stringify(posix.relative(CLIENT_ADDRESS, mount.address))
|
|
508
765
|
// 18.4 — what the handler's value declares that the browser's copy behaves by.
|
|
@@ -513,47 +770,30 @@ function wrappersOf(mounts: Mount[]): Map<string, string> {
|
|
|
513
770
|
: mount.copy === undefined
|
|
514
771
|
? ''
|
|
515
772
|
: `, ${JSON.stringify(mount.copy)}`
|
|
516
|
-
const
|
|
517
|
-
|
|
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)
|
|
773
|
+
const line = `export const ${mount.name} = __wire(${JSON.stringify(mount.method)}, new URL(${at}, import.meta.url).pathname${copy})\n`
|
|
774
|
+
lines.set(mount.source, (lines.get(mount.source) ?? '') + line)
|
|
522
775
|
}
|
|
523
|
-
const
|
|
524
|
-
for (const [source, body] of
|
|
525
|
-
|
|
776
|
+
const standIns = new Map<string, string>()
|
|
777
|
+
for (const [source, body] of lines)
|
|
778
|
+
standIns.set(
|
|
526
779
|
source,
|
|
527
|
-
`//
|
|
528
|
-
import { wire } from '${WIRE_SPECIFIER}'
|
|
529
|
-
|
|
780
|
+
`// 34.23 — what a browser holds in place of the server block.
|
|
781
|
+
import { wire as __wire } from '${WIRE_SPECIFIER}'
|
|
530
782
|
${body}`,
|
|
531
783
|
)
|
|
532
|
-
return
|
|
784
|
+
return standIns
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
|
|
788
|
+
export function paints(file: AbideFile): boolean {
|
|
789
|
+
return file.template.length > 0 || file.head.length > 0
|
|
533
790
|
}
|
|
534
791
|
|
|
535
792
|
// THE ENTRY. An app does not write it: which module boots the client is the build's knowledge,
|
|
536
793
|
// exactly as the route table is, and a hand-written entry is one more file to keep in step with a
|
|
537
794
|
// layout that moves.
|
|
538
795
|
const ENTRY_MODULE = `// Written by \`abide build\`. What boots this app in a browser.
|
|
539
|
-
import { start } from '
|
|
796
|
+
import { start } from '../${VIRTUAL_DIRECTORY}/${ROUTES_TABLE}'
|
|
540
797
|
|
|
541
798
|
start(document.body)
|
|
542
799
|
`
|
|
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
|
-
}
|