@barefootjs/xslate 0.30.5 → 0.31.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/adapter/expr/emitters.d.ts +1 -1
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +12 -4
- package/dist/adapter/xslate-adapter.d.ts +4 -5
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/index.js +14 -7
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.d.ts +38 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +6641 -0
- package/lib/BarefootJS/Backend/Xslate.pm +1 -1
- package/package.json +22 -9
- package/src/__tests__/vite.test.ts +126 -0
- package/src/__tests__/xslate-adapter.test.ts +146 -0
- package/src/adapter/expr/emitters.ts +33 -9
- package/src/adapter/xslate-adapter.ts +45 -10
- package/src/render-divergences.ts +13 -15
- package/src/vite.ts +216 -0
- package/dist/build.d.ts +0 -28
- package/dist/build.d.ts.map +0 -1
- package/dist/build.js +0 -189197
- package/src/build.ts +0 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/xslate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Text::Xslate (Kolon) adapter for BarefootJS — compiles IR to .tx templates and ships the Xslate rendering backend; runs under any PSGI/Plack app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"./test-render": {
|
|
18
18
|
"bun": "./src/test-render.ts"
|
|
19
19
|
},
|
|
20
|
-
"./
|
|
21
|
-
"types": "./dist/
|
|
22
|
-
"import": "./dist/
|
|
20
|
+
"./vite": {
|
|
21
|
+
"types": "./dist/vite.d.ts",
|
|
22
|
+
"import": "./dist/vite.js"
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "bun run build:js && bun run build:types",
|
|
33
|
-
"build:js": "bun build ./src/index.ts ./src/adapter/index.ts
|
|
33
|
+
"build:js": "bun build ./src/index.ts ./src/adapter/index.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared --external @barefootjs/vite --external vite && bun build ./src/vite.ts --outfile ./dist/vite.js --format esm --target node --external @barefootjs/vite --external vite --external typescript",
|
|
34
34
|
"build:types": "tsgo --emitDeclarationOnly --outDir ./dist",
|
|
35
35
|
"test": "bun test",
|
|
36
36
|
"clean": "rm -rf dist",
|
|
@@ -55,14 +55,27 @@
|
|
|
55
55
|
"directory": "packages/adapter-xslate"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@barefootjs/shared": "0.
|
|
58
|
+
"@barefootjs/shared": "0.31.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@barefootjs/jsx": ">=0.2.0"
|
|
61
|
+
"@barefootjs/jsx": ">=0.2.0",
|
|
62
|
+
"@barefootjs/vite": ">=0.2.0",
|
|
63
|
+
"vite": "^6.0.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"@barefootjs/vite": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"vite": {
|
|
70
|
+
"optional": true
|
|
71
|
+
}
|
|
62
72
|
},
|
|
63
73
|
"devDependencies": {
|
|
64
74
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
65
|
-
"@barefootjs/jsx": "0.
|
|
66
|
-
"
|
|
75
|
+
"@barefootjs/jsx": "0.31.0",
|
|
76
|
+
"@barefootjs/vite": "0.31.0",
|
|
77
|
+
"@barefootjs/client": "0.31.0",
|
|
78
|
+
"typescript": "^5.0.0",
|
|
79
|
+
"vite": "^6.0.0"
|
|
67
80
|
}
|
|
68
81
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage of `@barefootjs/xslate/vite`'s `barefoot()`:
|
|
3
|
+
*
|
|
4
|
+
* - one real `vite build()` end to end (mirrors `@barefootjs/go-template/
|
|
5
|
+
* vite`, `@barefootjs/hono/vite`, `@barefootjs/blade/vite`'s,
|
|
6
|
+
* `@barefootjs/jinja/vite`'s, and `@barefootjs/erb/vite`'s own
|
|
7
|
+
* `vite.test.ts` rigor — a plugin that only passes mocked unit tests
|
|
8
|
+
* hasn't been shown to work) against a checked-in fixture (not a system
|
|
9
|
+
* tmpdir) so `@barefootjs/client` resolves through the monorepo's real
|
|
10
|
+
* node_modules symlinks;
|
|
11
|
+
* - the `assets` → generated `bf-assets.json` behavior, exercised the same
|
|
12
|
+
* way (a real build, since it needs the real manifest Vite writes).
|
|
13
|
+
*
|
|
14
|
+
* Unlike Go, there is no `afterEmit`-driven type-combination step to test
|
|
15
|
+
* here (see `vite.ts`'s module docstring) — `XslateAdapter.generate()` never
|
|
16
|
+
* produces a `types` section at all, so `barefoot()` always returns a
|
|
17
|
+
* single-element array unless `assets` is set.
|
|
18
|
+
*/
|
|
19
|
+
import { describe, test, expect } from 'bun:test'
|
|
20
|
+
import { build } from 'vite'
|
|
21
|
+
import { mkdtemp, rm, readFile } from 'node:fs/promises'
|
|
22
|
+
import { tmpdir } from 'node:os'
|
|
23
|
+
import { join, resolve } from 'node:path'
|
|
24
|
+
import { barefoot, barefoot as defaultBarefoot } from '../vite.ts'
|
|
25
|
+
|
|
26
|
+
const FIXTURE_ROOT = resolve(import.meta.dirname, '../../e2e-fixture')
|
|
27
|
+
|
|
28
|
+
describe('@barefootjs/xslate/vite: real vite build', () => {
|
|
29
|
+
test('exports the same function as both named `barefoot` and default', () => {
|
|
30
|
+
expect(defaultBarefoot).toBe(barefoot)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('returns a single-element plugin array when `assets` is omitted', () => {
|
|
34
|
+
const plugins = barefoot({ components: ['src/components'], templates: 'views' })
|
|
35
|
+
expect(plugins).toHaveLength(1)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('writes a self-contained .tx template with scriptAssets baked in, same as core alone would do', async () => {
|
|
39
|
+
const outDir = await mkdtemp(join(tmpdir(), 'barefoot-xslate-vite-dist-'))
|
|
40
|
+
const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-xslate-vite-views-'))
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await build({
|
|
44
|
+
configFile: false,
|
|
45
|
+
root: FIXTURE_ROOT,
|
|
46
|
+
base: '/static/build/',
|
|
47
|
+
logLevel: 'warn',
|
|
48
|
+
build: { outDir, emptyOutDir: true },
|
|
49
|
+
plugins: barefoot({
|
|
50
|
+
components: ['src/components'],
|
|
51
|
+
templates: templatesDir,
|
|
52
|
+
}),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const template = await readFile(join(templatesDir, 'Counter.tx'), 'utf8')
|
|
56
|
+
expect(template).toContain("bf.register_script(")
|
|
57
|
+
} finally {
|
|
58
|
+
await rm(outDir, { recursive: true, force: true })
|
|
59
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
60
|
+
}
|
|
61
|
+
}, 60_000)
|
|
62
|
+
|
|
63
|
+
test('`assets` resolves a non-component entry\'s manifest-hashed URL into a generated JSON asset map', async () => {
|
|
64
|
+
const outDir = await mkdtemp(join(tmpdir(), 'barefoot-xslate-vite-dist-assets-'))
|
|
65
|
+
const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-xslate-vite-views-assets-'))
|
|
66
|
+
const assetsPath = join(FIXTURE_ROOT, 'dist/bf-assets.json')
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await build({
|
|
70
|
+
configFile: false,
|
|
71
|
+
root: FIXTURE_ROOT,
|
|
72
|
+
base: '/static/build/',
|
|
73
|
+
logLevel: 'warn',
|
|
74
|
+
build: {
|
|
75
|
+
outDir,
|
|
76
|
+
emptyOutDir: true,
|
|
77
|
+
// Registering the non-component entry is the CALLER's job (stock
|
|
78
|
+
// Vite config) — `assets` below only resolves the URL Vite
|
|
79
|
+
// already bundled it to, it doesn't request the bundling.
|
|
80
|
+
rollupOptions: { input: { bootstrap: resolve(FIXTURE_ROOT, 'client/bootstrap.ts') } },
|
|
81
|
+
},
|
|
82
|
+
plugins: barefoot({
|
|
83
|
+
components: ['src/components'],
|
|
84
|
+
templates: templatesDir,
|
|
85
|
+
assets: { Bootstrap: 'client/bootstrap.ts' },
|
|
86
|
+
}),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const manifest = JSON.parse(await readFile(join(outDir, '.vite/manifest.json'), 'utf8'))
|
|
90
|
+
const expectedUrl = `/static/build/${manifest['client/bootstrap.ts'].file}`
|
|
91
|
+
|
|
92
|
+
const content = JSON.parse(await readFile(assetsPath, 'utf8'))
|
|
93
|
+
expect(content).toEqual({ Bootstrap: expectedUrl })
|
|
94
|
+
} finally {
|
|
95
|
+
await rm(outDir, { recursive: true, force: true })
|
|
96
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
97
|
+
await rm(join(FIXTURE_ROOT, 'dist'), { recursive: true, force: true })
|
|
98
|
+
}
|
|
99
|
+
}, 60_000)
|
|
100
|
+
|
|
101
|
+
test('`assets` throws an actionable error when the entry was never registered as a Rollup input', async () => {
|
|
102
|
+
const outDir = await mkdtemp(join(tmpdir(), 'barefoot-xslate-vite-dist-assets-missing-'))
|
|
103
|
+
const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-xslate-vite-views-assets-missing-'))
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await expect(
|
|
107
|
+
build({
|
|
108
|
+
configFile: false,
|
|
109
|
+
root: FIXTURE_ROOT,
|
|
110
|
+
base: '/static/build/',
|
|
111
|
+
logLevel: 'silent',
|
|
112
|
+
build: { outDir, emptyOutDir: true },
|
|
113
|
+
plugins: barefoot({
|
|
114
|
+
components: ['src/components'],
|
|
115
|
+
templates: templatesDir,
|
|
116
|
+
assets: { Bootstrap: 'client/bootstrap.ts' },
|
|
117
|
+
}),
|
|
118
|
+
}),
|
|
119
|
+
).rejects.toThrow(/was not found in the build manifest/)
|
|
120
|
+
} finally {
|
|
121
|
+
await rm(outDir, { recursive: true, force: true })
|
|
122
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
123
|
+
await rm(join(FIXTURE_ROOT, 'dist'), { recursive: true, force: true })
|
|
124
|
+
}
|
|
125
|
+
}, 60_000)
|
|
126
|
+
})
|
|
@@ -572,3 +572,149 @@ function Widget({ rows }: { rows: { x: string }[] }) {
|
|
|
572
572
|
expect(template).toContain('<: $cfg.x :>')
|
|
573
573
|
})
|
|
574
574
|
})
|
|
575
|
+
|
|
576
|
+
describe('XslateAdapter - scriptAssets (Vite late-binding, PR1)', () => {
|
|
577
|
+
const CLIENT_COMPONENT = `
|
|
578
|
+
'use client'
|
|
579
|
+
import { createSignal } from '@barefootjs/client'
|
|
580
|
+
export function Counter() {
|
|
581
|
+
const [count, setCount] = createSignal(0)
|
|
582
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
583
|
+
}
|
|
584
|
+
`
|
|
585
|
+
|
|
586
|
+
test('emits one register_script per URL, in order, when scriptAssets is set', () => {
|
|
587
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
588
|
+
const { template } = new XslateAdapter().generate(ir, {
|
|
589
|
+
scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'],
|
|
590
|
+
})
|
|
591
|
+
const runtimeIdx = template.indexOf("$bf.register_script('/assets/runtime-abc123.js')")
|
|
592
|
+
const compIdx = template.indexOf("$bf.register_script('/assets/counter-def456.js')")
|
|
593
|
+
expect(runtimeIdx).toBeGreaterThanOrEqual(0)
|
|
594
|
+
expect(compIdx).toBeGreaterThanOrEqual(0)
|
|
595
|
+
expect(runtimeIdx).toBeLessThan(compIdx)
|
|
596
|
+
expect(template).toContain('_bf_reg0')
|
|
597
|
+
expect(template).toContain('_bf_reg1')
|
|
598
|
+
expect(template).not.toContain('/static/components/barefoot.js')
|
|
599
|
+
expect(template).not.toContain('Counter.client.js')
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
test('emits a single registration for a single-element scriptAssets array', () => {
|
|
603
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
604
|
+
const { template } = new XslateAdapter().generate(ir, {
|
|
605
|
+
scriptAssets: ['/assets/only-one.js'],
|
|
606
|
+
})
|
|
607
|
+
expect(template).toContain("$bf.register_script('/assets/only-one.js')")
|
|
608
|
+
expect(template.match(/register_script/g)?.length).toBe(1)
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
test('an empty scriptAssets array emits no script registrations', () => {
|
|
612
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
613
|
+
const { template } = new XslateAdapter().generate(ir, { scriptAssets: [] })
|
|
614
|
+
expect(template).not.toContain('register_script')
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
test('skipScriptRegistration still wins when scriptAssets is also set', () => {
|
|
618
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
619
|
+
const { template } = new XslateAdapter().generate(ir, {
|
|
620
|
+
skipScriptRegistration: true,
|
|
621
|
+
scriptAssets: ['/assets/should-not-appear.js'],
|
|
622
|
+
})
|
|
623
|
+
expect(template).not.toContain('register_script')
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
test('absent scriptAssets falls back to adapter-computed script paths', () => {
|
|
627
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
628
|
+
const computed = new XslateAdapter().generate(ir).template
|
|
629
|
+
const explicitUndefined = new XslateAdapter().generate(ir, { scriptAssets: undefined }).template
|
|
630
|
+
expect(computed).toContain("$bf.register_script('/static/components/barefoot.js')")
|
|
631
|
+
expect(computed).toContain("$bf.register_script('/static/components/Counter.client.js')")
|
|
632
|
+
expect(explicitUndefined).toBe(computed)
|
|
633
|
+
})
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
describe('XslateAdapter - preloadAssets', () => {
|
|
637
|
+
const CLIENT_COMPONENT = `
|
|
638
|
+
'use client'
|
|
639
|
+
import { createSignal } from '@barefootjs/client'
|
|
640
|
+
export function Counter() {
|
|
641
|
+
const [count, setCount] = createSignal(0)
|
|
642
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
643
|
+
}
|
|
644
|
+
`
|
|
645
|
+
|
|
646
|
+
test('non-empty preloadAssets + non-empty scriptAssets: preload registrations emitted, in order, before script registrations', () => {
|
|
647
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
648
|
+
const { template } = new XslateAdapter().generate(ir, {
|
|
649
|
+
scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'],
|
|
650
|
+
preloadAssets: ['/assets/index-pre1.js', '/assets/shared-pre2.js'],
|
|
651
|
+
})
|
|
652
|
+
const pre1Idx = template.indexOf(": my $_bf_pre0 = $bf.register_preload('/assets/index-pre1.js');")
|
|
653
|
+
const pre2Idx = template.indexOf(": my $_bf_pre1 = $bf.register_preload('/assets/shared-pre2.js');")
|
|
654
|
+
const script1Idx = template.indexOf(": my $_bf_reg0 = $bf.register_script('/assets/runtime-abc123.js');")
|
|
655
|
+
const script2Idx = template.indexOf(": my $_bf_reg1 = $bf.register_script('/assets/counter-def456.js');")
|
|
656
|
+
expect(pre1Idx).toBeGreaterThanOrEqual(0)
|
|
657
|
+
expect(pre2Idx).toBeGreaterThan(pre1Idx)
|
|
658
|
+
expect(script1Idx).toBeGreaterThan(pre2Idx)
|
|
659
|
+
expect(script2Idx).toBeGreaterThan(script1Idx)
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
test('preloadAssets: [] emits no preload registration', () => {
|
|
663
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
664
|
+
const { template } = new XslateAdapter().generate(ir, {
|
|
665
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
666
|
+
preloadAssets: [],
|
|
667
|
+
})
|
|
668
|
+
expect(template).not.toContain('register_preload')
|
|
669
|
+
expect(template).toContain("$bf.register_script('/assets/runtime-abc123.js')")
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
test('preloadAssets: undefined emits no preload registration', () => {
|
|
673
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
674
|
+
const { template } = new XslateAdapter().generate(ir, {
|
|
675
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
676
|
+
preloadAssets: undefined,
|
|
677
|
+
})
|
|
678
|
+
expect(template).not.toContain('register_preload')
|
|
679
|
+
expect(template).toContain("$bf.register_script('/assets/runtime-abc123.js')")
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
test('preloadAssets non-empty but scriptAssets: [] emits no preload registration (preloads are only meaningful alongside a real script)', () => {
|
|
683
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
684
|
+
const { template } = new XslateAdapter().generate(ir, {
|
|
685
|
+
scriptAssets: [],
|
|
686
|
+
preloadAssets: ['/assets/index-pre1.js'],
|
|
687
|
+
})
|
|
688
|
+
expect(template).not.toContain('register_preload')
|
|
689
|
+
expect(template).not.toContain('register_script')
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
test('skipScriptRegistration: true suppresses both preloads and scripts', () => {
|
|
693
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
694
|
+
const { template } = new XslateAdapter().generate(ir, {
|
|
695
|
+
skipScriptRegistration: true,
|
|
696
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
697
|
+
preloadAssets: ['/assets/index-pre1.js'],
|
|
698
|
+
})
|
|
699
|
+
expect(template).not.toContain('register_preload')
|
|
700
|
+
expect(template).not.toContain('register_script')
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
// Regression guard: a previous attempt emitted a literal
|
|
704
|
+
// `<link rel="modulepreload">` tag directly into the component template,
|
|
705
|
+
// which injected a rendered DOM node before the component's root and
|
|
706
|
+
// broke hydration across all eight integrations (blade, erb,
|
|
707
|
+
// go-template, jinja, mojolicious, rust, twig, xslate). Preload hints
|
|
708
|
+
// must ONLY ever be emitted as no-output register statements (here,
|
|
709
|
+
// `: my $_bf_preN = $bf.register_preload(...);`) that the adapter's
|
|
710
|
+
// runtime later renders itself — never as literal markup baked into the
|
|
711
|
+
// template.
|
|
712
|
+
test('never emits a literal <link tag into the template', () => {
|
|
713
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
714
|
+
const { template } = new XslateAdapter().generate(ir, {
|
|
715
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
716
|
+
preloadAssets: ['/assets/index-pre1.js'],
|
|
717
|
+
})
|
|
718
|
+
expect(template).not.toContain('<link')
|
|
719
|
+
})
|
|
720
|
+
})
|
|
@@ -70,15 +70,33 @@ const PREDICATE_METHODS = new Set<HigherOrderMethod>([
|
|
|
70
70
|
* (#2038) instead of silently degrading to the callback's receiver.
|
|
71
71
|
*/
|
|
72
72
|
export class XslateFilterEmitter implements ParsedExprEmitter {
|
|
73
|
+
// Plain field declarations + assignment, NOT TS constructor-parameter-
|
|
74
|
+
// property shorthand: Vite's `bundleConfigFile` externalizes any bare
|
|
75
|
+
// (non-relative) import when loading `vite.config.ts` (see
|
|
76
|
+
// `@barefootjs/xslate/vite`'s docstring), so this file can be loaded
|
|
77
|
+
// directly by Node's OWN native TypeScript type-stripping (enabled by
|
|
78
|
+
// default since Node 22.18/23.6) rather than esbuild — and Node's
|
|
79
|
+
// strip-only mode does not support parameter properties (`SyntaxError
|
|
80
|
+
// [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]`), only plain type annotations.
|
|
81
|
+
private readonly param: string
|
|
82
|
+
private readonly localVarMap: Map<string, string>
|
|
83
|
+
private readonly isStringName: (n: string) => boolean
|
|
84
|
+
// Records a BF101 for predicate shapes this emitter can only degrade
|
|
85
|
+
// (#2038). Optional so emitter construction stays possible without an
|
|
86
|
+
// adapter; a missing hook keeps the old silent-degrade emit.
|
|
87
|
+
private readonly onUnsupported?: (message: string, reason?: string) => void
|
|
88
|
+
|
|
73
89
|
constructor(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
param: string,
|
|
91
|
+
localVarMap: Map<string, string>,
|
|
92
|
+
isStringName: (n: string) => boolean = () => false,
|
|
93
|
+
onUnsupported?: (message: string, reason?: string) => void,
|
|
94
|
+
) {
|
|
95
|
+
this.param = param
|
|
96
|
+
this.localVarMap = localVarMap
|
|
97
|
+
this.isStringName = isStringName
|
|
98
|
+
this.onUnsupported = onUnsupported
|
|
99
|
+
}
|
|
82
100
|
|
|
83
101
|
identifier(name: string): string {
|
|
84
102
|
if (name === this.param) return `$${this.param}`
|
|
@@ -239,7 +257,13 @@ export class XslateFilterEmitter implements ParsedExprEmitter {
|
|
|
239
257
|
* - higher-order methods route through `$bf` array helpers.
|
|
240
258
|
*/
|
|
241
259
|
export class XslateTopLevelEmitter implements ParsedExprEmitter {
|
|
242
|
-
|
|
260
|
+
// Plain field + assignment, not a parameter property — see
|
|
261
|
+
// `XslateFilterEmitter`'s constructor comment above for why.
|
|
262
|
+
private readonly ctx: XslateEmitContext
|
|
263
|
+
|
|
264
|
+
constructor(ctx: XslateEmitContext) {
|
|
265
|
+
this.ctx = ctx
|
|
266
|
+
}
|
|
243
267
|
|
|
244
268
|
identifier(name: string): string {
|
|
245
269
|
// `undefined` / `null` nested inside a larger expression tree —
|
|
@@ -163,9 +163,6 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
163
163
|
name = 'xslate'
|
|
164
164
|
extension = '.tx'
|
|
165
165
|
templatesPerComponent = true
|
|
166
|
-
// Template-string target with no component layer: `bf build` emits a static
|
|
167
|
-
// import-map HTML snippet to include into the page <head>.
|
|
168
|
-
importMapInjection = 'html-snippet' as const
|
|
169
166
|
|
|
170
167
|
/**
|
|
171
168
|
* Identifier-path callees the Xslate runtime can render in template scope.
|
|
@@ -267,10 +264,10 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
267
264
|
* `staticLoopSourceBoundNames` above: that set is a whole-component
|
|
268
265
|
* union used only to guard static-const inlining (safe to over-suppress
|
|
269
266
|
* there — the fallback is just "don't inline"), whereas the boolean-prop
|
|
270
|
-
* and nullable-optional classification sites
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* the loop too
|
|
267
|
+
* and nullable-optional classification sites (#2488) and `emitSpread`'s
|
|
268
|
+
* local-const fallback (#2489) need to know whether a name is loop-bound
|
|
269
|
+
* AT THIS POSITION, because for those the coarse fallback would silently
|
|
270
|
+
* mis-render a genuine occurrence of the same name outside the loop too.
|
|
274
271
|
*/
|
|
275
272
|
private loopBoundNames: Map<string, number> = new Map()
|
|
276
273
|
|
|
@@ -334,7 +331,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
334
331
|
// Generate script registration
|
|
335
332
|
const scriptReg = options?.skipScriptRegistration
|
|
336
333
|
? ''
|
|
337
|
-
: this.generateScriptRegistrations(ir, options?.scriptBaseName)
|
|
334
|
+
: this.generateScriptRegistrations(ir, options?.scriptBaseName, options?.scriptAssets, options?.preloadAssets)
|
|
338
335
|
|
|
339
336
|
// SSR context consumers (`const x = useContext(Ctx)`): seed each local
|
|
340
337
|
// from the active provider value (or the `createContext` default). The
|
|
@@ -376,7 +373,38 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
376
373
|
// Script Registration
|
|
377
374
|
// ===========================================================================
|
|
378
375
|
|
|
379
|
-
private generateScriptRegistrations(
|
|
376
|
+
private generateScriptRegistrations(
|
|
377
|
+
ir: ComponentIR,
|
|
378
|
+
scriptBaseName?: string,
|
|
379
|
+
scriptAssets?: string[],
|
|
380
|
+
preloadAssets?: string[],
|
|
381
|
+
): string {
|
|
382
|
+
// `scriptAssets`, when present (including `[]`), fully supersedes the
|
|
383
|
+
// adapter-computed `barefootJsPath` / `clientJsBasePath` pair — see
|
|
384
|
+
// `AdapterGenerateOptions.scriptAssets`. The caller (e.g. the Vite
|
|
385
|
+
// plugin) has already decided the exact ordered URL list, including
|
|
386
|
+
// whether any script is needed at all.
|
|
387
|
+
if (scriptAssets) {
|
|
388
|
+
if (scriptAssets.length === 0) return ''
|
|
389
|
+
// `preloadAssets` is only meaningful alongside a non-empty
|
|
390
|
+
// `scriptAssets` (see `AdapterGenerateOptions.preloadAssets`), and
|
|
391
|
+
// every preload registration is emitted BEFORE every script
|
|
392
|
+
// registration — a hint that arrives after the script it describes
|
|
393
|
+
// is useless. Distinct `_bf_pre*` var prefix keeps these `my`
|
|
394
|
+
// bindings from colliding with the `_bf_reg*` script ones below
|
|
395
|
+
// (Kolon forbids re-`my` of the same name in one scope). Same
|
|
396
|
+
// throwaway-`my` no-output trick as `register_script`:
|
|
397
|
+
// `register_preload`'s return value is never printed.
|
|
398
|
+
const lines = (preloadAssets ?? []).map(
|
|
399
|
+
(url, i) => `: my $_bf_pre${i} = $bf.register_preload('${url}');`,
|
|
400
|
+
)
|
|
401
|
+
lines.push(
|
|
402
|
+
...scriptAssets.map((url, i) => `: my $_bf_reg${i} = $bf.register_script('${url}');`),
|
|
403
|
+
)
|
|
404
|
+
lines.push('')
|
|
405
|
+
return lines.join('\n')
|
|
406
|
+
}
|
|
407
|
+
|
|
380
408
|
const hasInteractivity = hasClientInteractivity(ir)
|
|
381
409
|
if (!hasInteractivity) return ''
|
|
382
410
|
|
|
@@ -1412,7 +1440,14 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1412
1440
|
// and route through the same conditional-spread lowering. Only
|
|
1413
1441
|
// function-scope (`!isModule`) consts whose value is NOT itself a bare
|
|
1414
1442
|
// identifier (loop guard) are considered.
|
|
1415
|
-
|
|
1443
|
+
//
|
|
1444
|
+
// `loopBoundNames` guard (#2489): an enclosing `.map()` callback's own
|
|
1445
|
+
// param can shadow this outer const's name (`.map((attrs) => <p
|
|
1446
|
+
// {...attrs} />)`) — without the guard this forwarded the OUTER
|
|
1447
|
+
// const's value at every iteration instead of the per-item value.
|
|
1448
|
+
// Must be the LIVE map, not `staticLoopSourceBoundNames` — the same
|
|
1449
|
+
// name spread at ROOT must still resolve the const.
|
|
1450
|
+
if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.loopBoundNames.has(trimmed)) {
|
|
1416
1451
|
const localConst = (this.localConstants ?? []).find(
|
|
1417
1452
|
c => c.name === trimmed && !c.isModule,
|
|
1418
1453
|
)
|
|
@@ -20,22 +20,20 @@ export const renderDivergences: RenderDivergences = {
|
|
|
20
20
|
// instead of a fixed regex-shape catalogue) now correctly seeds `todos`
|
|
21
21
|
// from `(props.initialTodos ?? []).map(t => ({ ...t, editing: false }))`.
|
|
22
22
|
|
|
23
|
-
// Onboarding TSX-fidelity fixtures (PR #2461): `expectedHtml`
|
|
24
|
-
// hand-authored to the CORRECT output
|
|
23
|
+
// Onboarding TSX-fidelity fixtures (PR #2461): `expectedHtml` was
|
|
24
|
+
// hand-authored to the CORRECT output while the emission bug lived in
|
|
25
25
|
// the shared compiler layer — every adapter, including the Hono
|
|
26
|
-
// reference,
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
26
|
+
// reference, used to emit the broken form. That shared-layer defect
|
|
27
|
+
// (#2460) is now FIXED (b4f5075): `expectedHtml` is generated from the
|
|
28
|
+
// Hono reference like any other fixture. The remaining gap is
|
|
29
|
+
// per-template-adapter — this adapter still keys its template vars /
|
|
30
|
+
// ssr-defaults / props bridge off the local binding instead of
|
|
31
|
+
// `sourceName ?? name` — tracked by #2524 (the 7 silent template
|
|
32
|
+
// adapters). Graduate by applying the same `sourceName ?? name` fix to
|
|
33
|
+
// this adapter's emission path and deleting these lines (and the
|
|
34
|
+
// matching hono `skipJsx` entries, already gone).
|
|
31
35
|
'aliased-destructured-prop':
|
|
32
|
-
'aliased destructured prop `{ n: count }` loses its rename — template vars, ssr-defaults, and the props bridge all key off the local name, so the prop is always undefined (https://github.com/piconic-ai/barefootjs/issues/
|
|
36
|
+
'aliased destructured prop `{ n: count }` loses its rename — template vars, ssr-defaults, and the props bridge all key off the local name, so the prop is always undefined (https://github.com/piconic-ai/barefootjs/issues/2524)',
|
|
33
37
|
'composite-row-child-aliased-prop':
|
|
34
|
-
'same
|
|
35
|
-
|
|
36
|
-
// #2482 audit follow-ups: loop-scope holes in per-adapter name
|
|
37
|
-
// classification. Graduate by applying the loop-bound-name guards
|
|
38
|
-
// described in each issue and deleting the line.
|
|
39
|
-
'loop-param-shadows-spread-const':
|
|
40
|
-
'a .map() param shadowing an object const is resolved by `emitSpread` against `localConstants` with no loop-shadow check, so every row spreads the OUTER const instead of the row value (https://github.com/piconic-ai/barefootjs/issues/2489)',
|
|
38
|
+
'same defect as `aliased-destructured-prop` (#2524), inside a keyed `.map()` loop row: the nested child\'s renamed prop (`{ n: count }`) is always undefined, so both rows render an empty count (https://github.com/piconic-ai/barefootjs/issues/2524)',
|
|
41
39
|
}
|