@abide/abide 0.40.0 → 0.40.2
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/AGENTS.md +319 -146
- package/CHANGELOG.md +48 -0
- package/README.md +80 -77
- package/package.json +1 -1
- package/src/lib/shared/assertExhaustive.ts +14 -0
- package/src/lib/ui/compile/TS_PRINTER.ts +7 -0
- package/src/lib/ui/compile/analyzeComponent.ts +15 -16
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +66 -0
- package/src/lib/ui/compile/assertTranspiles.ts +19 -0
- package/src/lib/ui/compile/compileModule.ts +56 -34
- package/src/lib/ui/compile/compileSSR.ts +1 -3
- package/src/lib/ui/compile/desugarSignals.ts +168 -107
- package/src/lib/ui/compile/generateBuild.ts +8 -1
- package/src/lib/ui/compile/generateSSR.ts +7 -0
- package/src/lib/ui/compile/hoistCells.ts +2 -2
- package/src/lib/ui/compile/lowerContext.ts +23 -10
- package/src/lib/ui/compile/lowerDocAccess.ts +29 -3
- package/src/lib/ui/compile/lowerScript.ts +64 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +160 -102
- package/src/lib/ui/compile/stripEffects.ts +22 -17
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
- package/src/lib/ui/dom/awaitBlock.ts +8 -1
- package/src/lib/ui/dom/each.ts +4 -0
- package/src/lib/ui/dom/switchBlock.ts +4 -0
- package/src/lib/ui/dom/when.ts +4 -0
- package/src/lib/ui/installInspectorBridge.ts +3 -1
- package/src/lib/ui/router.ts +107 -14
- package/src/lib/ui/runtime/NODE_STATE.ts +22 -0
- package/src/lib/ui/runtime/clientPage.ts +114 -9
- package/src/lib/ui/runtime/createComputedNode.ts +5 -3
- package/src/lib/ui/runtime/createEffectNode.ts +3 -1
- package/src/lib/ui/runtime/createSignalNode.ts +4 -2
- package/src/lib/ui/runtime/flushEffects.ts +8 -5
- package/src/lib/ui/runtime/historyEntries.ts +39 -1
- package/src/lib/ui/runtime/readNode.ts +8 -7
- package/src/lib/ui/runtime/runNode.ts +18 -2
- package/src/lib/ui/runtime/scope.ts +12 -1
- package/src/lib/ui/runtime/trigger.ts +40 -24
- package/src/lib/ui/runtime/types/ReactiveNode.ts +4 -1
- package/src/lib/ui/runtime/updateIfNecessary.ts +40 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { REACTIVE_CALLEES } from './REACTIVE_CALLEES.ts'
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
const factory = ts.factory
|
|
4
5
|
|
|
5
6
|
/* The reactive primitives that must be reached through a scope. A bare call to one of
|
|
6
7
|
these is a compile error: reactive state is owned by a scope and the surface must show
|
|
@@ -34,28 +35,28 @@ function assertScopedPrimitives(source: ts.SourceFile): void {
|
|
|
34
35
|
Desugars the signal surface into the document form. A component's `<script>`
|
|
35
36
|
declares reactive state as signals:
|
|
36
37
|
|
|
37
|
-
let count = state(0)
|
|
38
|
-
let items = state([])
|
|
39
|
-
const total = computed(() => count + items.length)
|
|
38
|
+
let count = scope().state(0)
|
|
39
|
+
let items = scope().state([])
|
|
40
|
+
const total = scope().computed(() => count() + items.length)
|
|
40
41
|
|
|
41
|
-
This
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
This walks the already-parsed script once to collect the binding names, then returns
|
|
43
|
+
a `ts.TransformerFactory` that rewrites the declarations onto a shared `model`
|
|
44
|
+
document: each plain `state(initial)` becomes an initialising assignment (`model.x =
|
|
45
|
+
initial`, in source order so a later state can read an earlier one), each `computed`
|
|
46
|
+
and destructured prop becomes a `scope().derive` slot, and `linked` /
|
|
47
|
+
`state(initial, transform)` stay `.value` cells routed onto the scope. Returning a
|
|
48
|
+
TRANSFORMER (not rebuilt source text) lets the caller (`lowerScript`) chain it with
|
|
49
|
+
reference renaming and doc-access lowering over ONE parsed tree — no print-then-reparse
|
|
50
|
+
between passes. Plain `state` becomes `model.x` access that `lowerDocAccess` lowers to
|
|
51
|
+
patches/reads. No reactive declarations → an identity transformer (the explicit
|
|
52
|
+
`const model = doc(...)` form still works).
|
|
51
53
|
*/
|
|
52
|
-
export function desugarSignals(
|
|
53
|
-
|
|
54
|
+
export function desugarSignals(source: ts.SourceFile): {
|
|
55
|
+
transformer: ts.TransformerFactory<ts.SourceFile>
|
|
54
56
|
stateNames: Set<string>
|
|
55
57
|
derivedNames: Set<string>
|
|
56
58
|
computedNames: Set<string>
|
|
57
59
|
} {
|
|
58
|
-
const source = ts.createSourceFile('script.ts', scriptBody, ts.ScriptTarget.Latest, true)
|
|
59
60
|
assertScopedPrimitives(source)
|
|
60
61
|
const stateNames = new Set<string>()
|
|
61
62
|
const derivedNames = new Set<string>()
|
|
@@ -103,43 +104,70 @@ export function desugarSignals(scriptBody: string): {
|
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
|
-
if (
|
|
107
|
-
stateNames.size === 0 &&
|
|
108
|
-
derivedNames.size === 0 &&
|
|
109
|
-
computedNames.size === 0 &&
|
|
110
|
-
!hasPropsDestructure
|
|
111
|
-
) {
|
|
112
|
-
return { code: scriptBody, stateNames, derivedNames, computedNames }
|
|
113
|
-
}
|
|
114
107
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const cellDeclarations = cellDeclarationLines(statement, printer, source)
|
|
125
|
-
if (stateAssignments !== undefined) {
|
|
126
|
-
lines.push(...stateAssignments)
|
|
127
|
-
} else if (computedDeclarations !== undefined) {
|
|
128
|
-
lines.push(...computedDeclarations)
|
|
129
|
-
} else if (propsDestructure !== undefined) {
|
|
130
|
-
lines.push(...propsDestructure)
|
|
131
|
-
} else if (cellDeclarations !== undefined) {
|
|
132
|
-
lines.push(...cellDeclarations)
|
|
133
|
-
} else {
|
|
134
|
-
lines.push(printer.printNode(ts.EmitHint.Unspecified, statement, source))
|
|
108
|
+
const hasReactive =
|
|
109
|
+
stateNames.size > 0 ||
|
|
110
|
+
derivedNames.size > 0 ||
|
|
111
|
+
computedNames.size > 0 ||
|
|
112
|
+
hasPropsDestructure
|
|
113
|
+
|
|
114
|
+
const transformer: ts.TransformerFactory<ts.SourceFile> = () => (root) => {
|
|
115
|
+
if (!hasReactive) {
|
|
116
|
+
return root
|
|
135
117
|
}
|
|
118
|
+
const statements: ts.Statement[] = []
|
|
119
|
+
/* A shared `model = scope()` host for the state slots, prepended once. */
|
|
120
|
+
if (stateNames.size > 0) {
|
|
121
|
+
statements.push(
|
|
122
|
+
constDeclaration(
|
|
123
|
+
'model',
|
|
124
|
+
factory.createCallExpression(factory.createIdentifier('scope'), undefined, []),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
for (const statement of root.statements) {
|
|
129
|
+
statements.push(...loweredStatement(statement))
|
|
130
|
+
}
|
|
131
|
+
return factory.updateSourceFile(root, statements)
|
|
136
132
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
133
|
+
|
|
134
|
+
return { transformer, stateNames, derivedNames, computedNames }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* The lowered form of a top-level statement: state slots → `model.x = init`
|
|
138
|
+
assignments, computed → `scope().derive` consts, props → derive consts (+ restProps),
|
|
139
|
+
cells → `scope().<callee>(...)` consts; anything else passes through verbatim. The
|
|
140
|
+
per-statement dispatch mirrors the name-collection pass. */
|
|
141
|
+
function loweredStatement(statement: ts.Statement): ts.Statement[] {
|
|
142
|
+
return (
|
|
143
|
+
stateAssignmentStatements(statement) ??
|
|
144
|
+
computedStatements(statement) ??
|
|
145
|
+
propsStatements(statement) ??
|
|
146
|
+
cellStatements(statement) ?? [statement]
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* `const NAME = <init>` — a fresh const declaration reusing the original initializer node. */
|
|
151
|
+
function constDeclaration(name: string, initializer: ts.Expression): ts.VariableStatement {
|
|
152
|
+
return factory.createVariableStatement(
|
|
153
|
+
undefined,
|
|
154
|
+
factory.createVariableDeclarationList(
|
|
155
|
+
[factory.createVariableDeclaration(name, undefined, undefined, initializer)],
|
|
156
|
+
ts.NodeFlags.Const,
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* `scope().<method>(...args)` — the reactive method routed onto the ambient scope. */
|
|
162
|
+
function scopeMethodCall(method: string, args: readonly ts.Expression[]): ts.CallExpression {
|
|
163
|
+
return factory.createCallExpression(
|
|
164
|
+
factory.createPropertyAccessExpression(
|
|
165
|
+
factory.createCallExpression(factory.createIdentifier('scope'), undefined, []),
|
|
166
|
+
method,
|
|
167
|
+
),
|
|
168
|
+
undefined,
|
|
169
|
+
args,
|
|
170
|
+
)
|
|
143
171
|
}
|
|
144
172
|
|
|
145
173
|
/* True for a read-only computed slot — `computed(compute)` with no write-through
|
|
@@ -159,15 +187,11 @@ function isComputedSlot(declaration: ts.VariableDeclaration): boolean {
|
|
|
159
187
|
routed onto the scope: `linked(seed)` → `const x = scope().linked(seed)`. The cell
|
|
160
188
|
stays a standalone non-serializing cell (its refs stay `name.value`); the rewrite only
|
|
161
189
|
removes the bare runtime import so the sole reactive surface is `scope()`. */
|
|
162
|
-
function
|
|
163
|
-
statement: ts.Statement,
|
|
164
|
-
printer: ts.Printer,
|
|
165
|
-
source: ts.SourceFile,
|
|
166
|
-
): string[] | undefined {
|
|
190
|
+
function cellStatements(statement: ts.Statement): ts.Statement[] | undefined {
|
|
167
191
|
if (!ts.isVariableStatement(statement)) {
|
|
168
192
|
return undefined
|
|
169
193
|
}
|
|
170
|
-
const
|
|
194
|
+
const statements: ts.Statement[] = []
|
|
171
195
|
for (const declaration of statement.declarationList.declarations) {
|
|
172
196
|
const callee = signalCallee(declaration)
|
|
173
197
|
if (
|
|
@@ -178,24 +202,18 @@ function cellDeclarationLines(
|
|
|
178
202
|
return undefined
|
|
179
203
|
}
|
|
180
204
|
const args = (declaration.initializer as ts.CallExpression).arguments
|
|
181
|
-
|
|
182
|
-
.join(', ')
|
|
183
|
-
lines.push(`const ${declaration.name.text} = scope().${callee}(${args})`)
|
|
205
|
+
statements.push(constDeclaration(declaration.name.text, scopeMethodCall(callee, args)))
|
|
184
206
|
}
|
|
185
|
-
return
|
|
207
|
+
return statements
|
|
186
208
|
}
|
|
187
209
|
|
|
188
210
|
/* `let total = computed(compute)` → `const total = scope().derive("total", compute)`
|
|
189
211
|
— a computed doc slot whose reader the references lower to `total()`. */
|
|
190
|
-
function
|
|
191
|
-
statement: ts.Statement,
|
|
192
|
-
printer: ts.Printer,
|
|
193
|
-
source: ts.SourceFile,
|
|
194
|
-
): string[] | undefined {
|
|
212
|
+
function computedStatements(statement: ts.Statement): ts.Statement[] | undefined {
|
|
195
213
|
if (!ts.isVariableStatement(statement)) {
|
|
196
214
|
return undefined
|
|
197
215
|
}
|
|
198
|
-
const
|
|
216
|
+
const statements: ts.Statement[] = []
|
|
199
217
|
for (const declaration of statement.declarationList.declarations) {
|
|
200
218
|
if (
|
|
201
219
|
!ts.isIdentifier(declaration.name) ||
|
|
@@ -204,16 +222,25 @@ function computedDeclarationLines(
|
|
|
204
222
|
) {
|
|
205
223
|
return undefined
|
|
206
224
|
}
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
225
|
+
const name = declaration.name.text
|
|
226
|
+
const compute =
|
|
227
|
+
(declaration.initializer as ts.CallExpression).arguments[0] ??
|
|
228
|
+
factory.createArrowFunction(
|
|
229
|
+
undefined,
|
|
230
|
+
undefined,
|
|
231
|
+
[],
|
|
232
|
+
undefined,
|
|
233
|
+
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
234
|
+
factory.createIdentifier('undefined'),
|
|
235
|
+
)
|
|
236
|
+
statements.push(
|
|
237
|
+
constDeclaration(
|
|
238
|
+
name,
|
|
239
|
+
scopeMethodCall('derive', [factory.createStringLiteral(name), compute]),
|
|
240
|
+
),
|
|
214
241
|
)
|
|
215
242
|
}
|
|
216
|
-
return
|
|
243
|
+
return statements
|
|
217
244
|
}
|
|
218
245
|
|
|
219
246
|
/* True for `state(initial, transform)` — the write-coercion transform forces a
|
|
@@ -308,65 +335,99 @@ function propsBindingKey(element: ts.BindingElement): string {
|
|
|
308
335
|
|
|
309
336
|
/* If `statement` is a `const {…, ...rest} = props()` destructure, returns one reactive
|
|
310
337
|
computed per named binding — `scope().derive("name", () => $props["key"]?.() ?? default)`,
|
|
311
|
-
read as `name()` — plus a `const rest = restProps($props, [consumed])`
|
|
312
|
-
binding; otherwise undefined. The `?? default` applies the binding's `= default`
|
|
313
|
-
when the prop is absent. */
|
|
314
|
-
function
|
|
315
|
-
statement: ts.Statement,
|
|
316
|
-
printer: ts.Printer,
|
|
317
|
-
source: ts.SourceFile,
|
|
318
|
-
): string[] | undefined {
|
|
338
|
+
read as `name()` — plus a `const rest = restProps($props, [consumed])` for a rest
|
|
339
|
+
binding; otherwise undefined. The `?? default` applies the binding's `= default`
|
|
340
|
+
fallback when the prop is absent. */
|
|
341
|
+
function propsStatements(statement: ts.Statement): ts.Statement[] | undefined {
|
|
319
342
|
if (!ts.isVariableStatement(statement)) {
|
|
320
343
|
return undefined
|
|
321
344
|
}
|
|
322
|
-
const
|
|
345
|
+
const statements: ts.Statement[] = []
|
|
323
346
|
for (const declaration of statement.declarationList.declarations) {
|
|
324
347
|
if (signalCallee(declaration) !== 'props' || !ts.isObjectBindingPattern(declaration.name)) {
|
|
325
348
|
return undefined
|
|
326
349
|
}
|
|
327
350
|
const { bindings, rest } = propsDestructure(declaration)
|
|
328
351
|
for (const { local, key, initializer } of bindings) {
|
|
329
|
-
|
|
352
|
+
/* `$props["key"]?.()` — the parent thunk, optionally called. */
|
|
353
|
+
const read = factory.createCallChain(
|
|
354
|
+
factory.createElementAccessExpression(
|
|
355
|
+
factory.createIdentifier('$props'),
|
|
356
|
+
factory.createStringLiteral(key),
|
|
357
|
+
),
|
|
358
|
+
factory.createToken(ts.SyntaxKind.QuestionDotToken),
|
|
359
|
+
undefined,
|
|
360
|
+
[],
|
|
361
|
+
)
|
|
362
|
+
/* `?? default` only when the binding declared a `= default` fallback. */
|
|
363
|
+
const body =
|
|
330
364
|
initializer === undefined
|
|
331
|
-
?
|
|
332
|
-
:
|
|
333
|
-
|
|
334
|
-
|
|
365
|
+
? read
|
|
366
|
+
: factory.createBinaryExpression(
|
|
367
|
+
read,
|
|
368
|
+
factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
|
|
369
|
+
factory.createParenthesizedExpression(initializer),
|
|
370
|
+
)
|
|
371
|
+
const thunk = factory.createArrowFunction(
|
|
372
|
+
undefined,
|
|
373
|
+
undefined,
|
|
374
|
+
[],
|
|
375
|
+
undefined,
|
|
376
|
+
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
377
|
+
body,
|
|
378
|
+
)
|
|
379
|
+
statements.push(
|
|
380
|
+
constDeclaration(
|
|
381
|
+
local,
|
|
382
|
+
scopeMethodCall('derive', [factory.createStringLiteral(local), thunk]),
|
|
383
|
+
),
|
|
335
384
|
)
|
|
336
385
|
}
|
|
337
386
|
/* The rest bag gathers every prop not named above (and not `$children`). */
|
|
338
387
|
if (rest !== undefined) {
|
|
339
|
-
const consumed = bindings.map((binding) => binding.key)
|
|
340
|
-
|
|
388
|
+
const consumed = bindings.map((binding) => factory.createStringLiteral(binding.key))
|
|
389
|
+
statements.push(
|
|
390
|
+
constDeclaration(
|
|
391
|
+
rest,
|
|
392
|
+
factory.createCallExpression(factory.createIdentifier('restProps'), undefined, [
|
|
393
|
+
factory.createIdentifier('$props'),
|
|
394
|
+
factory.createArrayLiteralExpression(consumed),
|
|
395
|
+
]),
|
|
396
|
+
),
|
|
397
|
+
)
|
|
341
398
|
}
|
|
342
399
|
}
|
|
343
|
-
return
|
|
400
|
+
return statements
|
|
344
401
|
}
|
|
345
402
|
|
|
346
403
|
/* If `statement` declares `state(...)` bindings, returns `model.<name> = <init>`
|
|
347
|
-
assignment
|
|
348
|
-
function
|
|
349
|
-
statement: ts.Statement,
|
|
350
|
-
printer: ts.Printer,
|
|
351
|
-
source: ts.SourceFile,
|
|
352
|
-
): string[] | undefined {
|
|
404
|
+
assignment statements (one per declaration); otherwise undefined. */
|
|
405
|
+
function stateAssignmentStatements(statement: ts.Statement): ts.Statement[] | undefined {
|
|
353
406
|
if (!ts.isVariableStatement(statement)) {
|
|
354
407
|
return undefined
|
|
355
408
|
}
|
|
356
|
-
const
|
|
409
|
+
const statements: ts.Statement[] = []
|
|
357
410
|
for (const declaration of statement.declarationList.declarations) {
|
|
358
411
|
if (!ts.isIdentifier(declaration.name) || !isPlainStateSlot(declaration)) {
|
|
359
412
|
/* Only a plain `state(initial)` becomes a slot; `state(initial, transform)`
|
|
360
|
-
(and everything else) is a `.value` cell —
|
|
413
|
+
(and everything else) is a `.value` cell — pass it through so the
|
|
361
414
|
runtime call (and its transform) survives. */
|
|
362
415
|
return undefined
|
|
363
416
|
}
|
|
364
|
-
const initial =
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
417
|
+
const initial =
|
|
418
|
+
(declaration.initializer as ts.CallExpression).arguments[0] ??
|
|
419
|
+
factory.createIdentifier('undefined')
|
|
420
|
+
statements.push(
|
|
421
|
+
factory.createExpressionStatement(
|
|
422
|
+
factory.createAssignment(
|
|
423
|
+
factory.createPropertyAccessExpression(
|
|
424
|
+
factory.createIdentifier('model'),
|
|
425
|
+
declaration.name.text,
|
|
426
|
+
),
|
|
427
|
+
initial,
|
|
428
|
+
),
|
|
429
|
+
),
|
|
430
|
+
)
|
|
370
431
|
}
|
|
371
|
-
return
|
|
432
|
+
return statements
|
|
372
433
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertExhaustive } from '../../shared/assertExhaustive.ts'
|
|
1
2
|
import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
|
|
2
3
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
3
4
|
import { asOutlet } from './asOutlet.ts'
|
|
@@ -371,7 +372,13 @@ export function generateBuild(
|
|
|
371
372
|
if (node.kind === 'snippet') {
|
|
372
373
|
return generateSnippet(node)
|
|
373
374
|
}
|
|
374
|
-
|
|
375
|
+
if (node.kind === 'each') {
|
|
376
|
+
return generateEach(node, parentVar, before)
|
|
377
|
+
}
|
|
378
|
+
/* Every TemplateNode kind is handled above; `node` is `never` here. A new kind
|
|
379
|
+
reaching this point is a compiler gap — fail loud instead of silently routing
|
|
380
|
+
it to the wrong branch. */
|
|
381
|
+
return assertExhaustive(node, 'template node kind')
|
|
375
382
|
}
|
|
376
383
|
|
|
377
384
|
/* Builds a sibling list, coalescing maximal runs of fully-static element subtrees
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertExhaustive } from '../../shared/assertExhaustive.ts'
|
|
1
2
|
import { OUTLET_CLOSE, OUTLET_OPEN } from '../runtime/OUTLET_MARKER.ts'
|
|
2
3
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
3
4
|
import { asOutlet } from './asOutlet.ts'
|
|
@@ -341,6 +342,12 @@ export function generateSSR(
|
|
|
341
342
|
}
|
|
342
343
|
return generateSlot(node, target, anchor)
|
|
343
344
|
}
|
|
345
|
+
/* Every non-element kind returned above; a kind reaching the element builder
|
|
346
|
+
that isn't an element is a compiler gap — fail loud rather than emit it as a
|
|
347
|
+
bogus `<undefined>` tag. (`node` narrows to `never` in this branch.) */
|
|
348
|
+
if (node.kind !== 'element') {
|
|
349
|
+
return assertExhaustive(node, 'template node kind')
|
|
350
|
+
}
|
|
344
351
|
let code = push(target, `<${node.tag}`)
|
|
345
352
|
/* Every `<style>` active at this element (own siblings + ancestors) — same set
|
|
346
353
|
the client stamps, so server and client markup carry identical attributes. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
|
+
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
The perf pass that follows `lowerDocAccess`. Within a render scope, a static path
|
|
@@ -35,8 +36,7 @@ export function hoistCells(code: string, docName: string): string {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const result = ts.transform(source, [hoistTransformer(docName, cellIdForPath)])
|
|
38
|
-
const
|
|
39
|
-
const output = printer.printFile(result.transformed[0] as ts.SourceFile)
|
|
39
|
+
const output = TS_PRINTER.printFile(result.transformed[0] as ts.SourceFile)
|
|
40
40
|
result.dispose()
|
|
41
41
|
return output
|
|
42
42
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import { docAccessTransformer } from './lowerDocAccess.ts'
|
|
2
3
|
import { nestedBindingNames } from './prepareNestedScript.ts'
|
|
3
|
-
import {
|
|
4
|
+
import { signalRefsTransformer } from './renameSignalRefs.ts'
|
|
5
|
+
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
4
6
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
5
7
|
import { unwrapParens } from './unwrapParens.ts'
|
|
6
8
|
|
|
@@ -27,19 +29,30 @@ export function lowerContext(
|
|
|
27
29
|
const derefScope = (): ReadonlySet<string> =>
|
|
28
30
|
localDerived.size === 0 ? derivedNames : new Set([...derivedNames, ...localDerived])
|
|
29
31
|
|
|
30
|
-
/*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/* Parse `code` once and chain the reference rename and doc-access lowering over the
|
|
33
|
+
one tree — the two string passes would each parse + reprint. `derefScope()` is read
|
|
34
|
+
per call so a nested-`<script>` binding pushed mid-compile is honoured. */
|
|
35
|
+
function lowerOnce(code: string): string {
|
|
36
|
+
const source = ts.createSourceFile('expr.ts', code, ts.ScriptTarget.Latest, true)
|
|
37
|
+
const result = ts.transform(source, [
|
|
38
|
+
signalRefsTransformer(stateNames, derefScope(), computedNames),
|
|
39
|
+
docAccessTransformer('model'),
|
|
40
|
+
])
|
|
41
|
+
const output = TS_PRINTER.printFile(result.transformed[0] as ts.SourceFile).trim()
|
|
42
|
+
result.dispose()
|
|
43
|
+
return output
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Lowers a single expression (no trailing `;`). Wrapped in parens so a bare object
|
|
47
|
+
literal (`{ a: 1 }`) parses as an expression, not a block of labeled statements,
|
|
48
|
+
through the rewrite; the wrapper is then peeled back off. */
|
|
34
49
|
function expression(code: string): string {
|
|
35
|
-
|
|
36
|
-
return unwrapParens(lowerDocAccess(renamed, 'model').trim().replace(/;$/, ''))
|
|
50
|
+
return unwrapParens(lowerOnce(`(${code})`).replace(/;$/, ''))
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
/* As above but keeps the trailing `;` for a statement/handler body. */
|
|
40
54
|
function statement(code: string): string {
|
|
41
|
-
|
|
42
|
-
return lowerDocAccess(renamed, 'model').trim()
|
|
55
|
+
return lowerOnce(code)
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
/* A two-way bind target is either an LVALUE (`count`, `model.lines[i]`) — reads as
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { escapeKey } from '../runtime/escapeKey.ts'
|
|
3
|
+
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
The linchpin compiler pass. Rewrites idiomatic data access on a reactive document
|
|
@@ -27,8 +28,7 @@ used as an index lowers too.
|
|
|
27
28
|
export function lowerDocAccess(code: string, docName: string): string {
|
|
28
29
|
const source = ts.createSourceFile('component.ts', code, ts.ScriptTarget.Latest, true)
|
|
29
30
|
const result = ts.transform(source, [docAccessTransformer(docName)])
|
|
30
|
-
const
|
|
31
|
-
const output = printer.printFile(result.transformed[0] as ts.SourceFile)
|
|
31
|
+
const output = TS_PRINTER.printFile(result.transformed[0] as ts.SourceFile)
|
|
32
32
|
result.dispose()
|
|
33
33
|
return output
|
|
34
34
|
}
|
|
@@ -49,7 +49,7 @@ const COMPOUND_OPERATORS = new Map<ts.SyntaxKind, ts.BinaryOperator>([
|
|
|
49
49
|
[ts.SyntaxKind.QuestionQuestionEqualsToken, ts.SyntaxKind.QuestionQuestionToken],
|
|
50
50
|
])
|
|
51
51
|
|
|
52
|
-
function docAccessTransformer(docName: string): ts.TransformerFactory<ts.SourceFile> {
|
|
52
|
+
export function docAccessTransformer(docName: string): ts.TransformerFactory<ts.SourceFile> {
|
|
53
53
|
return (context) => (root) => {
|
|
54
54
|
/* Collects the path of an access chain rooted at `docName`, visiting any
|
|
55
55
|
dynamic index so reads inside it lower too. undefined if not our doc. */
|
|
@@ -107,6 +107,32 @@ function docAccessTransformer(docName: string): ts.TransformerFactory<ts.SourceF
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
+
/* `path++` / `++path` / `path--` on a doc path → a replace patch (the same
|
|
111
|
+
shape as `+= 1`). A bare `++` would otherwise survive onto the lowered read
|
|
112
|
+
(`model.read("n")++` / `cell.get()++`) — invalid, since a call result is not
|
|
113
|
+
an lvalue. abide handlers are statements, so the postfix/prefix value is
|
|
114
|
+
discarded and both lower identically. Only `++`/`--` are handled here; other
|
|
115
|
+
unary operators (`!path`, `-path`) fall through so their operand lowers as a
|
|
116
|
+
normal read. */
|
|
117
|
+
if (
|
|
118
|
+
(ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node)) &&
|
|
119
|
+
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
120
|
+
node.operator === ts.SyntaxKind.MinusMinusToken)
|
|
121
|
+
) {
|
|
122
|
+
const segments = pathSegments(node.operand)
|
|
123
|
+
if (segments) {
|
|
124
|
+
const step =
|
|
125
|
+
node.operator === ts.SyntaxKind.PlusPlusToken
|
|
126
|
+
? ts.SyntaxKind.PlusToken
|
|
127
|
+
: ts.SyntaxKind.MinusToken
|
|
128
|
+
const next = ts.factory.createBinaryExpression(
|
|
129
|
+
docCall(docName, 'read', [buildPath(segments)]),
|
|
130
|
+
step,
|
|
131
|
+
ts.factory.createNumericLiteral(1),
|
|
132
|
+
)
|
|
133
|
+
return docCall(docName, 'replace', [buildPath(segments), next])
|
|
134
|
+
}
|
|
135
|
+
}
|
|
110
136
|
/* doc array `.push(a, b, …)` → one `add` patch per argument at the array's `-`
|
|
111
137
|
slot, matching native multi-arg push (each lands at the end in order). */
|
|
112
138
|
if (
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import { assertTranspiles } from './assertTranspiles.ts'
|
|
3
|
+
import { desugarSignals } from './desugarSignals.ts'
|
|
4
|
+
import { docAccessTransformer } from './lowerDocAccess.ts'
|
|
5
|
+
import { signalRefsTransformer } from './renameSignalRefs.ts'
|
|
6
|
+
import { stripEffectsTransformer } from './stripEffects.ts'
|
|
7
|
+
import { TS_PRINTER } from './TS_PRINTER.ts'
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
The component-script lowering, done in ONE parse. The script is parsed once, then
|
|
11
|
+
`desugarSignals` (signal declarations → `model` slots / `scope().derive`), reference
|
|
12
|
+
renaming (`count` → `model.count`), and doc-access lowering (`model.count` → patch/read)
|
|
13
|
+
run as a chained `ts.transform` over the SAME tree — each as a standalone string pass
|
|
14
|
+
would parse + reprint. desugar returns a transformer rather than rebuilt source text
|
|
15
|
+
precisely so it can chain here; the name sets it collects feed the rename transformer.
|
|
16
|
+
|
|
17
|
+
Imports are partitioned off the transformed tree structurally (`ts.isImportDeclaration`),
|
|
18
|
+
not by regex, so a multi-line import hoists correctly regardless of formatting. The
|
|
19
|
+
reassembled output is transpiled as a fail-loud guard: if a future un-handled rewrite
|
|
20
|
+
position corrupts the script (the failure mode the syntax fuzz corpus also guards), it
|
|
21
|
+
surfaces here as a located compile error instead of shipping a broken bundle.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export function lowerScript(scriptBody: string): {
|
|
25
|
+
body: string
|
|
26
|
+
imports: string
|
|
27
|
+
ssrBody: string
|
|
28
|
+
stateNames: Set<string>
|
|
29
|
+
derivedNames: Set<string>
|
|
30
|
+
computedNames: Set<string>
|
|
31
|
+
} {
|
|
32
|
+
const source = ts.createSourceFile('component.ts', scriptBody, ts.ScriptTarget.Latest, true)
|
|
33
|
+
const { transformer, stateNames, derivedNames, computedNames } = desugarSignals(source)
|
|
34
|
+
const result = ts.transform(source, [
|
|
35
|
+
transformer,
|
|
36
|
+
signalRefsTransformer(stateNames, derivedNames, computedNames),
|
|
37
|
+
docAccessTransformer('model'),
|
|
38
|
+
])
|
|
39
|
+
const transformed = result.transformed[0] as ts.SourceFile
|
|
40
|
+
/* Top-level imports must live at module scope, not inside the mount/render
|
|
41
|
+
function the script body becomes — hoist them off the tree. */
|
|
42
|
+
const importStatements = transformed.statements.filter(ts.isImportDeclaration)
|
|
43
|
+
const bodyStatements = transformed.statements.filter(
|
|
44
|
+
(statement) => !ts.isImportDeclaration(statement),
|
|
45
|
+
)
|
|
46
|
+
const imports = importStatements
|
|
47
|
+
.map((statement) => TS_PRINTER.printNode(ts.EmitHint.Unspecified, statement, transformed))
|
|
48
|
+
.join('\n')
|
|
49
|
+
const bodyFile = ts.factory.updateSourceFile(transformed, bodyStatements)
|
|
50
|
+
const body = TS_PRINTER.printFile(bodyFile).trim()
|
|
51
|
+
/* The SSR variant strips client-only `effect(...)` calls — run over the SAME lowered
|
|
52
|
+
tree (one extra transform + print, no reparse) instead of re-parsing the printed
|
|
53
|
+
script downstream. */
|
|
54
|
+
const ssrResult = ts.transform(bodyFile, [stripEffectsTransformer()])
|
|
55
|
+
const ssrBody = TS_PRINTER.printFile(ssrResult.transformed[0] as ts.SourceFile).trim()
|
|
56
|
+
ssrResult.dispose()
|
|
57
|
+
result.dispose()
|
|
58
|
+
|
|
59
|
+
assertTranspiles(
|
|
60
|
+
[imports, body].filter((part) => part !== '').join('\n'),
|
|
61
|
+
'component script lowering',
|
|
62
|
+
)
|
|
63
|
+
return { body, imports, ssrBody, stateNames, derivedNames, computedNames }
|
|
64
|
+
}
|