@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
|
@@ -8,6 +8,14 @@ import {
|
|
|
8
8
|
PositionTable,
|
|
9
9
|
} from './PositionTable.ts'
|
|
10
10
|
import type { AbideFile } from './parseAbide.ts'
|
|
11
|
+
import {
|
|
12
|
+
attributedImports,
|
|
13
|
+
IMPORT_ATTRIBUTES,
|
|
14
|
+
type ImportBinding,
|
|
15
|
+
importStatements,
|
|
16
|
+
newlinesIn,
|
|
17
|
+
SOURCE_SUFFIX,
|
|
18
|
+
} from './splitImports.ts'
|
|
11
19
|
import type {
|
|
12
20
|
Body,
|
|
13
21
|
Branch,
|
|
@@ -16,7 +24,7 @@ import type {
|
|
|
16
24
|
Setup,
|
|
17
25
|
TemplateNode,
|
|
18
26
|
} from './TemplateNode.ts'
|
|
19
|
-
import { tokenize } from './tokenize.ts'
|
|
27
|
+
import { nesting, tokenize } from './tokenize.ts'
|
|
20
28
|
|
|
21
29
|
export type Span = { start: number; end: number }
|
|
22
30
|
|
|
@@ -37,6 +45,8 @@ export type Span = { start: number; end: number }
|
|
|
37
45
|
export type Projection = {
|
|
38
46
|
text: string
|
|
39
47
|
modules: Span
|
|
48
|
+
/** 34.19 — the `<script server>` bodies, module scope like `modules` and the server's alone. */
|
|
49
|
+
servers: Span
|
|
40
50
|
scripts: Span
|
|
41
51
|
/** Every template position, ascending, each with its kind — what the checker's tree is asked. */
|
|
42
52
|
positions: PositionTable
|
|
@@ -83,6 +93,45 @@ export function projectShadow(file: AbideFile): Projection {
|
|
|
83
93
|
return projection
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
type ImportStatement = ReturnType<typeof importStatements>[number]
|
|
97
|
+
|
|
98
|
+
/** 34.22 — a binding as two blocks importing the same thing agree on it. */
|
|
99
|
+
function bindingKey(
|
|
100
|
+
statement: ImportStatement,
|
|
101
|
+
binding: ImportBinding,
|
|
102
|
+
): string {
|
|
103
|
+
return `${statement.specifier}|${statement.attributes}|${binding.imported}|${binding.local}`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 34.22 — what is left of an import once the bindings `drops` answers for are taken out:
|
|
108
|
+
* `undefined` where none is, `''` where every one is, and the statement over the rest otherwise,
|
|
109
|
+
* spelled from its tokens.
|
|
110
|
+
*/
|
|
111
|
+
function keptImport(
|
|
112
|
+
statement: ImportStatement,
|
|
113
|
+
drops: (statement: ImportStatement, binding: ImportBinding) => boolean,
|
|
114
|
+
): string | undefined {
|
|
115
|
+
const parts: string[] = []
|
|
116
|
+
const named: string[] = []
|
|
117
|
+
for (const binding of statement.bindings) {
|
|
118
|
+
if (drops(statement, binding)) continue
|
|
119
|
+
// The default's own slot and a namespace stand outside the braces; `{ default as x }` in.
|
|
120
|
+
if (
|
|
121
|
+
(binding.imported === 'default' &&
|
|
122
|
+
binding.written === binding.local) ||
|
|
123
|
+
binding.imported === '*'
|
|
124
|
+
)
|
|
125
|
+
parts.push(binding.written)
|
|
126
|
+
else named.push(binding.written)
|
|
127
|
+
}
|
|
128
|
+
const kept = parts.length + named.length
|
|
129
|
+
if (kept === statement.bindings.length) return undefined
|
|
130
|
+
if (kept === 0) return ''
|
|
131
|
+
if (named.length > 0) parts.push(`{ ${named.join(', ')} }`)
|
|
132
|
+
return `import ${statement.typeOnly ? 'type ' : ''}${parts.join(', ')} from '${statement.specifier}'${statement.attributes === '' ? '' : ` ${statement.attributes}`}`
|
|
133
|
+
}
|
|
134
|
+
|
|
86
135
|
function project(file: AbideFile): Projection {
|
|
87
136
|
const origins: Origin[] = []
|
|
88
137
|
// 34.13 — scaffolding ahead of every block, so each one and the template are in their scope.
|
|
@@ -102,17 +151,115 @@ function project(file: AbideFile): Projection {
|
|
|
102
151
|
}
|
|
103
152
|
|
|
104
153
|
// Blocks of one kind, joined as the file wrote them, each run placed where it stood.
|
|
105
|
-
const blocks = (
|
|
154
|
+
const blocks = (
|
|
155
|
+
bodies: string[],
|
|
156
|
+
starts: number[],
|
|
157
|
+
drops: (statement: ImportStatement, binding: ImportBinding) => boolean,
|
|
158
|
+
): Span => {
|
|
106
159
|
const start = text.length
|
|
107
160
|
for (let at = 0; at < bodies.length; at += 1) {
|
|
108
161
|
if (at > 0) text += '\n'
|
|
109
|
-
|
|
162
|
+
script(bodies[at] as string, starts[at], drops)
|
|
110
163
|
}
|
|
111
164
|
return { start, end: text.length }
|
|
112
165
|
}
|
|
113
|
-
|
|
166
|
+
// 34.21 — a block copied as written, save that a `.abide` imported as text names the module
|
|
167
|
+
// holding its text, and loses the attribute bun would otherwise read that module as text by.
|
|
168
|
+
//
|
|
169
|
+
// 34.22 — and a binding a `<script server>` and the file's `<script module>` or `<script>` both
|
|
170
|
+
// import is written once: the server's module holds the other blocks' imports too, and written
|
|
171
|
+
// twice it was a duplicate binding. `drops` says which copy goes. A blank line per line it
|
|
172
|
+
// spanned keeps the block line for line with the file.
|
|
173
|
+
const script = (
|
|
174
|
+
body: string,
|
|
175
|
+
origin: number | undefined,
|
|
176
|
+
drops: (statement: ImportStatement, binding: ImportBinding) => boolean,
|
|
177
|
+
): void => {
|
|
178
|
+
const edits: { start: number; end: number; written: string }[] = []
|
|
179
|
+
for (const one of attributedImports(body, 'text'))
|
|
180
|
+
edits.push({
|
|
181
|
+
start: one.literalStart,
|
|
182
|
+
end: one.end,
|
|
183
|
+
written: `'${one.specifier}${SOURCE_SUFFIX}'`,
|
|
184
|
+
})
|
|
185
|
+
// 23.19 — a mount is the build's to read, and no module of the checker's or bun's.
|
|
186
|
+
for (const one of attributedImports(body, 'routes'))
|
|
187
|
+
edits.push({
|
|
188
|
+
start: one.start,
|
|
189
|
+
end: one.end,
|
|
190
|
+
written: `;${'\n'.repeat(newlinesIn(body, one.start, one.end))}`,
|
|
191
|
+
})
|
|
192
|
+
for (const one of importStatements(body)) {
|
|
193
|
+
const kept = keptImport(one, drops)
|
|
194
|
+
if (kept === undefined) continue
|
|
195
|
+
edits.push({
|
|
196
|
+
start: one.start,
|
|
197
|
+
end: one.end,
|
|
198
|
+
written: `${kept === '' ? ';' : kept}${'\n'.repeat(newlinesIn(body, one.start, one.end))}`,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
edits.sort((one, two) => one.start - two.start)
|
|
202
|
+
let from = 0
|
|
203
|
+
for (const edit of edits) {
|
|
204
|
+
if (edit.start < from) continue
|
|
205
|
+
copied(
|
|
206
|
+
body.slice(from, edit.start),
|
|
207
|
+
origin === undefined ? undefined : origin + from,
|
|
208
|
+
)
|
|
209
|
+
place(origin === undefined ? undefined : origin + edit.start, 0)
|
|
210
|
+
text += edit.written
|
|
211
|
+
from = edit.end
|
|
212
|
+
}
|
|
213
|
+
copied(
|
|
214
|
+
body.slice(from),
|
|
215
|
+
origin === undefined ? undefined : origin + from,
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
// 34.22 — per binding the blocks both sides run import, whether one of them imports a value;
|
|
219
|
+
// and the bindings the server block imports as values.
|
|
220
|
+
const shared = new Map<string, boolean>()
|
|
221
|
+
for (const body of [...file.modules, ...file.scripts])
|
|
222
|
+
for (const one of importStatements(body))
|
|
223
|
+
for (const binding of one.bindings) {
|
|
224
|
+
const key = bindingKey(one, binding)
|
|
225
|
+
shared.set(key, shared.get(key) === true || !binding.typeOnly)
|
|
226
|
+
}
|
|
227
|
+
const serverValues = new Set<string>()
|
|
228
|
+
for (const body of file.servers)
|
|
229
|
+
for (const one of importStatements(body))
|
|
230
|
+
for (const binding of one.bindings)
|
|
231
|
+
if (!binding.typeOnly)
|
|
232
|
+
serverValues.add(bindingKey(one, binding))
|
|
233
|
+
// A TYPE-ONLY IMPORT NEVER STANDS FOR A VALUE ONE: erased, it binds nothing at run time, so where
|
|
234
|
+
// the server block imports the value, it is the other block's type-only copy that goes.
|
|
235
|
+
const typeOnlyOfServerValue = (
|
|
236
|
+
statement: ImportStatement,
|
|
237
|
+
binding: ImportBinding,
|
|
238
|
+
): boolean =>
|
|
239
|
+
binding.typeOnly && serverValues.has(bindingKey(statement, binding))
|
|
240
|
+
const modules = blocks(
|
|
241
|
+
file.modules,
|
|
242
|
+
file.origins.modules,
|
|
243
|
+
typeOnlyOfServerValue,
|
|
244
|
+
)
|
|
114
245
|
text += '\n;\n'
|
|
115
|
-
const
|
|
246
|
+
const servers = blocks(
|
|
247
|
+
file.servers,
|
|
248
|
+
file.origins.servers,
|
|
249
|
+
(statement, binding) => {
|
|
250
|
+
const value = shared.get(bindingKey(statement, binding))
|
|
251
|
+
if (value === undefined || (value === false && !binding.typeOnly))
|
|
252
|
+
return false
|
|
253
|
+
if (value && !binding.typeOnly) refuseTwoFaces(statement, binding)
|
|
254
|
+
return true
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
text += '\n;\n'
|
|
258
|
+
const scripts = blocks(
|
|
259
|
+
file.scripts,
|
|
260
|
+
file.origins.scripts,
|
|
261
|
+
typeOnlyOfServerValue,
|
|
262
|
+
)
|
|
116
263
|
const scriptsText = text.slice(scripts.start, scripts.end)
|
|
117
264
|
text += '\n'
|
|
118
265
|
const positions: Position[] = []
|
|
@@ -161,7 +308,7 @@ function project(file: AbideFile): Projection {
|
|
|
161
308
|
// 32.17 — A COMPONENT TAG IS A CALL, AND IT IS PROJECTED AS ONE: the name it invokes, handed the
|
|
162
309
|
// record the build hands it (`invocationProps`), so the checker holds each value to the prop the
|
|
163
310
|
// child declares (33.4), reports a name nobody imported, and answers a hover or a jump on the
|
|
164
|
-
// tag with the import's (38.
|
|
311
|
+
// tag with the import's (38.39). Spreads first and explicit keys after, which is the order 33.12
|
|
165
312
|
// resolves them in.
|
|
166
313
|
const invocation = (node: ElementNode): void => {
|
|
167
314
|
// 32.36 — CALLED OPTIONALLY, so a value that may be absent is checked as one that may
|
|
@@ -386,8 +533,17 @@ function project(file: AbideFile): Projection {
|
|
|
386
533
|
),
|
|
387
534
|
)
|
|
388
535
|
text += ')\n'
|
|
389
|
-
|
|
390
|
-
|
|
536
|
+
// 38.39 — the item and the index copied from where they stand, so a hover on either is
|
|
537
|
+
// the binding's. The index is a `number`: `= 0` alone types it the literal `0`.
|
|
538
|
+
const bound = file.origins.bindings.get(node)
|
|
539
|
+
text += 'for (const '
|
|
540
|
+
copied(node.item, bound?.item)
|
|
541
|
+
text += ` of ${ROWS_OF}(${subject})) {\n`
|
|
542
|
+
if (node.index !== '') {
|
|
543
|
+
text += 'const '
|
|
544
|
+
copied(node.index, bound?.index)
|
|
545
|
+
text += ': number = 0\n'
|
|
546
|
+
}
|
|
391
547
|
// `inBody` but for the key, which stands after the row's setup and before its nodes.
|
|
392
548
|
setup(node.setup)
|
|
393
549
|
declare(node.components)
|
|
@@ -402,6 +558,9 @@ function project(file: AbideFile): Projection {
|
|
|
402
558
|
}
|
|
403
559
|
declare(file.components)
|
|
404
560
|
walk(file.head)
|
|
561
|
+
// 35.17 — the document's attributes are read as any element's are.
|
|
562
|
+
if (file.document !== undefined)
|
|
563
|
+
walk([file.document.html, file.document.body])
|
|
405
564
|
walk(file.template)
|
|
406
565
|
if (subjects > 0) text += ROWS_OF_DECLARATION
|
|
407
566
|
if (inlineProps) text += INLINE_PROPS_IMPORT
|
|
@@ -409,12 +568,23 @@ function project(file: AbideFile): Projection {
|
|
|
409
568
|
// 34.3 — BOTH BLOCKS. A `<script>` is a function body on both sides, where an `await` at its top
|
|
410
569
|
// does not parse; a `<script module>` is module scope, where it would, and holds the import of
|
|
411
570
|
// every file that reaches it until it settles — the render waiting on it in all but name.
|
|
412
|
-
|
|
571
|
+
const modulesText = file.modules.join('\n')
|
|
572
|
+
const serversText = file.servers.join('\n')
|
|
573
|
+
refuseTopLevelAwait(modulesText, '<script module>')
|
|
574
|
+
refuseTopLevelAwait(serversText, '<script server>')
|
|
413
575
|
refuseTopLevelAwait(scriptsText, '<script>')
|
|
414
|
-
|
|
576
|
+
// A kind at a time: a token at the end of one kind's text would decide how the next one's first
|
|
577
|
+
// reads.
|
|
578
|
+
refuseExport(modulesText)
|
|
579
|
+
refuseExport(serversText)
|
|
580
|
+
refuseExport(scriptsText)
|
|
581
|
+
// 16.70 — what the server block binds, re-exported under a name no author writes, so the
|
|
582
|
+
// checker reads each one's declaring call as it reads a handler file's default.
|
|
583
|
+
text += privateExports(serversText)
|
|
415
584
|
return {
|
|
416
585
|
text,
|
|
417
586
|
modules,
|
|
587
|
+
servers,
|
|
418
588
|
scripts,
|
|
419
589
|
positions: new PositionTable(positions),
|
|
420
590
|
slots,
|
|
@@ -537,6 +707,7 @@ declare function ${ROWS_OF}<T>(subject: Iterable<T> | AsyncIterable<T> | null |
|
|
|
537
707
|
*/
|
|
538
708
|
function refuseTopLevelAwait(source: string, block: string): void {
|
|
539
709
|
if (!source.includes(AWAIT)) return
|
|
710
|
+
// Brackets alone, not `nesting`: a template's substitution is still the top level here.
|
|
540
711
|
let depth = 0
|
|
541
712
|
for (const token of tokenize(source)) {
|
|
542
713
|
const text = token.text
|
|
@@ -550,29 +721,89 @@ function refuseTopLevelAwait(source: string, block: string): void {
|
|
|
550
721
|
}
|
|
551
722
|
|
|
552
723
|
/**
|
|
553
|
-
* Throws where a
|
|
724
|
+
* Throws where a script block exports.
|
|
554
725
|
*
|
|
555
|
-
* 34.
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
* nor the fix.
|
|
726
|
+
* 34.17 — A `.abide`'S ONE EXPORT IS ITS COMPONENT. A handler another file calls is a handler file
|
|
727
|
+
* (16.2), a value two files share is a module under a lib directory, and what the framework reads
|
|
728
|
+
* off a file is a hook it calls (37.1). Refused rather than emitted, as 34.3's `await` is: the
|
|
729
|
+
* bundler's own error names neither the file nor the fix.
|
|
559
730
|
*/
|
|
560
|
-
function
|
|
731
|
+
function refuseExport(source: string): void {
|
|
561
732
|
if (!source.includes(EXPORT)) return
|
|
562
733
|
let depth = 0
|
|
563
734
|
let previous = ''
|
|
564
735
|
for (const token of tokenize(source)) {
|
|
565
736
|
const text = token.text
|
|
566
|
-
|
|
567
|
-
|
|
737
|
+
const moved = nesting(token.kind)
|
|
738
|
+
if (moved !== 0) depth += moved
|
|
568
739
|
else if (depth === 0 && text === EXPORT && previous !== '.')
|
|
569
740
|
throw new Error(
|
|
570
|
-
|
|
741
|
+
'abide: a .abide cannot export — its component is its one export. A handler another file calls is a handler file (a .ts beside the page, `export default GET(…)`); a value files share is a module under a lib directory. RULEBOOK 34.17.',
|
|
571
742
|
)
|
|
572
743
|
previous = text
|
|
573
744
|
}
|
|
574
745
|
}
|
|
575
746
|
|
|
747
|
+
/** The prefix a server block's bindings are re-exported under, for the build alone (16.70). */
|
|
748
|
+
export const PRIVATE_EXPORT = '__handler_'
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* 16.70 — the `export { … }` re-exporting each name the server blocks bind under `PRIVATE_EXPORT`,
|
|
752
|
+
* or `''` where they bind none. One spelling for the checker's view and the emitted module, which
|
|
753
|
+
* the build's handler table reads a private handler out of by the same name.
|
|
754
|
+
*/
|
|
755
|
+
export function privateExports(servers: string): string {
|
|
756
|
+
const listed: string[] = []
|
|
757
|
+
for (const name of serverBindings(servers))
|
|
758
|
+
listed.push(`${name} as ${PRIVATE_EXPORT}${name}`)
|
|
759
|
+
return listed.length === 0 ? '' : `export { ${listed.join(', ')} }\n`
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* The names a `<script server>` binds with `const`, `let` or `var` at its top level, a
|
|
764
|
+
* destructuring's aside: a handler is a call bound to one name.
|
|
765
|
+
*/
|
|
766
|
+
function serverBindings(source: string): string[] {
|
|
767
|
+
const names: string[] = []
|
|
768
|
+
if (source === '') return names
|
|
769
|
+
let depth = 0
|
|
770
|
+
let declaring = false
|
|
771
|
+
for (const token of tokenize(source)) {
|
|
772
|
+
const text = token.text
|
|
773
|
+
const moved = nesting(token.kind)
|
|
774
|
+
if (moved !== 0) {
|
|
775
|
+
depth += moved
|
|
776
|
+
declaring = false
|
|
777
|
+
} else if (depth !== 0) continue
|
|
778
|
+
else if (text === 'const' || text === 'let' || text === 'var')
|
|
779
|
+
declaring = true
|
|
780
|
+
else if (declaring && token.kind === SyntaxKind.Identifier) {
|
|
781
|
+
names.push(text)
|
|
782
|
+
declaring = false
|
|
783
|
+
} else declaring = false
|
|
784
|
+
}
|
|
785
|
+
return names
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* 34.22 — throws where the binding both blocks import is a `.abide`'s default or namespace: the
|
|
790
|
+
* server block's is the function binding its props (34.16) and the other block's is the module it
|
|
791
|
+
* compiles to (32.17), and one name in one module scope cannot hold both.
|
|
792
|
+
*/
|
|
793
|
+
function refuseTwoFaces(
|
|
794
|
+
statement: ImportStatement,
|
|
795
|
+
binding: ImportBinding,
|
|
796
|
+
): void {
|
|
797
|
+
if (
|
|
798
|
+
statement.specifier.endsWith('.abide') &&
|
|
799
|
+
!IMPORT_ATTRIBUTES.text.test(statement.attributes) &&
|
|
800
|
+
(binding.imported === 'default' || binding.imported === '*')
|
|
801
|
+
)
|
|
802
|
+
throw new Error(
|
|
803
|
+
`abide: <script server> imports ${binding.local} from '${statement.specifier}' as another script block of its file does — a server block's .abide is the function binding its props (34.16), the other block's is its module (32.17), and one name cannot be both. Import it under another name in the <script server>. RULEBOOK 34.22.`,
|
|
804
|
+
)
|
|
805
|
+
}
|
|
806
|
+
|
|
576
807
|
/**
|
|
577
808
|
* 34.13 — each type parameter a component declares, as a type its body cannot see inside.
|
|
578
809
|
*
|
|
@@ -76,8 +76,8 @@ async function everyPresentMember(
|
|
|
76
76
|
return present
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
// Where abide declares `Reactive`, `props`, `route` and `
|
|
80
|
-
// `METHODS_SOURCE` is.
|
|
79
|
+
// Where abide declares `Reactive`, `props`, `route`, `Selection` and `onConfig`, found in the project
|
|
80
|
+
// the way `METHODS_SOURCE` is.
|
|
81
81
|
const REACTIVE_SOURCE = Bun.fileURLToPath(
|
|
82
82
|
new URL('../shared/reactive/Reactive.ts', import.meta.url),
|
|
83
83
|
)
|
|
@@ -90,16 +90,20 @@ const ROUTE_SOURCE = Bun.fileURLToPath(
|
|
|
90
90
|
const SELECTION_SOURCE = Bun.fileURLToPath(
|
|
91
91
|
new URL('../shared/reactive/index.ts', import.meta.url),
|
|
92
92
|
)
|
|
93
|
+
const HOOKS_SOURCE = Bun.fileURLToPath(
|
|
94
|
+
new URL('../server/hooks.ts', import.meta.url),
|
|
95
|
+
)
|
|
93
96
|
|
|
94
97
|
/**
|
|
95
|
-
* Abide's own `Reactive`, `Selection`, `props` and `
|
|
96
|
-
* names them.
|
|
98
|
+
* Abide's own `Reactive`, `Selection`, `props`, `route` and `onConfig` declarations, as this
|
|
99
|
+
* project's checker names them.
|
|
97
100
|
*
|
|
98
101
|
* 31.3 — A `Reactive` IS TOLD BY ITS DECLARATION, never by how it prints. Every instantiation of
|
|
99
102
|
* abide's `Reactive` carries the symbol of the one type literal it is declared as, whatever alias
|
|
100
103
|
* or re-export the app reached it through — so `type Counter = Reactive<number>` is one, and an
|
|
101
104
|
* app's own type that happens to be spelled `Reactive` is not. `undefined` where the project never
|
|
102
|
-
* reaches abide, and then nothing in it is one. `props`
|
|
105
|
+
* reaches abide, and then nothing in it is one. `props` and `onConfig` are abide's files as the
|
|
106
|
+
* checker names them.
|
|
103
107
|
*/
|
|
104
108
|
export type Abide = {
|
|
105
109
|
/** 23.9 — abide's `route`, as the file the checker names it. */
|
|
@@ -110,14 +114,17 @@ export type Abide = {
|
|
|
110
114
|
*/
|
|
111
115
|
selection: CheckedSymbol | undefined
|
|
112
116
|
props: string | undefined
|
|
117
|
+
/** 27.13 — abide's `onConfig`, as the file the checker names it. */
|
|
118
|
+
onConfig: string | undefined
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
export async function abideIn(project: Project): Promise<Abide> {
|
|
116
|
-
const [declares, props, selects, route] = await Promise.all([
|
|
122
|
+
const [declares, props, selects, route, hooks] = await Promise.all([
|
|
117
123
|
project.program.getSourceFile(REACTIVE_SOURCE),
|
|
118
124
|
project.program.getSourceFile(PROPS_SOURCE),
|
|
119
125
|
project.program.getSourceFile(SELECTION_SOURCE),
|
|
120
126
|
project.program.getSourceFile(ROUTE_SOURCE),
|
|
127
|
+
project.program.getSourceFile(HOOKS_SOURCE),
|
|
121
128
|
])
|
|
122
129
|
let selection: CheckedSymbol | undefined
|
|
123
130
|
for (const statement of selects?.statements ?? [])
|
|
@@ -142,7 +149,13 @@ export async function abideIn(project: Project): Promise<Abide> {
|
|
|
142
149
|
reactive = await literal?.getSymbol()
|
|
143
150
|
break
|
|
144
151
|
}
|
|
145
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
route: route?.path,
|
|
154
|
+
reactive,
|
|
155
|
+
selection,
|
|
156
|
+
props: props?.path,
|
|
157
|
+
onConfig: hooks?.path,
|
|
158
|
+
}
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
/**
|
|
@@ -13,6 +13,8 @@ export type ParsedSource = {
|
|
|
13
13
|
text: string
|
|
14
14
|
file: AbideFile
|
|
15
15
|
quiet: boolean
|
|
16
|
+
/** 23.19 — where a mounted package's file stands in the app; `undefined` for one at its own path. */
|
|
17
|
+
position: string | undefined
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/** One `.abide` as the emitters and the checked lowering take it: parsed, and answered. */
|
|
@@ -25,7 +27,12 @@ export type ReadSource = ParsedSource & { answer: CheckerAnswer }
|
|
|
25
27
|
* parsed around it — a quiet one's is nobody's. Without it the refusal is thrown, as a build stops.
|
|
26
28
|
*/
|
|
27
29
|
export function parseSources(
|
|
28
|
-
sources: readonly {
|
|
30
|
+
sources: readonly {
|
|
31
|
+
path: string
|
|
32
|
+
text: string
|
|
33
|
+
quiet?: boolean
|
|
34
|
+
position?: string
|
|
35
|
+
}[],
|
|
29
36
|
refused?: Diagnosis[],
|
|
30
37
|
): ParsedSource[] {
|
|
31
38
|
const parsed: ParsedSource[] = []
|
|
@@ -39,7 +46,13 @@ export function parseSources(
|
|
|
39
46
|
if (!quiet) refused.push(refusal(source.path, thrown))
|
|
40
47
|
continue
|
|
41
48
|
}
|
|
42
|
-
parsed.push({
|
|
49
|
+
parsed.push({
|
|
50
|
+
path: source.path,
|
|
51
|
+
text: source.text,
|
|
52
|
+
file,
|
|
53
|
+
quiet,
|
|
54
|
+
position: source.position,
|
|
55
|
+
})
|
|
43
56
|
}
|
|
44
57
|
return parsed
|
|
45
58
|
}
|
package/src/compiler/readsIn.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isCallExpression,
|
|
9
9
|
isClassDeclaration,
|
|
10
10
|
isEnumDeclaration,
|
|
11
|
+
isExpressionStatement,
|
|
11
12
|
isFunctionDeclaration,
|
|
12
13
|
isFunctionExpression,
|
|
13
14
|
isIdentifier,
|
|
@@ -66,8 +67,9 @@ import { trackingWarnings } from './trackingWarnings.ts'
|
|
|
66
67
|
export type ShadowSpans = {
|
|
67
68
|
/** Where the shadow's scripts end and its template begins. */
|
|
68
69
|
scriptsEnd: number
|
|
69
|
-
/** The `<script module>`
|
|
70
|
+
/** The `<script module>`, `<script server>` and `<script>` bodies, in the shadow. */
|
|
70
71
|
modules: Span
|
|
72
|
+
servers: Span
|
|
71
73
|
scripts: Span
|
|
72
74
|
positions: PositionTable
|
|
73
75
|
}
|
|
@@ -123,7 +125,8 @@ export async function readsIn(
|
|
|
123
125
|
sourceFile: SourceFile,
|
|
124
126
|
shadowPath: string,
|
|
125
127
|
spans: ShadowSpans,
|
|
126
|
-
|
|
128
|
+
// The file below the app's root, which its route is read off (23.14).
|
|
129
|
+
routedPath: string,
|
|
127
130
|
file: AbideFile,
|
|
128
131
|
): Promise<{ answer: CheckerAnswer; warnings: Finding[] }> {
|
|
129
132
|
// A destructuring pattern binds names this does not see, and that is 31.12's case.
|
|
@@ -185,7 +188,14 @@ export async function readsIn(
|
|
|
185
188
|
// Awaited after the round trips below, so a 33.14 refusal would reject with nothing
|
|
186
189
|
// listening and be reported unhandled; `await props` still throws it.
|
|
187
190
|
props.catch(() => {})
|
|
188
|
-
const params = routeReads(checker, abide, sourceFile,
|
|
191
|
+
const params = routeReads(checker, abide, sourceFile, routedPath, reads)
|
|
192
|
+
const configs = configReads(
|
|
193
|
+
checker,
|
|
194
|
+
abide,
|
|
195
|
+
sourceFile,
|
|
196
|
+
spans.servers,
|
|
197
|
+
reads,
|
|
198
|
+
)
|
|
189
199
|
// No name is no round trip to ask about none.
|
|
190
200
|
const symbols =
|
|
191
201
|
names.length === 0 ? [] : await checker.getSymbolAtLocation(names)
|
|
@@ -249,6 +259,7 @@ export async function readsIn(
|
|
|
249
259
|
await Promise.all(holding)
|
|
250
260
|
await props
|
|
251
261
|
await params
|
|
262
|
+
await configs
|
|
252
263
|
const statics = new Set<number>()
|
|
253
264
|
const expressions = spans.positions.nodesIn(
|
|
254
265
|
sourceFile,
|
|
@@ -271,7 +282,7 @@ export async function readsIn(
|
|
|
271
282
|
await proveSlots(checker, expressions, reads, statics)
|
|
272
283
|
reads.sort(byPosition)
|
|
273
284
|
const warnings = await warningsFor(checker, located, spans.positions)
|
|
274
|
-
// 34.
|
|
285
|
+
// 34.18.
|
|
275
286
|
for (const warning of await sharedModuleWrites(
|
|
276
287
|
checker,
|
|
277
288
|
shadowPath,
|
|
@@ -683,7 +694,7 @@ const ROUTE_MEMBERS = new Set(['url', 'params', 'name', 'navigating'])
|
|
|
683
694
|
* a name, and this read is spelled as a member.
|
|
684
695
|
*
|
|
685
696
|
* 23.9 — AND `route.params` IN A PAGE OR A LAYOUT IS WHAT THAT FILE'S OWN ROUTE PROMISES. The file
|
|
686
|
-
* is the route: `
|
|
697
|
+
* is the route: `media/[id].abide` only ever renders where `id` matched, so its
|
|
687
698
|
* `route.params` has an `id`, and a layout's has its own segments and whatever a page below adds.
|
|
688
699
|
* Only the checker reads that — the running module reads the one `route` every page shares.
|
|
689
700
|
*/
|
|
@@ -691,11 +702,11 @@ async function routeReads(
|
|
|
691
702
|
checker: Checker,
|
|
692
703
|
abide: Abide,
|
|
693
704
|
sourceFile: Node,
|
|
694
|
-
|
|
705
|
+
routedPath: string,
|
|
695
706
|
reads: Read[],
|
|
696
707
|
): Promise<void> {
|
|
697
708
|
if (abide.route === undefined) return
|
|
698
|
-
const routed = fileKind(
|
|
709
|
+
const routed = fileKind(routedPath)
|
|
699
710
|
const kind = routed === 'page' || routed === 'layout' ? routed : undefined
|
|
700
711
|
const accesses: Node[] = []
|
|
701
712
|
const owners: Node[] = []
|
|
@@ -714,7 +725,7 @@ async function routeReads(
|
|
|
714
725
|
sourceFile.forEachChild(visit)
|
|
715
726
|
if (owners.length === 0) return
|
|
716
727
|
const symbols = await checker.getSymbolAtLocation(owners)
|
|
717
|
-
const pattern = routeOf(
|
|
728
|
+
const pattern = routeOf(routedPath)
|
|
718
729
|
for (let at = 0; at < accesses.length; at += 1) {
|
|
719
730
|
const symbol = symbols[at]
|
|
720
731
|
if (symbol === undefined) continue
|
|
@@ -777,6 +788,49 @@ function refuseDeclared(
|
|
|
777
788
|
)
|
|
778
789
|
}
|
|
779
790
|
|
|
791
|
+
/**
|
|
792
|
+
* 27.13 — each `onConfig(…)` standing as a statement at the top of a `<script server>`, as a read of
|
|
793
|
+
* its callee, pushed onto `reads`.
|
|
794
|
+
*
|
|
795
|
+
* The callee says which are abide's, as `props`'s does. A call anywhere else — inside a function,
|
|
796
|
+
* or handing its unfiling back to a name — is filed all the same, and declares nothing.
|
|
797
|
+
*/
|
|
798
|
+
async function configReads(
|
|
799
|
+
checker: Checker,
|
|
800
|
+
abide: Abide,
|
|
801
|
+
sourceFile: Node & { statements: readonly Node[] },
|
|
802
|
+
servers: Span,
|
|
803
|
+
reads: Read[],
|
|
804
|
+
): Promise<void> {
|
|
805
|
+
if (abide.onConfig === undefined) return
|
|
806
|
+
const callees: Node[] = []
|
|
807
|
+
for (const statement of sourceFile.statements) {
|
|
808
|
+
const start = statement.getStart()
|
|
809
|
+
if (start < servers.start) continue
|
|
810
|
+
if (start >= servers.end) break
|
|
811
|
+
if (
|
|
812
|
+
isExpressionStatement(statement) &&
|
|
813
|
+
isCallExpression(statement.expression) &&
|
|
814
|
+
isIdentifier(statement.expression.expression)
|
|
815
|
+
)
|
|
816
|
+
callees.push(statement.expression.expression)
|
|
817
|
+
}
|
|
818
|
+
if (callees.length === 0) return
|
|
819
|
+
const named = await checker.getSymbolAtLocation(callees)
|
|
820
|
+
for (let at = 0; at < callees.length; at += 1) {
|
|
821
|
+
const symbol = named[at]
|
|
822
|
+
if (symbol === undefined) continue
|
|
823
|
+
const callee = await aliasedSymbol(checker, symbol)
|
|
824
|
+
if (
|
|
825
|
+
callee.name !== 'onConfig' ||
|
|
826
|
+
callee.valueDeclaration?.path !== abide.onConfig
|
|
827
|
+
)
|
|
828
|
+
continue
|
|
829
|
+
const node = callees[at] as Node
|
|
830
|
+
reads.push(newRead(node.getStart(), node.end, 'config'))
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
780
834
|
/**
|
|
781
835
|
* The reads a `props<Props>()` destructuring contributes, pushed onto `reads`.
|
|
782
836
|
*
|
|
@@ -108,7 +108,7 @@ export const HEADS = '__heads'
|
|
|
108
108
|
* EVERY MARKUP POSITION THAT EVALUATES AN AUTHOR EXPRESSION, rather than the two that were found
|
|
109
109
|
* first. A GUARD HERE IS HALF A MECHANISM — it trades a throw for a HOLE, and something on the
|
|
110
110
|
* client has to fill that hole once the value lands: a text slot has `runHere` and `textHere`, a
|
|
111
|
-
* `{#for}` has `List.hydrate`'s count check, an attribute has its own binding
|
|
111
|
+
* `{#for}` has `List.hydrate`'s count check, an attribute has its own binding rerunning on the
|
|
112
112
|
* value that landed (32.3), and the `{#if}` chain has `isEmptyRegion` — which is why that one is
|
|
113
113
|
* guarded at the REGION rather than per subject, and says so at its own arm. The ESCAPER STAYS
|
|
114
114
|
* OUTSIDE: absence is `undefined` here and what it means is already decided where it lands (see
|
|
@@ -296,7 +296,7 @@ function renderPosition(
|
|
|
296
296
|
`abide: a ${at.kind} position holds a ${node.kind} node.`,
|
|
297
297
|
)
|
|
298
298
|
|
|
299
|
-
// 32.17 — A
|
|
299
|
+
// 32.17 — A CAPITALIZED TAG IS AN INVOCATION, and the capital is the whole of the
|
|
300
300
|
// distinction. It is emitted as a CALL, so the child's own module renders it.
|
|
301
301
|
if (isComponentTag(node.tag)) return invocationMarkup(node, at, lowering)
|
|
302
302
|
|
|
@@ -536,7 +536,7 @@ function serverComponent(
|
|
|
536
536
|
// standing side by side are two instances, each creating what no ancestor holds (10.6).
|
|
537
537
|
const { opened, closed } = shareScope(lowering, '')
|
|
538
538
|
return `function ${component.name}(${PROPS} = {}) {
|
|
539
|
-
${opened}${head}${setup}${definitions(sink, lowering)}return { head: '', body: ${renderMarkup(sink, lowering, SERVER_LANE)} }${closed}
|
|
539
|
+
${opened}${head}${setup}${definitions(sink, lowering)}return { head: '', body: ${renderMarkup(sink, lowering, SERVER_LANE)}, document: undefined }${closed}
|
|
540
540
|
}`
|
|
541
541
|
}
|
|
542
542
|
|