@barefootjs/jsx 0.20.0 → 0.21.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/dist/analyzer.d.ts.map +1 -1
- package/dist/builtin-lowering-plugins.d.ts.map +1 -1
- package/dist/date-lowering.d.ts +33 -0
- package/dist/date-lowering.d.ts.map +1 -0
- package/dist/expression-parser.d.ts +11 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +475 -286
- package/dist/ir-to-client-js/emit-reactive.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/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 +11 -0
- package/dist/rich-type-evidence.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/date-lowering.test.ts +232 -0
- package/src/__tests__/hydrate-template.test.ts +10 -5
- package/src/__tests__/rich-type-method-refusal.test.ts +21 -1
- package/src/analyzer.ts +19 -2
- package/src/builtin-lowering-plugins.ts +2 -1
- package/src/date-lowering.ts +117 -0
- package/src/expression-parser.ts +47 -0
- package/src/index.ts +1 -1
- package/src/ir-to-client-js/emit-reactive.ts +103 -2
- 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/types.ts +15 -0
- package/src/jsx-to-ir.ts +159 -3
- package/src/rich-type-evidence.ts +2 -2
package/src/expression-parser.ts
CHANGED
|
@@ -186,6 +186,53 @@ type MissingFromKindRegistry = Exclude<ParsedExpr['kind'], (typeof PARSED_EXPR_K
|
|
|
186
186
|
const _kindRegistryIsExhaustive: MissingFromKindRegistry extends never ? true : never = true
|
|
187
187
|
void _kindRegistryIsExhaustive
|
|
188
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Runtime registry of the catalogued `array-method` names — the positive
|
|
191
|
+
* counterpart to `UNSUPPORTED_METHODS`'s negative gate, and the denominator
|
|
192
|
+
* the coverage ledger's array-method floor test (`coverage-map.test.ts`)
|
|
193
|
+
* requires a covering fixture for. This is the catalogue half of the
|
|
194
|
+
* change-time coupling rule (`spec/subset-conformance.md` / `CLAUDE.md`):
|
|
195
|
+
* the exhaustiveness pin below makes adding a method to the `array-method`
|
|
196
|
+
* union without listing it here a compile error, and the floor test then
|
|
197
|
+
* makes shipping a listed method with no fixture a test failure.
|
|
198
|
+
*/
|
|
199
|
+
export const ARRAY_METHOD_NAMES = [
|
|
200
|
+
'join',
|
|
201
|
+
'includes',
|
|
202
|
+
'indexOf',
|
|
203
|
+
'lastIndexOf',
|
|
204
|
+
'at',
|
|
205
|
+
'concat',
|
|
206
|
+
'slice',
|
|
207
|
+
'reverse',
|
|
208
|
+
'toReversed',
|
|
209
|
+
'toLowerCase',
|
|
210
|
+
'toUpperCase',
|
|
211
|
+
'trim',
|
|
212
|
+
'trimStart',
|
|
213
|
+
'trimEnd',
|
|
214
|
+
'toFixed',
|
|
215
|
+
'split',
|
|
216
|
+
'startsWith',
|
|
217
|
+
'endsWith',
|
|
218
|
+
'replace',
|
|
219
|
+
'replaceAll',
|
|
220
|
+
'repeat',
|
|
221
|
+
'padStart',
|
|
222
|
+
'padEnd',
|
|
223
|
+
'flat',
|
|
224
|
+
] as const satisfies ReadonlyArray<Extract<ParsedExpr, { kind: 'array-method' }>['method']>
|
|
225
|
+
|
|
226
|
+
// Exhaustiveness pin: adding a method to the `array-method` union (either
|
|
227
|
+
// variant) without listing it in `ARRAY_METHOD_NAMES` fails to compile here
|
|
228
|
+
// (same drift defence as `PARSED_EXPR_KINDS`).
|
|
229
|
+
type MissingFromArrayMethodRegistry = Exclude<
|
|
230
|
+
Extract<ParsedExpr, { kind: 'array-method' }>['method'],
|
|
231
|
+
(typeof ARRAY_METHOD_NAMES)[number]
|
|
232
|
+
>
|
|
233
|
+
const _arrayMethodRegistryIsExhaustive: MissingFromArrayMethodRegistry extends never ? true : never = true
|
|
234
|
+
void _arrayMethodRegistryIsExhaustive
|
|
235
|
+
|
|
189
236
|
/**
|
|
190
237
|
* One property of an `object-literal` `ParsedExpr`. The key is the
|
|
191
238
|
* resolved (non-computed) property name — for `{ a: 1 }` and shorthand
|
package/src/index.ts
CHANGED
|
@@ -322,7 +322,7 @@ export { ErrorCodes, createError, formatError, generateCodeFrame } from './error
|
|
|
322
322
|
// Expression Parser
|
|
323
323
|
export { parseExpression, tsNodeToParsedExpr, asCallbackMethodCall, CALLBACK_METHODS, sortComparatorFromArrow, serializeParsedExpr, freeVarsInBody, freeIdentifiers, materializeGetterCalls, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, parseBlockBodyTolerant, foldBlockToExpr, predicateTernaryToLogical, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, hasUnsafeStyleValue, parseProviderObjectLiteral, type ProviderObjectMember, type FoldBlockOptions } from './expression-parser.ts'
|
|
324
324
|
export type { StyleObjectEntry } from './expression-parser.ts'
|
|
325
|
-
export { PARSED_EXPR_KINDS } from './expression-parser.ts'
|
|
325
|
+
export { PARSED_EXPR_KINDS, ARRAY_METHOD_NAMES } from './expression-parser.ts'
|
|
326
326
|
export type { ParsedExpr, ObjectLiteralProperty, ParsedStatement, SortComparator, SortKey, FlatDepth, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
|
|
327
327
|
export { buildLoopChainExpr } from './loop-chain.ts'
|
|
328
328
|
export type { LoopChainInputs } from './loop-chain.ts'
|
|
@@ -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)
|
|
@@ -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: [],
|
|
@@ -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
|
/**
|
|
@@ -4493,6 +4618,37 @@ function getAttributeValue(attr: ts.JsxAttribute, ctx: TransformContext): AttrVa
|
|
|
4493
4618
|
}
|
|
4494
4619
|
}
|
|
4495
4620
|
|
|
4621
|
+
// Bare `attr={record[key]}` (#2300): an element access whose base is a
|
|
4622
|
+
// local const object-literal `Record` indexed by a prop, written directly
|
|
4623
|
+
// as ANY string-attribute value rather than inside a template literal
|
|
4624
|
+
// (`class={record[key]}` is the motivating class-composition case, but this
|
|
4625
|
+
// is attribute-agnostic — it fires for any qualifying attribute). Lift it
|
|
4626
|
+
// into the SAME `lookup` part the `${record[key]}` template-literal form
|
|
4627
|
+
// produces (`tryResolveTemplateSpanFromConst` handles both), so every
|
|
4628
|
+
// adapter renders it through the shared, already-working lookup path
|
|
4629
|
+
// instead of a raw index-access that the typed / strict backends (Go,
|
|
4630
|
+
// minijinja, ERB, Jinja) mishandle for a function-local const — it is not
|
|
4631
|
+
// a prop field, so those emit an unpopulated `.Record`/nil lookup and the
|
|
4632
|
+
// value renders empty (or errors). `tryResolveTemplateSpanFromConst`
|
|
4633
|
+
// returns null for anything but the `IDENT[KEY]` → all-string-`Record`
|
|
4634
|
+
// shape, so any other element access falls through to the bare-expression
|
|
4635
|
+
// path unchanged.
|
|
4636
|
+
// Only a DYNAMIC key qualifies (a prop reference, the #2300 shape). A
|
|
4637
|
+
// static string / numeric literal key (`paths['icon']`) stays on the
|
|
4638
|
+
// bare-expression path — the adapters already resolve a constant-key index,
|
|
4639
|
+
// and it must remain a plain `expression` attr (jsx-to-ir regression pin),
|
|
4640
|
+
// not a single-case `lookup`.
|
|
4641
|
+
if (
|
|
4642
|
+
ts.isElementAccessExpression(expr) &&
|
|
4643
|
+
!ts.isStringLiteralLike(expr.argumentExpression) &&
|
|
4644
|
+
!ts.isNumericLiteral(expr.argumentExpression)
|
|
4645
|
+
) {
|
|
4646
|
+
const parts = tryResolveTemplateSpanFromConst(expr, ctx)
|
|
4647
|
+
if (parts) {
|
|
4648
|
+
return AttrValueOf.template(parts)
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
|
|
4496
4652
|
// `className={classes}` where `classes` is a local const bound to
|
|
4497
4653
|
// a template literal — resolve the template literal here and let
|
|
4498
4654
|
// adapters render the structured form. This is the cva-style
|
|
@@ -78,7 +78,7 @@ function isNullishArm(t: TypeInfo): boolean {
|
|
|
78
78
|
return t.kind === 'unknown' && (t.raw === 'null' || t.raw === 'undefined')
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function stripUnion(type: TypeInfo | null): TypeInfo | null {
|
|
81
|
+
export function stripUnion(type: TypeInfo | null): TypeInfo | null {
|
|
82
82
|
if (!type || type.kind !== 'union' || !type.unionTypes) return type
|
|
83
83
|
const nonNullish = type.unionTypes.filter((t) => !isNullishArm(t))
|
|
84
84
|
return nonNullish.length === 1 ? stripUnion(nonNullish[0]) : type
|
|
@@ -93,7 +93,7 @@ function stripUnion(type: TypeInfo | null): TypeInfo | null {
|
|
|
93
93
|
* reference, which `typeNodeToTypeInfo` intentionally resolves to
|
|
94
94
|
* `{ kind: 'interface', raw }` with no member walk of its own.
|
|
95
95
|
*/
|
|
96
|
-
function derefNamedType(type: TypeInfo, meta: EvidenceMetadata): TypeInfo {
|
|
96
|
+
export function derefNamedType(type: TypeInfo, meta: EvidenceMetadata): TypeInfo {
|
|
97
97
|
if (type.kind !== 'interface') return type
|
|
98
98
|
if (type.properties && type.properties.length > 0) return type
|
|
99
99
|
const name = baseTypeName(type.raw)
|