@barefootjs/jsx 0.27.0 → 0.28.1
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/compiler.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +955 -273
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +6 -0
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts +2 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +107 -0
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +9 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +203 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +98 -0
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +5 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +26 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/value-references.d.ts +48 -0
- package/dist/value-references.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +162 -36
- package/src/__tests__/client-js-generation.test.ts +4 -1
- package/src/__tests__/composite-branch-loop.test.ts +12 -3
- package/src/__tests__/conditional-mapArray-key.test.ts +7 -1
- package/src/__tests__/create-selector.test.ts +21 -7
- package/src/__tests__/ir-to-client-js/imports.test.ts +98 -1
- package/src/__tests__/lazy-row-eligibility.test.ts +692 -0
- package/src/__tests__/loop-branch-bare-expression-reactive-text.test.ts +98 -0
- package/src/__tests__/loop-fallback-wrap.test.ts +31 -12
- package/src/__tests__/loop-hoisted-template.test.ts +10 -6
- package/src/__tests__/static-loop-csr-materialize.test.ts +6 -1
- package/src/__tests__/type-only-import-leak.test.ts +60 -0
- package/src/analyzer.ts +3 -0
- package/src/compiler.ts +6 -2
- package/src/errors.ts +11 -0
- package/src/index.ts +4 -0
- package/src/ir-to-client-js/collect-elements.ts +48 -13
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +35 -12
- package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +305 -0
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +44 -12
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +440 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -0
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +22 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +478 -0
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +6 -1
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +23 -0
- package/src/ir-to-client-js/control-flow.ts +7 -2
- package/src/ir-to-client-js/imports.ts +53 -4
- package/src/jsx-to-ir.ts +18 -1
- package/src/value-references.ts +108 -0
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ComponentIR, IRNode } from '../types.ts'
|
|
6
6
|
import { isClientBuiltinName } from '../builtins.ts'
|
|
7
|
+
import { collectValueReferencedNames } from '../value-references.ts'
|
|
7
8
|
|
|
8
9
|
// All exports from @barefootjs/client/runtime that may be used in generated code
|
|
9
10
|
export const RUNTIME_IMPORT_CANDIDATES = [
|
|
10
11
|
'createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount',
|
|
11
|
-
'hydrate', 'insert', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'patchLeaf', 'createDisposableEffect',
|
|
12
|
+
'hydrate', 'insert', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'mapArrayLazy', 'patchLeaf', 'createDisposableEffect',
|
|
12
13
|
'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild',
|
|
13
14
|
'createPortal',
|
|
14
15
|
'provideContext', 'createContext', 'useContext',
|
|
@@ -17,7 +18,13 @@ export const RUNTIME_IMPORT_CANDIDATES = [
|
|
|
17
18
|
// Claim-plan interpreter (slot unification A2/A3, spec/slot-unification.md)
|
|
18
19
|
// — the "one claim mechanism" that replaced `patchSlotRange` and
|
|
19
20
|
// `updateClientMarker` (both deleted) as the content-slot update door.
|
|
20
|
-
|
|
21
|
+
// `lazyClaimSlots` is the read-capable twin of `lazySlots` over the same
|
|
22
|
+
// claim — emitted only by lazy loops that seed an outer-involving TEXT
|
|
23
|
+
// binding by read-compare-write (§9.3(1)).
|
|
24
|
+
// `textOrNode` is the 'text' door's Node guard: a child-position value that
|
|
25
|
+
// turns out to be a live Node must reach the writer as a Node so the claim
|
|
26
|
+
// can promote to 'markup', never as `String(node)`.
|
|
27
|
+
'claimSlots', 'lazySlots', 'lazyClaimSlots', 'textOrNode',
|
|
21
28
|
// Profile mode (#1690, SR3) — turn-boundary markers around event handlers.
|
|
22
29
|
'beginTurn', 'endTurn',
|
|
23
30
|
// Catalogued `Date` lowering (#2274/#2292) — the client counterpart to
|
|
@@ -74,7 +81,10 @@ export function collectUserDomImports(ir: ComponentIR): string[] {
|
|
|
74
81
|
for (const imp of ir.metadata.imports) {
|
|
75
82
|
if (runtimeSources.has(imp.source) && !imp.isTypeOnly) {
|
|
76
83
|
for (const spec of imp.specifiers) {
|
|
77
|
-
|
|
84
|
+
// Per-specifier type-only (`import { createSignal, type Signal }
|
|
85
|
+
// from '@barefootjs/client'`) must not emit `Signal` from the
|
|
86
|
+
// runtime subpath, which does not export it (#2432).
|
|
87
|
+
if (!spec.isDefault && !spec.isNamespace && !spec.isTypeOnly) {
|
|
78
88
|
// Compile-away built-ins (`<Async>` / `<Region>`) are lowered into
|
|
79
89
|
// the template — never emit their import into the client bundle,
|
|
80
90
|
// where it would be a phantom runtime import (#1915).
|
|
@@ -87,6 +97,42 @@ export function collectUserDomImports(ir: ComponentIR): string[] {
|
|
|
87
97
|
return userImports
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Build the "is this local name used as a value in the generated code?"
|
|
102
|
+
* test used to decide which imported specifiers survive into the client
|
|
103
|
+
* bundle. Prefers a real value-reference set over the historical
|
|
104
|
+
* `\bname\b` text scan (#2432: an object key or string literal that
|
|
105
|
+
* merely spells an imported name used to emit a phantom import). Falls
|
|
106
|
+
* back to a substring scan when the generated text cannot be parsed
|
|
107
|
+
* cleanly — a partial parse would under-report references and DROP a
|
|
108
|
+
* needed import. The reference set is computed at most once per call.
|
|
109
|
+
*
|
|
110
|
+
* The fallback is a plain `includes()`, not a `\bname\b` regex: `\b` is
|
|
111
|
+
* defined over `[A-Za-z0-9_]`, so a `$`-prefixed name (`$fetch`, as
|
|
112
|
+
* exported by `ofetch`) or a non-ASCII local both sit outside a word
|
|
113
|
+
* boundary and would never match — silently dropping the import, the one
|
|
114
|
+
* failure direction this helper must never take. Worse, splicing
|
|
115
|
+
* `localName` straight into `new RegExp(...)` treated `$` as the
|
|
116
|
+
* end-of-input anchor, so `\b$fetch\b` couldn't match `$fetch` at all.
|
|
117
|
+
* `includes()` is deliberately COARSER than a word-boundary scan (it
|
|
118
|
+
* matches `helper` inside `helperFoo` too) — that's fine here: the
|
|
119
|
+
* fallback's only job is "never under-report", and over-keeping an
|
|
120
|
+
* import whose binding already exists is harmless, while dropping one is
|
|
121
|
+
* fatal.
|
|
122
|
+
*/
|
|
123
|
+
export function makeValueUsageTest(generatedCode: string): (localName: string) => boolean {
|
|
124
|
+
let referenced: Set<string> | null | undefined
|
|
125
|
+
return (localName: string) => {
|
|
126
|
+
if (referenced === undefined) {
|
|
127
|
+
referenced = collectValueReferencedNames(generatedCode)
|
|
128
|
+
}
|
|
129
|
+
if (referenced !== null) {
|
|
130
|
+
return referenced.has(localName)
|
|
131
|
+
}
|
|
132
|
+
return generatedCode.includes(localName)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
90
136
|
/**
|
|
91
137
|
* Collect external (non-DOM, non-component) imports that are used in generated code.
|
|
92
138
|
* These are third-party libraries like @barefootjs/form, zod, etc. that need to be
|
|
@@ -95,6 +141,7 @@ export function collectUserDomImports(ir: ComponentIR): string[] {
|
|
|
95
141
|
export function collectExternalImports(ir: ComponentIR, generatedCode: string, localImportPrefixes?: string[]): string[] {
|
|
96
142
|
const componentNames = collectComponentNames(ir.root)
|
|
97
143
|
const importLines: string[] = []
|
|
144
|
+
const isUsedAsValue = makeValueUsageTest(generatedCode)
|
|
98
145
|
for (const imp of ir.metadata.imports) {
|
|
99
146
|
if (imp.isTypeOnly) continue
|
|
100
147
|
if (imp.source === '@barefootjs/client' || imp.source === RUNTIME_MODULE) continue
|
|
@@ -111,9 +158,11 @@ export function collectExternalImports(ir: ComponentIR, generatedCode: string, l
|
|
|
111
158
|
// Skip component names — they are rendered via initChild(), not imported directly.
|
|
112
159
|
const usedSpecs: string[] = []
|
|
113
160
|
for (const spec of imp.specifiers) {
|
|
161
|
+
// Per-specifier `import { type Foo }` has no value binding — #2432.
|
|
162
|
+
if (spec.isTypeOnly) continue
|
|
114
163
|
const localName = spec.alias || spec.name
|
|
115
164
|
if (componentNames.has(localName)) continue
|
|
116
|
-
if (
|
|
165
|
+
if (isUsedAsValue(localName)) {
|
|
117
166
|
usedSpecs.push(spec.alias ? `${spec.name} as ${spec.alias}` : spec.name)
|
|
118
167
|
}
|
|
119
168
|
}
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -2707,7 +2707,24 @@ function transformConditionalBranch(
|
|
|
2707
2707
|
const callsReactive = exprCallsReactiveGetters(node, ctx)
|
|
2708
2708
|
const hasCalls = exprHasFunctionCalls(node)
|
|
2709
2709
|
const reactive = isReactiveExpression(exprText, ctx, node) || isReactiveOrigin(branchOrigin)
|
|
2710
|
-
|
|
2710
|
+
// A branch whose entire value is a bare loop-item read (`row.label`) sets
|
|
2711
|
+
// neither `reactive` nor `callsReactive`: `render-item` is deliberately
|
|
2712
|
+
// excluded from `REACTIVE_BINDING_KINDS` (types.ts) because per-item
|
|
2713
|
+
// reactivity flows through the loop's own per-item signal accessor, not
|
|
2714
|
+
// this origin-based classification, and there is no call to trip
|
|
2715
|
+
// `callsReactive`/`hasCalls` either. Without a slotId here, a keyed loop
|
|
2716
|
+
// row that changes value without its condition flipping has nothing for
|
|
2717
|
+
// `collectLoopChildReactiveTexts` (ir-to-client-js/reactivity.ts) to
|
|
2718
|
+
// attach an update effect to, and the branch is frozen at its
|
|
2719
|
+
// mount-time value forever (the loop-branch-stale-text defect). Read the
|
|
2720
|
+
// freeRefs this function already computed above — no new parse, and no
|
|
2721
|
+
// regex re-scan of `exprText` (contrast the legacy `referencesLoopParam`
|
|
2722
|
+
// used by the sibling `transformConditional`/`transformLogicalAnd`
|
|
2723
|
+
// condition-side decisions, which token-matches the loop param name
|
|
2724
|
+
// against expression TEXT and can false-match inside an unrelated string
|
|
2725
|
+
// literal branch like `"this row is empty"`).
|
|
2726
|
+
const refsLoopParam = branchOrigin.freeRefs?.some(r => r.kind === 'render-item') ?? false
|
|
2727
|
+
const needsSlot = reactive || callsReactive || refsLoopParam
|
|
2711
2728
|
const slotId = needsSlot ? generateSlotId(ctx) : null
|
|
2712
2729
|
return {
|
|
2713
2730
|
type: 'expression',
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single door for "is this identifier a VALUE reference in emitted JS".
|
|
3
|
+
*
|
|
4
|
+
* Replaces `\bname\b` text scans at the import-emission sites (#2432): a
|
|
5
|
+
* regex scan can't tell a genuine value reference (`paperColor({ ... })`)
|
|
6
|
+
* from an object key or string literal that merely spells an imported
|
|
7
|
+
* name (`{ Theme: 'テーマ' }`). That false match used to make
|
|
8
|
+
* `collectExternalImports` re-emit a per-specifier type-only import
|
|
9
|
+
* (`import { paperColor, type Theme } from '../lib/theme'`) as a VALUE
|
|
10
|
+
* import, which the CLI's relative-import inliner then placed in the IIFE's
|
|
11
|
+
* `return { … }` with no binding — `ReferenceError: Theme is not defined`
|
|
12
|
+
* at load, killing the whole page's client JS.
|
|
13
|
+
*
|
|
14
|
+
* `packages/cli`'s `detectStrippedReferences` (in `resolve-imports.ts`)
|
|
15
|
+
* shares the same classifier for its own dangling-reference scan, so the
|
|
16
|
+
* two "is this a real use" checks in the pipeline can never drift apart.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import ts from 'typescript'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Identifier-position classifier: returns `true` when `id` is being USED
|
|
23
|
+
* as a value, `false` when it's a declaration name, property key, member-
|
|
24
|
+
* access name, or other non-reference slot.
|
|
25
|
+
*
|
|
26
|
+
* A ShorthandPropertyAssignment (`{ Theme }`) intentionally counts as a
|
|
27
|
+
* reference — it reads the binding, it doesn't just spell its name.
|
|
28
|
+
*
|
|
29
|
+
* Caveat: this is a syntactic test, not a scope analysis. If a local
|
|
30
|
+
* function parameter happens to share a name with an imported binding,
|
|
31
|
+
* references inside that function's body will count as references to
|
|
32
|
+
* the import (false positive). Acceptable: over-counting a reference
|
|
33
|
+
* just means we keep an import we didn't strictly need, which is a
|
|
34
|
+
* strict improvement over the alternative failure direction (dropping a
|
|
35
|
+
* needed import and producing a `ReferenceError`).
|
|
36
|
+
*/
|
|
37
|
+
export function isValueReferenceIdentifier(id: ts.Identifier): boolean {
|
|
38
|
+
const parent = id.parent
|
|
39
|
+
if (!parent) return false
|
|
40
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === id) return false
|
|
41
|
+
if (ts.isPropertyAssignment(parent) && parent.name === id) return false
|
|
42
|
+
if (
|
|
43
|
+
(ts.isMethodDeclaration(parent) ||
|
|
44
|
+
ts.isGetAccessorDeclaration(parent) ||
|
|
45
|
+
ts.isSetAccessorDeclaration(parent)) &&
|
|
46
|
+
parent.name === id
|
|
47
|
+
) {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
if (ts.isVariableDeclaration(parent) && parent.name === id) return false
|
|
51
|
+
if (ts.isFunctionDeclaration(parent) && parent.name === id) return false
|
|
52
|
+
if (ts.isFunctionExpression(parent) && parent.name === id) return false
|
|
53
|
+
if (ts.isClassDeclaration(parent) && parent.name === id) return false
|
|
54
|
+
if (ts.isClassExpression(parent) && parent.name === id) return false
|
|
55
|
+
if (ts.isParameter(parent) && parent.name === id) return false
|
|
56
|
+
if (ts.isBindingElement(parent) && (parent.name === id || parent.propertyName === id)) return false
|
|
57
|
+
if (ts.isLabeledStatement(parent) && parent.label === id) return false
|
|
58
|
+
if (ts.isBreakOrContinueStatement(parent) && parent.label === id) return false
|
|
59
|
+
// ImportSpecifier (`{ X }` or `{ X as Y }`) and ExportSpecifier have
|
|
60
|
+
// only `name`/`propertyName` as Identifier children — written as an
|
|
61
|
+
// explicit slot check for stylistic consistency with the other
|
|
62
|
+
// branches above.
|
|
63
|
+
if (ts.isImportSpecifier(parent) && (parent.name === id || parent.propertyName === id)) return false
|
|
64
|
+
if (ts.isExportSpecifier(parent) && (parent.name === id || parent.propertyName === id)) return false
|
|
65
|
+
if (ts.isImportClause(parent) && parent.name === id) return false
|
|
66
|
+
if (ts.isNamespaceImport(parent) && parent.name === id) return false
|
|
67
|
+
if (ts.isQualifiedName(parent) && parent.right === id) return false
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse `code` and collect the text of every identifier that is a VALUE
|
|
73
|
+
* reference per `isValueReferenceIdentifier`.
|
|
74
|
+
*
|
|
75
|
+
* Returns `null` when the text did not parse cleanly. `null` means
|
|
76
|
+
* "cannot answer" — callers MUST fall back to their previous (regex-scan)
|
|
77
|
+
* behaviour rather than treating it as an empty set. Narrowing on a
|
|
78
|
+
* partial parse would DROP a needed import, which is the failure
|
|
79
|
+
* direction we must never take (a phantom missing-import build failure
|
|
80
|
+
* is recoverable; a silently dead client bundle is not).
|
|
81
|
+
*/
|
|
82
|
+
export function collectValueReferencedNames(code: string): Set<string> | null {
|
|
83
|
+
let sourceFile: ts.SourceFile
|
|
84
|
+
try {
|
|
85
|
+
sourceFile = ts.createSourceFile(
|
|
86
|
+
'generated.js',
|
|
87
|
+
code,
|
|
88
|
+
ts.ScriptTarget.Latest,
|
|
89
|
+
/*setParentNodes*/ true,
|
|
90
|
+
ts.ScriptKind.JS,
|
|
91
|
+
)
|
|
92
|
+
} catch {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const diagnostics = (sourceFile as unknown as { parseDiagnostics?: readonly unknown[] }).parseDiagnostics
|
|
97
|
+
if (diagnostics && diagnostics.length > 0) return null
|
|
98
|
+
|
|
99
|
+
const names = new Set<string>()
|
|
100
|
+
function visit(node: ts.Node): void {
|
|
101
|
+
if (ts.isIdentifier(node) && isValueReferenceIdentifier(node)) {
|
|
102
|
+
names.add(node.text)
|
|
103
|
+
}
|
|
104
|
+
ts.forEachChild(node, visit)
|
|
105
|
+
}
|
|
106
|
+
visit(sourceFile)
|
|
107
|
+
return names
|
|
108
|
+
}
|