@barefootjs/jsx 0.19.1 → 0.21.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/dist/analyzer.d.ts.map +1 -1
- package/dist/builtin-lowering-plugins.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/date-lowering.d.ts +33 -0
- package/dist/date-lowering.d.ts.map +1 -0
- package/dist/index.js +663 -218
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/generate-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +14 -1
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/rich-type-evidence.d.ts +67 -0
- package/dist/rich-type-evidence.d.ts.map +1 -0
- package/dist/rich-type-refusal.d.ts +35 -0
- package/dist/rich-type-refusal.d.ts.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/client-js-generation.test.ts +59 -0
- package/src/__tests__/date-lowering.test.ts +232 -0
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +56 -0
- package/src/__tests__/rich-type-method-refusal.test.ts +323 -0
- package/src/analyzer.ts +21 -2
- package/src/builtin-lowering-plugins.ts +2 -1
- package/src/compiler.ts +3 -0
- package/src/date-lowering.ts +117 -0
- package/src/ir-to-client-js/emit-reactive.ts +103 -2
- package/src/ir-to-client-js/generate-init.ts +11 -4
- package/src/ir-to-client-js/imports.ts +4 -0
- package/src/ir-to-client-js/index.ts +2 -0
- package/src/ir-to-client-js/reactivity.ts +13 -2
- package/src/ir-to-client-js/types.ts +15 -0
- package/src/jsx-to-ir.ts +128 -3
- package/src/rich-type-evidence.ts +159 -0
- package/src/rich-type-refusal.ts +311 -0
- package/src/types.ts +9 -0
package/src/compiler.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { applyCssLayerPrefix } from './css-layer-prefixer.ts'
|
|
|
24
24
|
import { preprocessInlineJsxCallbacks } from './preprocess-inline-jsx-callbacks.ts'
|
|
25
25
|
import { extractSsrDefaults } from './ssr-defaults.ts'
|
|
26
26
|
import { computeSsrSeedPlan } from './ssr-seed-plan.ts'
|
|
27
|
+
import { checkRichTypeMethodCalls } from './rich-type-refusal.ts'
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Extended compile options with required adapter
|
|
@@ -137,6 +138,7 @@ function compileMultipleComponents(
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR)
|
|
141
|
+
checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors)
|
|
140
142
|
|
|
141
143
|
if (options.cssLayerPrefix) {
|
|
142
144
|
applyCssLayerPrefix(componentIR, options.cssLayerPrefix)
|
|
@@ -615,6 +617,7 @@ export function compileJSX(
|
|
|
615
617
|
|
|
616
618
|
// Pre-compute client JS analysis for adapter optimization
|
|
617
619
|
componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR)
|
|
620
|
+
checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors)
|
|
618
621
|
|
|
619
622
|
// Cross-file @client signal sources: identify which import sources
|
|
620
623
|
// need `.client.js` path rewriting in the client bundle.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Date` lowering plugin (#2274) — the first catalogued entry in the rich-type
|
|
3
|
+
* lowering seam #2273 left open (`rich-type-refusal.ts`'s module doc: "the
|
|
4
|
+
* seam #2274 and later plugins use to catalogue a rich-type API without
|
|
5
|
+
* touching this module"). A zero-arg call to one of `DATE_METHODS` on a
|
|
6
|
+
* receiver resolved (via `resolveReceiverType`) to a `Date`-typed prop lowers
|
|
7
|
+
* to a backend-neutral `helper-call` node on the `date` helper
|
|
8
|
+
* (`date(recv, op)`, spec/template-helpers.md); every adapter already renders
|
|
9
|
+
* `helper-call` generically (#2069), so no adapter-specific code is needed
|
|
10
|
+
* here — only the runtime `date` helper each backend ships.
|
|
11
|
+
*
|
|
12
|
+
* v1 scope is deliberately prop-rooted only: the matcher resolves receivers
|
|
13
|
+
* with `EMPTY_BINDINGS` (no loop-item / arrow-param context), mirroring
|
|
14
|
+
* `rich-type-refusal.ts`'s own top-level walk. A loop item's `.toISOString()`
|
|
15
|
+
* therefore stays BF021-refused rather than silently falling back to a
|
|
16
|
+
* generic (and wrong) lowering — widening to loop bindings is a deliberate
|
|
17
|
+
* future step, not an oversight.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { IRMetadata, TypeInfo } from './types.ts'
|
|
21
|
+
import type { ParsedExpr } from './expression-parser.ts'
|
|
22
|
+
import type { LoweringNode, LoweringPlugin } from './lowering-registry.ts'
|
|
23
|
+
import { resolveReceiverType, baseTypeName, stripUnion, derefNamedType } from './rich-type-evidence.ts'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Host rich-type names (`rich-type-evidence.ts`'s `HOST_RICH_TYPE_NAMES`)
|
|
27
|
+
* that additionally have a catalogued lowering — currently just `Date`.
|
|
28
|
+
* `analyzer.ts`'s `collectMemberTypes` widening keys off this set (not
|
|
29
|
+
* `HOST_RICH_TYPE_NAMES` wholesale) so a destructured prop only gets a real
|
|
30
|
+
* TypeInfo when a plugin actually exists to consume it; adding a future
|
|
31
|
+
* lowering (`Map`, …) is a one-line addition here, not a second gate to keep
|
|
32
|
+
* in sync.
|
|
33
|
+
*/
|
|
34
|
+
export const CATALOGUED_RICH_TYPE_NAMES: ReadonlySet<string> = new Set(['Date'])
|
|
35
|
+
|
|
36
|
+
/** Zero-arg `Date.prototype` methods the `date` helper catalogues (spec/template-helpers.md). */
|
|
37
|
+
export const DATE_METHODS: ReadonlySet<string> = new Set([
|
|
38
|
+
'getUTCFullYear',
|
|
39
|
+
'getUTCMonth',
|
|
40
|
+
'getUTCDate',
|
|
41
|
+
'getUTCHours',
|
|
42
|
+
'getUTCMinutes',
|
|
43
|
+
'getUTCSeconds',
|
|
44
|
+
'getTime',
|
|
45
|
+
'toISOString',
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
type Bindings = ReadonlyMap<string, TypeInfo | null>
|
|
49
|
+
const EMPTY_BINDINGS: Bindings = new Map()
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether `type` (or a property reachable from it through non-array,
|
|
53
|
+
* non-computed member chains) can ever resolve to a `Date` — the same
|
|
54
|
+
* receiver shapes `resolveReceiverType` walks for an actual call site. Used
|
|
55
|
+
* only as `prepare`'s cheap activation gate: a `false` here must never miss a
|
|
56
|
+
* receiver the matcher could later prove Date-typed (that would silently drop
|
|
57
|
+
* the lowering and mis-flag BF021), so this over-approximates by recursing
|
|
58
|
+
* into every nested object-shaped property, not just direct ones.
|
|
59
|
+
*
|
|
60
|
+
* `seen` guards a self-referential named type (`interface Tree { self: Tree
|
|
61
|
+
* }`) from an infinite walk; keyed by the type's own name so two distinct
|
|
62
|
+
* properties of the SAME named type aren't short-circuited against each
|
|
63
|
+
* other.
|
|
64
|
+
*/
|
|
65
|
+
function typeReachesDate(type: TypeInfo | null, meta: IRMetadata, seen: Set<string>): boolean {
|
|
66
|
+
const stripped = stripUnion(type)
|
|
67
|
+
if (!stripped) return false
|
|
68
|
+
// `kind: 'object'` is an INLINE type literal (`{ createdAt: Date }` — the
|
|
69
|
+
// shape `propsType` itself takes for the common case) and already carries
|
|
70
|
+
// `properties` directly; `kind: 'interface'` is a NAMED reference (`Date`
|
|
71
|
+
// itself, or a local `interface Props { … }`) that needs the Date-name
|
|
72
|
+
// check plus `derefNamedType` to reach its member list. Anything else
|
|
73
|
+
// (primitive/array/union/…) is a dead end.
|
|
74
|
+
if (stripped.kind === 'interface') {
|
|
75
|
+
const name = baseTypeName(stripped.raw)
|
|
76
|
+
if (name === 'Date') return true
|
|
77
|
+
if (seen.has(name)) return false
|
|
78
|
+
seen.add(name)
|
|
79
|
+
} else if (stripped.kind !== 'object') {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
const deref = derefNamedType(stripped, meta)
|
|
83
|
+
if (!deref.properties) return false
|
|
84
|
+
return deref.properties.some((p) => typeReachesDate(p.type, meta, seen))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `datePlugin`'s matcher: recognises `<Date-typed receiver>.<method>()` per
|
|
89
|
+
* the module doc, or declines (null) for anything else — a non-member
|
|
90
|
+
* callee, a non-catalogued method name, a call with arguments (every
|
|
91
|
+
* catalogued method is zero-arg on `Date.prototype`), or a receiver that
|
|
92
|
+
* doesn't resolve to `Date` (unknown, a different host rich type, or a
|
|
93
|
+
* same-named local `typeDefinitions` entry shadowing the built-in — mirrors
|
|
94
|
+
* `rich-type-refusal.ts`'s `inFileShadow` check).
|
|
95
|
+
*/
|
|
96
|
+
function matchDateCall(callee: ParsedExpr, args: readonly ParsedExpr[], metadata: IRMetadata): LoweringNode | null {
|
|
97
|
+
if (callee.kind !== 'member' || callee.computed) return null
|
|
98
|
+
if (args.length !== 0 || !DATE_METHODS.has(callee.property)) return null
|
|
99
|
+
const receiverType = resolveReceiverType(callee.object, metadata, EMPTY_BINDINGS)
|
|
100
|
+
if (!receiverType || receiverType.kind !== 'interface') return null
|
|
101
|
+
const typeName = baseTypeName(receiverType.raw)
|
|
102
|
+
if (typeName !== 'Date') return null
|
|
103
|
+
if (metadata.typeDefinitions.some((d) => d.name === typeName)) return null
|
|
104
|
+
return {
|
|
105
|
+
kind: 'helper-call',
|
|
106
|
+
helper: 'date',
|
|
107
|
+
args: [callee.object, { kind: 'literal', value: callee.property, literalType: 'string' }],
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const datePlugin: LoweringPlugin = {
|
|
112
|
+
name: 'date',
|
|
113
|
+
prepare(metadata) {
|
|
114
|
+
if (!metadata.propsType || !typeReachesDate(metadata.propsType, metadata, new Set())) return null
|
|
115
|
+
return (callee, args) => matchDateCall(callee, args, metadata)
|
|
116
|
+
},
|
|
117
|
+
}
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
* client-only expressions, and reactive component prop bindings.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import ts from 'typescript'
|
|
8
|
+
import type { AttrMeta, IRMetadata } from '../types.ts'
|
|
8
9
|
import { isBooleanAttr } from '../html-constants.ts'
|
|
9
10
|
import type { ClientJsContext } from './types.ts'
|
|
10
11
|
import { toHtmlAttrName, varSlotId, PROPS_PARAM } from './utils.ts'
|
|
11
12
|
import { createTemplateAwareStringProtector } from './html-template.ts'
|
|
13
|
+
import { datePlugin, DATE_METHODS } from '../date-lowering.ts'
|
|
14
|
+
import { tsNodeToParsedExpr } from '../expression-parser.ts'
|
|
15
|
+
import type { LoweringMatcher } from '../lowering-registry.ts'
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* Profile mode (#1690, SR3/SR4): the id appended to a DOM-binding effect so the
|
|
@@ -95,8 +99,104 @@ export function rewriteDestructuredPropsInExpr(expr: string, ctx: ClientJsContex
|
|
|
95
99
|
return restore(result)
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Bind `datePlugin`'s matcher (#2292) for this component's reactive-text
|
|
104
|
+
* emission. Reads the same four `EvidenceMetadata` fields
|
|
105
|
+
* (`rich-type-evidence.ts`) `jsx-to-ir.ts`'s `getDateLoweringMatcher` reads
|
|
106
|
+
* off the analyzer for the STATIC template path, so a prop-method call
|
|
107
|
+
* re-evaluated inside a `createEffect` lowers to the `date` helper under
|
|
108
|
+
* the exact same evidence the static template and every SSR adapter use.
|
|
109
|
+
* `ctx.propsType` is optional on `ClientJsContext` (threaded through only
|
|
110
|
+
* for this purpose, `index.ts`'s `createContext`); a context predating
|
|
111
|
+
* #2292 — or a hand-built test fixture — simply carries no Date evidence,
|
|
112
|
+
* so this returns null and callers emit the expression unchanged.
|
|
113
|
+
*/
|
|
114
|
+
function getReactiveDateLoweringMatcher(ctx: ClientJsContext): LoweringMatcher | null {
|
|
115
|
+
if (!ctx.propsType) return null
|
|
116
|
+
const metadataSlice: Pick<IRMetadata, 'propsType' | 'propsObjectName' | 'propsParams' | 'typeDefinitions'> = {
|
|
117
|
+
propsType: ctx.propsType,
|
|
118
|
+
propsObjectName: ctx.propsObjectName,
|
|
119
|
+
propsParams: ctx.propsParams,
|
|
120
|
+
typeDefinitions: ctx.typeDefinitions ?? [],
|
|
121
|
+
}
|
|
122
|
+
return datePlugin.prepare(metadataSlice as unknown as IRMetadata)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Reactive-path counterpart to `jsx-to-ir.ts`'s `lowerDateCalls` (#2292):
|
|
127
|
+
* without this, a Date-typed prop's catalogued accessor call re-evaluated
|
|
128
|
+
* inside `createEffect` (the Solid-style wrap-by-default fallback for any
|
|
129
|
+
* expression containing a function call, #937) still called the RAW
|
|
130
|
+
* `.toISOString()` / etc. on the hydrated STRING prop value and threw —
|
|
131
|
+
* the static template alone wasn't enough to fix hydration.
|
|
132
|
+
*
|
|
133
|
+
* `expr` here (`IRExpression.expr`, threaded through
|
|
134
|
+
* `ctx.dynamicElements`) is ALREADY the bare-identifier source form the
|
|
135
|
+
* SAME `createEffect` body closes over via the destructured-prop shim
|
|
136
|
+
* (`const createdAt = _p.createdAt ?? {}` at the top of `init()`) — so
|
|
137
|
+
* unlike the static-template path, the receiver does NOT need a `_p.`
|
|
138
|
+
* prefix: swapping the raw call for `date(<receiver>, "<op>")` is enough.
|
|
139
|
+
*
|
|
140
|
+
* No live `ts.Node` survives into this phase (`expr` is a plain string),
|
|
141
|
+
* so this re-parses it fresh via `ts.createSourceFile` — the same
|
|
142
|
+
* technique `expression-parser.ts`'s `parseExpression` uses — rather than
|
|
143
|
+
* a regex scan (per CLAUDE.md's structural-parsing rule): every candidate
|
|
144
|
+
* span comes from walking the freshly-parsed AST, and `matcher(...)` is
|
|
145
|
+
* the SAME `datePlugin` matcher the static path and every SSR adapter
|
|
146
|
+
* bind, so a call lowers here iff it would lower there too.
|
|
147
|
+
*/
|
|
148
|
+
function lowerDateCallsInReactiveExpr(expr: string, matcher: LoweringMatcher | null): string {
|
|
149
|
+
if (!matcher) return expr
|
|
150
|
+
let sourceFile: ts.SourceFile
|
|
151
|
+
try {
|
|
152
|
+
sourceFile = ts.createSourceFile('__reactive_expr__.ts', `(${expr});`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
153
|
+
} catch {
|
|
154
|
+
return expr
|
|
155
|
+
}
|
|
156
|
+
const stmt = sourceFile.statements[0]
|
|
157
|
+
if (!stmt || !ts.isExpressionStatement(stmt)) return expr
|
|
158
|
+
const root = ts.isParenthesizedExpression(stmt.expression) ? stmt.expression.expression : stmt.expression
|
|
159
|
+
|
|
160
|
+
const candidates: ts.CallExpression[] = []
|
|
161
|
+
const visit = (n: ts.Node): void => {
|
|
162
|
+
if (
|
|
163
|
+
ts.isCallExpression(n) &&
|
|
164
|
+
n.arguments.length === 0 &&
|
|
165
|
+
ts.isPropertyAccessExpression(n.expression) &&
|
|
166
|
+
!n.expression.questionDotToken &&
|
|
167
|
+
DATE_METHODS.has(n.expression.name.text)
|
|
168
|
+
) {
|
|
169
|
+
candidates.push(n)
|
|
170
|
+
}
|
|
171
|
+
ts.forEachChild(n, visit)
|
|
172
|
+
}
|
|
173
|
+
visit(root)
|
|
174
|
+
if (candidates.length === 0) return expr
|
|
175
|
+
|
|
176
|
+
// Template-aware: protect quoted strings AND template-literal static
|
|
177
|
+
// segments (leaving `${…}` interpolations exposed) so the non-global
|
|
178
|
+
// `.replace` can't rewrite a backtick constant that coincidentally
|
|
179
|
+
// matches the call text before the real call site (Copilot review, #2294).
|
|
180
|
+
const { protect, restore } = createTemplateAwareStringProtector()
|
|
181
|
+
let result = protect(expr)
|
|
182
|
+
for (const call of candidates) {
|
|
183
|
+
const propAccess = call.expression as ts.PropertyAccessExpression
|
|
184
|
+
const node = matcher(tsNodeToParsedExpr(propAccess), [])
|
|
185
|
+
if (!node || node.kind !== 'helper-call' || node.helper !== 'date') continue
|
|
186
|
+
const op = propAccess.name.text
|
|
187
|
+
const receiverText = propAccess.expression.getText(sourceFile)
|
|
188
|
+
const matchText = call.getText(sourceFile)
|
|
189
|
+
// Replacer-function form: a `$` sequence in `receiverText` would
|
|
190
|
+
// otherwise be reinterpreted as a `String.replace` pattern token and
|
|
191
|
+
// corrupt the output (repo precedent, #2285).
|
|
192
|
+
result = result.replace(matchText, () => `date(${receiverText}, "${op}")`)
|
|
193
|
+
}
|
|
194
|
+
return restore(result)
|
|
195
|
+
}
|
|
196
|
+
|
|
98
197
|
/** Emit createEffect blocks that update text nodes for reactive expressions. */
|
|
99
198
|
export function emitDynamicTextUpdates(lines: string[], ctx: ClientJsContext): void {
|
|
199
|
+
const dateLoweringMatcher = getReactiveDateLoweringMatcher(ctx)
|
|
100
200
|
// Group elements by expression to consolidate effects with same dependencies
|
|
101
201
|
const byExpression = new Map<string, typeof ctx.dynamicElements>()
|
|
102
202
|
for (const elem of ctx.dynamicElements) {
|
|
@@ -107,7 +207,8 @@ export function emitDynamicTextUpdates(lines: string[], ctx: ClientJsContext): v
|
|
|
107
207
|
byExpression.get(key)!.push(elem)
|
|
108
208
|
}
|
|
109
209
|
|
|
110
|
-
for (const [
|
|
210
|
+
for (const [rawExpr, elems] of byExpression) {
|
|
211
|
+
const expr = lowerDateCallsInReactiveExpr(rawExpr, dateLoweringMatcher)
|
|
111
212
|
// Separate conditional vs non-conditional elements
|
|
112
213
|
const conditionalElems = elems.filter(e => e.insideConditional)
|
|
113
214
|
const normalElems = elems.filter(e => !e.insideConditional)
|
|
@@ -99,16 +99,23 @@ export function generateInitFunction(
|
|
|
99
99
|
let generatedCode = rewritePropsObjectRef(lines.join('\n'), ctx.propsObjectName)
|
|
100
100
|
generatedCode += '\n' + hydrateLine
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
// Substitute module-level declarations BEFORE import detection: a
|
|
103
|
+
// module-level helper's body (e.g. `buildSheetVMs` calling
|
|
104
|
+
// `computeSheetGeometry`) only exists in `moduleConstantsCode`, so
|
|
105
|
+
// scanning `generatedCode` first would miss any import referenced
|
|
106
|
+
// only from that body and silently drop it (#2283).
|
|
103
107
|
const moduleConstantsCode = emitModuleLevelDeclarations(
|
|
104
108
|
classification.moduleLevelConstants,
|
|
105
109
|
classification.moduleLevelFunctions,
|
|
106
110
|
classification.moduleLevelSignals,
|
|
107
111
|
classification.moduleLevelMemos,
|
|
108
112
|
)
|
|
113
|
+
// Replacer-function form: a plain replacement string would let literal
|
|
114
|
+
// `$&`/`$1`/`$$` sequences in user helper bodies or import paths be
|
|
115
|
+
// reinterpreted by `String.replace`'s special-pattern handling.
|
|
116
|
+
const codeWithModuleConstants = generatedCode.replace(MODULE_CONSTANTS_PLACEHOLDER, () => moduleConstantsCode)
|
|
117
|
+
const allImportLines = resolveFinalImports(codeWithModuleConstants, ir, localImportPrefixes)
|
|
109
118
|
|
|
110
|
-
return
|
|
111
|
-
.replace(IMPORT_PLACEHOLDER, allImportLines)
|
|
112
|
-
.replace(MODULE_CONSTANTS_PLACEHOLDER, moduleConstantsCode)
|
|
119
|
+
return codeWithModuleConstants.replace(IMPORT_PLACEHOLDER, () => allImportLines)
|
|
113
120
|
}
|
|
114
121
|
|
|
@@ -17,6 +17,10 @@ export const RUNTIME_IMPORT_CANDIDATES = [
|
|
|
17
17
|
'tAfter',
|
|
18
18
|
// Profile mode (#1690, SR3) — turn-boundary markers around event handlers.
|
|
19
19
|
'beginTurn', 'endTurn',
|
|
20
|
+
// Catalogued `Date` lowering (#2274/#2292) — the client counterpart to
|
|
21
|
+
// every SSR adapter's `date` runtime helper (`date-lowering.ts`'s
|
|
22
|
+
// `datePlugin`).
|
|
23
|
+
'date',
|
|
20
24
|
] as const
|
|
21
25
|
|
|
22
26
|
/** @deprecated Use RUNTIME_IMPORT_CANDIDATES */
|
|
@@ -175,6 +175,8 @@ function createContext(
|
|
|
175
175
|
propsParams: ir.metadata.propsParams,
|
|
176
176
|
propsObjectName: ir.metadata.propsObjectName,
|
|
177
177
|
restPropsName: ir.metadata.restPropsName,
|
|
178
|
+
propsType: ir.metadata.propsType,
|
|
179
|
+
typeDefinitions: ir.metadata.typeDefinitions,
|
|
178
180
|
|
|
179
181
|
interactiveElements: [],
|
|
180
182
|
dynamicElements: [],
|
|
@@ -529,8 +529,19 @@ export function collectLoopChildReactiveTexts(
|
|
|
529
529
|
const originFreeIds = freeIdsFromRefs(n.origin?.freeRefs)
|
|
530
530
|
const expanded = expandConstantForReactivity(n.expr, ctx, originFreeIds)
|
|
531
531
|
// Include if expression reads signals OR references the loop parameter
|
|
532
|
-
// (loop param becomes a signal accessor via per-item signals).
|
|
533
|
-
|
|
532
|
+
// (loop param becomes a signal accessor via per-item signals). Falls
|
|
533
|
+
// back to the Solid-style AST-flag wrap decision — mirroring
|
|
534
|
+
// `collectLoopChildReactiveAttrs`'s `callsReactiveGetters` /
|
|
535
|
+
// `hasFunctionCalls` fallback (#1673) and the top-level text path's
|
|
536
|
+
// `decideWrapFromAstFlags` gate (`collectElements`'s `expression`
|
|
537
|
+
// handler) — so a loop-item text read through an opaque helper
|
|
538
|
+
// (`textAt(i)` where `const textAt = (i) => rows()[i]`, which
|
|
539
|
+
// `classifyReactivity` can't see through) still gets an update
|
|
540
|
+
// effect instead of silently freezing at its SSR value (#2282).
|
|
541
|
+
const reactive =
|
|
542
|
+
classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind !== 'none'
|
|
543
|
+
|| decideWrapFromAstFlags(n).wrap
|
|
544
|
+
if (!reactive) return
|
|
534
545
|
texts.push({
|
|
535
546
|
slotId: n.slotId,
|
|
536
547
|
expression: expanded.expr,
|
|
@@ -20,6 +20,8 @@ import type {
|
|
|
20
20
|
ParamInfo,
|
|
21
21
|
CompilerError,
|
|
22
22
|
ImportInfo,
|
|
23
|
+
TypeInfo,
|
|
24
|
+
TypeDefinition,
|
|
23
25
|
} from '../types.ts'
|
|
24
26
|
import type { CsrInlinabilityMap } from './csr-substitute.ts'
|
|
25
27
|
import type { SkeletonSlotPaths } from './html-template.ts'
|
|
@@ -67,6 +69,19 @@ export interface ClientJsContext {
|
|
|
67
69
|
propsParams: ParamInfo[]
|
|
68
70
|
propsObjectName: string | null
|
|
69
71
|
restPropsName: string | null
|
|
72
|
+
/**
|
|
73
|
+
* Threaded through (alongside `propsParams` above) so the reactive-effect
|
|
74
|
+
* emitter can bind its own `datePlugin` matcher (#2292) — see
|
|
75
|
+
* `emit-reactive.ts`'s `getReactiveDateLoweringMatcher`. Mirrors the
|
|
76
|
+
* `EvidenceMetadata` slice (`rich-type-evidence.ts`) the SSR adapters and
|
|
77
|
+
* `jsx-to-ir.ts`'s static-template lowering both consult; nothing else in
|
|
78
|
+
* this file reads a Date-typed prop's shape. Optional (rather than a hard
|
|
79
|
+
* requirement alongside `propsParams`) so existing hand-built
|
|
80
|
+
* `ClientJsContext` test fixtures that predate #2292 keep type-checking
|
|
81
|
+
* without every call site listing these two fields.
|
|
82
|
+
*/
|
|
83
|
+
propsType?: TypeInfo | null
|
|
84
|
+
typeDefinitions?: TypeDefinition[]
|
|
70
85
|
|
|
71
86
|
// Collected elements
|
|
72
87
|
interactiveElements: InteractiveElement[]
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type SourceLocation,
|
|
31
31
|
type TypeInfo,
|
|
32
32
|
type OriginInfo,
|
|
33
|
+
type IRMetadata,
|
|
33
34
|
isReactiveOrigin,
|
|
34
35
|
AttrValueOf,
|
|
35
36
|
} from './types.ts'
|
|
@@ -46,6 +47,9 @@ import {
|
|
|
46
47
|
} from './prop-rewrite.ts'
|
|
47
48
|
import { resolveFreeRefs, isNameBound as isNameBoundInEnv, type BindingEnvironment } from './free-refs.ts'
|
|
48
49
|
import { computeFileScope } from './ir-to-client-js/component-scope.ts'
|
|
50
|
+
import { createTemplateAwareStringProtector } from './ir-to-client-js/html-template.ts'
|
|
51
|
+
import { datePlugin, DATE_METHODS } from './date-lowering.ts'
|
|
52
|
+
import type { LoweringMatcher } from './lowering-registry.ts'
|
|
49
53
|
import { extractFreeIdentifiersFromNode, initializerShapeContainsJsx } from './analyzer.ts'
|
|
50
54
|
import { iterateJsTokens, replaceInExprContexts } from './scanner/js-scanner.ts'
|
|
51
55
|
import { toHTMLAttrName, decodeEntities } from '@barefootjs/shared'
|
|
@@ -164,6 +168,13 @@ interface TransformContext {
|
|
|
164
168
|
* aliased `import { Async as Boundary }` maps `<Boundary>` to the built-in.
|
|
165
169
|
*/
|
|
166
170
|
_clientBuiltinTags?: Map<string, ClientBuiltinTag>
|
|
171
|
+
/**
|
|
172
|
+
* Cached `datePlugin` matcher (#2292), bound once to this component's
|
|
173
|
+
* metadata. `undefined` = not yet computed; `null` = computed and
|
|
174
|
+
* inactive (this component's props never reach a `Date`, `datePlugin`'s
|
|
175
|
+
* own `prepare` gate). See `getDateLoweringMatcher`.
|
|
176
|
+
*/
|
|
177
|
+
_dateLoweringMatcher?: LoweringMatcher | null
|
|
167
178
|
}
|
|
168
179
|
|
|
169
180
|
/**
|
|
@@ -285,14 +296,128 @@ function exprHasFunctionCalls(expr: ts.Expression): boolean {
|
|
|
285
296
|
return found
|
|
286
297
|
}
|
|
287
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Bind (and cache on `ctx`) `datePlugin`'s matcher (#2292) for this
|
|
301
|
+
* component. Reuses the SAME `LoweringPlugin` the SSR adapters bind via
|
|
302
|
+
* `prepareLoweringMatchers` — not a re-implementation of its receiver-type
|
|
303
|
+
* resolution — so a call lowers on the client iff `datePlugin` would lower
|
|
304
|
+
* it on the SSR path (parity is mandatory per #2292).
|
|
305
|
+
*
|
|
306
|
+
* `datePlugin.prepare` only reads `propsType` / `propsObjectName` /
|
|
307
|
+
* `propsParams` / `typeDefinitions` off its `IRMetadata` parameter (see
|
|
308
|
+
* `rich-type-evidence.ts`'s `EvidenceMetadata` — the `Pick` of exactly
|
|
309
|
+
* those four fields). `ctx.analyzer` carries live, fully-populated values
|
|
310
|
+
* for all four by the time any expression is transformed (analysis runs
|
|
311
|
+
* to completion before `jsxToIR`'s AST walk begins), so a slice of just
|
|
312
|
+
* those fields is sufficient — the cast bridges that narrower shape to
|
|
313
|
+
* the wider `IRMetadata` parameter type every lowering plugin declares.
|
|
314
|
+
*/
|
|
315
|
+
function getDateLoweringMatcher(ctx: TransformContext): LoweringMatcher | null {
|
|
316
|
+
if (ctx._dateLoweringMatcher === undefined) {
|
|
317
|
+
const a = ctx.analyzer
|
|
318
|
+
const metadataSlice: Pick<IRMetadata, 'propsType' | 'propsObjectName' | 'propsParams' | 'typeDefinitions'> = {
|
|
319
|
+
propsType: a.propsType,
|
|
320
|
+
propsObjectName: a.propsObjectName,
|
|
321
|
+
propsParams: a.propsParams,
|
|
322
|
+
typeDefinitions: a.typeDefinitions,
|
|
323
|
+
}
|
|
324
|
+
ctx._dateLoweringMatcher = datePlugin.prepare(metadataSlice as unknown as IRMetadata)
|
|
325
|
+
}
|
|
326
|
+
return ctx._dateLoweringMatcher
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Client-side counterpart to `datePlugin` (#2274 was SSR-only; #2292
|
|
331
|
+
* closes the gap). The client emitter (`ir-to-client-js/`) emits raw,
|
|
332
|
+
* prop-rewritten source strings and never consults the lowering registry
|
|
333
|
+
* (its module doc) — so left alone, a Date-typed prop's catalogued
|
|
334
|
+
* accessor call leaks through as `_p.createdAt.toISOString()`, which
|
|
335
|
+
* throws at hydration: props are JSON round-tripped with no type-aware
|
|
336
|
+
* revival (`hydrate.ts`'s `parseProps`), so the prop arrives as its ISO
|
|
337
|
+
* string, not a `Date` instance.
|
|
338
|
+
*
|
|
339
|
+
* Walks `expr`'s AST for zero-arg calls to a `DATE_METHODS` name (a cheap
|
|
340
|
+
* syntactic pre-filter) and confirms each candidate against the SAME
|
|
341
|
+
* `datePlugin` matcher the SSR adapters use, via `tsNodeToParsedExpr` —
|
|
342
|
+
* the identical receiver-type resolution, not a re-implementation, so a
|
|
343
|
+
* call lowers here iff it would lower on the SSR path. A match splices
|
|
344
|
+
* `date(<receiver>, "<op>")` in place of the raw call; `imports.ts`'s
|
|
345
|
+
* `detectUsedImports` regex-scans the emitted `date(` call against
|
|
346
|
+
* `RUNTIME_IMPORT_CANDIDATES` to auto-import the runtime helper.
|
|
347
|
+
*
|
|
348
|
+
* The splice is a plain (non-global) text `.replace` per candidate, run
|
|
349
|
+
* in AST (left-to-right, source) order — not a JS-parsing regex, since
|
|
350
|
+
* every candidate span comes from walking `expr`'s real AST first. This
|
|
351
|
+
* is safe specifically because any call the matcher accepts has, by
|
|
352
|
+
* construction, no TS-only syntax anywhere in its own span: the matcher
|
|
353
|
+
* only resolves evidence through a bare identifier or a non-computed
|
|
354
|
+
* member chain (`resolveReceiverType`'s two supported `ParsedExpr`
|
|
355
|
+
* shapes), and the call itself takes zero arguments. So `ctx.getJS` of
|
|
356
|
+
* that one sub-node — which strips only type syntax — is guaranteed
|
|
357
|
+
* byte-identical to its raw source slice, and thus guaranteed to appear
|
|
358
|
+
* verbatim as a contiguous substring of `text` regardless of unrelated
|
|
359
|
+
* type-stripping elsewhere in the enclosing expression. String spans in
|
|
360
|
+
* `text` are protected first (`createTemplateAwareStringProtector` — both
|
|
361
|
+
* quoted strings AND template-literal *static* segments, leaving `${…}`
|
|
362
|
+
* interpolations exposed) so a coincidentally-identical string constant —
|
|
363
|
+
* e.g. a backtick `` `createdAt.toISOString()` `` sitting before the real
|
|
364
|
+
* call — can never be mistaken for a call site by the non-global
|
|
365
|
+
* `.replace`.
|
|
366
|
+
*/
|
|
367
|
+
function lowerDateCalls(text: string, expr: ts.Node, ctx: TransformContext): string {
|
|
368
|
+
const matcher = getDateLoweringMatcher(ctx)
|
|
369
|
+
if (!matcher) return text
|
|
370
|
+
|
|
371
|
+
const candidates: ts.CallExpression[] = []
|
|
372
|
+
function visit(n: ts.Node) {
|
|
373
|
+
if (
|
|
374
|
+
ts.isCallExpression(n) &&
|
|
375
|
+
n.arguments.length === 0 &&
|
|
376
|
+
ts.isPropertyAccessExpression(n.expression) &&
|
|
377
|
+
!n.expression.questionDotToken &&
|
|
378
|
+
DATE_METHODS.has(n.expression.name.text)
|
|
379
|
+
) {
|
|
380
|
+
candidates.push(n)
|
|
381
|
+
}
|
|
382
|
+
ts.forEachChild(n, visit)
|
|
383
|
+
}
|
|
384
|
+
visit(expr)
|
|
385
|
+
if (candidates.length === 0) return text
|
|
386
|
+
|
|
387
|
+
const { protect, restore } = createTemplateAwareStringProtector()
|
|
388
|
+
let result = protect(text)
|
|
389
|
+
for (const call of candidates) {
|
|
390
|
+
const propAccess = call.expression as ts.PropertyAccessExpression
|
|
391
|
+
const node = matcher(tsNodeToParsedExpr(propAccess), [])
|
|
392
|
+
if (!node || node.kind !== 'helper-call' || node.helper !== 'date') continue
|
|
393
|
+
const op = propAccess.name.text
|
|
394
|
+
const receiverText = ctx.getJS(propAccess.expression)
|
|
395
|
+
const matchText = ctx.getJS(call)
|
|
396
|
+
// Replacer-function form: a `$` sequence in `receiverText` (a prop named
|
|
397
|
+
// `$1`, say) would otherwise be reinterpreted as a `String.replace`
|
|
398
|
+
// pattern token and corrupt the output (repo precedent, #2285).
|
|
399
|
+
result = result.replace(matchText, () => `date(${receiverText}, "${op}")`)
|
|
400
|
+
}
|
|
401
|
+
return restore(result)
|
|
402
|
+
}
|
|
403
|
+
|
|
288
404
|
/**
|
|
289
405
|
* Rewrite bare destructured prop references in expression text.
|
|
290
406
|
* Thin wrapper that caches prop names on ctx and delegates to the shared core.
|
|
291
407
|
* Returns undefined if no rewriting is needed (SolidJS-style or no props).
|
|
292
408
|
*/
|
|
293
409
|
function rewriteBarePropRefs(text: string, expr: ts.Node, ctx: TransformContext): string | undefined {
|
|
410
|
+
// #2292: lower a Date-typed prop's catalogued accessor call BEFORE the
|
|
411
|
+
// bare-prop-name rewrite below, so the receiver identifier still picks
|
|
412
|
+
// up the usual `_p.` prefix (destructured mode) or falls through to the
|
|
413
|
+
// CSR template emitter's separate `props.` → `_p.` rewrite
|
|
414
|
+
// (`html-template.ts`'s `transformExpr`, props-object mode). Runs
|
|
415
|
+
// unconditionally — ahead of the `propNames` gate — because Date
|
|
416
|
+
// evidence comes from `ctx.analyzer.propsType`, independent of whether
|
|
417
|
+
// this component destructures its props.
|
|
418
|
+
const dateLowered = lowerDateCalls(text, expr, ctx)
|
|
294
419
|
let propNames = getDestructuredPropNames(ctx)
|
|
295
|
-
if (!propNames) return undefined
|
|
420
|
+
if (!propNames) return dateLowered === text ? undefined : dateLowered
|
|
296
421
|
// #2222: a name bound as an enclosing loop callback's item/index param
|
|
297
422
|
// refers to the loop binding, not the prop, at THIS transform position —
|
|
298
423
|
// `ctx.loopParams` is the live loop-param set (destructured binding
|
|
@@ -303,7 +428,7 @@ function rewriteBarePropRefs(text: string, expr: ts.Node, ctx: TransformContext)
|
|
|
303
428
|
// its set on ctx and must not be mutated.
|
|
304
429
|
if (ctx.loopParams.size > 0) {
|
|
305
430
|
const filtered = new Set([...propNames].filter(n => !ctx.loopParams.has(n)))
|
|
306
|
-
if (filtered.size === 0) return undefined
|
|
431
|
+
if (filtered.size === 0) return dateLowered === text ? undefined : dateLowered
|
|
307
432
|
propNames = filtered
|
|
308
433
|
}
|
|
309
434
|
// #1425: union any prop refs that `expr` reaches via branch-local
|
|
@@ -316,7 +441,7 @@ function rewriteBarePropRefs(text: string, expr: ts.Node, ctx: TransformContext)
|
|
|
316
441
|
// `_branchScopePropDeps` at branch entry; here we just walk `expr`
|
|
317
442
|
// for references to those locals and union the matching dep sets.
|
|
318
443
|
const extraPropRefs = collectBranchLocalPropRefsViaSubstitution(expr, ctx)
|
|
319
|
-
return rewriteBarePropRefsCore(
|
|
444
|
+
return rewriteBarePropRefsCore(dateLowered, expr, propNames, extraPropRefs)
|
|
320
445
|
}
|
|
321
446
|
|
|
322
447
|
/**
|