@barefootjs/jsx 0.31.0 → 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
package/src/analyzer.ts
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
import ts from 'typescript'
|
|
10
10
|
import type { ImportSpecifier, TypeInfo, ParamInfo, ReactiveFactoryInfo, DeclinedReactiveFactory, RequiredFactoryImport, FactoryRenameSite, SourceLocation } from './types.ts'
|
|
11
|
-
import { parseExpression, parseBlockBodyTolerant, foldBlockToExpr } from './expression-parser.ts'
|
|
11
|
+
import { parseExpression, parseBlockBodyTolerant, foldBlockToExpr, tsNodeToParsedExpr } from './expression-parser.ts'
|
|
12
12
|
import type { CallbackBodyAcceptor } from './adapters/interface.ts'
|
|
13
13
|
import { rewriteBarePropRefs } from './prop-rewrite.ts'
|
|
14
|
+
import { buildPropAliasMap } from './props-binding.ts'
|
|
14
15
|
import { incrementCounter } from './instrumentation.ts'
|
|
15
16
|
import {
|
|
16
17
|
type AnalyzerContext,
|
|
@@ -1273,7 +1274,8 @@ function collectSignal(node: ts.VariableDeclaration, ctx: AnalyzerContext): void
|
|
|
1273
1274
|
if (!ctx.propsObjectName && callExpr.arguments[0]) {
|
|
1274
1275
|
const propNames = new Set(ctx.propsParams.map(p => p.name))
|
|
1275
1276
|
if (propNames.size > 0) {
|
|
1276
|
-
|
|
1277
|
+
const propAliases = buildPropAliasMap(ctx.propsParams)
|
|
1278
|
+
templateInitialValue = rewriteBarePropRefs(initialValue, callExpr.arguments[0], propNames, undefined, propAliases)
|
|
1277
1279
|
}
|
|
1278
1280
|
}
|
|
1279
1281
|
|
|
@@ -1613,7 +1615,8 @@ function collectMemo(node: ts.VariableDeclaration, ctx: AnalyzerContext): void {
|
|
|
1613
1615
|
if (!ctx.propsObjectName && callExpr.arguments[0]) {
|
|
1614
1616
|
const propNames = new Set(ctx.propsParams.map(p => p.name))
|
|
1615
1617
|
if (propNames.size > 0) {
|
|
1616
|
-
|
|
1618
|
+
const propAliases = buildPropAliasMap(ctx.propsParams)
|
|
1619
|
+
templateComputation = rewriteBarePropRefs(computation, callExpr.arguments[0], propNames, undefined, propAliases)
|
|
1617
1620
|
}
|
|
1618
1621
|
}
|
|
1619
1622
|
|
|
@@ -3129,9 +3132,15 @@ function collectConstant(
|
|
|
3129
3132
|
let templateValue: string | undefined
|
|
3130
3133
|
if (value && ctx.propsParams.length > 0 && node.initializer) {
|
|
3131
3134
|
let propNames: Set<string>
|
|
3135
|
+
// Local → caller-facing key (#2524 CSR half) — only Case 1 propsParams
|
|
3136
|
+
// entries can carry `sourceName`; Case 2's alias set below is keyed by
|
|
3137
|
+
// the destructured-from-`props.X` local name, which by its own
|
|
3138
|
+
// `isPureAlias` construction always equals the source key.
|
|
3139
|
+
let propAliases: Map<string, string> | undefined
|
|
3132
3140
|
if (!ctx.propsObjectName) {
|
|
3133
3141
|
// Case 1: destructured-arg — all propsParams are bare locals.
|
|
3134
3142
|
propNames = new Set(ctx.propsParams.map(p => p.name))
|
|
3143
|
+
propAliases = buildPropAliasMap(ctx.propsParams)
|
|
3135
3144
|
} else {
|
|
3136
3145
|
// Case 2: `(props)`-arg — restrict to body-destructured pure aliases
|
|
3137
3146
|
// of `props.X` (entries pushed by the body-destructure expansion at
|
|
@@ -3157,7 +3166,7 @@ function collectConstant(
|
|
|
3157
3166
|
)
|
|
3158
3167
|
}
|
|
3159
3168
|
if (propNames.size > 0) {
|
|
3160
|
-
const rewritten = rewriteBarePropRefs(value, node.initializer, propNames)
|
|
3169
|
+
const rewritten = rewriteBarePropRefs(value, node.initializer, propNames, undefined, propAliases)
|
|
3161
3170
|
if (rewritten !== undefined) {
|
|
3162
3171
|
templateValue = rewritten
|
|
3163
3172
|
// For the body-destructure case, also rewrite the init-body
|
|
@@ -3307,6 +3316,12 @@ function extractProps(param: ts.ParameterDeclaration, ctx: AnalyzerContext): voi
|
|
|
3307
3316
|
const resolvedType: TypeInfo = member?.type ?? { kind: 'unknown', raw: 'unknown' }
|
|
3308
3317
|
|
|
3309
3318
|
const defaultContainsArrow = element.initializer ? nodeContainsArrow(element.initializer) : false
|
|
3319
|
+
// Structured mirror of `defaultValue`, from the binding element's OWN
|
|
3320
|
+
// `initializer` node — `tsNodeToParsedExpr` converts the already-parsed
|
|
3321
|
+
// AST directly, no re-parse of the `defaultValue` text (same convention
|
|
3322
|
+
// as `SignalInfo.parsed`). An unsupported shape leaves `parsed` unset;
|
|
3323
|
+
// consumers fall back to `defaultValue` text.
|
|
3324
|
+
const parsedDefault = element.initializer ? tsNodeToParsedExpr(element.initializer) : undefined
|
|
3310
3325
|
ctx.propsParams.push({
|
|
3311
3326
|
name: localName,
|
|
3312
3327
|
type: resolvedType,
|
|
@@ -3314,6 +3329,7 @@ function extractProps(param: ts.ParameterDeclaration, ctx: AnalyzerContext): voi
|
|
|
3314
3329
|
// the caller may omit the prop.
|
|
3315
3330
|
optional: !!member?.optional || !!element.initializer,
|
|
3316
3331
|
defaultValue,
|
|
3332
|
+
...(parsedDefault && parsedDefault.kind !== 'unsupported' && { parsed: parsedDefault }),
|
|
3317
3333
|
defaultContainsArrow: defaultContainsArrow || undefined,
|
|
3318
3334
|
// Only aliased bindings carry the source key — see ParamInfo.sourceName.
|
|
3319
3335
|
...(sourcePropName !== localName && { sourceName: sourcePropName }),
|
|
@@ -3631,21 +3647,47 @@ function inferTypeFromValue(value: string): TypeInfo {
|
|
|
3631
3647
|
return { kind: 'unknown', raw: 'unknown' }
|
|
3632
3648
|
}
|
|
3633
3649
|
|
|
3650
|
+
/**
|
|
3651
|
+
* Identifiers that appear as a direct call (`name(...)`) in `code`,
|
|
3652
|
+
* collected off a real TS parse — never a regex over the raw text, which
|
|
3653
|
+
* false-matches inside comments and string literals (the repo-wide
|
|
3654
|
+
* "no regex over JS syntax" rule). A doc comment mentioning
|
|
3655
|
+
* `otherSignal()` inside a memo body must NOT become a dependency
|
|
3656
|
+
* (phantom `internalValue`/`controlledValue` deps on slider's
|
|
3657
|
+
* `percentage`, #2581 review). A raw token scan is not enough either:
|
|
3658
|
+
* without a parser driving `reScanTemplateToken`, everything between a
|
|
3659
|
+
* substitution's closing `}` and the next backtick is mis-lexed as code
|
|
3660
|
+
* (and the template tail swallows real code after it), so a read
|
|
3661
|
+
* following a `${...}` template — xyflow's `visibleClass` reading
|
|
3662
|
+
* `animated()` after the `${BF_FLOW_EDGE_SELECTED}` line — would vanish.
|
|
3663
|
+
*/
|
|
3664
|
+
function collectCalledIdentifiers(code: string): Set<string> {
|
|
3665
|
+
const called = new Set<string>()
|
|
3666
|
+
const sf = ts.createSourceFile('__deps__.tsx', code, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX)
|
|
3667
|
+
const visit = (node: ts.Node): void => {
|
|
3668
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
3669
|
+
called.add(node.expression.text)
|
|
3670
|
+
}
|
|
3671
|
+
ts.forEachChild(node, visit)
|
|
3672
|
+
}
|
|
3673
|
+
visit(sf)
|
|
3674
|
+
return called
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3634
3677
|
function extractDependencies(code: string, ctx: AnalyzerContext): string[] {
|
|
3678
|
+
const called = collectCalledIdentifiers(code)
|
|
3635
3679
|
const deps: string[] = []
|
|
3636
3680
|
|
|
3637
3681
|
// Find signal getter calls: signalName()
|
|
3638
3682
|
for (const signal of ctx.signals) {
|
|
3639
|
-
|
|
3640
|
-
if (pattern.test(code)) {
|
|
3683
|
+
if (called.has(signal.getter)) {
|
|
3641
3684
|
deps.push(signal.getter)
|
|
3642
3685
|
}
|
|
3643
3686
|
}
|
|
3644
3687
|
|
|
3645
3688
|
// Find memo calls: memoName()
|
|
3646
3689
|
for (const memo of ctx.memos) {
|
|
3647
|
-
|
|
3648
|
-
if (pattern.test(code)) {
|
|
3690
|
+
if (called.has(memo.name)) {
|
|
3649
3691
|
deps.push(memo.name)
|
|
3650
3692
|
}
|
|
3651
3693
|
}
|
package/src/compiler.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
CompileResult,
|
|
12
12
|
FileOutput,
|
|
13
13
|
} from './types.ts'
|
|
14
|
+
import ts from 'typescript'
|
|
14
15
|
import type { TemplateAdapter } from './adapters/interface.ts'
|
|
15
16
|
import { analyzeComponent, listComponentFunctions, createProgramForFile, needsTypeBasedDetection } from './analyzer.ts'
|
|
16
17
|
import { jsxToIR } from './jsx-to-ir.ts'
|
|
@@ -21,12 +22,13 @@ import { emitModuleLevelDeclarations } from './ir-to-client-js/emit-module-level
|
|
|
21
22
|
import { RUNTIME_MODULE, detectUsedImports as detectUsedImportsFromCode, makeValueUsageTest } from './ir-to-client-js/imports.ts'
|
|
22
23
|
import { setActiveComponentScope, computeFileScope } from './ir-to-client-js/component-scope.ts'
|
|
23
24
|
import { generateModuleExports, collectInlineExportedNames } from './module-exports.ts'
|
|
24
|
-
import { applyCssLayerPrefix } from './css-layer-prefixer.ts'
|
|
25
|
+
import { applyCssLayerPrefix, applyCssLayerPrefixToFile } from './css-layer-prefixer.ts'
|
|
25
26
|
import { preprocessInlineJsxCallbacks } from './preprocess-inline-jsx-callbacks.ts'
|
|
26
27
|
import { extractSsrDefaults } from './ssr-defaults.ts'
|
|
27
28
|
import { computeSsrSeedPlan } from './ssr-seed-plan.ts'
|
|
28
29
|
import { checkRichTypeMethodCalls } from './rich-type-refusal.ts'
|
|
29
|
-
import { ErrorCodes } from './errors.ts'
|
|
30
|
+
import { ErrorCodes, createError } from './errors.ts'
|
|
31
|
+
import { collectComponentNamesFromIR } from './ir-to-client-js/child-components.ts'
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Extended compile options with required adapter
|
|
@@ -163,11 +165,88 @@ function compileMultipleComponents(
|
|
|
163
165
|
// call for why this must run before adapter.generate/generateClientJs.
|
|
164
166
|
decideClientOnlyElision(componentIR.root)
|
|
165
167
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
entries.push({ componentIR, ctx })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// BF048 — refuse loudly when an emitted component template references a
|
|
172
|
+
// same-file sibling that was in `componentNames` (so `listComponentFunctions`
|
|
173
|
+
// treated it as a component to compile — the 'use client' branch of #932
|
|
174
|
+
// does exactly that for multi-return JSX dispatch shapes) but never made
|
|
175
|
+
// it into `entries` (its own `analyzeComponent`/`jsxToIR` pass produced no
|
|
176
|
+
// usable IR — most commonly a top-level `switch` dispatch, which
|
|
177
|
+
// `visitComponentBody` preserves as a verbatim init statement rather than
|
|
178
|
+
// folding into `conditionalReturns`, so `ctx.jsxReturn` stays null).
|
|
179
|
+
// Pre-fix this compiled clean and the emitted `renderChild`/`initChild`/
|
|
180
|
+
// `createComponent` call threw `ReferenceError: <Name> is not defined` at
|
|
181
|
+
// SSR/hydrate time (#2556) — the silence itself is the bug. Detection
|
|
182
|
+
// walks the IR component-reference graph (the same walk that drives
|
|
183
|
+
// `@bf-child` import markers, `collectComponentNamesFromIR`), never the
|
|
184
|
+
// emitted template text.
|
|
185
|
+
//
|
|
186
|
+
// Restricted to TOP-LEVEL declarations: `listComponentFunctions` recurses
|
|
187
|
+
// into function bodies, so `componentNames` also carries component-scope
|
|
188
|
+
// local factories (`const Inner = () => <span/>` inside a component).
|
|
189
|
+
// Those never compile to standalone templates either, but their call
|
|
190
|
+
// sites are handled by the JSX-function-inlining pass — flagging them
|
|
191
|
+
// here would fail legal programs (the ir-dynamic-tag / resolver-alias
|
|
192
|
+
// corpus shapes). Only a name declared at module top level can be the
|
|
193
|
+
// dropped-sibling shape this diagnostic exists for.
|
|
194
|
+
//
|
|
195
|
+
// Restricted to 'use client' FILES: only the 'use client' branch of #932
|
|
196
|
+
// widens `listComponentFunctions` to multi-return dispatch shapes; in a
|
|
197
|
+
// non-client file an uncompiled sibling is preserved verbatim (that is
|
|
198
|
+
// the escape hatch the message below recommends), so flagging it there
|
|
199
|
+
// would fail exactly the programs the fix tells users to write.
|
|
200
|
+
if (entries.some(e => e.componentIR.metadata.isClientComponent)) {
|
|
201
|
+
const topLevelNames = new Set<string>()
|
|
202
|
+
{
|
|
203
|
+
const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
|
204
|
+
for (const stmt of sf.statements) {
|
|
205
|
+
if (ts.isFunctionDeclaration(stmt) && stmt.name) topLevelNames.add(stmt.name.text)
|
|
206
|
+
else if (ts.isVariableStatement(stmt)) {
|
|
207
|
+
for (const d of stmt.declarationList.declarations) {
|
|
208
|
+
if (ts.isIdentifier(d.name)) topLevelNames.add(d.name.text)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const compiledNames = new Set(entries.map(e => e.componentIR.metadata.componentName))
|
|
214
|
+
const uncompiledSiblings = new Set(
|
|
215
|
+
componentNames.filter(name => !compiledNames.has(name) && topLevelNames.has(name)),
|
|
216
|
+
)
|
|
217
|
+
if (uncompiledSiblings.size > 0) {
|
|
218
|
+
for (const { componentIR } of entries) {
|
|
219
|
+
const referenced = new Set<string>()
|
|
220
|
+
collectComponentNamesFromIR([componentIR.root], referenced)
|
|
221
|
+
for (const name of referenced) {
|
|
222
|
+
if (!uncompiledSiblings.has(name)) continue
|
|
223
|
+
errors.push(createError(
|
|
224
|
+
ErrorCodes.SIBLING_COMPONENT_NOT_COMPILED,
|
|
225
|
+
componentIR.root.loc,
|
|
226
|
+
{
|
|
227
|
+
message:
|
|
228
|
+
`Component '${componentIR.metadata.componentName}' references sibling ` +
|
|
229
|
+
`'<${name}>', which did not compile to a template in this 'use client' file ` +
|
|
230
|
+
`(likely a multi-return JSX dispatch — a \`switch\` or \`if\`/\`else\` chain ` +
|
|
231
|
+
`across multiple JSX-returning branches). This reference would throw ` +
|
|
232
|
+
`\`ReferenceError: ${name} is not defined\` at SSR/hydrate time. Extract '${name}' ` +
|
|
233
|
+
`to a separate non-"use client" file (where #932 preserves it verbatim), or ` +
|
|
234
|
+
`rewrite it as a single-return ternary/conditional chain so the component ` +
|
|
235
|
+
`pipeline can compile it.`,
|
|
236
|
+
},
|
|
237
|
+
))
|
|
238
|
+
}
|
|
239
|
+
}
|
|
168
240
|
}
|
|
241
|
+
}
|
|
169
242
|
|
|
170
|
-
|
|
243
|
+
// CSS layer prefixing must be FILE-WIDE: per-IR application diverges the
|
|
244
|
+
// per-component copies of a shared module-scope constant (prefixed in the
|
|
245
|
+
// components whose class attrs reference it, raw elsewhere), and module
|
|
246
|
+
// shape emission (#2570) would then declare the constant twice. See
|
|
247
|
+
// `applyCssLayerPrefixToFile`.
|
|
248
|
+
if (options.cssLayerPrefix) {
|
|
249
|
+
applyCssLayerPrefixToFile(entries.map(e => e.componentIR), options.cssLayerPrefix)
|
|
171
250
|
}
|
|
172
251
|
|
|
173
252
|
// BF050 is a per-FILE diagnostic (it points at the brand-package import
|
|
@@ -220,13 +299,35 @@ function compileMultipleComponents(
|
|
|
220
299
|
}
|
|
221
300
|
}
|
|
222
301
|
|
|
223
|
-
// Module-scope statements (
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
302
|
+
// Module-scope statements (types, consts, functions, SSR-side context
|
|
303
|
+
// bindings) are file-wide, but each component's IR may carry a different
|
|
304
|
+
// SUBSET — the analyzer collects the module declarations lexically
|
|
305
|
+
// preceding the component, so in a file that interleaves constants with
|
|
306
|
+
// component functions the blocks are unequal prefixes of one another
|
|
307
|
+
// (carousel does this). Whole-block string dedup would then emit every
|
|
308
|
+
// prefix and redeclare each name (TS2300/TS2451), while per-LINE dedup
|
|
309
|
+
// would corrupt multi-line declarations (repeated `})` lines collapse).
|
|
310
|
+
// So dedup at the TOP-LEVEL-STATEMENT level via a TS parse of each block
|
|
311
|
+
// — the repo-wide idiom for splitting emitted source (never regex/line
|
|
312
|
+
// matching). Identical statements re-collected across components dedup
|
|
313
|
+
// to their first occurrence, preserving source order.
|
|
314
|
+
const moduleStatementSeen = new Set<string>()
|
|
315
|
+
const moduleStatementsOrdered: string[] = []
|
|
316
|
+
const collectModuleStatements = (block: string): void => {
|
|
317
|
+
const sf = ts.createSourceFile(
|
|
318
|
+
'__bf_module_decls.tsx',
|
|
319
|
+
block,
|
|
320
|
+
ts.ScriptTarget.Latest,
|
|
321
|
+
/* setParentNodes */ false,
|
|
322
|
+
ts.ScriptKind.TSX,
|
|
323
|
+
)
|
|
324
|
+
for (const stmt of sf.statements) {
|
|
325
|
+
const text = stmt.getText(sf)
|
|
326
|
+
if (moduleStatementSeen.has(text)) continue
|
|
327
|
+
moduleStatementSeen.add(text)
|
|
328
|
+
moduleStatementsOrdered.push(text)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
230
331
|
|
|
231
332
|
// Component-name scope: rewrite `hydrate` / `renderChild` / `initChild` /
|
|
232
333
|
// `createComponent` / `upsertChild` keys for non-exported helpers
|
|
@@ -271,6 +372,7 @@ function compileMultipleComponents(
|
|
|
271
372
|
componentIR,
|
|
272
373
|
fileWideInlineExported,
|
|
273
374
|
options.rewriteRelativeImport,
|
|
375
|
+
{ skipValueDeclarations: adapterOutput.sections.moduleConstantsIncludeExports },
|
|
274
376
|
)
|
|
275
377
|
|
|
276
378
|
const s = adapterOutput.sections
|
|
@@ -278,10 +380,7 @@ function compileMultipleComponents(
|
|
|
278
380
|
const types = s.types
|
|
279
381
|
const component = s.component + (s.defaultExport || '')
|
|
280
382
|
const mc = s.moduleConstants
|
|
281
|
-
if (mc
|
|
282
|
-
moduleConstantsSet.add(mc)
|
|
283
|
-
moduleConstantsOrdered.push(mc)
|
|
284
|
-
}
|
|
383
|
+
if (mc) collectModuleStatements(mc)
|
|
285
384
|
|
|
286
385
|
allOutputs.push({
|
|
287
386
|
componentName: componentIR.metadata.componentName,
|
|
@@ -415,7 +514,7 @@ function compileMultipleComponents(
|
|
|
415
514
|
// Combine all components
|
|
416
515
|
const combinedTemplate = [
|
|
417
516
|
mergedImports,
|
|
418
|
-
|
|
517
|
+
moduleStatementsOrdered.join('\n\n'),
|
|
419
518
|
uniqueTypes.join('\n\n'),
|
|
420
519
|
uniqueModuleExports.length > 0 ? uniqueModuleExports.join('\n') : '',
|
|
421
520
|
...allOutputs.map(o => o.component),
|
|
@@ -721,7 +820,9 @@ export function compileJSX(
|
|
|
721
820
|
if (adapter.templatesPerComponent) {
|
|
722
821
|
content = adapterOutput.template
|
|
723
822
|
} else {
|
|
724
|
-
const moduleExports = generateModuleExports(componentIR, undefined, options.rewriteRelativeImport
|
|
823
|
+
const moduleExports = generateModuleExports(componentIR, undefined, options.rewriteRelativeImport, {
|
|
824
|
+
skipValueDeclarations: s.moduleConstantsIncludeExports,
|
|
825
|
+
})
|
|
725
826
|
content = [s.imports, s.moduleConstants ?? '', s.types, moduleExports, s.component]
|
|
726
827
|
.filter(Boolean).join('\n\n') + (s.defaultExport || '')
|
|
727
828
|
}
|
|
@@ -80,9 +80,87 @@ export function prefixConstantValue(rawValue: string, layerName: string): string
|
|
|
80
80
|
* Prefixes static class attributes and class-related constant values.
|
|
81
81
|
*/
|
|
82
82
|
export function applyCssLayerPrefix(ir: ComponentIR, layerName: string): void {
|
|
83
|
+
applyCssLayerPrefixToFile([ir], layerName)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* File-wide variant: apply the prefix across ALL of a file's component IRs
|
|
88
|
+
* with ONE union of referenced constants.
|
|
89
|
+
*
|
|
90
|
+
* Each component's IR carries its own `ConstantInfo` copies of the file's
|
|
91
|
+
* module-scope constants, and per-IR application prefixed only the copies
|
|
92
|
+
* the component's own class attributes referenced. That was invisible while
|
|
93
|
+
* every component re-declared the constants inside its body, but module
|
|
94
|
+
* shape emission (#2570) hoists them to ONE module-scope declaration —
|
|
95
|
+
* per-component divergence then emits the same constant twice with
|
|
96
|
+
* different values ("tabsClasses has already been declared", tabs in the
|
|
97
|
+
* site/ui build). Applying the union keeps every IR's copy byte-identical,
|
|
98
|
+
* so the compiler's statement-level dedup collapses them to a single,
|
|
99
|
+
* consistently-prefixed declaration.
|
|
100
|
+
*/
|
|
101
|
+
export function applyCssLayerPrefixToFile(irs: readonly ComponentIR[], layerName: string): void {
|
|
83
102
|
const referencedConstants = new Set<string>()
|
|
84
|
-
const constantNames = new Set(
|
|
103
|
+
const constantNames = new Set<string>()
|
|
104
|
+
for (const ir of irs) {
|
|
105
|
+
for (const c of ir.metadata.localConstants) constantNames.add(c.name)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Walk every IR tree and process className/class attributes, collecting
|
|
109
|
+
// referenced constants into the shared union.
|
|
110
|
+
for (const ir of irs) {
|
|
111
|
+
collectAndPrefixAttrs(ir, layerName, referencedConstants, constantNames)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Resolve transitive references (constants referencing other constants) —
|
|
115
|
+
// against every IR's constant list, since a name may only carry a value
|
|
116
|
+
// in the IRs whose lexical prefix includes it.
|
|
117
|
+
let changed = true
|
|
118
|
+
while (changed) {
|
|
119
|
+
changed = false
|
|
120
|
+
for (const constName of [...referencedConstants]) {
|
|
121
|
+
for (const ir of irs) {
|
|
122
|
+
const constant = ir.metadata.localConstants.find(c => c.name === constName)
|
|
123
|
+
if (!constant || !constant.value) continue
|
|
124
|
+
for (const id of extractIdentifiers(constant.value)) {
|
|
125
|
+
if (constantNames.has(id) && !referencedConstants.has(id)) {
|
|
126
|
+
referencedConstants.add(id)
|
|
127
|
+
changed = true
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
85
133
|
|
|
134
|
+
// Apply prefixing to every IR's copy of each referenced constant.
|
|
135
|
+
//
|
|
136
|
+
// `typedValue` (the type-annotation-preserving initializer text used by
|
|
137
|
+
// `preserveTypes` emitters — Hono's `generateModuleScopeDeclarations` /
|
|
138
|
+
// SSR body block prefer `typedValue ?? value`) is a separate string that
|
|
139
|
+
// must be prefixed in lockstep with `value`, or an `as const` / annotated
|
|
140
|
+
// class-map constant is emitted unprefixed even though the class
|
|
141
|
+
// attributes referencing it were prefixed (#2575). `prefixConstantValue`
|
|
142
|
+
// only rewrites string-literal contents (see its docstring), so an `as
|
|
143
|
+
// const` suffix or type annotation elsewhere in the text passes through
|
|
144
|
+
// unchanged.
|
|
145
|
+
for (const ir of irs) {
|
|
146
|
+
for (const constant of ir.metadata.localConstants) {
|
|
147
|
+
if (!referencedConstants.has(constant.name)) continue
|
|
148
|
+
if (constant.value) {
|
|
149
|
+
constant.value = prefixConstantValue(constant.value, layerName)
|
|
150
|
+
}
|
|
151
|
+
if (constant.typedValue) {
|
|
152
|
+
constant.typedValue = prefixConstantValue(constant.typedValue, layerName)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function collectAndPrefixAttrs(
|
|
159
|
+
ir: ComponentIR,
|
|
160
|
+
layerName: string,
|
|
161
|
+
referencedConstants: Set<string>,
|
|
162
|
+
constantNames: ReadonlySet<string>,
|
|
163
|
+
): void {
|
|
86
164
|
// Walk IR tree and process className/class attributes
|
|
87
165
|
walkIR(ir.root, (node) => {
|
|
88
166
|
if (node.type !== 'element') return
|
|
@@ -118,28 +196,6 @@ export function applyCssLayerPrefix(ir: ComponentIR, layerName: string): void {
|
|
|
118
196
|
}
|
|
119
197
|
})
|
|
120
198
|
|
|
121
|
-
// Resolve transitive references (constants referencing other constants)
|
|
122
|
-
let changed = true
|
|
123
|
-
while (changed) {
|
|
124
|
-
changed = false
|
|
125
|
-
for (const constName of [...referencedConstants]) {
|
|
126
|
-
const constant = ir.metadata.localConstants.find(c => c.name === constName)
|
|
127
|
-
if (!constant || !constant.value) continue
|
|
128
|
-
for (const id of extractIdentifiers(constant.value)) {
|
|
129
|
-
if (constantNames.has(id) && !referencedConstants.has(id)) {
|
|
130
|
-
referencedConstants.add(id)
|
|
131
|
-
changed = true
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Apply prefixing to referenced constants
|
|
138
|
-
for (const constant of ir.metadata.localConstants) {
|
|
139
|
-
if (referencedConstants.has(constant.name) && constant.value) {
|
|
140
|
-
constant.value = prefixConstantValue(constant.value, layerName)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
199
|
}
|
|
144
200
|
|
|
145
201
|
// =============================================================================
|
|
@@ -168,7 +224,7 @@ function prefixIRTemplateParts(parts: IRTemplatePart[], layerName: string): void
|
|
|
168
224
|
function collectConstantRefs(
|
|
169
225
|
expr: string,
|
|
170
226
|
refs: Set<string>,
|
|
171
|
-
validNames:
|
|
227
|
+
validNames: ReadonlySet<string>,
|
|
172
228
|
): void {
|
|
173
229
|
for (const id of extractIdentifiers(expr)) {
|
|
174
230
|
if (validNames.has(id)) {
|
package/src/errors.ts
CHANGED
|
@@ -43,6 +43,16 @@ export const ErrorCodes = {
|
|
|
43
43
|
// ReferenceError at hydrate. Fail loud with workaround pointers
|
|
44
44
|
// (#1414 cell 5).
|
|
45
45
|
JSX_BRANCH_LOCAL_IN_CALLBACK: 'BF047',
|
|
46
|
+
// A `'use client'` file's emitted template references a same-file
|
|
47
|
+
// sibling component (`<NavIcon .../>`, a `.map()` child, …) whose own
|
|
48
|
+
// body never produced a template — most commonly a multi-return JSX
|
|
49
|
+
// dispatch (`switch` / `if`-`else` chain) that `compileMultipleComponents`
|
|
50
|
+
// silently dropped from its output. Pre-fix this compiled clean and threw
|
|
51
|
+
// `ReferenceError: <Name> is not defined` at SSR/hydrate time (#2556). The
|
|
52
|
+
// equivalent shape in a NON-"use client" file is fine — #932 preserves the
|
|
53
|
+
// helper verbatim instead of compiling it as a component — so this code
|
|
54
|
+
// fires only for the client-component compilation path.
|
|
55
|
+
SIBLING_COMPONENT_NOT_COMPILED: 'BF048',
|
|
46
56
|
|
|
47
57
|
// Import errors (BF050-BF059)
|
|
48
58
|
SHARED_PROGRAM_REQUIRED: 'BF050',
|
|
@@ -146,6 +156,14 @@ const errorMessages: Record<ErrorCode, string> = {
|
|
|
146
156
|
"JSX-typed local declared inside an `if`-block cannot be referenced from a callback body (ref / event handler). " +
|
|
147
157
|
"Render it as a child instead: `<div ref={...}>{local}</div>`.",
|
|
148
158
|
|
|
159
|
+
[ErrorCodes.SIBLING_COMPONENT_NOT_COMPILED]:
|
|
160
|
+
"Referenced component did not compile to a template, so this reference would throw " +
|
|
161
|
+
"`ReferenceError` at render time. Multi-return JSX dispatch (a `switch` or `if`/`else` " +
|
|
162
|
+
"chain across multiple JSX-returning branches) cannot compile as a component in a " +
|
|
163
|
+
"'use client' file. Extract it to a separate non-\"use client\" file (where it is preserved " +
|
|
164
|
+
"verbatim, see #932), or rewrite it as a single-return ternary/conditional chain so the " +
|
|
165
|
+
"component pipeline can compile it.",
|
|
166
|
+
|
|
149
167
|
[ErrorCodes.SHARED_PROGRAM_REQUIRED]:
|
|
150
168
|
'Shared ts.Program required for type-based reactivity classification. This source imports a Reactive<T>-branded library (e.g. @barefootjs/form) whose getters cannot be classified by regex alone. Pass `options.program` (built via `createProgramForCorpus`) so the analyzer can resolve the brand through the TypeChecker.',
|
|
151
169
|
|
package/src/html-types.ts
CHANGED
|
@@ -248,6 +248,30 @@ export interface SVGPresentationAttributes {
|
|
|
248
248
|
filter?: string
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// SVG Root (`<svg>`) Attributes
|
|
253
|
+
// ============================================================================
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Attributes for the root `<svg>` element, narrowed to `SVGSVGElement` for
|
|
257
|
+
* `ref`. A curated subset mirroring the intrinsic `svg` entry — not the full
|
|
258
|
+
* SVG attribute surface.
|
|
259
|
+
*
|
|
260
|
+
* Public so components that render an `<svg>` as their root and accept
|
|
261
|
+
* passthrough props (icons, spinners, and similar) can type their props with
|
|
262
|
+
* this instead of `HTMLBaseAttributes` — spreading `HTMLBaseAttributes` onto
|
|
263
|
+
* an `<svg>` fails to type-check because its `ref` is `(element: HTMLElement)
|
|
264
|
+
* => void`, incompatible with the `<svg>` intrinsic's
|
|
265
|
+
* `(element: SVGSVGElement) => void` (#2573).
|
|
266
|
+
*/
|
|
267
|
+
export type SVGSVGAttributes = SVGBaseAttributes & SVGPresentationAttributes & {
|
|
268
|
+
viewBox?: string
|
|
269
|
+
xmlns?: string
|
|
270
|
+
width?: number | string
|
|
271
|
+
height?: number | string
|
|
272
|
+
ref?: (element: SVGSVGElement) => void
|
|
273
|
+
}
|
|
274
|
+
|
|
251
275
|
/**
|
|
252
276
|
* Marker reference attributes shared by `<path>`, `<line>`, `<polyline>`,
|
|
253
277
|
* and `<polygon>`. Accepts both camelCase and kebab-case spellings.
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,10 @@ export { compileJSX, buildMetadata } from './compiler.ts'
|
|
|
9
9
|
export type { CompileResult, CompileOptions, CompileOptionsWithAdapter, FileOutput } from './compiler.ts'
|
|
10
10
|
|
|
11
11
|
// SSR template-variable defaults (manifest seeds for stash-based adapters)
|
|
12
|
-
export { extractSsrDefaults } from './ssr-defaults.ts'
|
|
12
|
+
export { extractSsrDefaults, deriveStashFromDefaults } from './ssr-defaults.ts'
|
|
13
|
+
|
|
14
|
+
// Shared props-destructure binding + alias-map helpers (#2524)
|
|
15
|
+
export { propsDestructureBinding, buildPropAliasMap, isIdentifierName } from './props-binding.ts'
|
|
13
16
|
export type { SsrDefault } from './ssr-defaults.ts'
|
|
14
17
|
|
|
15
18
|
// Backend-neutral SSR seed plan (in-template derived signal/memo seeding)
|
|
@@ -358,4 +361,7 @@ export type {
|
|
|
358
361
|
ImgHTMLAttributes,
|
|
359
362
|
LabelHTMLAttributes,
|
|
360
363
|
OptionHTMLAttributes,
|
|
364
|
+
|
|
365
|
+
// SVG attributes (for components whose root is an `<svg>`, e.g. icons)
|
|
366
|
+
SVGSVGAttributes,
|
|
361
367
|
} from './html-types.ts'
|
|
@@ -933,8 +933,11 @@ function collectFromElement(element: IRElement, ctx: ClientJsContext, insideCond
|
|
|
933
933
|
// alone was wrong on both counts — it keys on HTML attr names
|
|
934
934
|
// (`aria-invalid` ≠ source key `error`) and omits event handlers.
|
|
935
935
|
// (#1467)
|
|
936
|
+
// `applyRestAttrs` filters `Object.keys(_p)` — which is always
|
|
937
|
+
// caller-keyed (#2524 CSR half) — so the exclude list must use
|
|
938
|
+
// the caller-facing key too, not the local binding name.
|
|
936
939
|
const consumedKeys =
|
|
937
|
-
spreadVal === elemRestName ? ctx.propsParams.map(p => p.name) : []
|
|
940
|
+
spreadVal === elemRestName ? ctx.propsParams.map(p => p.sourceName ?? p.name) : []
|
|
938
941
|
const staticAttrKeys = element.attrs
|
|
939
942
|
.filter(a => a.name !== '...')
|
|
940
943
|
.map(a => a.name)
|
|
@@ -91,10 +91,12 @@ export function rewriteDestructuredPropsInExpr(expr: string, ctx: ClientJsContex
|
|
|
91
91
|
const pattern = new RegExp(`(?<![-.])\\b${prop.name}\\b`, 'g')
|
|
92
92
|
if (!pattern.test(result)) continue
|
|
93
93
|
|
|
94
|
+
// `_p` is always keyed by the caller-facing name (#2524 CSR half).
|
|
95
|
+
const callerKey = prop.sourceName ?? prop.name
|
|
94
96
|
const defaultVal = prop.defaultValue
|
|
95
97
|
const replacement = defaultVal
|
|
96
|
-
? `(${PROPS_PARAM}.${
|
|
97
|
-
: `${PROPS_PARAM}.${
|
|
98
|
+
? `(${PROPS_PARAM}.${callerKey} ?? ${prop.defaultContainsArrow ? `(${defaultVal})` : defaultVal})`
|
|
99
|
+
: `${PROPS_PARAM}.${callerKey}`
|
|
98
100
|
result = result.replace(new RegExp(`(?<![-.])\\b${prop.name}\\b`, 'g'), replacement)
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -25,9 +25,10 @@ export function emitPropsEventHandlers(
|
|
|
25
25
|
for (const handlerName of usedFunctions) {
|
|
26
26
|
if (localNames.has(handlerName)) continue
|
|
27
27
|
if (neededProps.has(handlerName)) continue
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
30
|
-
|
|
28
|
+
const prop = ctx.propsParams.find(p => p.name === handlerName)
|
|
29
|
+
if (!prop) continue
|
|
30
|
+
// `_p` is always keyed by the caller-facing name (#2524 CSR half).
|
|
31
|
+
lines.push(` const ${handlerName} = ${PROPS_PARAM}.${prop.sourceName ?? handlerName}`)
|
|
31
32
|
addedAny = true
|
|
32
33
|
}
|
|
33
34
|
if (addedAny) lines.push('')
|
|
@@ -41,23 +41,26 @@ export function emitPropsExtraction(
|
|
|
41
41
|
|
|
42
42
|
for (const propName of neededProps) {
|
|
43
43
|
const prop = ctx.propsParams.find(p => p.name === propName)
|
|
44
|
+
// `_p` is always keyed by the caller-facing name (#2524 CSR half); the
|
|
45
|
+
// local binding on the left of `const` stays `propName`.
|
|
46
|
+
const callerKey = prop?.sourceName ?? propName
|
|
44
47
|
const usage = propUsage.get(propName)
|
|
45
48
|
const defaultVal = prop?.defaultValue
|
|
46
49
|
if (defaultVal) {
|
|
47
50
|
// `props.onInput ?? () => {}` is a syntax error — `??` binds tighter
|
|
48
51
|
// than the arrow head. Wrap arrow defaults in parens.
|
|
49
52
|
const wrappedDefault = prop?.defaultContainsArrow ? `(${defaultVal})` : defaultVal
|
|
50
|
-
lines.push(` const ${propName} = ${PROPS_PARAM}.${
|
|
53
|
+
lines.push(` const ${propName} = ${PROPS_PARAM}.${callerKey} ?? ${wrappedDefault}`)
|
|
51
54
|
} else if (usage?.usedAsLoopArray) {
|
|
52
|
-
lines.push(` const ${propName} = ${PROPS_PARAM}.${
|
|
55
|
+
lines.push(` const ${propName} = ${PROPS_PARAM}.${callerKey} ?? []`)
|
|
53
56
|
} else if (propHasPropertyAccess(usage) && !propsUsedAsConditions.has(propName)) {
|
|
54
|
-
lines.push(` const ${propName} = ${PROPS_PARAM}.${
|
|
57
|
+
lines.push(` const ${propName} = ${PROPS_PARAM}.${callerKey} ?? {}`)
|
|
55
58
|
} else {
|
|
56
59
|
// No synthesized default for a defaultless optional (`{ size }:
|
|
57
60
|
// { size?: number }`): the JS binding is `undefined` when absent, and
|
|
58
61
|
// a zero default would diverge from SSR (`size ?? 1` seeds 1
|
|
59
62
|
// server-side; a `_p.size ?? 0` extraction would hydrate to 0).
|
|
60
|
-
lines.push(` const ${propName} = ${PROPS_PARAM}.${
|
|
63
|
+
lines.push(` const ${propName} = ${PROPS_PARAM}.${callerKey}`)
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
lines.push('')
|
|
@@ -159,7 +159,8 @@ function resolveSignalInitialValue(
|
|
|
159
159
|
// Case 2b: synthesize the prop accessor + default.
|
|
160
160
|
const prop = lookups.propByName.get(controlled.propName)
|
|
161
161
|
const defaultVal = prop?.defaultValue ?? inferDefaultValue(signal.type)
|
|
162
|
-
|
|
162
|
+
// `_p` is always keyed by the caller-facing name (#2524 CSR half).
|
|
163
|
+
return `${PROPS_PARAM}.${prop?.sourceName ?? controlled.propName} ?? ${defaultVal}`
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
// Case 3: non-controlled `propsObjectName.X` (only when destructured-
|
|
@@ -181,9 +182,11 @@ function buildControlledSignalEffect(
|
|
|
181
182
|
if (!signal.setter) return null // read-only — no sync needed
|
|
182
183
|
|
|
183
184
|
const prop = lookups.propByName.get(controlled.propName)
|
|
185
|
+
// `_p` is always keyed by the caller-facing name (#2524 CSR half).
|
|
186
|
+
const callerKey = prop?.sourceName ?? controlled.propName
|
|
184
187
|
const accessorExpr = prop?.defaultValue
|
|
185
|
-
? `(${PROPS_PARAM}.${
|
|
186
|
-
: `${PROPS_PARAM}.${
|
|
188
|
+
? `(${PROPS_PARAM}.${callerKey} ?? ${prop.defaultValue})`
|
|
189
|
+
: `${PROPS_PARAM}.${callerKey}`
|
|
187
190
|
|
|
188
191
|
return { setter: signal.setter, accessorExpr }
|
|
189
192
|
}
|