@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.
Files changed (65) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/compiler.d.ts.map +1 -1
  3. package/dist/errors.d.ts +1 -0
  4. package/dist/errors.d.ts.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +955 -273
  8. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +6 -0
  9. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts +2 -1
  11. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts +7 -0
  13. package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +107 -0
  15. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -0
  16. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +9 -1
  17. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +203 -0
  19. package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -0
  20. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -0
  21. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/stringify/branch-loop.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +98 -0
  24. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -0
  25. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +5 -0
  26. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  28. package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/imports.d.ts +26 -2
  30. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  31. package/dist/value-references.d.ts +48 -0
  32. package/dist/value-references.d.ts.map +1 -0
  33. package/package.json +2 -2
  34. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +162 -36
  35. package/src/__tests__/client-js-generation.test.ts +4 -1
  36. package/src/__tests__/composite-branch-loop.test.ts +12 -3
  37. package/src/__tests__/conditional-mapArray-key.test.ts +7 -1
  38. package/src/__tests__/create-selector.test.ts +21 -7
  39. package/src/__tests__/ir-to-client-js/imports.test.ts +98 -1
  40. package/src/__tests__/lazy-row-eligibility.test.ts +692 -0
  41. package/src/__tests__/loop-branch-bare-expression-reactive-text.test.ts +98 -0
  42. package/src/__tests__/loop-fallback-wrap.test.ts +31 -12
  43. package/src/__tests__/loop-hoisted-template.test.ts +10 -6
  44. package/src/__tests__/static-loop-csr-materialize.test.ts +6 -1
  45. package/src/__tests__/type-only-import-leak.test.ts +60 -0
  46. package/src/analyzer.ts +3 -0
  47. package/src/compiler.ts +6 -2
  48. package/src/errors.ts +11 -0
  49. package/src/index.ts +4 -0
  50. package/src/ir-to-client-js/collect-elements.ts +48 -13
  51. package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +6 -0
  52. package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +35 -12
  53. package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
  54. package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +305 -0
  55. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +44 -12
  56. package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +440 -0
  57. package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -0
  58. package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +22 -0
  59. package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +478 -0
  60. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +6 -1
  61. package/src/ir-to-client-js/control-flow/stringify/loop.ts +23 -0
  62. package/src/ir-to-client-js/control-flow.ts +7 -2
  63. package/src/ir-to-client-js/imports.ts +53 -4
  64. package/src/jsx-to-ir.ts +18 -1
  65. 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
- 'claimSlots', 'lazySlots',
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
- if (!spec.isDefault && !spec.isNamespace) {
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 (new RegExp(`\\b${localName}\\b`).test(generatedCode)) {
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
- const needsSlot = reactive || callsReactive
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
+ }