@abide/abide 0.104.0 → 0.105.1
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 +22 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +17 -17
- package/docs/RULEBOOK.md +50 -12
- package/docs/abide.md +9 -9
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +45 -86
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -20
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +11 -9
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +180 -512
- package/src/compiler/componentGraph.ts +2 -71
- package/src/compiler/declaredExports.ts +79 -60
- package/src/compiler/emitClient.ts +11 -13
- package/src/compiler/emitComponent.ts +21 -6
- package/src/compiler/emitSource.ts +20 -20
- package/src/compiler/handlersUnder.ts +46 -36
- package/src/compiler/hookCalls.ts +152 -0
- package/src/compiler/mountsOf.ts +31 -54
- package/src/compiler/readSources.ts +27 -42
- package/src/compiler/readsIn.ts +5 -16
- package/src/compiler/resolverIn.ts +77 -0
- package/src/server/abideModules.ts +92 -123
- package/src/server/answerRequest.ts +18 -4
- package/src/server/answerSocket.ts +136 -76
- package/src/server/answerValue.ts +28 -15
- package/src/server/appTree.ts +49 -71
- package/src/server/claudeApi.ts +2 -5
- package/src/server/clientBundle.ts +198 -0
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +93 -57
- package/src/server/createApp.ts +90 -16
- package/src/server/deferredBuilds.ts +175 -0
- package/src/server/devWorker.ts +107 -319
- package/src/server/faceOf.ts +120 -0
- package/src/server/fenceServer.ts +10 -18
- package/src/server/index.ts +3 -0
- package/src/server/placement.ts +167 -0
- package/src/server/remote.ts +14 -42
- package/src/server/render.ts +25 -17
- package/src/server/renderComponent.ts +9 -5
- package/src/server/requestScope.ts +10 -4
- package/src/server/responseRelay.ts +389 -0
- package/src/server/runCompiled.ts +9 -12
- package/src/server/runDesktop.ts +6 -9
- package/src/server/serveBuild.ts +26 -73
- package/src/server/serveDev.ts +19 -176
- package/src/server/streamEvent.ts +29 -0
- package/src/server/withAbideSources.ts +2 -1
- package/src/shared/HandlerRoute.ts +68 -0
- package/src/shared/REFUSAL_KEY.ts +5 -0
- package/src/shared/answerOf.ts +105 -78
- package/src/shared/filledPath.ts +1 -9
- package/src/shared/framing.ts +127 -128
- package/src/shared/queryText.ts +15 -7
- package/src/shared/reactive/Cursor.ts +4 -2
- package/src/shared/reactive/Reactive.ts +285 -197
- package/src/shared/reactive/index.ts +11 -8
- package/src/shared/reactive/pairReactive.ts +1 -1
- package/src/shared/reactive/scoped.ts +4 -3
- package/src/shared/reactive/share.ts +11 -1
- package/src/shared/routePattern.ts +0 -123
- package/src/shared/rpc.ts +17 -24
- package/src/shared/rpcRequest.ts +18 -47
- package/src/shared/takeEarlyCall.ts +12 -37
- package/src/shared/url.ts +1 -1
- package/src/ui/Level.ts +276 -0
- package/src/ui/List.ts +17 -17
- package/src/ui/Router.ts +20 -87
- package/src/ui/applyHead.ts +92 -79
- package/src/ui/index.ts +4 -2
- package/src/ui/leave.ts +5 -0
- package/src/ui/reconcile.ts +23 -104
- package/src/ui/regions.ts +27 -2
- package/src/ui/rows.ts +123 -2
- package/src/ui/wire.ts +12 -27
- package/src/compiler/hooksCalled.ts +0 -35
- package/src/server/clientBuildOptions.ts +0 -54
- package/src/server/refuseAddressCollision.ts +0 -37
- package/src/shared/reactive/chunksOfStream.ts +0 -33
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import { dirname
|
|
2
|
-
import {
|
|
3
|
-
type FileKind,
|
|
4
|
-
fileKind,
|
|
5
|
-
placeOf,
|
|
6
|
-
routeOf,
|
|
7
|
-
} from '#shared/routePattern.ts'
|
|
1
|
+
import { dirname } from 'node:path' // bun exposes no path helpers of its own
|
|
8
2
|
import { type AbideFile, scriptBodies } from './parseAbide.ts'
|
|
9
|
-
import { abideImports
|
|
3
|
+
import { abideImports } from './splitImports.ts'
|
|
10
4
|
|
|
11
5
|
type Resolve = (specifier: string, from: string) => string | undefined
|
|
12
6
|
|
|
@@ -60,66 +54,3 @@ export function abideImportsOf(
|
|
|
60
54
|
}
|
|
61
55
|
return resolved
|
|
62
56
|
}
|
|
63
|
-
|
|
64
|
-
/** One source in the app graph: where it is below the root, what it is, and its address. */
|
|
65
|
-
export type AppNode<Source> = Source & {
|
|
66
|
-
relative: string
|
|
67
|
-
/** 23.19 — the path it answers by: its own below the root, or the one its mount gives it. */
|
|
68
|
-
position: string
|
|
69
|
-
kind: FileKind
|
|
70
|
-
/** 23.3 — the address of a page or an error page, and `undefined` for anything else. */
|
|
71
|
-
route: string | undefined
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* The app as one graph, built once per build or check: every source with its kind and address,
|
|
76
|
-
* and which files are invoked as components.
|
|
77
|
-
*/
|
|
78
|
-
export function appGraph<
|
|
79
|
-
Source extends {
|
|
80
|
-
path: string
|
|
81
|
-
file: AbideFile
|
|
82
|
-
position?: string | undefined
|
|
83
|
-
},
|
|
84
|
-
>(
|
|
85
|
-
root: string,
|
|
86
|
-
sources: readonly Source[],
|
|
87
|
-
resolve: Resolve,
|
|
88
|
-
): { nodes: AppNode<Source>[]; invoked: Set<string> } {
|
|
89
|
-
const nodes: AppNode<Source>[] = []
|
|
90
|
-
for (const source of sources) {
|
|
91
|
-
// Below the root for the app's own files, and above it — `../node_modules/…` — for a
|
|
92
|
-
// dependency's (34.24).
|
|
93
|
-
const relative = relativeTo(root, source.path)
|
|
94
|
-
const position = source.position ?? relative
|
|
95
|
-
// 34.20 — a server block of the app's own under a `server` or `public` directory, refused
|
|
96
|
-
// here before anything reads its handlers: neither directory holds a component that runs.
|
|
97
|
-
const place = placeOf(position)
|
|
98
|
-
if (
|
|
99
|
-
source.file.servers.length > 0 &&
|
|
100
|
-
(place === 'server' || place === 'public')
|
|
101
|
-
)
|
|
102
|
-
throw new Error(
|
|
103
|
-
`abide: ${relative} has a <script server>, and it is under a ${place} directory, where nothing has an address. Move the file out of it, or the code into a module under a server directory. RULEBOOK 34.20.`,
|
|
104
|
-
)
|
|
105
|
-
// 34.21 — a file imported as text is found at build, not when its module first loads.
|
|
106
|
-
for (const script of scriptBodies(source.file))
|
|
107
|
-
for (const { specifier } of attributedImports(script, 'text'))
|
|
108
|
-
if (resolve(specifier, dirname(source.path)) === undefined)
|
|
109
|
-
throw new Error(
|
|
110
|
-
`abide: ${relative} imports ${specifier} as text, and there is no such file. RULEBOOK 34.21.`,
|
|
111
|
-
)
|
|
112
|
-
const kind = fileKind(position)
|
|
113
|
-
nodes.push({
|
|
114
|
-
...source,
|
|
115
|
-
relative,
|
|
116
|
-
position,
|
|
117
|
-
kind,
|
|
118
|
-
route:
|
|
119
|
-
kind === 'page' || kind === 'error'
|
|
120
|
-
? routeOf(position)
|
|
121
|
-
: undefined,
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
return { nodes, invoked: componentGraph(sources, resolve).invoked }
|
|
125
|
-
}
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
isReactiveMember,
|
|
45
45
|
valueTypeOf,
|
|
46
46
|
} from './reactiveTypes.ts'
|
|
47
|
+
import type { Refusals } from './Refusals.ts'
|
|
47
48
|
import type { Diagnosis } from './TypeSession.ts'
|
|
48
49
|
|
|
49
50
|
/**
|
|
@@ -57,12 +58,16 @@ import type { Diagnosis } from './TypeSession.ts'
|
|
|
57
58
|
* export here: it is erased, and has no address to answer at.
|
|
58
59
|
*
|
|
59
60
|
* `projectName` is the tsconfig the project was opened on, which a module outside it is refused by.
|
|
61
|
+
* 38.33 — WHAT IS REFUSED OF A MODULE IS REFUSED AT ITS FILE, `named[at]` for `paths[at]`, every
|
|
62
|
+
* export read either way: a module with a refusal is `undefined`, so nothing refuses it twice.
|
|
60
63
|
*/
|
|
61
64
|
export async function declaredExports(
|
|
62
65
|
project: Project,
|
|
63
66
|
projectName: string,
|
|
64
67
|
paths: readonly string[],
|
|
65
|
-
|
|
68
|
+
named: readonly string[],
|
|
69
|
+
refusals: Refusals,
|
|
70
|
+
): Promise<(DeclaredExport[] | undefined)[]> {
|
|
66
71
|
const checker = project.checker
|
|
67
72
|
// ABIDE'S FILE AS THE CHECKER NAMES IT, not as this module does: it canonicalizes a path to
|
|
68
73
|
// the disk's case rules, so on a case-insensitive disk `/Users/…/Code` comes back
|
|
@@ -71,79 +76,93 @@ export async function declaredExports(
|
|
|
71
76
|
?.path
|
|
72
77
|
const abide = await abideIn(project)
|
|
73
78
|
return allSettled(
|
|
74
|
-
paths.map(async (path) => {
|
|
79
|
+
paths.map(async (path, at) => {
|
|
80
|
+
const file = named[at] as string
|
|
75
81
|
const sourceFile = await project.program.getSourceFile(path)
|
|
76
|
-
if (sourceFile === undefined)
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
if (sourceFile === undefined) {
|
|
83
|
+
refusals.at(
|
|
84
|
+
file,
|
|
85
|
+
new Error(
|
|
86
|
+
`abide: ${path} is not inside the project ${projectName}, so what it exports cannot be read (16.73). Give the app a tsconfig whose include reaches it.`,
|
|
87
|
+
),
|
|
79
88
|
)
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
80
91
|
const module = await checker.getSymbolAtLocation(sourceFile)
|
|
81
92
|
if (module === undefined) return []
|
|
82
93
|
const exports = await checker.getExportsOfModule(module)
|
|
94
|
+
let refused = false
|
|
83
95
|
const found = await allSettled(
|
|
84
96
|
exports.map(async (exported) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
target,
|
|
91
|
-
methodsPath,
|
|
92
|
-
)
|
|
93
|
-
const unread: Diagnosis[] = []
|
|
94
|
-
for (const value of declared?.unread ?? []) {
|
|
95
|
-
const file = value.getSourceFile()
|
|
96
|
-
unread.push(
|
|
97
|
-
diagnosisAt(
|
|
98
|
-
file.fileName === sourceFile.fileName
|
|
99
|
-
? path
|
|
100
|
-
: file.fileName,
|
|
101
|
-
file.text,
|
|
102
|
-
value.getStart(),
|
|
103
|
-
'warning',
|
|
104
|
-
`abide: \`${value.parent.getText()}\` is not a value the build can read, so \`${exported.name}\`'s browser copy is built without it. Write it as a literal, or as a \`const\` whose type is one. RULEBOOK 18.15.`,
|
|
105
|
-
),
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
// 19.9 — a handler's args, as its annotation types them.
|
|
109
|
-
const handlerType =
|
|
110
|
-
declared === undefined
|
|
111
|
-
? undefined
|
|
112
|
-
: await checker.getTypeOfSymbol(target)
|
|
113
|
-
if (handlerType !== undefined && abide.reactive) {
|
|
114
|
-
const at = await nestedReactiveIn(
|
|
97
|
+
try {
|
|
98
|
+
const target = await aliasedSymbol(checker, exported)
|
|
99
|
+
if ((target.flags & SymbolFlags.Value) === 0)
|
|
100
|
+
return undefined
|
|
101
|
+
const declared = await declaringMethod(
|
|
115
102
|
checker,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
'',
|
|
119
|
-
{ seen: new Set(), depth: 0 },
|
|
103
|
+
target,
|
|
104
|
+
methodsPath,
|
|
120
105
|
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
106
|
+
const unread: Diagnosis[] = []
|
|
107
|
+
for (const value of declared?.unread ?? []) {
|
|
108
|
+
const file = value.getSourceFile()
|
|
109
|
+
unread.push(
|
|
110
|
+
diagnosisAt(
|
|
111
|
+
file.fileName === sourceFile.fileName
|
|
112
|
+
? path
|
|
113
|
+
: file.fileName,
|
|
114
|
+
file.text,
|
|
115
|
+
value.getStart(),
|
|
116
|
+
'warning',
|
|
117
|
+
`abide: \`${value.parent.getText()}\` is not a value the build can read, so \`${exported.name}\`'s browser copy is built without it. Write it as a literal, or as a \`const\` whose type is one. RULEBOOK 18.15.`,
|
|
118
|
+
),
|
|
124
119
|
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
ui: declared?.ui ?? true,
|
|
130
|
-
cli: declared?.cli ?? true,
|
|
131
|
-
mcp: declared?.mcp ?? true,
|
|
132
|
-
description: declared?.description,
|
|
133
|
-
copy: declared?.copy,
|
|
134
|
-
defaults: declared?.defaults,
|
|
135
|
-
args:
|
|
136
|
-
handlerType === undefined
|
|
120
|
+
}
|
|
121
|
+
// 19.9 — a handler's args, as its annotation types them.
|
|
122
|
+
const handlerType =
|
|
123
|
+
declared === undefined
|
|
137
124
|
? undefined
|
|
138
|
-
: await
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
125
|
+
: await checker.getTypeOfSymbol(target)
|
|
126
|
+
if (handlerType !== undefined && abide.reactive) {
|
|
127
|
+
const at = await nestedReactiveIn(
|
|
128
|
+
checker,
|
|
129
|
+
abide.reactive,
|
|
130
|
+
await valueOfRpc(checker, handlerType),
|
|
131
|
+
'',
|
|
132
|
+
{ seen: new Set(), depth: 0 },
|
|
133
|
+
)
|
|
134
|
+
if (at !== undefined)
|
|
135
|
+
throw new Error(
|
|
136
|
+
`abide: ${path} — \`${exported.name}\` returns a \`Reactive\` at \`${at}\`, which the wire would carry as the function it is rather than the value it holds. Read it there, or return the \`Reactive\` itself to have it adopted (11.12). RULEBOOK 16.39.`,
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
name: exported.name,
|
|
141
|
+
method: declared?.method,
|
|
142
|
+
ui: declared?.ui ?? true,
|
|
143
|
+
cli: declared?.cli ?? true,
|
|
144
|
+
mcp: declared?.mcp ?? true,
|
|
145
|
+
description: declared?.description,
|
|
146
|
+
copy: declared?.copy,
|
|
147
|
+
defaults: declared?.defaults,
|
|
148
|
+
args:
|
|
149
|
+
handlerType === undefined
|
|
150
|
+
? undefined
|
|
151
|
+
: await argsSchemaOf(
|
|
152
|
+
checker,
|
|
153
|
+
handlerType,
|
|
154
|
+
declared?.defaulted ?? [],
|
|
155
|
+
),
|
|
156
|
+
unread,
|
|
157
|
+
}
|
|
158
|
+
} catch (thrown) {
|
|
159
|
+
refused = true
|
|
160
|
+
refusals.at(file, thrown)
|
|
161
|
+
return undefined
|
|
144
162
|
}
|
|
145
163
|
}),
|
|
146
164
|
)
|
|
165
|
+
if (refused) return undefined
|
|
147
166
|
const handlers: DeclaredExport[] = []
|
|
148
167
|
for (const entry of found)
|
|
149
168
|
if (entry !== undefined) handlers.push(entry)
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '#shared/ANCHORS.ts'
|
|
8
8
|
import { anchorMarkup } from '#shared/anchorMarkup.ts'
|
|
9
9
|
import { CHILDREN } from '#shared/CHILDREN.ts'
|
|
10
|
+
import { RUNTIME_SPECIFIER, SHARED_SPECIFIER } from '#shared/SPECIFIERS.ts'
|
|
10
11
|
import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
|
|
11
12
|
import {
|
|
12
13
|
Cursor,
|
|
@@ -39,7 +40,6 @@ import {
|
|
|
39
40
|
type ValueRender,
|
|
40
41
|
} from './renderMarkup.ts'
|
|
41
42
|
import { SHARED_NAMES } from './SHARED_NAMES.ts'
|
|
42
|
-
import { RUNTIME_SPECIFIER, SHARED_SPECIFIER } from '#shared/SPECIFIERS.ts'
|
|
43
43
|
import {
|
|
44
44
|
insideShare,
|
|
45
45
|
resumedShare,
|
|
@@ -213,19 +213,18 @@ export function emitClient(
|
|
|
213
213
|
// to read when this level leaves or a level under it does. One thunk, which the apply below
|
|
214
214
|
// reads too, so the markup expression is emitted once.
|
|
215
215
|
const registered = heads
|
|
216
|
-
? `if (${ROUTED}) ${ROUTED}.
|
|
216
|
+
? `if (${ROUTED}) ${ROUTED}.setHead(() => ${headMarkup})\n`
|
|
217
217
|
: ''
|
|
218
218
|
const applied = heads
|
|
219
|
-
? `if (${ROUTED}) ${lowering.use('applyHead')}(${ROUTED}.
|
|
219
|
+
? `if (${ROUTED}) ${lowering.use('applyHead')}(${ROUTED}.headMarkup()); `
|
|
220
220
|
: ''
|
|
221
221
|
// Applied with the fill either way, so the watch's first run only subscribes: what it would
|
|
222
222
|
// write is already there, from the server or from the line above.
|
|
223
|
-
if (liveHeads)
|
|
224
|
-
lines.push(
|
|
225
|
-
`if (${ROUTED}) ${lowering.use('liveHead')}(${SCOPE}, ${ROUTED}.head)`,
|
|
226
|
-
)
|
|
223
|
+
if (liveHeads) lines.push(`if (${ROUTED}) ${ROUTED}.followHead(${SCOPE})`)
|
|
227
224
|
// 39.1, 39.6 — A COMPONENT'S HEAD IS ITS PAGE'S, on a navigation as on the server: added to the
|
|
228
|
-
// level
|
|
225
|
+
// level it stands in (`contributeHead`), a row grown later included. Asked of the
|
|
226
|
+
// region before the fill makes it look painted; owned by the component's scope as its first
|
|
227
|
+
// unit, so the head leaves with the region — and, 39.13, follows its value until then.
|
|
229
228
|
const ownHead = routed ? undefined : headSink(file)
|
|
230
229
|
const ownMarkup =
|
|
231
230
|
ownHead === undefined
|
|
@@ -234,7 +233,8 @@ export function emitClient(
|
|
|
234
233
|
const contributed =
|
|
235
234
|
ownMarkup === "''"
|
|
236
235
|
? ''
|
|
237
|
-
:
|
|
236
|
+
: `const __headed = ${lowering.use('contributeHead')}(${REGION_ROOT}, () => ${ownMarkup})\n`
|
|
237
|
+
if (contributed !== '') lines.unshift(`${SCOPE}.own(__headed)`)
|
|
238
238
|
const builds = carriesOwnMarkup && page.positions.length > 0
|
|
239
239
|
let fills = contributed
|
|
240
240
|
if (applied !== '' || builds) {
|
|
@@ -406,7 +406,7 @@ function deferredMarkup(
|
|
|
406
406
|
function __markup(${REGION_ROOT}) {
|
|
407
407
|
${insideShare(lowering, SETTLED_AT, markup)}
|
|
408
408
|
}
|
|
409
|
-
if (${ROUTED}) { ${ROUTED}.
|
|
409
|
+
if (${ROUTED}) { ${ROUTED}.leaveMarkup(__markup, ${SETTLED_AT}); return${declared ? ` ${SCOPE}` : ''} }
|
|
410
410
|
return __markup(${REGION_ROOT})`
|
|
411
411
|
}
|
|
412
412
|
|
|
@@ -738,9 +738,7 @@ if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
|
|
|
738
738
|
}
|
|
739
739
|
}
|
|
740
740
|
if (writes.length > 0)
|
|
741
|
-
attributes.push(
|
|
742
|
-
`const __a${slot} = ${element}\n${writes.join('\n')}`,
|
|
743
|
-
)
|
|
741
|
+
attributes.push(`const __a${slot} = ${element}\n${writes.join('\n')}`)
|
|
744
742
|
for (const unit of binds) attributes.push(unit)
|
|
745
743
|
for (const unit of spreads) attributes.push(unit)
|
|
746
744
|
return { handlers, attributes, elements }
|
|
@@ -22,6 +22,8 @@ const BODY = '__body'
|
|
|
22
22
|
// A page's or a layout's deferred build (41.19).
|
|
23
23
|
const BUILT = '__built'
|
|
24
24
|
const BUILD = '__build'
|
|
25
|
+
// The calls renders had made as a component's setup began (41.28).
|
|
26
|
+
const CALLS_BEFORE = '__callsBefore'
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* The server module for a parsed `.abide` file: one ESM module exporting its component.
|
|
@@ -37,8 +39,8 @@ const BUILD = '__build'
|
|
|
37
39
|
*
|
|
38
40
|
* `settles` IS A PAGE OR A LAYOUT, which `render` builds a level at a time (41.19): its setup runs
|
|
39
41
|
* at the call and its markup is built on the first read of `head` or `body`, so the calls the
|
|
40
|
-
* setup started are given the turn to answer
|
|
41
|
-
*
|
|
42
|
+
* setup started are given the turn to answer. Every other component answers the same way only where
|
|
43
|
+
* its setup made a call (41.28), and builds where it is called otherwise.
|
|
42
44
|
*/
|
|
43
45
|
export function emitComponent(
|
|
44
46
|
file: AbideFile,
|
|
@@ -121,8 +123,8 @@ ${privateExports(file.servers.join('\n'))}`
|
|
|
121
123
|
// child the markup invokes must not be set up twice. The build resumes the share scope the
|
|
122
124
|
// setup ran in (10.5), since by the time it is read the caller's has long been left — and that
|
|
123
125
|
// scope is `scope` on the answer, so `composePage` sets the level below up inside it too.
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
+
const deferred =
|
|
127
|
+
(): string => ` const ${SETTLED_AT} = ${lowering.use('currentShare')}()
|
|
126
128
|
let ${BUILT}
|
|
127
129
|
const ${BUILD} = () => {
|
|
128
130
|
if (${BUILT} !== undefined) return ${BUILT}
|
|
@@ -135,10 +137,23 @@ ${privateExports(file.servers.join('\n'))}`
|
|
|
135
137
|
get body() { return ${BUILD}().body },
|
|
136
138
|
get document() { return ${BUILD}().document },
|
|
137
139
|
}`
|
|
138
|
-
|
|
140
|
+
// 41.28 — EVERY OTHER COMPONENT BUILDS WHERE IT IS CALLED unless its setup
|
|
141
|
+
// made a call, and then answers as a page does, for `renderComponent` to build after the turn.
|
|
142
|
+
// THE BUILD IS WRITTEN TWICE rather than always run through `__build`: the closure measured ~4%
|
|
143
|
+
// a row on `bench:component`'s card under JSC, the second copy nothing but server module bytes.
|
|
144
|
+
// One with no setup cannot call, and is written once.
|
|
145
|
+
const waits = !settles && componentBody.trim() !== ''
|
|
146
|
+
const level = settles
|
|
147
|
+
? deferred()
|
|
148
|
+
: waits
|
|
149
|
+
? ` if (${lowering.use('callsInRender')}() !== ${CALLS_BEFORE}) {
|
|
150
|
+
${deferred()}
|
|
151
|
+
}
|
|
152
|
+
${build}`
|
|
153
|
+
: build
|
|
139
154
|
return `${moduleImports(lowering, () => SERVER_SPECIFIER, [moduleSetup, serverSetup, setup])}
|
|
140
155
|
${moduleSetup.body === '' ? '' : `${moduleSetup.body}\n`}${serverBody}export default function Component(${lowering.takesProps() ? `${PROPS} = {}` : ''}) {
|
|
141
|
-
${opened}${componentBody}
|
|
156
|
+
${waits ? ` const ${CALLS_BEFORE} = ${lowering.use('callsInRender')}()\n` : ''}${opened}${componentBody}
|
|
142
157
|
${level}${closed}
|
|
143
158
|
}
|
|
144
159
|
`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { STYLE_QUERY } from '#server/MODULE_QUERIES.ts'
|
|
2
|
-
import {
|
|
2
|
+
import type { Placement } from '#server/placement.ts'
|
|
3
3
|
import type { CheckerAnswer } from './CheckerAnswer.ts'
|
|
4
4
|
import { emitClient } from './emitClient.ts'
|
|
5
5
|
import { emitComponent } from './emitComponent.ts'
|
|
@@ -18,34 +18,34 @@ import { sourceMapOf } from './sourceMapOf.ts'
|
|
|
18
18
|
* One `.abide`'s modules, in the mode the build compiles it in: its server module, its client
|
|
19
19
|
* module (`''` where it has none) and its scoped stylesheet.
|
|
20
20
|
*
|
|
21
|
-
* Everything that mode depends on is the
|
|
22
|
-
* it (`componentGraph`), so a test handing over the same
|
|
21
|
+
* Everything that mode depends on is where the file is placed (`placement`) and whether another
|
|
22
|
+
* file invokes it (`componentGraph`), so a test handing over the same facts compiles what the build
|
|
23
|
+
* would.
|
|
23
24
|
*/
|
|
24
25
|
export function emitSource(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
source: Pick<Placement, 'relative' | 'kind' | 'scope'> & {
|
|
27
|
+
file: AbideFile
|
|
28
|
+
answer: CheckerAnswer
|
|
29
|
+
invoked: boolean
|
|
30
|
+
},
|
|
29
31
|
/**
|
|
30
32
|
* 16.28 — what the browser's module binds in place of the server block: the wrapper of each
|
|
31
33
|
* private handler it declares, under the handler's name (`standInsOf`).
|
|
32
34
|
*/
|
|
33
35
|
standIns = '',
|
|
34
36
|
/** 38.97 — the `.abide`'s text, where the build wants each module's source map. */
|
|
35
|
-
|
|
36
|
-
/** 23.19 — the path it answers by, where a mount gives it one other than `relative`. */
|
|
37
|
-
position = relative,
|
|
37
|
+
text?: string,
|
|
38
38
|
): {
|
|
39
39
|
server: string
|
|
40
40
|
client: string
|
|
41
|
-
/** 38.97 — each module's `mappings` into the `.abide`, where `
|
|
41
|
+
/** 38.97 — each module's `mappings` into the `.abide`, where `text` was given. */
|
|
42
42
|
serverMappings: string | undefined
|
|
43
43
|
clientMappings: string | undefined
|
|
44
44
|
sheet: string | undefined
|
|
45
45
|
/** 39.8 — whether a `<style>` names a view transition, read here rather than at run time. */
|
|
46
46
|
transitions: boolean
|
|
47
47
|
} {
|
|
48
|
-
const kind =
|
|
48
|
+
const { relative, kind, file, answer, invoked } = source
|
|
49
49
|
// 34.4 — A COMPONENT WITH STYLES: its elements carry its token, and the styles, scoped,
|
|
50
50
|
// are a stylesheet its client module imports — which the bundle collects into the
|
|
51
51
|
// stylesheet the document links (38.23).
|
|
@@ -87,9 +87,9 @@ export function emitSource(
|
|
|
87
87
|
// has no `.client.ts`, so a file shipping no runtime is a fact about the build rather than a
|
|
88
88
|
// claim about the bundler.
|
|
89
89
|
const serverChunks: LoweredChunk[] | undefined =
|
|
90
|
-
|
|
90
|
+
text === undefined ? undefined : []
|
|
91
91
|
const clientChunks: LoweredChunk[] | undefined =
|
|
92
|
-
|
|
92
|
+
text === undefined ? undefined : []
|
|
93
93
|
const client = emitClient(
|
|
94
94
|
file,
|
|
95
95
|
answer,
|
|
@@ -107,7 +107,7 @@ export function emitSource(
|
|
|
107
107
|
file,
|
|
108
108
|
answer,
|
|
109
109
|
kind !== 'component',
|
|
110
|
-
|
|
110
|
+
source.scope,
|
|
111
111
|
serverChunks,
|
|
112
112
|
)
|
|
113
113
|
const name = relative.slice(relative.lastIndexOf('/') + 1)
|
|
@@ -118,24 +118,24 @@ export function emitSource(
|
|
|
118
118
|
standIns,
|
|
119
119
|
)
|
|
120
120
|
let mapped: Mapped | undefined
|
|
121
|
-
if (
|
|
121
|
+
if (text !== undefined) {
|
|
122
122
|
mapped = MAPPED.get(relative)
|
|
123
123
|
if (
|
|
124
124
|
mapped === undefined ||
|
|
125
|
-
mapped.source !==
|
|
125
|
+
mapped.source !== text ||
|
|
126
126
|
mapped.server !== server ||
|
|
127
127
|
mapped.client !== browser
|
|
128
128
|
) {
|
|
129
129
|
const projected = projectShadow(file).origins
|
|
130
130
|
mapped = {
|
|
131
|
-
source,
|
|
131
|
+
source: text,
|
|
132
132
|
server,
|
|
133
133
|
client: browser,
|
|
134
134
|
serverMappings: sourceMapOf(
|
|
135
135
|
server,
|
|
136
136
|
serverChunks ?? [],
|
|
137
137
|
projected,
|
|
138
|
-
|
|
138
|
+
text,
|
|
139
139
|
),
|
|
140
140
|
clientMappings:
|
|
141
141
|
browser === ''
|
|
@@ -144,7 +144,7 @@ export function emitSource(
|
|
|
144
144
|
browser,
|
|
145
145
|
clientChunks ?? [],
|
|
146
146
|
projected,
|
|
147
|
-
|
|
147
|
+
text,
|
|
148
148
|
),
|
|
149
149
|
}
|
|
150
150
|
MAPPED.set(relative, mapped)
|
|
@@ -10,18 +10,14 @@
|
|
|
10
10
|
// that is not a handler a refusal here rather than an address that silently never answers.
|
|
11
11
|
|
|
12
12
|
import type { Mount } from '#server/answerRpc.ts'
|
|
13
|
-
import {
|
|
13
|
+
import { frameworkRouteRefusal } from '#server/appTree.ts'
|
|
14
14
|
import { surfaceName } from '#server/surfaceOf.ts'
|
|
15
15
|
import { PRIVATE_HANDLERS_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
placeOf,
|
|
19
|
-
routeOf,
|
|
20
|
-
scopeOf,
|
|
21
|
-
} from '#shared/routePattern.ts'
|
|
16
|
+
import { parseRoutePattern } from '#shared/routePattern.ts'
|
|
17
|
+
import type { PlacedHandler, PlacedSource } from './appLayout.ts'
|
|
22
18
|
import { declaredExports } from './declaredExports.ts'
|
|
23
|
-
import type { AbideFile } from './parseAbide.ts'
|
|
24
19
|
import { PRIVATE_EXPORT } from './projectShadow.ts'
|
|
20
|
+
import type { Refusals } from './Refusals.ts'
|
|
25
21
|
import type { Diagnosis, TypeSession } from './TypeSession.ts'
|
|
26
22
|
|
|
27
23
|
/** A handler file, by its path and its path below the app's root. */
|
|
@@ -32,19 +28,11 @@ export type HandlerFile = {
|
|
|
32
28
|
position: string
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
/** A `.abide` the build read. */
|
|
36
|
-
type Source =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* `abide check` collects every one at its file (38.33).
|
|
41
|
-
*/
|
|
42
|
-
export type Refuse = (relative: string, thrown: unknown) => void
|
|
43
|
-
|
|
44
|
-
/** The build's answer to a refusal: the first one stops it. */
|
|
45
|
-
export const THROW: Refuse = (_relative, thrown) => {
|
|
46
|
-
throw thrown
|
|
47
|
-
}
|
|
31
|
+
/** A `.abide` the build read, placed. */
|
|
32
|
+
type Source = Pick<
|
|
33
|
+
PlacedSource,
|
|
34
|
+
'path' | 'relative' | 'position' | 'place' | 'scope' | 'file'
|
|
35
|
+
>
|
|
48
36
|
|
|
49
37
|
/**
|
|
50
38
|
* The app's handler mounts, with the warnings the build raised reading them.
|
|
@@ -55,28 +43,49 @@ export const THROW: Refuse = (_relative, thrown) => {
|
|
|
55
43
|
export async function handlersUnder(
|
|
56
44
|
session: TypeSession,
|
|
57
45
|
sources: readonly Source[],
|
|
58
|
-
files: readonly
|
|
59
|
-
|
|
46
|
+
files: readonly Pick<
|
|
47
|
+
PlacedHandler,
|
|
48
|
+
'path' | 'relative' | 'route' | 'scope'
|
|
49
|
+
>[],
|
|
50
|
+
refusals: Refusals,
|
|
60
51
|
): Promise<{ mounts: Mount[]; warnings: Diagnosis[] }> {
|
|
61
52
|
const serving: Source[] = []
|
|
62
53
|
for (const source of sources)
|
|
63
54
|
if (source.file.servers.length > 0) serving.push(source)
|
|
64
55
|
const paths: string[] = []
|
|
65
|
-
|
|
56
|
+
const named: string[] = []
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
paths.push(file.path)
|
|
59
|
+
named.push(file.relative)
|
|
60
|
+
}
|
|
66
61
|
const project = await session.projectWith(paths)
|
|
67
|
-
for (const source of serving)
|
|
62
|
+
for (const source of serving) {
|
|
63
|
+
paths.push(session.projectionOf(source.path))
|
|
64
|
+
named.push(source.path)
|
|
65
|
+
}
|
|
66
|
+
// A module whose read was refused is `undefined`, refused at its file already.
|
|
68
67
|
const declared =
|
|
69
68
|
project === undefined || paths.length === 0
|
|
70
|
-
? []
|
|
71
|
-
: await declaredExports(
|
|
69
|
+
? paths.map(() => [])
|
|
70
|
+
: await declaredExports(
|
|
71
|
+
project,
|
|
72
|
+
session.tsconfig,
|
|
73
|
+
paths,
|
|
74
|
+
named,
|
|
75
|
+
refusals,
|
|
76
|
+
)
|
|
72
77
|
const mounts: Mount[] = []
|
|
73
78
|
const warnings: Diagnosis[] = []
|
|
74
79
|
const names = new Map<string, string>()
|
|
75
80
|
for (let at = 0; at < files.length; at += 1) {
|
|
76
|
-
const {
|
|
81
|
+
const {
|
|
82
|
+
relative,
|
|
83
|
+
route: address,
|
|
84
|
+
scope,
|
|
85
|
+
} = files[at] as (typeof files)[number]
|
|
86
|
+
const exported = declared[at]
|
|
87
|
+
if (exported === undefined) continue
|
|
77
88
|
try {
|
|
78
|
-
const address = routeOf(position)
|
|
79
|
-
const exported = declared[at] ?? []
|
|
80
89
|
const handler = exported.find((each) => each.name === 'default')
|
|
81
90
|
// 16.73 — the default and nothing else, declared by one of the five methods.
|
|
82
91
|
for (const each of exported)
|
|
@@ -90,7 +99,8 @@ export async function handlersUnder(
|
|
|
90
99
|
throw new Error(
|
|
91
100
|
`abide: ${relative} is a handler file — a .ts directly in an address directory — and exports no handler as its default. Export one (\`export default GET(…)\`), or move the file into a lib directory. RULEBOOK 16.73, 23.22.`,
|
|
92
101
|
)
|
|
93
|
-
|
|
102
|
+
const framework = frameworkRouteRefusal(address, relative)
|
|
103
|
+
if (framework !== undefined) throw framework
|
|
94
104
|
refuseUndeclaredParameters(address, handler.args, relative)
|
|
95
105
|
// 20.13 — one name per handler on every generated surface.
|
|
96
106
|
const surface = surfaceName(handler.method, address)
|
|
@@ -106,7 +116,7 @@ export async function handlersUnder(
|
|
|
106
116
|
source: relative,
|
|
107
117
|
name: address.slice(address.lastIndexOf('/') + 1),
|
|
108
118
|
export: 'default',
|
|
109
|
-
scope
|
|
119
|
+
scope,
|
|
110
120
|
private: false,
|
|
111
121
|
ui: handler.ui,
|
|
112
122
|
cli: handler.cli,
|
|
@@ -118,14 +128,14 @@ export async function handlersUnder(
|
|
|
118
128
|
})
|
|
119
129
|
warnings.push(...handler.unread)
|
|
120
130
|
} catch (thrown) {
|
|
121
|
-
|
|
131
|
+
refusals.at(relative, thrown)
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
134
|
for (let at = 0; at < serving.length; at += 1) {
|
|
125
|
-
const { relative, position } = serving[at] as Source
|
|
135
|
+
const { relative, position, place, scope } = serving[at] as Source
|
|
126
136
|
// 34.24 — a dependency's file answers where a mount places it, and its handlers below the
|
|
127
137
|
// path it has there.
|
|
128
|
-
const below =
|
|
138
|
+
const below = place === 'dependency' ? relative : position
|
|
129
139
|
for (const each of declared[files.length + at] ?? []) {
|
|
130
140
|
if (each.method === undefined) continue
|
|
131
141
|
if (!each.name.startsWith(PRIVATE_EXPORT)) continue
|
|
@@ -136,7 +146,7 @@ export async function handlersUnder(
|
|
|
136
146
|
source: relative,
|
|
137
147
|
name,
|
|
138
148
|
export: each.name,
|
|
139
|
-
scope
|
|
149
|
+
scope,
|
|
140
150
|
private: true,
|
|
141
151
|
ui: each.ui,
|
|
142
152
|
cli: false,
|