@barefootjs/vite 0.31.4 → 0.31.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/vite",
3
- "version": "0.31.4",
3
+ "version": "0.31.5",
4
4
  "description": "Vite plugin for BarefootJS: Vite/Rollup owns bundling, hashing, chunking, tree-shaking and minification of client assets, BarefootJS keeps only the JSX to (template, client JS) compile",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,17 +38,17 @@
38
38
  "directory": "packages/vite"
39
39
  },
40
40
  "dependencies": {
41
- "@barefootjs/shared": "0.31.4"
41
+ "@barefootjs/shared": "0.31.5"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@barefootjs/jsx": ">=0.2.0",
45
45
  "vite": "^6.0.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@barefootjs/client": "0.31.4",
49
- "@barefootjs/go-template": "0.31.4",
50
- "@barefootjs/hono": "0.31.4",
51
- "@barefootjs/jsx": "0.31.4",
48
+ "@barefootjs/client": "0.31.5",
49
+ "@barefootjs/go-template": "0.31.5",
50
+ "@barefootjs/hono": "0.31.5",
51
+ "@barefootjs/jsx": "0.31.5",
52
52
  "typescript": "^5.0.0",
53
53
  "vite": "^6.0.0"
54
54
  }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Regression (#2598): a mutable binding that survives into the emitted SSR
3
+ * template must keep the declarations that WRITE it.
4
+ *
5
+ * Reachability is seeded from the RENDERED JSX, after the client-only
6
+ * attributes are stripped — `ref={setRef}` leaves no `setRef` behind, and
7
+ * `onClick={handleClick}` renders as `onClick={() => {}}`. Pruning code
8
+ * reachable only from a handler is deliberate; it is client-only.
9
+ *
10
+ * The hole is a `let` that outlives its writer. It survives because some
11
+ * OTHER surviving declaration reads it, while its only assignment sat in a
12
+ * pruned handler — so the template declares it, reads it, and never assigns
13
+ * it. TypeScript then concludes it is permanently `null`, narrows every
14
+ * guarded use to `never`, and each member access fails with TS2339.
15
+ *
16
+ * The fixture reproduces the shape from the wild (piconic-ai/koma's
17
+ * FrameEditor), and the asymmetry that makes it reachable at all:
18
+ *
19
+ * <pre ref={handleHighlightRef} /> intrinsic — `ref` is STRIPPED, so
20
+ * the handler is pruned
21
+ * <Editable ref={handleTextareaRef}/> child component — props survive
22
+ * VERBATIM, so its handler is kept,
23
+ * which keeps `syncScroll`, which
24
+ * keeps `highlightEl` alive with no
25
+ * writer left
26
+ *
27
+ * A fixture with only intrinsic elements does NOT reproduce: the whole
28
+ * cluster is pruned together and nothing is left to narrow. The child
29
+ * component is load-bearing.
30
+ */
31
+ import { describe, test, expect, afterAll } from 'bun:test'
32
+ import { build } from 'vite'
33
+ import { mkdtemp, rm, readFile } from 'node:fs/promises'
34
+ import { tmpdir } from 'node:os'
35
+ import { join, resolve } from 'node:path'
36
+ import ts from 'typescript'
37
+ import { HonoAdapter } from '@barefootjs/hono/adapter'
38
+ import { barefoot } from '../plugin.ts'
39
+
40
+ const FIXTURE_ROOT = resolve(import.meta.dirname, '../../e2e-fixture-refwriter')
41
+ const APP_ROOT = join(FIXTURE_ROOT, 'app')
42
+ const COMPONENTS_DIR = join(FIXTURE_ROOT, 'components')
43
+
44
+ describe('writers of surviving mutable bindings', () => {
45
+ let outDir: string | undefined
46
+ const templatesDir = join(APP_ROOT, 'dist/components')
47
+
48
+ afterAll(async () => {
49
+ // Guarded: if the test throws before `mkdtemp` returns, `outDir` is still
50
+ // undefined and an unguarded `rm` would throw here, replacing the real
51
+ // failure with a cleanup TypeError.
52
+ if (outDir) await rm(outDir, { recursive: true, force: true })
53
+ await rm(join(APP_ROOT, 'dist'), { recursive: true, force: true })
54
+ })
55
+
56
+ test('retains a ref handler whose binding survives, and the template type-checks', async () => {
57
+ outDir = await mkdtemp(join(tmpdir(), 'barefoot-vite-refwriter-dist-'))
58
+
59
+ await build({
60
+ configFile: false,
61
+ root: APP_ROOT,
62
+ base: '/static/',
63
+ logLevel: 'warn',
64
+ build: { outDir, emptyOutDir: true },
65
+ plugins: [
66
+ barefoot({
67
+ adapter: new HonoAdapter(),
68
+ components: [COMPONENTS_DIR],
69
+ templates: templatesDir,
70
+ }),
71
+ ],
72
+ })
73
+
74
+ const templatePath = join(templatesDir, 'ScrollSync.tsx')
75
+ const template = await readFile(templatePath, 'utf8')
76
+
77
+ // The binding survives (some surviving declaration reads it) …
78
+ expect(template).toContain('let highlightEl')
79
+ // … so its writer must survive with it. This is the line the bug dropped.
80
+ expect(template).toContain('highlightEl = el')
81
+
82
+ // `renderSeq` is written with `++` rather than `=`, from a declaration
83
+ // retained only by this same closure — pins that update expressions
84
+ // count as writes, not just assignment operators.
85
+ expect(template).toContain('let renderSeq')
86
+ expect(template).toContain('renderSeq++')
87
+
88
+ // The symptom itself, not a proxy for it: type-check the emitted
89
+ // template. Asserting on the retained source line alone would still pass
90
+ // if some later change made the binding un-narrowable for a different
91
+ // reason.
92
+ const program = ts.createProgram([templatePath], {
93
+ strict: true,
94
+ noEmit: true,
95
+ target: ts.ScriptTarget.ESNext,
96
+ module: ts.ModuleKind.ESNext,
97
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
98
+ jsx: ts.JsxEmit.ReactJSX,
99
+ jsxImportSource: '@barefootjs/hono/jsx',
100
+ lib: ['lib.esnext.d.ts', 'lib.dom.d.ts'],
101
+ allowImportingTsExtensions: true,
102
+ skipLibCheck: true,
103
+ })
104
+ // Asserted over the WHOLE error set, not a TS2339-only filter: this
105
+ // fixture's emitted template compiles completely clean today, so any
106
+ // error at all — a syntax break, an unresolved import, a wrong JSX
107
+ // setting — is a real regression, and a narrow filter would let those
108
+ // through while still claiming the template type-checks. TS2339 on
109
+ // `never` is simply the member of that set this PR is about.
110
+ const errors = ts
111
+ .getPreEmitDiagnostics(program)
112
+ .filter((d) => d.category === ts.DiagnosticCategory.Error)
113
+ .map((d) => `TS${d.code}: ${ts.flattenDiagnosticMessageText(d.messageText, ' ')}`)
114
+ expect(errors).toEqual([])
115
+ }, 60_000)
116
+ })