@barefootjs/jsx 0.31.1 → 0.31.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/adapters/interface.d.ts +11 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts +92 -1
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/css-layer-prefixer.d.ts +16 -0
- package/dist/css-layer-prefixer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/html-types.d.ts +19 -0
- package/dist/html-types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1402 -1124
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-event-handlers.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-extraction.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +6 -4
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-runtime/index.d.ts +2 -8
- package/dist/jsx-runtime/index.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/module-exports.d.ts +9 -1
- package/dist/module-exports.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +7 -2
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +40 -0
- package/dist/props-binding.d.ts.map +1 -0
- package/dist/relocate.d.ts +9 -0
- package/dist/relocate.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts +43 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/template-parts.d.ts +53 -0
- package/dist/template-parts.d.ts.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/adapter-output.test.ts +8 -4
- package/src/__tests__/aliased-destructured-prop-csr.test.ts +112 -0
- package/src/__tests__/css-layer-prefixer.test.ts +72 -0
- package/src/__tests__/form-control-value-ssr.test.ts +48 -3
- package/src/__tests__/memo-deps-comments.test.ts +99 -0
- package/src/__tests__/multi-return-sibling-diagnostic.test.ts +241 -0
- package/src/__tests__/ssr-defaults.test.ts +124 -1
- package/src/__tests__/staged-ir/08-relocate-unit.test.ts +1 -0
- package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +1 -0
- package/src/adapters/interface.ts +11 -0
- package/src/adapters/jsx-adapter.ts +266 -4
- package/src/adapters/test-adapter.ts +13 -10
- package/src/analyzer.ts +50 -8
- package/src/compiler.ts +119 -18
- package/src/css-layer-prefixer.ts +80 -24
- package/src/errors.ts +18 -0
- package/src/html-types.ts +24 -0
- package/src/index.ts +7 -1
- package/src/ir-to-client-js/collect-elements.ts +4 -1
- package/src/ir-to-client-js/emit-reactive.ts +4 -2
- package/src/ir-to-client-js/phases/props-event-handlers.ts +4 -3
- package/src/ir-to-client-js/phases/props-extraction.ts +7 -4
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +6 -3
- package/src/ir-to-client-js/utils.ts +5 -24
- package/src/jsx-runtime/index.ts +2 -7
- package/src/jsx-to-ir.ts +75 -42
- package/src/module-exports.ts +11 -2
- package/src/prop-rewrite.ts +25 -5
- package/src/props-binding.ts +70 -0
- package/src/relocate.ts +19 -2
- package/src/ssr-defaults.ts +70 -0
- package/src/template-parts.ts +81 -0
- package/src/types.ts +10 -0
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
// computations that derive from those signals (`count() * 2`).
|
|
14
14
|
|
|
15
15
|
import { describe, test, expect } from 'bun:test'
|
|
16
|
-
import { extractSsrDefaults } from '../ssr-defaults'
|
|
16
|
+
import { extractSsrDefaults, deriveStashFromDefaults } from '../ssr-defaults'
|
|
17
|
+
import type { SsrDefault } from '../ssr-defaults'
|
|
17
18
|
import { analyzeComponent } from '../analyzer'
|
|
18
19
|
import { buildMetadata } from '../compiler'
|
|
19
20
|
|
|
@@ -253,3 +254,125 @@ describe('extractSsrDefaults', () => {
|
|
|
253
254
|
expect(defaults?.classes).toEqual({ value: 'a b c d tail' })
|
|
254
255
|
})
|
|
255
256
|
})
|
|
257
|
+
|
|
258
|
+
// TS twin of the Ruby/Python/PHP/Perl/Rust `derive*FromDefaults` runtime
|
|
259
|
+
// ports (#2524 SSR half). Matches `derive_vars_from_defaults`'s edge cases
|
|
260
|
+
// exactly — see that Ruby method's docstring
|
|
261
|
+
// (packages/adapter-erb/lib/barefoot_js.rb:337-360) for the reference
|
|
262
|
+
// semantics this mirrors.
|
|
263
|
+
describe('deriveStashFromDefaults', () => {
|
|
264
|
+
test('aliased prop: resolves the CALLER-facing propName, not the local key', () => {
|
|
265
|
+
// `{ n: count }` — extractSsrDefaults keys the entry by the LOCAL
|
|
266
|
+
// binding (`count`) with `propName: 'n'`. The caller supplies `n`.
|
|
267
|
+
const defaults: Record<string, SsrDefault> = {
|
|
268
|
+
count: { propName: 'n', value: null },
|
|
269
|
+
}
|
|
270
|
+
expect(deriveStashFromDefaults(defaults, { n: 7 })).toEqual({ count: 7 })
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
test('un-aliased prop: propName equals the local key, resolves the same way', () => {
|
|
274
|
+
const defaults: Record<string, SsrDefault> = {
|
|
275
|
+
n: { propName: 'n', value: null },
|
|
276
|
+
}
|
|
277
|
+
expect(deriveStashFromDefaults(defaults, { n: 7 })).toEqual({ n: 7 })
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('caller omits the prop: falls back to the static default value', () => {
|
|
281
|
+
const defaults: Record<string, SsrDefault> = {
|
|
282
|
+
count: { propName: 'n', value: 3 },
|
|
283
|
+
}
|
|
284
|
+
expect(deriveStashFromDefaults(defaults, {})).toEqual({ count: 3 })
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
test('caller supplies null / undefined for the propName: falls back to the static value', () => {
|
|
288
|
+
// Mirrors every runtime port's "present AND defined" check — an
|
|
289
|
+
// explicit null/undefined caller value does not count as an override.
|
|
290
|
+
const defaults: Record<string, SsrDefault> = {
|
|
291
|
+
count: { propName: 'n', value: 3 },
|
|
292
|
+
}
|
|
293
|
+
expect(deriveStashFromDefaults(defaults, { n: null })).toEqual({ count: 3 })
|
|
294
|
+
expect(deriveStashFromDefaults(defaults, { n: undefined })).toEqual({ count: 3 })
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test('caller supplies a falsy-but-defined propName value: the caller value wins', () => {
|
|
298
|
+
// `0` / `''` / `false` are legitimate override values, not "absent".
|
|
299
|
+
const defaults: Record<string, SsrDefault> = {
|
|
300
|
+
count: { propName: 'n', value: 99 },
|
|
301
|
+
}
|
|
302
|
+
expect(deriveStashFromDefaults(defaults, { n: 0 })).toEqual({ count: 0 })
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test('propName-less entry (signal / memo local): always uses the static value', () => {
|
|
306
|
+
// A caller cannot override an internal signal/memo by construction —
|
|
307
|
+
// even a same-named caller prop must not leak in.
|
|
308
|
+
const defaults: Record<string, SsrDefault> = {
|
|
309
|
+
doubled: { value: 10 },
|
|
310
|
+
}
|
|
311
|
+
expect(deriveStashFromDefaults(defaults, { doubled: 999 })).toEqual({ doubled: 10 })
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('isRestProps entry: prefers the caller-assembled rest bag under its OWN key', () => {
|
|
315
|
+
const defaults: Record<string, SsrDefault> = {
|
|
316
|
+
rest: { isRestProps: true, value: {} },
|
|
317
|
+
}
|
|
318
|
+
expect(deriveStashFromDefaults(defaults, { rest: { href: '/x' } })).toEqual({
|
|
319
|
+
rest: { href: '/x' },
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test('isRestProps entry: falls back to the static value (normally {}) when the caller supplied none', () => {
|
|
324
|
+
const defaults: Record<string, SsrDefault> = {
|
|
325
|
+
rest: { isRestProps: true, value: {} },
|
|
326
|
+
}
|
|
327
|
+
expect(deriveStashFromDefaults(defaults, {})).toEqual({ rest: {} })
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('isRestProps entry: an explicit `undefined` caller value still counts as supplied', () => {
|
|
331
|
+
// Unlike the ordinary propName branch (nullish check), the isRestProps
|
|
332
|
+
// branch tests presence via `in` — the rest bag is a caller-assembled
|
|
333
|
+
// aggregate, not a single scalar with a meaningful "absent" state, so
|
|
334
|
+
// an explicit `undefined` still wins over the static fallback instead
|
|
335
|
+
// of falling through to it.
|
|
336
|
+
const defaults: Record<string, SsrDefault> = {
|
|
337
|
+
rest: { isRestProps: true, value: { fallback: true } },
|
|
338
|
+
}
|
|
339
|
+
const result = deriveStashFromDefaults(defaults, { rest: undefined })
|
|
340
|
+
expect('rest' in result).toBe(true)
|
|
341
|
+
expect(result.rest).toBeUndefined()
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('a bare (non-object) entry passes through as-is', () => {
|
|
345
|
+
// Defensive parity with every runtime port's `ref($d) eq 'HASH'` /
|
|
346
|
+
// `isinstance(d, dict)` guard — never emitted by `extractSsrDefaults`
|
|
347
|
+
// itself, but a caller may hand this a manifest round-tripped through a
|
|
348
|
+
// generic JSON domain.
|
|
349
|
+
const defaults = { flag: true } as unknown as Record<string, SsrDefault>
|
|
350
|
+
expect(deriveStashFromDefaults(defaults, {})).toEqual({ flag: true })
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
test('a literal null entry hits the same non-object passthrough guard', () => {
|
|
354
|
+
// `d === null` is checked ALONGSIDE `typeof d !== 'object'` (not just
|
|
355
|
+
// the latter) — `typeof null === 'object'` in JS, so without the
|
|
356
|
+
// explicit `d === null` check a null entry would wrongly fall into the
|
|
357
|
+
// object-shaped branches below and throw reading `d.isRestProps`.
|
|
358
|
+
const defaults = { flag: null } as unknown as Record<string, SsrDefault>
|
|
359
|
+
expect(deriveStashFromDefaults(defaults, {})).toEqual({ flag: null })
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test('mixed defaults map resolves every entry kind independently', () => {
|
|
363
|
+
const defaults: Record<string, SsrDefault> = {
|
|
364
|
+
count: { propName: 'n', value: null },
|
|
365
|
+
text: { propName: 'text', value: null },
|
|
366
|
+
doubled: { value: 0 },
|
|
367
|
+
rest: { isRestProps: true, value: {} },
|
|
368
|
+
}
|
|
369
|
+
expect(
|
|
370
|
+
deriveStashFromDefaults(defaults, { n: 7, rest: { extra: 'x' } }),
|
|
371
|
+
).toEqual({
|
|
372
|
+
count: 7,
|
|
373
|
+
text: null,
|
|
374
|
+
doubled: 0,
|
|
375
|
+
rest: { extra: 'x' },
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
})
|
|
@@ -20,6 +20,7 @@ function envWith(
|
|
|
20
20
|
propsForLift: new Set(
|
|
21
21
|
bindings.filter(([, k]) => k === 'prop').map(([n]) => n),
|
|
22
22
|
),
|
|
23
|
+
propSourceNames: options?.propSourceNames ?? new Map(),
|
|
23
24
|
propsObjectName: options?.propsObjectName ?? 'props',
|
|
24
25
|
allowFallback: options?.allowFallback ?? true,
|
|
25
26
|
}
|
|
@@ -35,6 +35,7 @@ function envWith(
|
|
|
35
35
|
propsForLift: new Set(
|
|
36
36
|
bindings.filter(([, k]) => k === 'prop').map(([n]) => n),
|
|
37
37
|
),
|
|
38
|
+
propSourceNames: options?.propSourceNames ?? new Map(),
|
|
38
39
|
propsObjectName: options?.propsObjectName ?? 'props',
|
|
39
40
|
allowFallback: options?.allowFallback ?? true,
|
|
40
41
|
templatePrimitives: options?.templatePrimitives,
|
|
@@ -28,6 +28,17 @@ export interface TemplateSections {
|
|
|
28
28
|
* component in a source file.
|
|
29
29
|
*/
|
|
30
30
|
moduleConstants?: string
|
|
31
|
+
/**
|
|
32
|
+
* When true, the `moduleConstants` section already carries the module's
|
|
33
|
+
* EXPORTED value declarations (consts/functions, `export` keyword
|
|
34
|
+
* included) interleaved in source order — so the compiler must NOT
|
|
35
|
+
* re-emit them via `generateModuleExports` (only `export { … }`
|
|
36
|
+
* specifier blocks remain its job). JSX adapters set this; keeping the
|
|
37
|
+
* exported and non-exported declarations in one source-ordered section
|
|
38
|
+
* is what prevents a non-exported const that reads an exported one from
|
|
39
|
+
* being emitted above it (a module-load TDZ crash — #2570's input-otp).
|
|
40
|
+
*/
|
|
41
|
+
moduleConstantsIncludeExports?: boolean
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
export interface AdapterOutput {
|
|
@@ -6,15 +6,19 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type {
|
|
9
|
+
AttrValue,
|
|
9
10
|
ComponentIR,
|
|
10
11
|
IRNode,
|
|
12
|
+
IRTemplatePart,
|
|
11
13
|
ImportSpecifier,
|
|
12
14
|
} from '../types.ts'
|
|
15
|
+
import { templatePartsToJsExpr } from '../template-parts.ts'
|
|
13
16
|
import { BF_SCOPE, BF_SLOT, BF_COND } from '@barefootjs/shared'
|
|
14
17
|
import { BaseAdapter } from './interface.ts'
|
|
15
18
|
import type { CallbackBodyAcceptor } from './interface.ts'
|
|
16
19
|
import { ENV_SIGNAL_CLIENT_FACTORY } from './env-signal.ts'
|
|
17
20
|
import { formatParamWithType, findReachableNames } from '../module-exports.ts'
|
|
21
|
+
import { extractFreeIdentifiersFromText } from '../ir-to-client-js/csr-substitute.ts'
|
|
18
22
|
|
|
19
23
|
export interface JsxAdapterConfig {
|
|
20
24
|
/** Use typed versions (typedInitialValue, etc.) for type-safe .tsx output */
|
|
@@ -144,11 +148,15 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
144
148
|
const initialValue = rawInitialValue.trim().startsWith('{') ? `(${rawInitialValue})` : rawInitialValue
|
|
145
149
|
|
|
146
150
|
// When preserveTypes and typedInitialValue is absent but signal.type has a meaningful
|
|
147
|
-
// type from a generic parameter, add a type assertion to prevent TS inference issues
|
|
151
|
+
// type from a generic parameter, add a type assertion to prevent TS inference issues.
|
|
152
|
+
// A bare `object` raw is the analyzer's coarse KIND, not a real type — asserting
|
|
153
|
+
// `as object` only destroys the initializer's inferred literal type (a
|
|
154
|
+
// `{ x: 0, y: 0 }` signal getter stops matching `() => { x: number; y: number }`).
|
|
148
155
|
const needsTypeAssertion = preserveTypes
|
|
149
156
|
&& !signal.typedInitialValue
|
|
150
157
|
&& signal.type.kind !== 'unknown'
|
|
151
158
|
&& signal.type.kind !== 'primitive'
|
|
159
|
+
&& signal.type.raw !== 'object'
|
|
152
160
|
if (needsTypeAssertion) {
|
|
153
161
|
lines.push(` const ${signal.getter} = () => ${initialValue} as ${signal.type.raw}`)
|
|
154
162
|
} else {
|
|
@@ -173,12 +181,24 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
173
181
|
lines.push(` const ${memo.name} = ${computation}`)
|
|
174
182
|
}
|
|
175
183
|
|
|
176
|
-
// Include local constants — skip unreachable ones (only used in event
|
|
184
|
+
// Include local constants — skip unreachable ones (only used in event
|
|
185
|
+
// handlers). Genuinely module-scope constants are NOT localised here:
|
|
186
|
+
// they're emitted at module scope by `generateModuleScopeDeclarations`
|
|
187
|
+
// so module-scope types that reference them (via `typeof`) keep
|
|
188
|
+
// resolving, matching the client bundle's `emitModuleLevelDeclarations`
|
|
189
|
+
// (#2570). `moduleScopeDeclarationNames` — not the raw `isModule` flag
|
|
190
|
+
// — decides the split; see its docstring.
|
|
191
|
+
const moduleScopeNames = this.moduleScopeDeclarationNames(ir)
|
|
177
192
|
for (const constant of ir.metadata.localConstants) {
|
|
178
193
|
if (constant.isExported) continue
|
|
194
|
+
if (moduleScopeNames.has(constant.name)) continue
|
|
179
195
|
const keyword = constant.declarationKind ?? 'const'
|
|
180
196
|
if (!constant.value) {
|
|
181
|
-
|
|
197
|
+
// No initializer (e.g. `let emblaApi: EmblaCarouselType | undefined`)
|
|
198
|
+
// — carry the declared type annotation through so `.tsx` output
|
|
199
|
+
// doesn't fall back to implicit `any` (TS7034/TS7005, #2573).
|
|
200
|
+
const typeAnnotation = preserveTypes && constant.type ? `: ${constant.type.raw}` : ''
|
|
201
|
+
lines.push(` ${keyword} ${constant.name}${typeAnnotation}`)
|
|
182
202
|
continue
|
|
183
203
|
}
|
|
184
204
|
const value = constant.value.trim()
|
|
@@ -196,8 +216,11 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
196
216
|
lines.push(` ${keyword} ${constant.name} = ${constValue}`)
|
|
197
217
|
}
|
|
198
218
|
|
|
199
|
-
// Include local functions — skip unreachable ones (only used in event
|
|
219
|
+
// Include local functions — skip unreachable ones (only used in event
|
|
220
|
+
// handlers). Genuinely module-scope functions stay at module scope,
|
|
221
|
+
// same as constants above.
|
|
200
222
|
for (const func of localFunctions) {
|
|
223
|
+
if (moduleScopeNames.has(func.name)) continue
|
|
201
224
|
if (!reachable.has(func.name)) continue
|
|
202
225
|
// Prefer the source-verbatim signature when types are preserved so
|
|
203
226
|
// type-predicate annotations (`element is { tag: unknown; … }`) and
|
|
@@ -219,6 +242,207 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
219
242
|
return lines.join('\n')
|
|
220
243
|
}
|
|
221
244
|
|
|
245
|
+
// ===========================================================================
|
|
246
|
+
// Module-Scope Declarations
|
|
247
|
+
// ===========================================================================
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Names that genuinely live at module scope in the emitted template.
|
|
251
|
+
*
|
|
252
|
+
* The analyzer's `isModule` flag is NOT a lexical-scope oracle: the
|
|
253
|
+
* module walker recurses into component bodies, so a component-body
|
|
254
|
+
* helper (calendar's `renderMonthGrid`, xyflow's edge handlers) can
|
|
255
|
+
* carry `isModule: true` while closing over component state. Hoisting
|
|
256
|
+
* such a helper breaks every reference (TS2304/TS2552 — and worse,
|
|
257
|
+
* wrong runtime scope). So candidates are demoted by a forward-
|
|
258
|
+
* reachability fixpoint, mirroring the client bundle's
|
|
259
|
+
* `computeDeclarationScopes` (`ir-to-client-js/compute-scope.ts`): a
|
|
260
|
+
* candidate that references component scope — a signal getter/setter, a
|
|
261
|
+
* memo, a prop, a body const/function, or an already-demoted candidate
|
|
262
|
+
* — is component-scoped, transitively. References are REAL identifier
|
|
263
|
+
* nodes from a TS AST walk (`extractFreeIdentifiersFromText`), not text
|
|
264
|
+
* matches — a component-scope name occurring inside a candidate's
|
|
265
|
+
* string literal (`[data-state="open"]` vs a body const `open`) is not
|
|
266
|
+
* a reference and must not demote it.
|
|
267
|
+
*
|
|
268
|
+
* EXPORTED candidates are never demoted: `export` is only legal at
|
|
269
|
+
* module top level, so an exported declaration is lexically module-
|
|
270
|
+
* scoped by construction — and a spurious demotion would make it
|
|
271
|
+
* vanish entirely (the body loop skips exported declarations and
|
|
272
|
+
* `generateModuleExports` is told to skip value declarations).
|
|
273
|
+
*
|
|
274
|
+
* Memoized per IR: `generateModuleScopeDeclarations` (module emission)
|
|
275
|
+
* and `generateSignalInitializers` (body emission) must agree on the
|
|
276
|
+
* split or a declaration is emitted twice or not at all.
|
|
277
|
+
*/
|
|
278
|
+
private readonly moduleScopeNamesCache = new WeakMap<ComponentIR, Set<string>>()
|
|
279
|
+
|
|
280
|
+
protected moduleScopeDeclarationNames(ir: ComponentIR): Set<string> {
|
|
281
|
+
const cached = this.moduleScopeNamesCache.get(ir)
|
|
282
|
+
if (cached) return cached
|
|
283
|
+
|
|
284
|
+
const componentScope = new Set<string>()
|
|
285
|
+
for (const sig of ir.metadata.signals) {
|
|
286
|
+
if (sig.isModule) continue
|
|
287
|
+
componentScope.add(sig.getter)
|
|
288
|
+
if (sig.setter) componentScope.add(sig.setter)
|
|
289
|
+
}
|
|
290
|
+
for (const memo of ir.metadata.memos) {
|
|
291
|
+
if (!memo.isModule) componentScope.add(memo.name)
|
|
292
|
+
}
|
|
293
|
+
for (const p of ir.metadata.propsParams) componentScope.add(p.name)
|
|
294
|
+
if (ir.metadata.propsObjectName) componentScope.add(ir.metadata.propsObjectName)
|
|
295
|
+
if (ir.metadata.restPropsName) componentScope.add(ir.metadata.restPropsName)
|
|
296
|
+
for (const c of ir.metadata.localConstants) {
|
|
297
|
+
if (!c.isModule) componentScope.add(c.name)
|
|
298
|
+
}
|
|
299
|
+
for (const f of ir.metadata.localFunctions) {
|
|
300
|
+
if (!f.isModule) componentScope.add(f.name)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Exported declarations join the emit set unconditionally (see the
|
|
304
|
+
// docstring); only non-exported candidates enter the demotion
|
|
305
|
+
// fixpoint, each carrying its REAL free-identifier set.
|
|
306
|
+
const exported = new Set<string>()
|
|
307
|
+
const candidates = new Map<string, ReadonlySet<string>>()
|
|
308
|
+
for (const c of ir.metadata.localConstants) {
|
|
309
|
+
if (!c.isModule) continue
|
|
310
|
+
// JSX-valued consts are inlined at their usage sites at IR level
|
|
311
|
+
// (#547/#569) — same skip as the client's `classifyConstant`.
|
|
312
|
+
if (c.isJsx || c.isJsxFunction) continue
|
|
313
|
+
if (c.isExported) {
|
|
314
|
+
exported.add(c.name)
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
// The analyzer precomputes the value's free identifiers; fall back
|
|
318
|
+
// to the same AST walk for values it didn't cover.
|
|
319
|
+
candidates.set(c.name, c.freeIdentifiers ?? extractFreeIdentifiersFromText(c.value ?? ''))
|
|
320
|
+
}
|
|
321
|
+
for (const f of ir.metadata.localFunctions) {
|
|
322
|
+
if (!f.isModule) continue
|
|
323
|
+
// JSX-returning helpers (single- and multi-return) are inlined at
|
|
324
|
+
// their call sites (#569/#932); emitting them too would resurrect
|
|
325
|
+
// them as dead module-scope declarations — and the multi-return
|
|
326
|
+
// bodies carry raw source JSX that must not reach the template
|
|
327
|
+
// verbatim. Same skips as the client's `computeDeclarationScopes`.
|
|
328
|
+
if (f.isJsxFunction || f.isMultiReturnJsxHelper) continue
|
|
329
|
+
if (f.isExported) {
|
|
330
|
+
exported.add(f.name)
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
// Wrap as an ARROW so the extractor's parameter shadowing applies
|
|
334
|
+
// (it tracks arrow params only) — param references then don't count
|
|
335
|
+
// as free, matching the client fixpoint's `refs.delete(p.name)`.
|
|
336
|
+
const params = f.typedParams !== undefined
|
|
337
|
+
? f.typedParams
|
|
338
|
+
: f.params.map(formatParamWithType).join(', ')
|
|
339
|
+
candidates.set(f.name, extractFreeIdentifiersFromText(`(${params}) => ${f.body}`))
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const referencesAny = (refs: ReadonlySet<string>, names: ReadonlySet<string>): boolean => {
|
|
343
|
+
for (const ref of refs) {
|
|
344
|
+
if (names.has(ref)) return true
|
|
345
|
+
}
|
|
346
|
+
return false
|
|
347
|
+
}
|
|
348
|
+
let changed = true
|
|
349
|
+
while (changed) {
|
|
350
|
+
changed = false
|
|
351
|
+
for (const [name, refs] of candidates) {
|
|
352
|
+
if (referencesAny(refs, componentScope)) {
|
|
353
|
+
candidates.delete(name)
|
|
354
|
+
componentScope.add(name)
|
|
355
|
+
changed = true
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const result = new Set([...exported, ...candidates.keys()])
|
|
361
|
+
this.moduleScopeNamesCache.set(ir, result)
|
|
362
|
+
return result
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Module-scope type, constant, and function declarations for the
|
|
367
|
+
* emitted template, kept at MODULE scope in SOURCE ORDER — the emitted
|
|
368
|
+
* module preserves the source module's shape, as the client bundle
|
|
369
|
+
* already does (`emitModuleLevelDeclarations` in `ir-to-client-js`).
|
|
370
|
+
* Root cure for the #2570 family: type declarations are re-emitted
|
|
371
|
+
* verbatim at module scope, so any value they reference through
|
|
372
|
+
* `typeof` (a type alias's `keyof typeof strokePaths`, a props
|
|
373
|
+
* annotation's `keyof typeof modes`) must be declared there too.
|
|
374
|
+
* Localising those values into each component body — the previous
|
|
375
|
+
* shape — failed the query with TS2304, and an unresolved
|
|
376
|
+
* `keyof typeof` degrades to `keyof any`, silently widening the type
|
|
377
|
+
* to `string | number | symbol` and taking every downstream check
|
|
378
|
+
* with it.
|
|
379
|
+
*
|
|
380
|
+
* EXPORTED module declarations are emitted here too (with their
|
|
381
|
+
* `export` keyword), interleaved with the non-exported ones in source
|
|
382
|
+
* order — not split off to `generateModuleExports`' section, which is
|
|
383
|
+
* emitted after this one and so would put an exported const AFTER a
|
|
384
|
+
* non-exported const that reads it (input-otp's `patternPresets`
|
|
385
|
+
* reading `REGEXP_ONLY_DIGITS`): a module-load TDZ crash, not just
|
|
386
|
+
* TS2448. The compiler skips value declarations in
|
|
387
|
+
* `generateModuleExports` when `moduleConstantsIncludeExports` is set
|
|
388
|
+
* on the sections.
|
|
389
|
+
*
|
|
390
|
+
* Emission is deliberately UNFILTERED by per-component reachability: an
|
|
391
|
+
* unused module declaration in the emitted template is harmless and
|
|
392
|
+
* matches the source module. In a multi-component file each component's
|
|
393
|
+
* adapter output carries its own block (the analyzer collects the module
|
|
394
|
+
* declarations lexically preceding the component, so blocks can be
|
|
395
|
+
* unequal prefixes); the compiler merges them with top-level-STATEMENT
|
|
396
|
+
* dedup, so shared declarations land exactly once in source order.
|
|
397
|
+
*
|
|
398
|
+
* `new WeakMap()` bindings stay client-only, and exported
|
|
399
|
+
* `createContext()` bindings stay unemitted, exactly as before.
|
|
400
|
+
*/
|
|
401
|
+
protected generateModuleScopeDeclarations(ir: ComponentIR): string {
|
|
402
|
+
const { preserveTypes } = this.jsxConfig
|
|
403
|
+
const moduleNames = this.moduleScopeDeclarationNames(ir)
|
|
404
|
+
const entries: Array<{ line: number, text: string }> = []
|
|
405
|
+
|
|
406
|
+
for (const t of ir.metadata.typeDefinitions) {
|
|
407
|
+
entries.push({ line: t.loc.start.line, text: t.definition })
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
for (const c of ir.metadata.localConstants) {
|
|
411
|
+
if (!c.isModule || !moduleNames.has(c.name)) continue
|
|
412
|
+
const keyword = c.declarationKind ?? 'const'
|
|
413
|
+
const exportKw = c.isExported ? 'export ' : ''
|
|
414
|
+
if (!c.value) {
|
|
415
|
+
entries.push({ line: c.loc.start.line, text: `${exportKw}${keyword} ${c.name}` })
|
|
416
|
+
continue
|
|
417
|
+
}
|
|
418
|
+
const trimmed = c.value.trim()
|
|
419
|
+
if (/^new WeakMap\b/.test(trimmed)) continue
|
|
420
|
+
if (c.isExported && /^createContext\b/.test(trimmed)) continue
|
|
421
|
+
const value = preserveTypes ? (c.typedValue ?? c.value) : c.value
|
|
422
|
+
entries.push({ line: c.loc.start.line, text: `${exportKw}${keyword} ${c.name} = ${value}` })
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
for (const f of ir.metadata.localFunctions) {
|
|
426
|
+
if (!f.isModule || !moduleNames.has(f.name)) continue
|
|
427
|
+
const params = preserveTypes && f.typedParams !== undefined
|
|
428
|
+
? f.typedParams
|
|
429
|
+
: f.params.map(formatParamWithType).join(', ')
|
|
430
|
+
const returnAnnotation = preserveTypes && f.typedReturnType
|
|
431
|
+
? `: ${f.typedReturnType}`
|
|
432
|
+
: ''
|
|
433
|
+
const body = preserveTypes ? (f.typedBody ?? f.body) : f.body
|
|
434
|
+
const asyncKw = f.isAsync ? 'async ' : ''
|
|
435
|
+
const exportKw = f.isExported ? 'export ' : ''
|
|
436
|
+
entries.push({
|
|
437
|
+
line: f.loc.start.line,
|
|
438
|
+
text: `${exportKw}${asyncKw}function ${f.name}(${params})${returnAnnotation} ${body}`,
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
entries.sort((a, b) => a.line - b.line)
|
|
443
|
+
return entries.map(e => e.text).join('\n')
|
|
444
|
+
}
|
|
445
|
+
|
|
222
446
|
// ===========================================================================
|
|
223
447
|
// Raw Node Rendering
|
|
224
448
|
// ===========================================================================
|
|
@@ -233,6 +457,44 @@ export abstract class JsxAdapter extends BaseAdapter {
|
|
|
233
457
|
return this.renderNode(node)
|
|
234
458
|
}
|
|
235
459
|
|
|
460
|
+
// ===========================================================================
|
|
461
|
+
// Template Part Rendering
|
|
462
|
+
// ===========================================================================
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Render a structured `template` variant's parts as JS template-literal
|
|
466
|
+
* source for this adapter's .tsx output, carrying the `preserveTypes`
|
|
467
|
+
* index annotation on inlined `lookup` records (#2565 — see
|
|
468
|
+
* `lookupPartToJsExpr`).
|
|
469
|
+
*/
|
|
470
|
+
protected renderTemplatePartsAsJs(parts: readonly IRTemplatePart[]): string {
|
|
471
|
+
return templatePartsToJsExpr(parts, { typed: this.jsxConfig.preserveTypes })
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The JS source for an `expression` attribute / component-prop value.
|
|
476
|
+
*
|
|
477
|
+
* A component-prop `template` is collapsed into an `expression` at IR
|
|
478
|
+
* construction time (component props are runtime values, not HTML
|
|
479
|
+
* attribute bodies), which drops it out of `renderTemplatePartsAsJs`'s
|
|
480
|
+
* reach — the same inlined-record index, minus the type annotation
|
|
481
|
+
* (#2565). The collapse keeps its `parts`, so re-render from those when
|
|
482
|
+
* `expr` is still EXACTLY the neutral collapse. Any later rewrite of
|
|
483
|
+
* `expr` (a presence peel, a prop-ref rewrite) fails the identity check
|
|
484
|
+
* and wins, so this can only ever add the annotation, never undo a
|
|
485
|
+
* downstream edit.
|
|
486
|
+
*/
|
|
487
|
+
protected expressionValueToJs(value: Extract<AttrValue, { kind: 'expression' }>): string {
|
|
488
|
+
if (
|
|
489
|
+
this.jsxConfig.preserveTypes &&
|
|
490
|
+
value.parts &&
|
|
491
|
+
value.expr === templatePartsToJsExpr(value.parts)
|
|
492
|
+
) {
|
|
493
|
+
return this.renderTemplatePartsAsJs(value.parts)
|
|
494
|
+
}
|
|
495
|
+
return value.expr
|
|
496
|
+
}
|
|
497
|
+
|
|
236
498
|
// ===========================================================================
|
|
237
499
|
// Hydration Markers
|
|
238
500
|
// ===========================================================================
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Generates simple JSX output without framework-specific features.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import ts from 'typescript'
|
|
8
9
|
import type {
|
|
9
10
|
ComponentIR,
|
|
10
11
|
IRNode,
|
|
@@ -20,6 +21,7 @@ import type {
|
|
|
20
21
|
import type { AdapterOutput, TemplateSections } from './interface.ts'
|
|
21
22
|
import { type JsxAdapterConfig, JsxAdapter } from './jsx-adapter.ts'
|
|
22
23
|
import { rewriteImportsForTemplate } from './template-imports.ts'
|
|
24
|
+
import { propsDestructureBinding } from '../props-binding.ts'
|
|
23
25
|
|
|
24
26
|
export class TestAdapter extends JsxAdapter {
|
|
25
27
|
name = 'test'
|
|
@@ -31,6 +33,7 @@ export class TestAdapter extends JsxAdapter {
|
|
|
31
33
|
this.componentName = ir.metadata.componentName
|
|
32
34
|
|
|
33
35
|
const imports = this.generateImports(ir)
|
|
36
|
+
const moduleConstants = this.generateModuleScopeDeclarations(ir)
|
|
34
37
|
const types = this.generateTypes(ir)
|
|
35
38
|
const component = this.generateComponent(ir)
|
|
36
39
|
|
|
@@ -43,10 +46,12 @@ export class TestAdapter extends JsxAdapter {
|
|
|
43
46
|
types: types || '',
|
|
44
47
|
component,
|
|
45
48
|
defaultExport,
|
|
49
|
+
moduleConstants,
|
|
50
|
+
moduleConstantsIncludeExports: true,
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
// Assemble template for backward compat
|
|
49
|
-
const template = [imports, types, component].filter(Boolean).join('\n\n') + defaultExport
|
|
54
|
+
const template = [imports, moduleConstants, types, component].filter(Boolean).join('\n\n') + defaultExport
|
|
50
55
|
|
|
51
56
|
return {
|
|
52
57
|
template,
|
|
@@ -84,9 +89,9 @@ export class TestAdapter extends JsxAdapter {
|
|
|
84
89
|
generateTypes(ir: ComponentIR): string | null {
|
|
85
90
|
const lines: string[] = []
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
// Source type declarations are emitted once at module scope by
|
|
93
|
+
// `generateModuleScopeDeclarations` (#2570) — only the synthesized
|
|
94
|
+
// hydration alias is per-component.
|
|
90
95
|
|
|
91
96
|
// Only generate PropsWithHydration when destructured-props pattern uses it
|
|
92
97
|
const propsTypeName = ir.metadata.propsType?.raw
|
|
@@ -122,8 +127,10 @@ export class TestAdapter extends JsxAdapter {
|
|
|
122
127
|
const bodyRefText = [jsxBody, signalInits, scopeIdLine].join('\n')
|
|
123
128
|
const bfScopeAlias = /\b__bfScope\b/.test(bodyRefText) ? '__bfScope' : '__bfScope: _bfScope'
|
|
124
129
|
|
|
130
|
+
// `key: local` rename-aware bindings, shared with the Hono SSR
|
|
131
|
+
// destructure — one renderer, zero drift (`propsDestructureBinding`).
|
|
125
132
|
const propsParams = ir.metadata.propsParams
|
|
126
|
-
.map((p: ParamInfo) => (p
|
|
133
|
+
.map((p: ParamInfo) => propsDestructureBinding(p))
|
|
127
134
|
.join(', ')
|
|
128
135
|
|
|
129
136
|
const restPropsName = ir.metadata.restPropsName
|
|
@@ -325,11 +332,7 @@ export class TestAdapter extends JsxAdapter {
|
|
|
325
332
|
// Simple stringifier for `template`-kind values; tests only need a
|
|
326
333
|
// recognisable JSX shape, not byte-exact reproduction.
|
|
327
334
|
const v = value as { kind: 'template'; parts: import('../types.ts').IRTemplatePart[] }
|
|
328
|
-
return
|
|
329
|
-
if (p.type === 'string') return p.value
|
|
330
|
-
if (p.type === 'ternary') return `\${${p.condition} ? '${p.whenTrue}' : '${p.whenFalse}'}`
|
|
331
|
-
return `\${(${JSON.stringify(p.cases)})[${p.key}]}`
|
|
332
|
-
}).join('') + '`'
|
|
335
|
+
return this.renderTemplatePartsAsJs(v.parts)
|
|
333
336
|
}
|
|
334
337
|
|
|
335
338
|
private renderComponentProps(comp: IRComponent): string {
|