@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
|
@@ -26,7 +26,7 @@ import { decodeCharacterReferences } from './decodeCharacterReferences.ts'
|
|
|
26
26
|
import { moduleImports } from './emitComponent.ts'
|
|
27
27
|
import { invocationProps, passedName } from './invocationProps.ts'
|
|
28
28
|
import { isNamedAlone } from './isNamedAlone.ts'
|
|
29
|
-
import { Lowering, PROPS } from './Lowering.ts'
|
|
29
|
+
import { type LoweredChunk, Lowering, PROPS } from './Lowering.ts'
|
|
30
30
|
import type { AbideFile } from './parseAbide.ts'
|
|
31
31
|
import {
|
|
32
32
|
CLIENT_LANE,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
type ValueRender,
|
|
40
40
|
} from './renderMarkup.ts'
|
|
41
41
|
import { SHARED_NAMES } from './SHARED_NAMES.ts'
|
|
42
|
-
import { RUNTIME_SPECIFIER, SHARED_SPECIFIER } from '
|
|
42
|
+
import { RUNTIME_SPECIFIER, SHARED_SPECIFIER } from '#shared/SPECIFIERS.ts'
|
|
43
43
|
import {
|
|
44
44
|
insideShare,
|
|
45
45
|
resumedShare,
|
|
@@ -143,11 +143,11 @@ export function emitClient(
|
|
|
143
143
|
file: AbideFile,
|
|
144
144
|
answer: CheckerAnswer = UNCHECKED,
|
|
145
145
|
selfFill: SelfFill = 'none',
|
|
146
|
-
/**
|
|
147
|
-
|
|
146
|
+
/** 38.97 — where given, every chunk the module is written around (`sourceMapOf`). */
|
|
147
|
+
chunks?: LoweredChunk[],
|
|
148
148
|
): string {
|
|
149
149
|
const carriesOwnMarkup = selfFill !== 'none'
|
|
150
|
-
const lowering = new Lowering(file, answer)
|
|
150
|
+
const lowering = new Lowering(file, answer, false, chunks)
|
|
151
151
|
// THE TEMPLATE IS PLACED ONCE (`placeTemplate`) and everything this module emits is rendered
|
|
152
152
|
// from what was placed: the mount's walk, a region's build, and the markup a region made once
|
|
153
153
|
// fills from — the same positions the server's markup is rendered from.
|
|
@@ -163,11 +163,25 @@ export function emitClient(
|
|
|
163
163
|
page,
|
|
164
164
|
new RegionEmission(lowering),
|
|
165
165
|
)
|
|
166
|
+
// 35.17 — THE DOCUMENT'S OWN `<html>` AND `<body>`, bound as any element is: the server wrote
|
|
167
|
+
// the values standing at render, and these keep writing them. Nothing to find, so no walk.
|
|
168
|
+
if (file.document !== undefined)
|
|
169
|
+
for (const [element, node, slot] of [
|
|
170
|
+
['document.documentElement', file.document.html, 'html'],
|
|
171
|
+
['document.body', file.document.body, 'body'],
|
|
172
|
+
] as const) {
|
|
173
|
+
const classified = classifyAttributes(node, lowering)
|
|
174
|
+
if (!classified.some(isBound)) continue
|
|
175
|
+
const units = elementUnits(element, classified, slot, lowering)
|
|
176
|
+
for (const unit of units.handlers) lines.push(unit)
|
|
177
|
+
for (const unit of units.attributes) lines.push(unit)
|
|
178
|
+
for (const unit of units.elements) lines.push(unit)
|
|
179
|
+
}
|
|
166
180
|
|
|
167
181
|
// 34.2 — A `<script module>` IS PROCESS-WIDE IN A BROWSER, which is MODULE SCOPE: it is
|
|
168
182
|
// evaluated once when this module is first imported rather than once per `mount`, so a second
|
|
169
183
|
// mount of the same page reads what the first one left.
|
|
170
|
-
const moduleSetup = splitImports(lowering.modules()
|
|
184
|
+
const moduleSetup = splitImports(lowering.modules())
|
|
171
185
|
// 32.17 — A COMPONENT FILLS ITS OWN REGION where the server wrote none. A row the list grew by
|
|
172
186
|
// is built in the browser, and the child's markup is in the child's SERVER module — which the
|
|
173
187
|
// browser may not reach. So the caller writes the anchor pair alone and the child fills it
|
|
@@ -210,14 +224,25 @@ export function emitClient(
|
|
|
210
224
|
lines.push(
|
|
211
225
|
`if (${ROUTED}) ${lowering.use('liveHead')}(${SCOPE}, ${ROUTED}.head)`,
|
|
212
226
|
)
|
|
227
|
+
// 39.1, 39.6 — A COMPONENT'S HEAD IS ITS PAGE'S, on a navigation as on the server: added to the
|
|
228
|
+
// level the router is mounting (`contributeHead`), which a row grown later is not.
|
|
229
|
+
const ownHead = routed ? undefined : headSink(file)
|
|
230
|
+
const ownMarkup =
|
|
231
|
+
ownHead === undefined
|
|
232
|
+
? "''"
|
|
233
|
+
: renderMarkup(ownHead, lowering, CLIENT_LANE)
|
|
234
|
+
const contributed =
|
|
235
|
+
ownMarkup === "''"
|
|
236
|
+
? ''
|
|
237
|
+
: `${lowering.use('contributeHead')}(${REGION_ROOT}, () => ${ownMarkup})\n`
|
|
213
238
|
const builds = carriesOwnMarkup && page.positions.length > 0
|
|
214
|
-
let fills =
|
|
239
|
+
let fills = contributed
|
|
215
240
|
if (applied !== '' || builds) {
|
|
216
241
|
const build = builds
|
|
217
242
|
? `function __build(__document) {\n ${emitMaker(page, lowering)}\n}\n`
|
|
218
243
|
: ''
|
|
219
244
|
// THE HEAD GOES BEFORE THE FILL, so the guard is written out where there is one to apply.
|
|
220
|
-
fills
|
|
245
|
+
fills +=
|
|
221
246
|
applied === ''
|
|
222
247
|
? `${registered}${build}${lowering.use('fillEmpty')}(${REGION_ROOT}, __build)\n`
|
|
223
248
|
: `${registered}${build}if (${lowering.use('isEmptyRegion')}(${REGION_ROOT})) { ${applied}${builds ? `${lowering.use('fillRegion')}(${REGION_ROOT}, __build)` : ''} }\n`
|
|
@@ -236,12 +261,19 @@ export function emitClient(
|
|
|
236
261
|
// A FILE MOUNTED AGAINST AN EMPTY REGION SHIPS A MODULE REGARDLESS: the router or the caller's
|
|
237
262
|
// client module imports it by name, so the no-op is what it ships.
|
|
238
263
|
//
|
|
239
|
-
// 41.6 — BUT A `<script>` IS SOMETHING TO DO: hydration
|
|
264
|
+
// 41.6 — BUT A `<script>` IS SOMETHING TO DO: hydration reruns every setup, and a static
|
|
240
265
|
// component's can still open a watch, write a shared value or listen on the window.
|
|
241
266
|
const live = lines.length > 0
|
|
242
|
-
const setup = splitImports(lowering.scripts()
|
|
243
|
-
if (!live && fills === '' && setup.body === '')
|
|
244
|
-
|
|
267
|
+
const setup = splitImports(lowering.scripts())
|
|
268
|
+
if (!live && fills === '' && setup.body === '') {
|
|
269
|
+
const mount = carriesOwnMarkup ? 'export default () => {}\n' : ''
|
|
270
|
+
// 34.2 — A `<script module>` RUNS IN THE BROWSER TOO, live markup or none: its module scope
|
|
271
|
+
// is the browser's as much as the server's.
|
|
272
|
+
if (moduleSetup.body === '' && moduleSetup.imports === '') return mount
|
|
273
|
+
return `${moduleImports(lowering, (name) => (SHARED_NAMES.has(name) ? SHARED_SPECIFIER : RUNTIME_SPECIFIER), [moduleSetup])}
|
|
274
|
+
${moduleSetup.body}
|
|
275
|
+
${mount}`
|
|
276
|
+
}
|
|
245
277
|
|
|
246
278
|
// A SCOPE IS WHAT THERE IS TO STOP, so a module with nothing live opens none. A static child
|
|
247
279
|
// still ships a module — it fills the anchors of a row the browser grew — and making it
|
|
@@ -606,64 +638,15 @@ ${SCOPE}.watch(() => {
|
|
|
606
638
|
)
|
|
607
639
|
continue
|
|
608
640
|
}
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
const
|
|
617
|
-
for (const
|
|
618
|
-
if (!isBound(one)) continue
|
|
619
|
-
if (one.kind === 'handler')
|
|
620
|
-
handlers.push(
|
|
621
|
-
`${element}.addEventListener(${JSON.stringify(one.event)}, ${lowering.expression(one.attribute)})`,
|
|
622
|
-
)
|
|
623
|
-
// 32.5, 32.7, 32.8 — THE PROPERTY AND THE VALUE, IN STEP BOTH WAYS: a watch on this
|
|
624
|
-
// region's scope writes the property, and a listener writes it back.
|
|
625
|
-
else if (one.kind === 'bound')
|
|
626
|
-
binds.push(
|
|
627
|
-
`${lowering.use('bindProperty')}(${element}, ${JSON.stringify(one.bind)}, ${lowering.reference(one.attribute)}, ${SCOPE})`,
|
|
628
|
-
)
|
|
629
|
-
// 32.11 — THE SPREAD, KEPT WRITTEN: an attribute it stops carrying is removed.
|
|
630
|
-
else if (one.kind === 'spread')
|
|
631
|
-
spreads.push(
|
|
632
|
-
`${lowering.use('bindSpread')}(${element}, () => (${lowering.expression(one.attribute)}), ${JSON.stringify(one.explicit)}, ${SCOPE})`,
|
|
633
|
-
)
|
|
634
|
-
// 32.9 — THE NODE, HANDED OVER, once. Not a watch: what the value takes is the element,
|
|
635
|
-
// which is the same node on every run a watch would have made — and the callback
|
|
636
|
-
// form's `Disposer` goes on this region's scope, so a widget it set up is torn down
|
|
637
|
-
// when the nodes it was set up over leave.
|
|
638
|
-
else if (one.kind === 'element')
|
|
639
|
-
elements.push(
|
|
640
|
-
`const __r${slot} = ${lowering.use('bindElement')}(${element}, ${lowering.reference(one.attribute)})
|
|
641
|
-
if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
|
|
642
|
-
)
|
|
643
|
-
// 32.20 IS THE INTERPOLATION'S CLAUSE ON THIS SIDE TOO, and 32.37's absence with it:
|
|
644
|
-
// `setAttribute` answers through `attributeText`, as the server's markup did, so a
|
|
645
|
-
// `javascript:` the server refused is refused the moment the value moves too.
|
|
646
|
-
// A WATCH PER ATTRIBUTE, not one over the group: two attributes reading two
|
|
647
|
-
// different values would otherwise each wake for the other's write.
|
|
648
|
-
else {
|
|
649
|
-
const { write, name } =
|
|
650
|
-
one.kind === 'merged' && one.toggles !== undefined
|
|
651
|
-
? one.toggles
|
|
652
|
-
: {
|
|
653
|
-
write: 'attribute' as const,
|
|
654
|
-
name: one.attribute.name,
|
|
655
|
-
}
|
|
656
|
-
writes.push(
|
|
657
|
-
`${SCOPE}.watch(() => ${lowering.use(WRITES[write])}(__a${slot}, ${JSON.stringify(name)}, ${lowering.expression(one.attribute)}))`,
|
|
658
|
-
)
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
if (writes.length > 0)
|
|
662
|
-
attributes.push(
|
|
663
|
-
`const __a${slot} = ${element}\n${writes.join('\n')}`,
|
|
664
|
-
)
|
|
665
|
-
for (const unit of binds) attributes.push(unit)
|
|
666
|
-
for (const unit of spreads) attributes.push(unit)
|
|
641
|
+
const units = elementUnits(
|
|
642
|
+
`${lowering.use('elementHere')}(${at.name}, ${JSON.stringify(at.tag)})`,
|
|
643
|
+
bound.get(at) as ClassifiedAttribute[],
|
|
644
|
+
slot,
|
|
645
|
+
lowering,
|
|
646
|
+
)
|
|
647
|
+
for (const unit of units.handlers) handlers.push(unit)
|
|
648
|
+
for (const unit of units.attributes) attributes.push(unit)
|
|
649
|
+
for (const unit of units.elements) elements.push(unit)
|
|
667
650
|
}
|
|
668
651
|
for (const unit of texts) lines.push(unit)
|
|
669
652
|
for (const unit of handlers) lines.push(unit)
|
|
@@ -688,6 +671,81 @@ if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
|
|
|
688
671
|
return region
|
|
689
672
|
}
|
|
690
673
|
|
|
674
|
+
/**
|
|
675
|
+
* One bound element's units — its listeners, its attribute bindings and the node it hands over —
|
|
676
|
+
* against the element `element` evaluates to.
|
|
677
|
+
*
|
|
678
|
+
* 32.3 — THE ATTRIBUTE BINDING. The server wrote the value standing at render; this is what keeps
|
|
679
|
+
* writing it. ONCE PER ELEMENT, NOT PER ATTRIBUTE: `<a href={u} title={t}>` is one element and two
|
|
680
|
+
* bindings, and a refusal loses exactly what it should either way, because every binding in the
|
|
681
|
+
* group needs the element the check refused. The document's own `<html>` and `<body>` are bound
|
|
682
|
+
* through here too (35.17), an element being whatever `element` reaches.
|
|
683
|
+
*/
|
|
684
|
+
function elementUnits(
|
|
685
|
+
element: string,
|
|
686
|
+
classified: ClassifiedAttribute[],
|
|
687
|
+
slot: string,
|
|
688
|
+
lowering: Lowering,
|
|
689
|
+
): { handlers: string[]; attributes: string[]; elements: string[] } {
|
|
690
|
+
const handlers: string[] = []
|
|
691
|
+
const attributes: string[] = []
|
|
692
|
+
const elements: string[] = []
|
|
693
|
+
const writes: string[] = []
|
|
694
|
+
const binds: string[] = []
|
|
695
|
+
const spreads: string[] = []
|
|
696
|
+
for (const one of classified) {
|
|
697
|
+
if (!isBound(one)) continue
|
|
698
|
+
if (one.kind === 'handler')
|
|
699
|
+
handlers.push(
|
|
700
|
+
`${element}.addEventListener(${JSON.stringify(one.event)}, ${lowering.expression(one.attribute)})`,
|
|
701
|
+
)
|
|
702
|
+
// 32.5, 32.7, 32.8 — THE PROPERTY AND THE VALUE, IN STEP BOTH WAYS: a watch on this
|
|
703
|
+
// region's scope writes the property, and a listener writes it back.
|
|
704
|
+
else if (one.kind === 'bound')
|
|
705
|
+
binds.push(
|
|
706
|
+
`${lowering.use('bindProperty')}(${element}, ${JSON.stringify(one.bind)}, ${lowering.reference(one.attribute)}, ${SCOPE})`,
|
|
707
|
+
)
|
|
708
|
+
// 32.11 — THE SPREAD, KEPT WRITTEN: an attribute it stops carrying is removed.
|
|
709
|
+
else if (one.kind === 'spread')
|
|
710
|
+
spreads.push(
|
|
711
|
+
`${lowering.use('bindSpread')}(${element}, () => (${lowering.expression(one.attribute)}), ${JSON.stringify(one.explicit)}, ${SCOPE})`,
|
|
712
|
+
)
|
|
713
|
+
// 32.9 — THE NODE, HANDED OVER, once. Not a watch: what the value takes is the element,
|
|
714
|
+
// which is the same node on every run a watch would have made — and the callback
|
|
715
|
+
// form's `Disposer` goes on this region's scope, so a widget it set up is torn down
|
|
716
|
+
// when the nodes it was set up over leave.
|
|
717
|
+
else if (one.kind === 'element')
|
|
718
|
+
elements.push(
|
|
719
|
+
`const __r${slot} = ${lowering.use('bindElement')}(${element}, ${lowering.reference(one.attribute)})
|
|
720
|
+
if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
|
|
721
|
+
)
|
|
722
|
+
// 32.20 IS THE INTERPOLATION'S CLAUSE ON THIS SIDE TOO, and 32.37's absence with it:
|
|
723
|
+
// `setAttribute` answers through `attributeText`, as the server's markup did, so a
|
|
724
|
+
// `javascript:` the server refused is refused the moment the value moves too.
|
|
725
|
+
// A WATCH PER ATTRIBUTE, not one over the group: two attributes reading two
|
|
726
|
+
// different values would otherwise each wake for the other's write.
|
|
727
|
+
else {
|
|
728
|
+
const { write, name } =
|
|
729
|
+
one.kind === 'merged' && one.toggles !== undefined
|
|
730
|
+
? one.toggles
|
|
731
|
+
: {
|
|
732
|
+
write: 'attribute' as const,
|
|
733
|
+
name: one.attribute.name,
|
|
734
|
+
}
|
|
735
|
+
writes.push(
|
|
736
|
+
`${SCOPE}.watch(() => ${lowering.use(WRITES[write])}(__a${slot}, ${JSON.stringify(name)}, ${lowering.expression(one.attribute)}))`,
|
|
737
|
+
)
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (writes.length > 0)
|
|
741
|
+
attributes.push(
|
|
742
|
+
`const __a${slot} = ${element}\n${writes.join('\n')}`,
|
|
743
|
+
)
|
|
744
|
+
for (const unit of binds) attributes.push(unit)
|
|
745
|
+
for (const unit of spreads) attributes.push(unit)
|
|
746
|
+
return { handlers, attributes, elements }
|
|
747
|
+
}
|
|
748
|
+
|
|
691
749
|
// A region's bindings go through its SCOPE, so what replaced the region can stop them (see
|
|
692
750
|
// `Scope`). In the framework's own `__` namespace because a row binds the AUTHOR's item name as a
|
|
693
751
|
// parameter in the same scope.
|
|
@@ -934,6 +992,12 @@ function emitFillable(
|
|
|
934
992
|
name: string,
|
|
935
993
|
sink: Cursor,
|
|
936
994
|
region: RegionEmission,
|
|
995
|
+
/**
|
|
996
|
+
* 10.5 — CHILDREN ARE MOUNTED BY THE CHILD, so they are its descendants: the scope current when
|
|
997
|
+
* it mounts them is the one their components find, as it is the one the server built them in.
|
|
998
|
+
* The caller's own is the fallback for a mount with none current.
|
|
999
|
+
*/
|
|
1000
|
+
children = false,
|
|
937
1001
|
): void {
|
|
938
1002
|
const lowering = region.lowering
|
|
939
1003
|
const filled = emitRegion(sink, region.nested())
|
|
@@ -948,6 +1012,10 @@ function emitFillable(
|
|
|
948
1012
|
`${lowering.use('fillRegion')}(${REGION_ROOT}, ${name}_build)`,
|
|
949
1013
|
lowering,
|
|
950
1014
|
sink.defersSetup,
|
|
1015
|
+
false,
|
|
1016
|
+
children
|
|
1017
|
+
? `(${lowering.use('currentShare')}() ?? ${SHARE_AT})`
|
|
1018
|
+
: SHARE_AT,
|
|
951
1019
|
),
|
|
952
1020
|
)
|
|
953
1021
|
}
|
|
@@ -972,6 +1040,8 @@ function mountFunction(
|
|
|
972
1040
|
* mount binds into it. A refusal stops only what the mount opened, past `__mark`.
|
|
973
1041
|
*/
|
|
974
1042
|
declared = false,
|
|
1043
|
+
/** The share scope a `resumes` mount puts back. */
|
|
1044
|
+
resumed = SHARE_AT,
|
|
975
1045
|
): string {
|
|
976
1046
|
// A BODY THAT BINDS NOTHING HAS NOTHING TO STOP AND NOTHING TO REFUSE, so it opens no scope
|
|
977
1047
|
// and no `try`: every mount of it answers the one shared empty scope. A static row's two
|
|
@@ -995,7 +1065,7 @@ function mountFunction(
|
|
|
995
1065
|
].join('\n')
|
|
996
1066
|
// 10.5 — the instance's own scope, put back for a mount that happens later than its caller's.
|
|
997
1067
|
// Only where a component stands below this region: nothing else can read it.
|
|
998
|
-
return `const ${name}_mount = (${parameters}, __rebuilt) => {\n${resumes ? insideShare(lowering,
|
|
1068
|
+
return `const ${name}_mount = (${parameters}, __rebuilt) => {\n${resumes ? insideShare(lowering, resumed, mount) : mount}\n}`
|
|
999
1069
|
}
|
|
1000
1070
|
|
|
1001
1071
|
/**
|
|
@@ -1023,7 +1093,7 @@ function emitInvocation(
|
|
|
1023
1093
|
const children = at.inner[0]
|
|
1024
1094
|
if (children !== undefined) {
|
|
1025
1095
|
const kids = region.claim('kd')
|
|
1026
|
-
emitFillable(kids, children, region)
|
|
1096
|
+
emitFillable(kids, children, region, true)
|
|
1027
1097
|
props.push(
|
|
1028
1098
|
`${JSON.stringify(CHILDREN)}: () => ({ build: ${kids}_build, mount: ${kids}_mount })`,
|
|
1029
1099
|
)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
|
|
1
2
|
import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
|
|
2
3
|
import { Cursor, placeTemplate } from './Cursor.ts'
|
|
3
|
-
import { Lowering, PROPS } from './Lowering.ts'
|
|
4
|
+
import { type LoweredChunk, Lowering, PROPS } from './Lowering.ts'
|
|
4
5
|
import type { AbideFile } from './parseAbide.ts'
|
|
6
|
+
import { privateExports } from './projectShadow.ts'
|
|
5
7
|
import {
|
|
6
8
|
concat,
|
|
7
9
|
definitions,
|
|
@@ -11,9 +13,9 @@ import {
|
|
|
11
13
|
renderMarkup,
|
|
12
14
|
SERVER_LANE,
|
|
13
15
|
} from './renderMarkup.ts'
|
|
14
|
-
import { SERVER_SPECIFIER } from './SPECIFIERS.ts'
|
|
15
16
|
import { insideShare, SETTLED_AT, shareScope } from './shareScope.ts'
|
|
16
17
|
import { splitImports, withoutProps } from './splitImports.ts'
|
|
18
|
+
import type { TemplateNode } from './TemplateNode.ts'
|
|
17
19
|
|
|
18
20
|
// The name the body is bound to while a component's head contributions land (`HEADS`).
|
|
19
21
|
const BODY = '__body'
|
|
@@ -42,12 +44,14 @@ export function emitComponent(
|
|
|
42
44
|
file: AbideFile,
|
|
43
45
|
answer: CheckerAnswer = UNCHECKED,
|
|
44
46
|
settles = false,
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
+
/** The route a `<script server>` files its hooks at (37.1). */
|
|
48
|
+
route = '/',
|
|
49
|
+
/** 38.97 — where given, every chunk the module is written around (`sourceMapOf`). */
|
|
50
|
+
chunks?: LoweredChunk[],
|
|
47
51
|
): string {
|
|
48
52
|
// Only what the page actually uses. An emitted module carrying an import nothing calls is a
|
|
49
53
|
// name the app's own tooling reports as unused, in a file nobody wrote.
|
|
50
|
-
const lowering = new Lowering(file, answer)
|
|
54
|
+
const lowering = new Lowering(file, answer, false, chunks)
|
|
51
55
|
|
|
52
56
|
const page = new Cursor()
|
|
53
57
|
page.components = file.components
|
|
@@ -62,15 +66,31 @@ export function emitComponent(
|
|
|
62
66
|
// `Lowering.used` (`lowering.use`), so emitting a contribution the merge then drops leaves the
|
|
63
67
|
// module importing a name nothing calls. `headSink` is the client lane's too, which is what keeps the two from drifting.
|
|
64
68
|
const head = renderMarkup(headSink(file), lowering, SERVER_LANE)
|
|
69
|
+
// 35.15, 35.17 — THE DOCUMENT'S OWN TWO ELEMENTS, rendered as any element is and empty, so
|
|
70
|
+
// `render` writes their attributes onto the document's and every live one is read per request.
|
|
71
|
+
const document =
|
|
72
|
+
file.document === undefined
|
|
73
|
+
? 'undefined'
|
|
74
|
+
: `{ html: ${renderMarkup(placed([file.document.html]), lowering, SERVER_LANE)}, body: ${renderMarkup(placed([file.document.body]), lowering, SERVER_LANE)} }`
|
|
65
75
|
|
|
66
76
|
// 31.1 and 31.4 hold INSIDE a `.abide` file, not just in its template, so the script block is
|
|
67
77
|
// lowered with the same rewrite the slots get.
|
|
68
78
|
//
|
|
69
|
-
// 34.2 — A `<script module>` IS MODULE SCOPE, here as in the browser
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
// 34.2 — A `<script module>` IS MODULE SCOPE, here as in the browser. What must stay per request
|
|
80
|
+
// is a `reactive`, which is by 11.32 wherever it is declared.
|
|
81
|
+
const moduleSetup = splitImports(lowering.modules())
|
|
82
|
+
// 34.19 — A `<script server>` IS MODULE SCOPE ON THIS SIDE ALONE, run between two calls that
|
|
83
|
+
// file every hook it makes at the file's route (37.1). Its body cannot await at its top level
|
|
84
|
+
// (34.3), so nothing else runs inside the window.
|
|
85
|
+
const serverSetup = splitImports(lowering.servers(), 'source')
|
|
86
|
+
const serverBody =
|
|
87
|
+
serverSetup.body === ''
|
|
88
|
+
? ''
|
|
89
|
+
: `${lowering.use('fileRoute')}(${JSON.stringify(route)})
|
|
90
|
+
${serverSetup.body}
|
|
91
|
+
${lowering.use('fileRoute')}(undefined)
|
|
92
|
+
${privateExports(file.servers.join('\n'))}`
|
|
93
|
+
const setup = splitImports(lowering.scripts())
|
|
74
94
|
const componentBody = setup.body
|
|
75
95
|
|
|
76
96
|
// A child invoked inside `<head>` pushes to the same array from the head expression.
|
|
@@ -95,6 +115,7 @@ export function emitComponent(
|
|
|
95
115
|
return {
|
|
96
116
|
head: ${head}${heads ? ` + ${HEADS}.join('')` : ''},
|
|
97
117
|
body: ${BODY},
|
|
118
|
+
document: ${document},
|
|
98
119
|
}`
|
|
99
120
|
// 41.19 — BUILT ON THE FIRST READ, and ONCE: `render` reads `head` and then `body`, and a
|
|
100
121
|
// child the markup invokes must not be set up twice. The build resumes the share scope the
|
|
@@ -112,10 +133,11 @@ export function emitComponent(
|
|
|
112
133
|
scope: ${SETTLED_AT},
|
|
113
134
|
get head() { return ${BUILD}().head },
|
|
114
135
|
get body() { return ${BUILD}().body },
|
|
136
|
+
get document() { return ${BUILD}().document },
|
|
115
137
|
}`
|
|
116
138
|
: build
|
|
117
|
-
return `${moduleImports(lowering, () => SERVER_SPECIFIER, [moduleSetup, setup])}
|
|
118
|
-
${moduleSetup.body === '' ? '' : `${moduleSetup.body}\n`}export default function Component(${lowering.takesProps() ? `${PROPS} = {}` : ''}) {
|
|
139
|
+
return `${moduleImports(lowering, () => SERVER_SPECIFIER, [moduleSetup, serverSetup, setup])}
|
|
140
|
+
${moduleSetup.body === '' ? '' : `${moduleSetup.body}\n`}${serverBody}export default function Component(${lowering.takesProps() ? `${PROPS} = {}` : ''}) {
|
|
119
141
|
${opened}${componentBody}
|
|
120
142
|
${level}${closed}
|
|
121
143
|
}
|
|
@@ -160,3 +182,10 @@ function importClause(names: string[]): string {
|
|
|
160
182
|
for (const name of names) written.push(`${name} as __${name}`)
|
|
161
183
|
return written.join(', ')
|
|
162
184
|
}
|
|
185
|
+
|
|
186
|
+
// `nodes` placed on a sink of their own, as `headSink` places a head.
|
|
187
|
+
function placed(nodes: TemplateNode[]): Cursor {
|
|
188
|
+
const sink = new Cursor()
|
|
189
|
+
placeTemplate(nodes, sink)
|
|
190
|
+
return sink
|
|
191
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { STYLE_QUERY } from '#server/MODULE_QUERIES.ts'
|
|
2
|
+
import { fileKind, scopeOf } from '#shared/routePattern.ts'
|
|
2
3
|
import type { CheckerAnswer } from './CheckerAnswer.ts'
|
|
3
4
|
import { emitClient } from './emitClient.ts'
|
|
4
5
|
import { emitComponent } from './emitComponent.ts'
|
|
6
|
+
import type { LoweredChunk } from './Lowering.ts'
|
|
5
7
|
import type { AbideFile } from './parseAbide.ts'
|
|
8
|
+
import { projectShadow } from './projectShadow.ts'
|
|
6
9
|
import {
|
|
7
10
|
markScoped,
|
|
8
11
|
namesViewTransition,
|
|
9
12
|
scopeCss,
|
|
10
13
|
scopeToken,
|
|
11
14
|
} from './scopeStyles.ts'
|
|
15
|
+
import { sourceMapOf } from './sourceMapOf.ts'
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* One `.abide`'s modules, in the mode the build compiles it in: its server module, its client
|
|
@@ -22,18 +26,29 @@ export function emitSource(
|
|
|
22
26
|
file: AbideFile,
|
|
23
27
|
answer: CheckerAnswer,
|
|
24
28
|
invoked: boolean,
|
|
25
|
-
|
|
29
|
+
/**
|
|
30
|
+
* 16.28 — what the browser's module binds in place of the server block: the wrapper of each
|
|
31
|
+
* private handler it declares, under the handler's name (`standInsOf`).
|
|
32
|
+
*/
|
|
33
|
+
standIns = '',
|
|
34
|
+
/** 38.97 — the `.abide`'s text, where the build wants each module's source map. */
|
|
35
|
+
source?: string,
|
|
36
|
+
/** 23.19 — the path it answers by, where a mount gives it one other than `relative`. */
|
|
37
|
+
position = relative,
|
|
26
38
|
): {
|
|
27
39
|
server: string
|
|
28
40
|
client: string
|
|
41
|
+
/** 38.97 — each module's `mappings` into the `.abide`, where `source` was given. */
|
|
42
|
+
serverMappings: string | undefined
|
|
43
|
+
clientMappings: string | undefined
|
|
29
44
|
sheet: string | undefined
|
|
30
45
|
/** 39.8 — whether a `<style>` names a view transition, read here rather than at run time. */
|
|
31
46
|
transitions: boolean
|
|
32
47
|
} {
|
|
33
|
-
const kind = fileKind(
|
|
48
|
+
const kind = fileKind(position)
|
|
34
49
|
// 34.4 — A COMPONENT WITH STYLES: its elements carry its token, and the styles, scoped,
|
|
35
50
|
// are a stylesheet its client module imports — which the bundle collects into the
|
|
36
|
-
// stylesheet the document links (38.
|
|
51
|
+
// stylesheet the document links (38.23).
|
|
37
52
|
// 34.11 — each body's own `<style>`, scoped to the token its elements carry; the
|
|
38
53
|
// component's token is added only where a component-level `<style>` requires it.
|
|
39
54
|
const token = scopeToken(relative)
|
|
@@ -66,26 +81,100 @@ export function emitSource(
|
|
|
66
81
|
// nothing imports it, the router is what invokes it, and a reader who navigated to a
|
|
67
82
|
// page under it may arrive with the layout already standing or with nothing at all.
|
|
68
83
|
// An `error.abide` is never navigated to — only the server holds the error table.
|
|
69
|
-
const
|
|
84
|
+
const selfFilled = kind === 'page' || kind === 'layout'
|
|
70
85
|
// The client module is emitted only where there is something to hydrate, or where something
|
|
71
86
|
// fills or imports it by name (`emitClient` answers which). A static component nothing invokes
|
|
72
87
|
// has no `.client.ts`, so a file shipping no runtime is a fact about the build rather than a
|
|
73
88
|
// claim about the bundler.
|
|
89
|
+
const serverChunks: LoweredChunk[] | undefined =
|
|
90
|
+
source === undefined ? undefined : []
|
|
91
|
+
const clientChunks: LoweredChunk[] | undefined =
|
|
92
|
+
source === undefined ? undefined : []
|
|
74
93
|
const client = emitClient(
|
|
75
94
|
file,
|
|
76
95
|
answer,
|
|
77
|
-
invoked
|
|
78
|
-
|
|
96
|
+
invoked
|
|
97
|
+
? selfFilled
|
|
98
|
+
? 'both'
|
|
99
|
+
: 'invoked'
|
|
100
|
+
: selfFilled
|
|
101
|
+
? 'routed'
|
|
102
|
+
: 'none',
|
|
103
|
+
clientChunks,
|
|
104
|
+
)
|
|
105
|
+
// 41.19 — a page and a layout are built a level at a time by `render`.
|
|
106
|
+
const server = emitComponent(
|
|
107
|
+
file,
|
|
108
|
+
answer,
|
|
109
|
+
kind !== 'component',
|
|
110
|
+
scopeOf(position),
|
|
111
|
+
serverChunks,
|
|
79
112
|
)
|
|
80
113
|
const name = relative.slice(relative.lastIndexOf('/') + 1)
|
|
114
|
+
const browser = withStandIns(
|
|
115
|
+
client !== '' && sheet !== undefined
|
|
116
|
+
? `import './${name}${STYLE_QUERY}'\n${client}`
|
|
117
|
+
: client,
|
|
118
|
+
standIns,
|
|
119
|
+
)
|
|
120
|
+
let mapped: Mapped | undefined
|
|
121
|
+
if (source !== undefined) {
|
|
122
|
+
mapped = MAPPED.get(relative)
|
|
123
|
+
if (
|
|
124
|
+
mapped === undefined ||
|
|
125
|
+
mapped.source !== source ||
|
|
126
|
+
mapped.server !== server ||
|
|
127
|
+
mapped.client !== browser
|
|
128
|
+
) {
|
|
129
|
+
const projected = projectShadow(file).origins
|
|
130
|
+
mapped = {
|
|
131
|
+
source,
|
|
132
|
+
server,
|
|
133
|
+
client: browser,
|
|
134
|
+
serverMappings: sourceMapOf(
|
|
135
|
+
server,
|
|
136
|
+
serverChunks ?? [],
|
|
137
|
+
projected,
|
|
138
|
+
source,
|
|
139
|
+
),
|
|
140
|
+
clientMappings:
|
|
141
|
+
browser === ''
|
|
142
|
+
? undefined
|
|
143
|
+
: sourceMapOf(
|
|
144
|
+
browser,
|
|
145
|
+
clientChunks ?? [],
|
|
146
|
+
projected,
|
|
147
|
+
source,
|
|
148
|
+
),
|
|
149
|
+
}
|
|
150
|
+
MAPPED.set(relative, mapped)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
81
153
|
return {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
? `import './${name.replace(/\.abide$/, '.style.css')}'\n${client}`
|
|
87
|
-
: client,
|
|
154
|
+
server,
|
|
155
|
+
client: browser,
|
|
156
|
+
serverMappings: mapped?.serverMappings,
|
|
157
|
+
clientMappings: mapped?.clientMappings,
|
|
88
158
|
sheet,
|
|
89
159
|
transitions,
|
|
90
160
|
}
|
|
91
161
|
}
|
|
162
|
+
|
|
163
|
+
type Mapped = {
|
|
164
|
+
source: string
|
|
165
|
+
server: string
|
|
166
|
+
client: string
|
|
167
|
+
serverMappings: string
|
|
168
|
+
clientMappings: string | undefined
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// THE LAST MAPS PER FILE, for a process that compiles again (`abide dev`, the language server): a
|
|
172
|
+
// module's maps are a function of its text and the `.abide`'s, and a save changes one file. Reused
|
|
173
|
+
// only where all three texts match, so two apps in one process sharing a path cannot cross.
|
|
174
|
+
const MAPPED = new Map<string, Mapped>()
|
|
175
|
+
|
|
176
|
+
/** The browser's module with each private handler's wrapper after it. */
|
|
177
|
+
function withStandIns(client: string, standIns: string): string {
|
|
178
|
+
if (standIns === '') return client
|
|
179
|
+
return client === '' ? standIns : `${client}\n${standIns}`
|
|
180
|
+
}
|