@barefootjs/jsx 0.28.0 → 0.29.0
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 +349 -252
- package/dist/ir-to-client-js/control-flow/stringify/composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +14 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.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 +61 -0
- package/dist/value-references.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/ir-to-client-js/imports.test.ts +116 -1
- package/src/__tests__/loop-row-creation-paths.test.ts +169 -0
- package/src/__tests__/type-only-import-leak.test.ts +60 -0
- package/src/__tests__/value-references.test.ts +88 -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/control-flow/stringify/composite-loop.ts +4 -0
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +23 -4
- package/src/ir-to-client-js/imports.ts +50 -2
- package/src/value-references.ts +130 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composite-loop.d.ts","sourceRoot":"","sources":["../../../../src/ir-to-client-js/control-flow/stringify/composite-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAOH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAEzD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"composite-loop.d.ts","sourceRoot":"","sources":["../../../../src/ir-to-client-js/control-flow/stringify/composite-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAOH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAEzD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,CA2GrF"}
|
|
@@ -125,6 +125,20 @@ export declare function emitLoopItemElementSetup(lines: string[], opts: {
|
|
|
125
125
|
indent: string;
|
|
126
126
|
/** Single-root layout: 'inline' (plain / branch-plain) or 'multiline' (composite). */
|
|
127
127
|
singleRootLayout: 'inline' | 'multiline';
|
|
128
|
+
/**
|
|
129
|
+
* Emit `mountRowRoot(__el)` on the FRESH branch, connecting the row at the
|
|
130
|
+
* mount point `mapArray` handed down before the body's tail runs.
|
|
131
|
+
*
|
|
132
|
+
* Only bodies that initialise something inside the row need it — the tail
|
|
133
|
+
* is where `useContext` would otherwise resolve against a detached element
|
|
134
|
+
* and fall through to the global last-writer-wins store. A row with no
|
|
135
|
+
* nested init has nothing to resolve, so plain loops leave this off and
|
|
136
|
+
* their emission (and the `mapArrayLazy` measurements) are untouched.
|
|
137
|
+
*
|
|
138
|
+
* Never on the hydration branch: that row came from SSR markup and is in
|
|
139
|
+
* the document already.
|
|
140
|
+
*/
|
|
141
|
+
mountRow?: boolean;
|
|
128
142
|
}): void;
|
|
129
143
|
/**
|
|
130
144
|
* Multi-root template clone for loop bodies that emit a JSX Fragment with
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-parse.d.ts","sourceRoot":"","sources":["../../../../src/ir-to-client-js/control-flow/stringify/template-parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAkBH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAsB3D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIvE;AAkDD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAKvH;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAIjF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAajF;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,OAAO,CAAA;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,sFAAsF;IACtF,gBAAgB,EAAE,QAAQ,GAAG,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"template-parse.d.ts","sourceRoot":"","sources":["../../../../src/ir-to-client-js/control-flow/stringify/template-parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAkBH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAsB3D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIvE;AAkDD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAKvH;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAIjF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAajF;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,OAAO,CAAA;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,sFAAsF;IACtF,gBAAgB,EAAE,QAAQ,GAAG,WAAW,CAAA;IACxC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,GACA,IAAI,CA4BN;AAED;;;;;;;;;GASG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,MAAM,EAAE,CAeV"}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Import detection and DOM import management.
|
|
3
3
|
*/
|
|
4
4
|
import type { ComponentIR } from '../types.ts';
|
|
5
|
-
export declare const RUNTIME_IMPORT_CANDIDATES: readonly ['createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount', 'hydrate', 'insert', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'mapArrayLazy', 'patchLeaf', 'createDisposableEffect', 'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild', 'createPortal', 'provideContext', 'createContext', 'useContext', 'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText', 'escapeTextOrNode', 'qsa', 'qsaItem', 'qsaChildScope', 'qsaChildScopes', 'upsertChildItem', '__slot', '__bfSlot', '__bfText', 'claimSlots', 'lazySlots', 'lazyClaimSlots', 'textOrNode', 'beginTurn', 'endTurn', 'date', 'formatDate'];
|
|
5
|
+
export declare const RUNTIME_IMPORT_CANDIDATES: readonly ['createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount', 'hydrate', 'insert', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'mapArrayLazy', 'patchLeaf', 'createDisposableEffect', 'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild', 'mountRowRoot', 'createPortal', 'provideContext', 'createContext', 'useContext', 'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText', 'escapeTextOrNode', 'qsa', 'qsaItem', 'qsaChildScope', 'qsaChildScopes', 'upsertChildItem', '__slot', '__bfSlot', '__bfText', 'claimSlots', 'lazySlots', 'lazyClaimSlots', 'textOrNode', 'beginTurn', 'endTurn', 'date', 'formatDate'];
|
|
6
6
|
/** @deprecated Use RUNTIME_IMPORT_CANDIDATES */
|
|
7
|
-
export declare const DOM_IMPORT_CANDIDATES: readonly ["createSignal", "createMemo", "createEffect", "onCleanup", "onMount", "hydrate", "insert", "getLoopChildren", "getLoopNodes", "mapArray", "mapArrayAnchored", "mapArrayLazy", "patchLeaf", "createDisposableEffect", "createComponent", "renderChild", "registerComponent", "registerTemplate", "initChild", "upsertChild", "createPortal", "provideContext", "createContext", "useContext", "forwardProps", "applyRestAttrs", "splitProps", "spreadAttrs", "styleToCss", "escapeAttr", "escapeText", "escapeTextOrNode", "qsa", "qsaItem", "qsaChildScope", "qsaChildScopes", "upsertChildItem", "__slot", "__bfSlot", "__bfText", "claimSlots", "lazySlots", "lazyClaimSlots", "textOrNode", "beginTurn", "endTurn", "date", "formatDate"];
|
|
7
|
+
export declare const DOM_IMPORT_CANDIDATES: readonly ["createSignal", "createMemo", "createEffect", "onCleanup", "onMount", "hydrate", "insert", "getLoopChildren", "getLoopNodes", "mapArray", "mapArrayAnchored", "mapArrayLazy", "patchLeaf", "createDisposableEffect", "createComponent", "renderChild", "registerComponent", "registerTemplate", "initChild", "upsertChild", "mountRowRoot", "createPortal", "provideContext", "createContext", "useContext", "forwardProps", "applyRestAttrs", "splitProps", "spreadAttrs", "styleToCss", "escapeAttr", "escapeText", "escapeTextOrNode", "qsa", "qsaItem", "qsaChildScope", "qsaChildScopes", "upsertChildItem", "__slot", "__bfSlot", "__bfText", "claimSlots", "lazySlots", "lazyClaimSlots", "textOrNode", "beginTurn", "endTurn", "date", "formatDate"];
|
|
8
8
|
export declare const RUNTIME_MODULE = "@barefootjs/client/runtime";
|
|
9
9
|
export declare const IMPORT_PLACEHOLDER = "/* __BAREFOOTJS_DOM_IMPORTS__ */";
|
|
10
10
|
export declare const MODULE_CONSTANTS_PLACEHOLDER = "/* __MODULE_LEVEL_CONSTANTS__ */";
|
|
@@ -17,6 +17,30 @@ export declare function detectUsedImports(code: string): Set<string>;
|
|
|
17
17
|
* compiler-emitted /runtime subpath).
|
|
18
18
|
*/
|
|
19
19
|
export declare function collectUserDomImports(ir: ComponentIR): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Build the "is this local name used as a value in the generated code?"
|
|
22
|
+
* test used to decide which imported specifiers survive into the client
|
|
23
|
+
* bundle. Prefers a real value-reference set over the historical
|
|
24
|
+
* `\bname\b` text scan (#2432: an object key or string literal that
|
|
25
|
+
* merely spells an imported name used to emit a phantom import). Falls
|
|
26
|
+
* back to a substring scan when the generated text cannot be parsed
|
|
27
|
+
* cleanly — a partial parse would under-report references and DROP a
|
|
28
|
+
* needed import. The reference set is computed at most once per call.
|
|
29
|
+
*
|
|
30
|
+
* The fallback is a plain `includes()`, not a `\bname\b` regex: `\b` is
|
|
31
|
+
* defined over `[A-Za-z0-9_]`, so a `$`-prefixed name (`$fetch`, as
|
|
32
|
+
* exported by `ofetch`) or a non-ASCII local both sit outside a word
|
|
33
|
+
* boundary and would never match — silently dropping the import, the one
|
|
34
|
+
* failure direction this helper must never take. Worse, splicing
|
|
35
|
+
* `localName` straight into `new RegExp(...)` treated `$` as the
|
|
36
|
+
* end-of-input anchor, so `\b$fetch\b` couldn't match `$fetch` at all.
|
|
37
|
+
* `includes()` is deliberately COARSER than a word-boundary scan (it
|
|
38
|
+
* matches `helper` inside `helperFoo` too) — that's fine here: the
|
|
39
|
+
* fallback's only job is "never under-report", and over-keeping an
|
|
40
|
+
* import whose binding already exists is harmless, while dropping one is
|
|
41
|
+
* fatal.
|
|
42
|
+
*/
|
|
43
|
+
export declare function makeValueUsageTest(generatedCode: string): (localName: string) => boolean;
|
|
20
44
|
/**
|
|
21
45
|
* Collect external (non-DOM, non-component) imports that are used in generated code.
|
|
22
46
|
* These are third-party libraries like @barefootjs/form, zod, etc. that need to be
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../src/ir-to-client-js/imports.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAU,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../src/ir-to-client-js/imports.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAU,MAAM,aAAa,CAAA;AAKtD,eAAO,MAAM,yBAAyB,YACpC,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,SAAS,EACpE,SAAS,EAAE,QAAQ,EAAE,iBAAiB,EAAE,cAAc,EAAE,UAAU,EAAE,kBAAkB,EAAE,cAAc,EAAE,WAAW,EAAE,wBAAwB,EAC7I,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,WAAW,EAAE,aAAa,EAKrG,cAAc,EACd,cAAc,EACd,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAC/C,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAC3H,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAUxG,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAEzD,WAAW,EAAE,SAAS,EAItB,MAAM,EAIN,YAAY,CACJ,CAAA;AAEV,gDAAgD;AAChD,eAAO,MAAM,qBAAqB,wuBAA4B,CAAA;AAE9D,eAAO,MAAM,cAAc,+BAA+B,CAAA;AAE1D,eAAO,MAAM,kBAAkB,qCAAqC,CAAA;AACpE,eAAO,MAAM,4BAA4B,qCAAqC,CAAA;AAE9E;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAqB3D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,WAAW,GAAG,MAAM,EAAE,CAoB/D;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAWxF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAE,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAsCvH"}
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
import ts from 'typescript';
|
|
19
|
+
/**
|
|
20
|
+
* Identifier-position classifier: returns `true` when `id` is being USED
|
|
21
|
+
* as a value, `false` when it's a declaration name, property key, member-
|
|
22
|
+
* access name, or other non-reference slot.
|
|
23
|
+
*
|
|
24
|
+
* A ShorthandPropertyAssignment (`{ Theme }`) intentionally counts as a
|
|
25
|
+
* reference — it reads the binding, it doesn't just spell its name.
|
|
26
|
+
*
|
|
27
|
+
* CONTRACT: this classifies identifier positions in **JavaScript** source.
|
|
28
|
+
* Both current callers parse with `ts.ScriptKind.JS` —
|
|
29
|
+
* `collectValueReferencedNames` below, and `detectStrippedReferences` in
|
|
30
|
+
* `packages/cli/src/lib/resolve-imports.ts`, which parses the assembled
|
|
31
|
+
* bundle. TypeScript-only positions are deliberately NOT handled: an
|
|
32
|
+
* identifier in a type position (`const x: Foo = …`, a
|
|
33
|
+
* `TypeReferenceNode`) is still reported as a value reference, and so are
|
|
34
|
+
* `interface` / `type` / `enum` declaration names. Do not point this at
|
|
35
|
+
* TypeScript source — it would over-report there.
|
|
36
|
+
*
|
|
37
|
+
* Caveat: this is a syntactic test, not a scope analysis. If a local
|
|
38
|
+
* function parameter happens to share a name with an imported binding,
|
|
39
|
+
* references inside that function's body will count as references to
|
|
40
|
+
* the import (false positive). For the import-emission caller
|
|
41
|
+
* (`makeValueUsageTest` in `ir-to-client-js/imports.ts`), over-counting is
|
|
42
|
+
* the safe direction — an extra import whose binding exists is harmless.
|
|
43
|
+
* It is NOT safe for the BF053 caller (`detectStrippedReferences`): there,
|
|
44
|
+
* an over-reported reference to a stripped binding is a false build
|
|
45
|
+
* error on legal code. That asymmetry is why every exclusion branch below
|
|
46
|
+
* matters — each one is a case that would otherwise misfire BF053.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isValueReferenceIdentifier(id: ts.Identifier): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Parse `code` and collect the text of every identifier that is a VALUE
|
|
51
|
+
* reference per `isValueReferenceIdentifier`.
|
|
52
|
+
*
|
|
53
|
+
* Returns `null` when the text did not parse cleanly. `null` means
|
|
54
|
+
* "cannot answer" — callers MUST fall back to their previous (regex-scan)
|
|
55
|
+
* behaviour rather than treating it as an empty set. Narrowing on a
|
|
56
|
+
* partial parse would DROP a needed import, which is the failure
|
|
57
|
+
* direction we must never take (a phantom missing-import build failure
|
|
58
|
+
* is recoverable; a silently dead client bundle is not).
|
|
59
|
+
*/
|
|
60
|
+
export declare function collectValueReferencedNames(code: string): Set<string> | null;
|
|
61
|
+
//# sourceMappingURL=value-references.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"value-references.d.ts","sourceRoot":"","sources":["../src/value-references.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAA;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,EAAE,CAAC,UAAU,GAAG,OAAO,CAyCrE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CA0B5E"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/jsx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "JSX compiler for BarefootJS - transforms JSX to server HTML + client JS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"directory": "packages/jsx"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@barefootjs/shared": "0.
|
|
56
|
+
"@barefootjs/shared": "0.29.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@barefootjs/client": ">=0.2.0",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import { collectExternalImports } from '../../ir-to-client-js/imports'
|
|
2
|
+
import { collectExternalImports, collectUserDomImports } from '../../ir-to-client-js/imports'
|
|
3
3
|
import type { ComponentIR, ImportInfo, SourceLocation } from '../../types'
|
|
4
4
|
|
|
5
5
|
const dummyLoc: SourceLocation = { file: 'test.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } }
|
|
@@ -229,4 +229,119 @@ describe('collectExternalImports', () => {
|
|
|
229
229
|
"import { z } from 'zod'",
|
|
230
230
|
])
|
|
231
231
|
})
|
|
232
|
+
|
|
233
|
+
// #2432: `collectExternalImports` used a `\bname\b` text scan to decide
|
|
234
|
+
// which specifiers survive into the client bundle. That scan never
|
|
235
|
+
// consulted the per-specifier `isTypeOnly` flag, so a per-specifier
|
|
236
|
+
// `import { paperColor, type Theme } from '../lib/theme'` re-emitted
|
|
237
|
+
// `Theme` as a VALUE import whenever the word "Theme" merely appeared in
|
|
238
|
+
// the generated code (e.g. as an object key) — the CLI's relative-import
|
|
239
|
+
// inliner then put `Theme` in the IIFE's `return { … }` with no binding,
|
|
240
|
+
// producing `ReferenceError: Theme is not defined` at load.
|
|
241
|
+
test('per-specifier type-only specifier is not emitted, while its value sibling is (#2432)', () => {
|
|
242
|
+
const ir: ReturnType<typeof makeIR> = makeIR([{
|
|
243
|
+
source: '../lib/theme',
|
|
244
|
+
specifiers: [
|
|
245
|
+
{ name: 'paperColor', alias: null, isDefault: false, isNamespace: false },
|
|
246
|
+
{ name: 'Theme', alias: null, isDefault: false, isNamespace: false, isTypeOnly: true },
|
|
247
|
+
],
|
|
248
|
+
isTypeOnly: false,
|
|
249
|
+
loc: dummyLoc,
|
|
250
|
+
}])
|
|
251
|
+
const code = "const labels = { Theme: 'テーマ' };\nconst c = paperColor({ paper: '#fff' });"
|
|
252
|
+
const result = collectExternalImports(ir, code)
|
|
253
|
+
expect(result).toEqual(["import { paperColor } from '../lib/theme'"])
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('a value specifier that appears ONLY as an object key is not emitted (#2432)', () => {
|
|
257
|
+
const ir = makeIR([makeImport('./utils', ['helper'])])
|
|
258
|
+
const code = 'const o = { helper: 1 };'
|
|
259
|
+
const result = collectExternalImports(ir, code)
|
|
260
|
+
expect(result).toEqual([])
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('a value specifier that appears ONLY inside a string literal is not emitted (#2432)', () => {
|
|
264
|
+
const ir = makeIR([makeImport('./utils', ['helper'])])
|
|
265
|
+
const code = "const s = 'helper is a function';"
|
|
266
|
+
const result = collectExternalImports(ir, code)
|
|
267
|
+
expect(result).toEqual([])
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test('a value specifier that appears ONLY as a property-access name is not emitted (#2432)', () => {
|
|
271
|
+
const ir = makeIR([makeImport('./utils', ['helper'])])
|
|
272
|
+
const code = 'obj.helper();'
|
|
273
|
+
const result = collectExternalImports(ir, code)
|
|
274
|
+
expect(result).toEqual([])
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('a value specifier used via shorthand property IS emitted (#2432)', () => {
|
|
278
|
+
const ir = makeIR([makeImport('./utils', ['helper'])])
|
|
279
|
+
const code = 'const o = { helper };'
|
|
280
|
+
const result = collectExternalImports(ir, code)
|
|
281
|
+
expect(result).toEqual(["import { helper } from './utils'"])
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('unparseable generated text falls back to the text scan (#2432)', () => {
|
|
285
|
+
const ir = makeIR([makeImport('some-lib', ['helper'])])
|
|
286
|
+
const code = 'MyComponent helper()'
|
|
287
|
+
const result = collectExternalImports(ir, code)
|
|
288
|
+
expect(result).toEqual(["import { helper } from 'some-lib'"])
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// These two pin the fallback branch specifically (generated text that does
|
|
292
|
+
// NOT parse cleanly, so `makeValueUsageTest` falls back to a substring
|
|
293
|
+
// scan rather than the AST-based value-reference set). #2432: the
|
|
294
|
+
// fallback used to be a `new RegExp(`\\b${localName}\\b`)` scan, which
|
|
295
|
+
// silently DROPPED the import for either shape below — `\b` isn't defined
|
|
296
|
+
// for `$` or non-ASCII characters, and `$` is also a regex metacharacter
|
|
297
|
+
// (end-of-input anchor) when spliced unescaped into `new RegExp(...)`, so
|
|
298
|
+
// `\b$fetch\b` could never match `$fetch` at all. Dropping the import is
|
|
299
|
+
// the one failure direction this helper must never take.
|
|
300
|
+
test('fallback matches a $-prefixed specifier (#2432)', () => {
|
|
301
|
+
const ir = makeIR([makeImport('ofetch', ['$fetch'])])
|
|
302
|
+
const code = 'MyComponent $fetch()'
|
|
303
|
+
const result = collectExternalImports(ir, code)
|
|
304
|
+
expect(result).toEqual(["import { $fetch } from 'ofetch'"])
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('fallback matches a non-ASCII specifier (#2432)', () => {
|
|
308
|
+
const ir = makeIR([makeImport('some-lib', ['日本語'])])
|
|
309
|
+
const code = 'MyComponent 日本語()'
|
|
310
|
+
const result = collectExternalImports(ir, code)
|
|
311
|
+
expect(result).toEqual(["import { 日本語 } from 'some-lib'"])
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// #2432 follow-up: a class field name is a member key, not a read — the
|
|
315
|
+
// same treatment as an object-literal key above.
|
|
316
|
+
test('a specifier that appears ONLY as a class field name is not emitted (#2432 follow-up)', () => {
|
|
317
|
+
const ir = makeIR([makeImport('./utils', ['helper'])])
|
|
318
|
+
const code = 'class Widget { helper = 1 }'
|
|
319
|
+
const result = collectExternalImports(ir, code)
|
|
320
|
+
expect(result).toEqual([])
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
// #2432 follow-up: a COMPUTED class field name (`[helper]`) IS a read of
|
|
324
|
+
// the binding — pins that the class-field exclusion didn't over-exclude.
|
|
325
|
+
test('a specifier that appears as a COMPUTED class field name IS emitted (#2432 follow-up)', () => {
|
|
326
|
+
const ir = makeIR([makeImport('./utils', ['helper'])])
|
|
327
|
+
const code = 'class Widget { [helper] = 1 }'
|
|
328
|
+
const result = collectExternalImports(ir, code)
|
|
329
|
+
expect(result).toEqual(["import { helper } from './utils'"])
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
describe('collectUserDomImports', () => {
|
|
334
|
+
test('per-specifier type-only specifier is not emitted from the runtime subpath (#2432)', () => {
|
|
335
|
+
const ir = makeIR([{
|
|
336
|
+
source: '@barefootjs/client',
|
|
337
|
+
specifiers: [
|
|
338
|
+
{ name: 'createSignal', alias: null, isDefault: false, isNamespace: false },
|
|
339
|
+
{ name: 'Signal', alias: null, isDefault: false, isNamespace: false, isTypeOnly: true },
|
|
340
|
+
],
|
|
341
|
+
isTypeOnly: false,
|
|
342
|
+
loc: dummyLoc,
|
|
343
|
+
}])
|
|
344
|
+
const result = collectUserDomImports(ir)
|
|
345
|
+
expect(result).toEqual(['createSignal'])
|
|
346
|
+
})
|
|
232
347
|
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which code creates a loop row's root element, and whether that row is
|
|
3
|
+
* connected before its body's tail runs.
|
|
4
|
+
*
|
|
5
|
+
* The "connect before init" invariant exists because `useContext` resolves by
|
|
6
|
+
* walking `parentElement`: a child that initialises inside a detached row finds
|
|
7
|
+
* no ancestors and falls through to the global last-writer-wins context store,
|
|
8
|
+
* silently reading whichever provider on the page wrote last. So the creation
|
|
9
|
+
* site has to know its destination in advance, and there are two ways to tell
|
|
10
|
+
* it — a PLACEHOLDER already in the tree (`mountAt`, child slots), or the
|
|
11
|
+
* AMBIENT row mount point.
|
|
12
|
+
*
|
|
13
|
+
* Both work because a runtime FUNCTION creates the element and can take an
|
|
14
|
+
* argument. A row whose root is a template clone written inline in the emitted
|
|
15
|
+
* body has no such function, so the compiler emits `mountRowRoot(clone)` to
|
|
16
|
+
* consume the same ambient point — but only for the variant that initialises
|
|
17
|
+
* anything inside the row. A row with no nested init has nothing that could
|
|
18
|
+
* resolve wrongly, and skipping it there keeps the high-volume `mapArrayLazy`
|
|
19
|
+
* emission untouched.
|
|
20
|
+
*
|
|
21
|
+
* This matrix pins that split against compiled output rather than
|
|
22
|
+
* recollection. `mountsRow` is the load-bearing column: if it ever flips to
|
|
23
|
+
* false for a shape that initialises a child, that shape has silently gone
|
|
24
|
+
* back to initialising detached, and nothing else in the suite would say so —
|
|
25
|
+
* the wrong value it produces is only wrong when a second provider of the same
|
|
26
|
+
* context happens to be on the page.
|
|
27
|
+
*/
|
|
28
|
+
import { describe, test, expect } from 'bun:test'
|
|
29
|
+
import { compileJSX } from '../compiler'
|
|
30
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
31
|
+
|
|
32
|
+
interface RowShape {
|
|
33
|
+
/** How the row's root element comes into existence. */
|
|
34
|
+
rowRoot: 'createComponent' | 'template-clone'
|
|
35
|
+
/**
|
|
36
|
+
* Does a child inside the row reach its `init` through a placeholder slot
|
|
37
|
+
* (`upsertChild` / `upsertChildItem`)?
|
|
38
|
+
*
|
|
39
|
+
* Narrower than "something initialises in this row" — a conditional child
|
|
40
|
+
* arrives via `insert` + `initChild` and reads false here. Read it alongside
|
|
41
|
+
* `mountsRow`, never as a substitute.
|
|
42
|
+
*
|
|
43
|
+
* `initChild` on the row ROOT is deliberately excluded: that is the
|
|
44
|
+
* hydration branch adopting an SSR-rendered row, which is in the document by
|
|
45
|
+
* construction and never part of this problem.
|
|
46
|
+
*/
|
|
47
|
+
nestedChildInit: boolean
|
|
48
|
+
/** Multi-root (Fragment) body — the sibling-walk lookup path. */
|
|
49
|
+
multiRoot: boolean
|
|
50
|
+
/** Is the fresh row connected before the body's tail runs? */
|
|
51
|
+
mountsRow: boolean
|
|
52
|
+
/** Which list runtime the loop compiles to. */
|
|
53
|
+
loopRuntime: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function classify(loopBody: string): RowShape {
|
|
57
|
+
const source = `"use client"
|
|
58
|
+
import { createSignal } from '@barefootjs/client'
|
|
59
|
+
import { Chip } from './chip'
|
|
60
|
+
function T() {
|
|
61
|
+
const [items] = createSignal([{ id: '1', name: 'a' }])
|
|
62
|
+
return <ul>{items().map((it) => ${loopBody})}</ul>
|
|
63
|
+
}
|
|
64
|
+
export { T }`
|
|
65
|
+
const result = compileJSX(source, 'T.tsx', { adapter: new TestAdapter() })
|
|
66
|
+
const hard = result.errors.filter((e) => e.severity !== 'warning')
|
|
67
|
+
if (hard.length > 0) throw new Error(`unexpected compile error: ${hard.map((e) => e.code).join(',')}`)
|
|
68
|
+
const js = result.files.find((f) => f.type === 'clientJs')?.content ?? ''
|
|
69
|
+
|
|
70
|
+
// Slice from the list call to the end of the emitted init function.
|
|
71
|
+
// Everything the row does at setup time is in there, and the top-level
|
|
72
|
+
// `createComponent('T', …)` export sits after it, so it cannot be mistaken
|
|
73
|
+
// for a row root. Starting at the call also excludes the import line, so
|
|
74
|
+
// `mountsRow` reflects a real call site rather than the import of one.
|
|
75
|
+
const call = /mapArray(?:Lazy|Anchored)?\(/.exec(js)
|
|
76
|
+
if (!call) throw new Error('no list runtime call emitted')
|
|
77
|
+
const hydrateAt = js.indexOf('\nhydrate(')
|
|
78
|
+
const region = js.slice(call.index, hydrateAt === -1 ? undefined : hydrateAt)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
rowRoot: /cloneNode\(/.test(region) ? 'template-clone' : 'createComponent',
|
|
82
|
+
nestedChildInit: /upsertChild\(|upsertChildItem\(/.test(region),
|
|
83
|
+
multiRoot: /__bfExtras|upsertChildItem\(|qsaItem\(/.test(region),
|
|
84
|
+
mountsRow: /mountRowRoot\(/.test(region),
|
|
85
|
+
loopRuntime: /mapArray(?:Lazy|Anchored)?/.exec(region)?.[0] ?? 'unknown',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('loop row creation paths — who makes the row root, and when it is connected', () => {
|
|
90
|
+
test('component row root: the runtime creates it, so it takes the mount point itself', () => {
|
|
91
|
+
// `<Chip/>` IS the row. `createComponent` consumes the ambient point in its
|
|
92
|
+
// own step 7b, so no separate call is emitted.
|
|
93
|
+
expect(classify('<Chip key={it.id} id={it.id} />')).toEqual({
|
|
94
|
+
rowRoot: 'createComponent',
|
|
95
|
+
nestedChildInit: false,
|
|
96
|
+
multiRoot: false,
|
|
97
|
+
mountsRow: false,
|
|
98
|
+
loopRuntime: 'mapArray',
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('markup row root with no component inside: a clone, and deliberately not mounted', () => {
|
|
103
|
+
// Nothing initialises inside this row, so there is no `useContext` call
|
|
104
|
+
// during setup to resolve wrongly — and this is the high-volume path, so
|
|
105
|
+
// leaving it alone keeps the `mapArrayLazy` emission and the measurements
|
|
106
|
+
// taken against it untouched. `mountsRow: false` here is a decision.
|
|
107
|
+
expect(classify('<li key={it.id}>{it.name}</li>')).toEqual({
|
|
108
|
+
rowRoot: 'template-clone',
|
|
109
|
+
nestedChildInit: false,
|
|
110
|
+
multiRoot: false,
|
|
111
|
+
mountsRow: false,
|
|
112
|
+
loopRuntime: 'mapArrayLazy',
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('markup row root with a nested component: clone, mounted before the tail', () => {
|
|
117
|
+
// The row root is user markup, so it is not and cannot become a component;
|
|
118
|
+
// `mountRowRoot` is what gives it a destination. Flip `mountsRow` to false
|
|
119
|
+
// and `upsertChild` goes back to running the child's `init` against a
|
|
120
|
+
// detached element.
|
|
121
|
+
expect(classify('<li key={it.id}><Chip id={it.id} /></li>')).toEqual({
|
|
122
|
+
rowRoot: 'template-clone',
|
|
123
|
+
nestedChildInit: true,
|
|
124
|
+
multiRoot: false,
|
|
125
|
+
mountsRow: true,
|
|
126
|
+
loopRuntime: 'mapArray',
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('two nested components in one markup row: same case, one mount', () => {
|
|
131
|
+
expect(classify('<li key={it.id}><Chip id={it.id} /><Chip id={it.name} /></li>')).toEqual({
|
|
132
|
+
rowRoot: 'template-clone',
|
|
133
|
+
nestedChildInit: true,
|
|
134
|
+
multiRoot: false,
|
|
135
|
+
mountsRow: true,
|
|
136
|
+
loopRuntime: 'mapArray',
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('Fragment row with a nested component: mounted, and the multi-root lookup copes', () => {
|
|
141
|
+
// A Fragment root can never be a component, so this shape only became
|
|
142
|
+
// reachable for a mount point once the clone path got one. It is also the
|
|
143
|
+
// multi-root lookup, whose sibling walk in `qsa-item.ts` used to end with
|
|
144
|
+
// `return` and skip the `__bfExtras` stash the moment the primary was
|
|
145
|
+
// attached — a `break` now, so an attached primary still finds its extras.
|
|
146
|
+
expect(classify('<><Chip id={it.id} /><span>{it.name}</span></>')).toEqual({
|
|
147
|
+
rowRoot: 'template-clone',
|
|
148
|
+
nestedChildInit: true,
|
|
149
|
+
multiRoot: true,
|
|
150
|
+
mountsRow: true,
|
|
151
|
+
loopRuntime: 'mapArray',
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('conditional child in a markup row: mounted, though it inits through `insert`', () => {
|
|
156
|
+
// `nestedChildInit` is false because this child arrives via `insert` +
|
|
157
|
+
// `initChild` rather than `upsertChild` — but it is still a child
|
|
158
|
+
// initialising inside the row, and the emission is keyed on the loop
|
|
159
|
+
// variant (composite), not on which call the child arrives through. This
|
|
160
|
+
// row is why the two columns have to be read separately.
|
|
161
|
+
expect(classify('<li key={it.id}>{it.name ? <Chip id={it.id} /> : null}</li>')).toEqual({
|
|
162
|
+
rowRoot: 'template-clone',
|
|
163
|
+
nestedChildInit: false,
|
|
164
|
+
multiRoot: false,
|
|
165
|
+
mountsRow: true,
|
|
166
|
+
loopRuntime: 'mapArray',
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end compiler pin for issue #2432's minimal reproduction.
|
|
3
|
+
*
|
|
4
|
+
* `collectExternalImports` used to decide which imported specifiers to
|
|
5
|
+
* re-emit into generated client JS with a `\bname\b` text scan over the
|
|
6
|
+
* generated code, never consulting the PER-SPECIFIER `isTypeOnly` flag
|
|
7
|
+
* (only the whole-declaration `import type { ... }` flag). So
|
|
8
|
+
* `import { paperColor, type Theme } from '../lib/theme'` re-emitted
|
|
9
|
+
* `Theme` as a VALUE import whenever the word "Theme" merely appeared in
|
|
10
|
+
* the emitted code — here, as an object key (`{ Theme: 'テーマ' }`).
|
|
11
|
+
*
|
|
12
|
+
* The CLI's relative-import inliner then places every requested name in
|
|
13
|
+
* the target module's top-level IIFE `return { … }`. `Theme` is a
|
|
14
|
+
* type-only export with no runtime binding, so the `return` referenced an
|
|
15
|
+
* undeclared name — `ReferenceError: Theme is not defined` at load,
|
|
16
|
+
* which kills the *entire* page's client JS before hydrate ever runs.
|
|
17
|
+
* The failure is silent from the user's perspective: the SSR-rendered
|
|
18
|
+
* markup is already on screen, so the page looks fine even though no
|
|
19
|
+
* interactivity ever wires up. See piconic-ai/barefootjs#2432.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, test, expect } from 'bun:test'
|
|
23
|
+
import { compileJSX } from '../compiler'
|
|
24
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
25
|
+
|
|
26
|
+
const adapter = new TestAdapter()
|
|
27
|
+
|
|
28
|
+
describe('type-only import leak (#2432)', () => {
|
|
29
|
+
test('a per-specifier type-only import is never re-emitted into client JS', () => {
|
|
30
|
+
const source = `
|
|
31
|
+
'use client'
|
|
32
|
+
import { createSignal } from '@barefootjs/client'
|
|
33
|
+
import { paperColor, type Theme } from '../lib/theme'
|
|
34
|
+
|
|
35
|
+
export function ThemedBadge() {
|
|
36
|
+
const labels = { Theme: 'テーマ' }
|
|
37
|
+
const [color, setColor] = createSignal(paperColor({ paper: '#fff' }))
|
|
38
|
+
return <div title={labels.Theme}>{color()}</div>
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
const result = compileJSX(source, 'ThemedBadge.tsx', { adapter })
|
|
43
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
44
|
+
|
|
45
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
46
|
+
expect(clientJs).toBeDefined()
|
|
47
|
+
const code = clientJs!.content
|
|
48
|
+
|
|
49
|
+
// The value specifier survives as a real import.
|
|
50
|
+
expect(code).toContain("import { paperColor } from '../lib/theme'")
|
|
51
|
+
|
|
52
|
+
// No import statement anywhere in the bundle mentions `Theme` — the
|
|
53
|
+
// only legal destination for a value import of that name would be a
|
|
54
|
+
// binding that doesn't exist in the compiled `../lib/theme` module.
|
|
55
|
+
const importLines = code.split('\n').filter(line => line.trim().startsWith('import'))
|
|
56
|
+
for (const line of importLines) {
|
|
57
|
+
expect(line).not.toContain('Theme')
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `packages/jsx/src/value-references.ts` — the single-door
|
|
3
|
+
* "is this identifier a VALUE reference" classifier shared by
|
|
4
|
+
* `collectExternalImports` (jsx) and `detectStrippedReferences` (cli).
|
|
5
|
+
*
|
|
6
|
+
* There was no direct test file for this module before this one. It was
|
|
7
|
+
* added as a follow-up to #2432: a Copilot review on the original PR
|
|
8
|
+
* flagged that `ts.PropertyDeclaration` (class field) names were not
|
|
9
|
+
* excluded, so a class field like `helper` in `class C { helper = 1 }`
|
|
10
|
+
* was misclassified as a value reference. Harmless for the jsx-side
|
|
11
|
+
* caller (an extra kept import), but a false BF053 build error for the
|
|
12
|
+
* cli-side caller when the same name is a stripped import binding.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, test, expect } from 'bun:test'
|
|
16
|
+
import { collectValueReferencedNames } from '../value-references'
|
|
17
|
+
|
|
18
|
+
describe('collectValueReferencedNames', () => {
|
|
19
|
+
test('class field name only is NOT a reference', () => {
|
|
20
|
+
const result = collectValueReferencedNames('class C { helper = 1 }')
|
|
21
|
+
expect(result).not.toBeNull()
|
|
22
|
+
expect(result?.has('helper')).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('static class field name only is NOT a reference', () => {
|
|
26
|
+
const result = collectValueReferencedNames('class C { static helper = 1 }')
|
|
27
|
+
expect(result).not.toBeNull()
|
|
28
|
+
expect(result?.has('helper')).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('computed class field name IS a reference (does not over-exclude)', () => {
|
|
32
|
+
const result = collectValueReferencedNames('class C { [helper] = 1 }')
|
|
33
|
+
expect(result).not.toBeNull()
|
|
34
|
+
expect(result?.has('helper')).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('class method name is NOT a reference', () => {
|
|
38
|
+
const result = collectValueReferencedNames('class C { helper() {} }')
|
|
39
|
+
expect(result).not.toBeNull()
|
|
40
|
+
expect(result?.has('helper')).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('object-literal key is NOT a reference', () => {
|
|
44
|
+
const result = collectValueReferencedNames('const o = { helper: 1 };')
|
|
45
|
+
expect(result).not.toBeNull()
|
|
46
|
+
expect(result?.has('helper')).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('property access name is NOT a reference', () => {
|
|
50
|
+
const result = collectValueReferencedNames('obj.helper();')
|
|
51
|
+
expect(result).not.toBeNull()
|
|
52
|
+
expect(result?.has('helper')).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('shorthand property IS a reference (reads the binding)', () => {
|
|
56
|
+
const result = collectValueReferencedNames('const o = { helper };')
|
|
57
|
+
expect(result).not.toBeNull()
|
|
58
|
+
expect(result?.has('helper')).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('a genuine call IS a reference', () => {
|
|
62
|
+
const result = collectValueReferencedNames('helper();')
|
|
63
|
+
expect(result).not.toBeNull()
|
|
64
|
+
expect(result?.has('helper')).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('new.target: "target" is NOT a reference', () => {
|
|
68
|
+
const result = collectValueReferencedNames('function f() { return new.target; }')
|
|
69
|
+
expect(result).not.toBeNull()
|
|
70
|
+
expect(result?.has('target')).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Both halves of the MetaProperty exclusion are pinned: `new.target`
|
|
74
|
+
// above, and `import.meta` here — the one Copilot flagged as uncovered
|
|
75
|
+
// on #2439. `import.meta.url` also incidentally covers the
|
|
76
|
+
// property-access half (`url` is excluded too).
|
|
77
|
+
test('import.meta: "meta" is NOT a reference', () => {
|
|
78
|
+
const result = collectValueReferencedNames('const u = import.meta.url;')
|
|
79
|
+
expect(result).not.toBeNull()
|
|
80
|
+
expect(result?.has('meta')).toBe(false)
|
|
81
|
+
expect(result?.has('url')).toBe(false)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('unparseable text returns null ("cannot answer")', () => {
|
|
85
|
+
const result = collectValueReferencedNames('const x = ;;; {{{ ]]]')
|
|
86
|
+
expect(result).toBeNull()
|
|
87
|
+
})
|
|
88
|
+
})
|
package/src/analyzer.ts
CHANGED
|
@@ -3974,6 +3974,9 @@ function importsBrowserOnlyClientApi(ctx: AnalyzerContext): boolean {
|
|
|
3974
3974
|
if (imp.source !== '@barefootjs/client') continue
|
|
3975
3975
|
if (imp.isTypeOnly) continue
|
|
3976
3976
|
for (const spec of imp.specifiers) {
|
|
3977
|
+
// A per-specifier type-only import (`import { type onMount }`) is not
|
|
3978
|
+
// a browser-only API use — it has no runtime binding (#2432).
|
|
3979
|
+
if (spec.isTypeOnly) continue
|
|
3977
3980
|
const importedName = spec.name
|
|
3978
3981
|
if (BROWSER_ONLY_CLIENT_APIS.has(importedName)) return true
|
|
3979
3982
|
}
|