@barefootjs/go-template 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/go-template-adapter.d.ts +3 -4
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +10 -26
- package/dist/go-types.d.ts +20 -0
- package/dist/go-types.d.ts.map +1 -0
- package/dist/index.js +11 -27
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.d.ts +57 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/{build.js → vite.js} +4850 -496
- package/package.json +21 -8
- package/src/__tests__/go-template-adapter.test.ts +145 -0
- package/src/__tests__/{build.test.ts → go-types.test.ts} +1 -1
- package/src/__tests__/vite.test.ts +278 -0
- package/src/adapter/go-template-adapter.ts +31 -6
- package/src/{build.ts → go-types.ts} +2 -86
- package/src/render-divergences.ts +12 -8
- package/src/test-render.ts +2 -2
- package/src/vite.ts +303 -0
- package/dist/build.d.ts +0 -53
- package/dist/build.d.ts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/go-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Go html/template adapter for BarefootJS - generates Go template files from IR",
|
|
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": [
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "bun run build:js && bun run build:types",
|
|
31
|
-
"build:js": "bun build ./src/index.ts ./src/adapter/index.ts
|
|
31
|
+
"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 --external typescript && bun build ./src/vite.ts --outfile ./dist/vite.js --format esm --target node --external @barefootjs/vite --external vite --external typescript",
|
|
32
32
|
"build:types": "tsgo --emitDeclarationOnly --outDir ./dist",
|
|
33
33
|
"test": "bun test",
|
|
34
34
|
"clean": "rm -rf dist",
|
|
@@ -49,14 +49,27 @@
|
|
|
49
49
|
"directory": "packages/adapter-go-template"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@barefootjs/shared": "0.
|
|
52
|
+
"@barefootjs/shared": "0.31.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@barefootjs/jsx": ">=0.2.0",
|
|
56
|
-
"
|
|
56
|
+
"@barefootjs/vite": ">=0.2.0",
|
|
57
|
+
"typescript": "^5.0.0",
|
|
58
|
+
"vite": "^6.0.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"@barefootjs/vite": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"vite": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
57
67
|
},
|
|
58
68
|
"devDependencies": {
|
|
59
69
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
60
|
-
"@barefootjs/
|
|
70
|
+
"@barefootjs/client": "0.31.0",
|
|
71
|
+
"@barefootjs/jsx": "0.31.0",
|
|
72
|
+
"@barefootjs/vite": "0.31.0",
|
|
73
|
+
"vite": "^6.0.0"
|
|
61
74
|
}
|
|
62
75
|
}
|
|
@@ -5619,3 +5619,148 @@ export function C({ items, enabled }: { items: Item[]; enabled: boolean }) {
|
|
|
5619
5619
|
}
|
|
5620
5620
|
})
|
|
5621
5621
|
})
|
|
5622
|
+
|
|
5623
|
+
describe('GoTemplateAdapter - scriptAssets (Vite late-binding)', () => {
|
|
5624
|
+
const CLIENT_COMPONENT = `
|
|
5625
|
+
'use client'
|
|
5626
|
+
import { createSignal } from '@barefootjs/client'
|
|
5627
|
+
export function Counter() {
|
|
5628
|
+
const [count, setCount] = createSignal(0)
|
|
5629
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
5630
|
+
}
|
|
5631
|
+
`
|
|
5632
|
+
|
|
5633
|
+
test('emits one {{.Scripts.Register}} per URL, in order, when scriptAssets is set', () => {
|
|
5634
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
5635
|
+
const { template } = new GoTemplateAdapter().generate(ir, {
|
|
5636
|
+
scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'],
|
|
5637
|
+
})
|
|
5638
|
+
const runtimeIdx = template.indexOf('{{.Scripts.Register "/assets/runtime-abc123.js"}}')
|
|
5639
|
+
const compIdx = template.indexOf('{{.Scripts.Register "/assets/counter-def456.js"}}')
|
|
5640
|
+
expect(runtimeIdx).toBeGreaterThanOrEqual(0)
|
|
5641
|
+
expect(compIdx).toBeGreaterThanOrEqual(0)
|
|
5642
|
+
expect(runtimeIdx).toBeLessThan(compIdx)
|
|
5643
|
+
expect(template).toContain('{{if .Scripts}}')
|
|
5644
|
+
expect(template).not.toContain('/static/client/barefoot.js')
|
|
5645
|
+
expect(template).not.toContain('Counter.client.js')
|
|
5646
|
+
})
|
|
5647
|
+
|
|
5648
|
+
test('emits a single registration for a single-element scriptAssets array', () => {
|
|
5649
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
5650
|
+
const { template } = new GoTemplateAdapter().generate(ir, {
|
|
5651
|
+
scriptAssets: ['/assets/only-one.js'],
|
|
5652
|
+
})
|
|
5653
|
+
expect(template).toContain('{{.Scripts.Register "/assets/only-one.js"}}')
|
|
5654
|
+
expect(template.match(/\.Scripts\.Register/g)?.length).toBe(1)
|
|
5655
|
+
})
|
|
5656
|
+
|
|
5657
|
+
test('an empty scriptAssets array emits no script registrations', () => {
|
|
5658
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
5659
|
+
const { template } = new GoTemplateAdapter().generate(ir, { scriptAssets: [] })
|
|
5660
|
+
expect(template).not.toContain('.Scripts.Register')
|
|
5661
|
+
expect(template).not.toContain('{{if .Scripts}}')
|
|
5662
|
+
})
|
|
5663
|
+
|
|
5664
|
+
test('skipScriptRegistration still wins when scriptAssets is also set', () => {
|
|
5665
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
5666
|
+
const { template } = new GoTemplateAdapter().generate(ir, {
|
|
5667
|
+
skipScriptRegistration: true,
|
|
5668
|
+
scriptAssets: ['/assets/should-not-appear.js'],
|
|
5669
|
+
})
|
|
5670
|
+
expect(template).not.toContain('.Scripts.Register')
|
|
5671
|
+
})
|
|
5672
|
+
|
|
5673
|
+
test('absent scriptAssets falls back to adapter-computed script paths', () => {
|
|
5674
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
5675
|
+
const computed = new GoTemplateAdapter().generate(ir).template
|
|
5676
|
+
const explicitUndefined = new GoTemplateAdapter().generate(ir, { scriptAssets: undefined }).template
|
|
5677
|
+
expect(computed).toContain('{{.Scripts.Register "/static/client/barefoot.js"}}')
|
|
5678
|
+
expect(computed).toContain('{{.Scripts.Register "/static/client/Counter.client.js"}}')
|
|
5679
|
+
expect(explicitUndefined).toBe(computed)
|
|
5680
|
+
})
|
|
5681
|
+
})
|
|
5682
|
+
|
|
5683
|
+
describe('GoTemplateAdapter - preloadAssets', () => {
|
|
5684
|
+
const CLIENT_COMPONENT = `
|
|
5685
|
+
'use client'
|
|
5686
|
+
import { createSignal } from '@barefootjs/client'
|
|
5687
|
+
export function Counter() {
|
|
5688
|
+
const [count, setCount] = createSignal(0)
|
|
5689
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
5690
|
+
}
|
|
5691
|
+
`
|
|
5692
|
+
|
|
5693
|
+
test('non-empty preloadAssets + non-empty scriptAssets: preload registrations emitted, in order, before script registrations', () => {
|
|
5694
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
5695
|
+
const { template } = new GoTemplateAdapter().generate(ir, {
|
|
5696
|
+
scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'],
|
|
5697
|
+
preloadAssets: ['/assets/index-pre1.js', '/assets/shared-pre2.js'],
|
|
5698
|
+
})
|
|
5699
|
+
const pre1Idx = template.indexOf('{{.Scripts.RegisterPreload "/assets/index-pre1.js"}}')
|
|
5700
|
+
const pre2Idx = template.indexOf('{{.Scripts.RegisterPreload "/assets/shared-pre2.js"}}')
|
|
5701
|
+
const script1Idx = template.indexOf('{{.Scripts.Register "/assets/runtime-abc123.js"}}')
|
|
5702
|
+
const script2Idx = template.indexOf('{{.Scripts.Register "/assets/counter-def456.js"}}')
|
|
5703
|
+
expect(pre1Idx).toBeGreaterThanOrEqual(0)
|
|
5704
|
+
expect(pre2Idx).toBeGreaterThan(pre1Idx)
|
|
5705
|
+
expect(script1Idx).toBeGreaterThan(pre2Idx)
|
|
5706
|
+
expect(script2Idx).toBeGreaterThan(script1Idx)
|
|
5707
|
+
})
|
|
5708
|
+
|
|
5709
|
+
test('preloadAssets: [] emits no preload registration', () => {
|
|
5710
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
5711
|
+
const { template } = new GoTemplateAdapter().generate(ir, {
|
|
5712
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
5713
|
+
preloadAssets: [],
|
|
5714
|
+
})
|
|
5715
|
+
expect(template).not.toContain('RegisterPreload')
|
|
5716
|
+
expect(template).toContain('{{.Scripts.Register "/assets/runtime-abc123.js"}}')
|
|
5717
|
+
})
|
|
5718
|
+
|
|
5719
|
+
test('preloadAssets: undefined emits no preload registration', () => {
|
|
5720
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
5721
|
+
const { template } = new GoTemplateAdapter().generate(ir, {
|
|
5722
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
5723
|
+
preloadAssets: undefined,
|
|
5724
|
+
})
|
|
5725
|
+
expect(template).not.toContain('RegisterPreload')
|
|
5726
|
+
expect(template).toContain('{{.Scripts.Register "/assets/runtime-abc123.js"}}')
|
|
5727
|
+
})
|
|
5728
|
+
|
|
5729
|
+
test('preloadAssets non-empty but scriptAssets: [] emits no preload registration (preloads are only meaningful alongside a real script)', () => {
|
|
5730
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
5731
|
+
const { template } = new GoTemplateAdapter().generate(ir, {
|
|
5732
|
+
scriptAssets: [],
|
|
5733
|
+
preloadAssets: ['/assets/index-pre1.js'],
|
|
5734
|
+
})
|
|
5735
|
+
expect(template).not.toContain('RegisterPreload')
|
|
5736
|
+
expect(template).not.toContain('.Scripts.Register')
|
|
5737
|
+
})
|
|
5738
|
+
|
|
5739
|
+
test('skipScriptRegistration: true suppresses both preloads and scripts', () => {
|
|
5740
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
5741
|
+
const { template } = new GoTemplateAdapter().generate(ir, {
|
|
5742
|
+
skipScriptRegistration: true,
|
|
5743
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
5744
|
+
preloadAssets: ['/assets/index-pre1.js'],
|
|
5745
|
+
})
|
|
5746
|
+
expect(template).not.toContain('RegisterPreload')
|
|
5747
|
+
expect(template).not.toContain('.Scripts.Register')
|
|
5748
|
+
})
|
|
5749
|
+
|
|
5750
|
+
// Regression guard: a previous attempt emitted a literal
|
|
5751
|
+
// `<link rel="modulepreload">` tag directly into the component template,
|
|
5752
|
+
// which injected a rendered DOM node before the component's root and
|
|
5753
|
+
// broke hydration across all eight integrations (blade, erb,
|
|
5754
|
+
// go-template, jinja, mojolicious, rust, twig, xslate). Preload hints
|
|
5755
|
+
// must ONLY ever be emitted as no-output register statements (here,
|
|
5756
|
+
// `{{.Scripts.RegisterPreload "..."}}`) that the adapter's runtime later
|
|
5757
|
+
// renders itself — never as literal markup baked into the template.
|
|
5758
|
+
test('never emits a literal <link tag into the template', () => {
|
|
5759
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
5760
|
+
const { template } = new GoTemplateAdapter().generate(ir, {
|
|
5761
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
5762
|
+
preloadAssets: ['/assets/index-pre1.js'],
|
|
5763
|
+
})
|
|
5764
|
+
expect(template).not.toContain('<link')
|
|
5765
|
+
})
|
|
5766
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { combineGoTypes, deduplicateGoTypes } from '../
|
|
2
|
+
import { combineGoTypes, deduplicateGoTypes } from '../go-types'
|
|
3
3
|
|
|
4
4
|
describe('combineGoTypes stdlib imports', () => {
|
|
5
5
|
// The combined types file strips each component's own import block and
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage of `@barefootjs/go-template/vite`'s `barefoot()`:
|
|
3
|
+
*
|
|
4
|
+
* - one real `vite build()` end to end (mirrors `packages/vite`'s own
|
|
5
|
+
* `e2e-vite-build.test.ts` rigor — a plugin that only passes mocked unit
|
|
6
|
+
* tests hasn't been shown to work) against a checked-in fixture (not a
|
|
7
|
+
* system tmpdir) so `@barefootjs/client` resolves through the
|
|
8
|
+
* monorepo's real node_modules symlinks;
|
|
9
|
+
* - the `afterEmit`-driven `components.go` combining behavior (empty
|
|
10
|
+
* `types`, write-if-changed) exercised by calling the returned plugin's
|
|
11
|
+
* own hooks directly, same style as `packages/vite`'s `plugin.test.ts` —
|
|
12
|
+
* `compileCanonical` never resolves `@barefootjs/client` as a real
|
|
13
|
+
* import (it's plain JSX→JS codegen, not a module load), so these don't
|
|
14
|
+
* need the fixture's real dependency graph at all.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, test, expect, afterEach } from 'bun:test'
|
|
17
|
+
import { build } from 'vite'
|
|
18
|
+
import { mkdtemp, rm, mkdir, writeFile, readFile, stat } from 'node:fs/promises'
|
|
19
|
+
import { tmpdir } from 'node:os'
|
|
20
|
+
import { join, resolve } from 'node:path'
|
|
21
|
+
import { barefoot, barefoot as defaultBarefoot } from '../vite.ts'
|
|
22
|
+
|
|
23
|
+
// biome-ignore lint: hooks are called directly, bypassing Vite's own
|
|
24
|
+
// dispatch/typing — the same cast `packages/vite/src/__tests__/
|
|
25
|
+
// plugin.test.ts` uses to unit-test a Vite plugin's hooks in isolation.
|
|
26
|
+
type AnyPlugin = any
|
|
27
|
+
|
|
28
|
+
const FIXTURE_ROOT = resolve(import.meta.dirname, '../../e2e-fixture')
|
|
29
|
+
|
|
30
|
+
describe('@barefootjs/go-template/vite: real vite build', () => {
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await rm(join(FIXTURE_ROOT, 'components.go'), { force: true })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('exports the same function as both named `barefoot` and default', () => {
|
|
36
|
+
expect(defaultBarefoot).toBe(barefoot)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('returns a single-element plugin array when `assets` is omitted', () => {
|
|
40
|
+
const plugins = barefoot({ components: ['src/components'], templates: 'views' })
|
|
41
|
+
expect(plugins).toHaveLength(1)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// core's `barefoot()` attaches `api.options` (see `BarefootPluginApi`) to
|
|
45
|
+
// the SAME plugin object this wrapper returns unchanged as `plugins[0]`
|
|
46
|
+
// — this pins that composition doesn't lose it, for either array shape
|
|
47
|
+
// (`assets` omitted vs. present, which adds a SECOND, unrelated
|
|
48
|
+
// companion plugin alongside it).
|
|
49
|
+
test('surfaces core\'s plugin.api.options unchanged on the returned plugin, with the GoTemplateAdapter it constructed', () => {
|
|
50
|
+
const plugins = barefoot({ components: ['src/components', '../shared/blog'], templates: 'views' }) as any[]
|
|
51
|
+
const core = plugins[0]
|
|
52
|
+
expect(core.name).toBe('barefoot')
|
|
53
|
+
expect(core.api.options.components).toEqual(['src/components', '../shared/blog'])
|
|
54
|
+
expect(core.api.options.templates).toBe('views')
|
|
55
|
+
expect(core.api.options.adapter.constructor.name).toBe('GoTemplateAdapter')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('still surfaces api.options on plugins[0] when `assets` adds a second companion plugin', () => {
|
|
59
|
+
const plugins = barefoot({
|
|
60
|
+
components: ['src/components'],
|
|
61
|
+
templates: 'views',
|
|
62
|
+
assets: { Bootstrap: 'client/bootstrap.ts' },
|
|
63
|
+
}) as any[]
|
|
64
|
+
expect(plugins).toHaveLength(2)
|
|
65
|
+
expect(plugins[0].api.options.components).toEqual(['src/components'])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('writes a compilable combined components.go after `vite build`, using the SAME combineGoTypes as ./build', async () => {
|
|
69
|
+
const outDir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-dist-'))
|
|
70
|
+
const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-views-'))
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await build({
|
|
74
|
+
configFile: false,
|
|
75
|
+
root: FIXTURE_ROOT,
|
|
76
|
+
base: '/static/build/',
|
|
77
|
+
logLevel: 'warn',
|
|
78
|
+
build: { outDir, emptyOutDir: true },
|
|
79
|
+
plugins: [
|
|
80
|
+
barefoot({
|
|
81
|
+
components: ['src/components'],
|
|
82
|
+
templates: templatesDir,
|
|
83
|
+
packageName: 'main',
|
|
84
|
+
typesOutputFile: 'components.go',
|
|
85
|
+
}),
|
|
86
|
+
],
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Template got its scriptAssets baked in, same as core alone would do.
|
|
90
|
+
const template = await readFile(join(templatesDir, 'Counter.tmpl'), 'utf8')
|
|
91
|
+
expect(template).toContain('Scripts.Register')
|
|
92
|
+
|
|
93
|
+
// components.go exists, is combined (one package header, randomID
|
|
94
|
+
// defined), and compiles-shaped (no per-component leftover headers).
|
|
95
|
+
const componentsGo = await readFile(join(FIXTURE_ROOT, 'components.go'), 'utf8')
|
|
96
|
+
expect(componentsGo).toContain('package main')
|
|
97
|
+
expect(componentsGo).toContain('func randomID(n int) string {')
|
|
98
|
+
expect(componentsGo).toContain('CounterProps')
|
|
99
|
+
expect(componentsGo.match(/^package main$/gm)).toHaveLength(1)
|
|
100
|
+
} finally {
|
|
101
|
+
await rm(outDir, { recursive: true, force: true })
|
|
102
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
103
|
+
}
|
|
104
|
+
}, 60_000)
|
|
105
|
+
|
|
106
|
+
test('`assets` resolves a non-component entry\'s manifest-hashed URL into the `production`-tagged, gitignored sibling file', async () => {
|
|
107
|
+
const outDir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-dist-assets-'))
|
|
108
|
+
const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-views-assets-'))
|
|
109
|
+
// A real `build()` is the `mode: 'build'` pass, so this writes the
|
|
110
|
+
// PRODUCTION-tagged sibling (`bf_assets_prod.go`), not the dev-tagged
|
|
111
|
+
// `bf_assets.go` default filename — see writeAssetMap's mode branch.
|
|
112
|
+
const assetsGoPath = join(FIXTURE_ROOT, 'bf_assets_prod.go')
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await build({
|
|
116
|
+
configFile: false,
|
|
117
|
+
root: FIXTURE_ROOT,
|
|
118
|
+
base: '/static/build/',
|
|
119
|
+
logLevel: 'warn',
|
|
120
|
+
build: {
|
|
121
|
+
outDir,
|
|
122
|
+
emptyOutDir: true,
|
|
123
|
+
// Registering the non-component entry is the CALLER's job (stock
|
|
124
|
+
// Vite config) — `assets` below only resolves the URL Vite
|
|
125
|
+
// already bundled it to, it doesn't request the bundling.
|
|
126
|
+
rollupOptions: { input: { bootstrap: resolve(FIXTURE_ROOT, 'client/bootstrap.ts') } },
|
|
127
|
+
},
|
|
128
|
+
plugins: barefoot({
|
|
129
|
+
components: ['src/components'],
|
|
130
|
+
templates: templatesDir,
|
|
131
|
+
assets: { Bootstrap: 'client/bootstrap.ts' },
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const manifest = JSON.parse(await readFile(join(outDir, '.vite/manifest.json'), 'utf8'))
|
|
136
|
+
const expectedUrl = `/static/build/${manifest['client/bootstrap.ts'].file}`
|
|
137
|
+
|
|
138
|
+
const content = await readFile(assetsGoPath, 'utf8')
|
|
139
|
+
expect(content).toContain('//go:build production')
|
|
140
|
+
expect(content).toContain('package main')
|
|
141
|
+
expect(content).toContain(`"Bootstrap": ${JSON.stringify(expectedUrl)}`)
|
|
142
|
+
|
|
143
|
+
// The dev-tagged default filename must NOT be touched by a build pass
|
|
144
|
+
// — that file only gets (re)written by the dev pass, and a stray
|
|
145
|
+
// build-triggered write to it would defeat its "stable, safe to
|
|
146
|
+
// commit" property (see the module docstring).
|
|
147
|
+
await expect(readFile(join(FIXTURE_ROOT, 'bf_assets.go'), 'utf8')).rejects.toThrow()
|
|
148
|
+
} finally {
|
|
149
|
+
await rm(outDir, { recursive: true, force: true })
|
|
150
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
151
|
+
await rm(assetsGoPath, { force: true })
|
|
152
|
+
}
|
|
153
|
+
}, 60_000)
|
|
154
|
+
|
|
155
|
+
test('`assets` throws an actionable error when the entry was never registered as a Rollup input', async () => {
|
|
156
|
+
const outDir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-dist-assets-missing-'))
|
|
157
|
+
const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-views-assets-missing-'))
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
await expect(
|
|
161
|
+
build({
|
|
162
|
+
configFile: false,
|
|
163
|
+
root: FIXTURE_ROOT,
|
|
164
|
+
base: '/static/build/',
|
|
165
|
+
logLevel: 'silent',
|
|
166
|
+
build: { outDir, emptyOutDir: true },
|
|
167
|
+
plugins: barefoot({
|
|
168
|
+
components: ['src/components'],
|
|
169
|
+
templates: templatesDir,
|
|
170
|
+
assets: { Bootstrap: 'client/bootstrap.ts' },
|
|
171
|
+
}),
|
|
172
|
+
}),
|
|
173
|
+
).rejects.toThrow(/was not found in the build manifest/)
|
|
174
|
+
} finally {
|
|
175
|
+
await rm(outDir, { recursive: true, force: true })
|
|
176
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
177
|
+
await rm(join(FIXTURE_ROOT, 'bf_assets_prod.go'), { force: true })
|
|
178
|
+
}
|
|
179
|
+
}, 60_000)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('@barefootjs/go-template/vite: afterEmit → components.go, via direct hook calls', () => {
|
|
183
|
+
let dir: string
|
|
184
|
+
|
|
185
|
+
afterEach(async () => {
|
|
186
|
+
if (dir) await rm(dir, { recursive: true, force: true })
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
async function setup(): Promise<{ plugin: AnyPlugin; templatesDir: string; componentsGoPath: string }> {
|
|
190
|
+
dir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-hooks-'))
|
|
191
|
+
await mkdir(join(dir, 'src/components'), { recursive: true })
|
|
192
|
+
await writeFile(
|
|
193
|
+
join(dir, 'src/components/Counter.tsx'),
|
|
194
|
+
'\'use client\'\nimport { createSignal } from \'@barefootjs/client\'\nexport function Counter(props: { initial: number }) {\n const [count] = createSignal(props.initial)\n return <button>{count()}</button>\n}\n',
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
const templatesDir = join(dir, 'internal/views')
|
|
198
|
+
const plugin: AnyPlugin = barefoot({ components: ['src/components'], templates: templatesDir })[0]
|
|
199
|
+
await plugin.config({ root: dir }, { command: 'build', mode: 'production' })
|
|
200
|
+
plugin.configResolved({ root: dir, base: '/static/build/', build: { outDir: 'dist', manifest: true } })
|
|
201
|
+
|
|
202
|
+
await mkdir(join(dir, 'dist/.vite'), { recursive: true })
|
|
203
|
+
await writeFile(
|
|
204
|
+
join(dir, 'dist/.vite/manifest.json'),
|
|
205
|
+
JSON.stringify({ 'src/components/Counter.tsx': { file: 'assets/Counter-abc123.js', isEntry: true } }),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return { plugin, templatesDir, componentsGoPath: join(dir, 'components.go') }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
test('combines the discovered component\'s types into components.go', async () => {
|
|
212
|
+
const { plugin, componentsGoPath } = await setup()
|
|
213
|
+
await plugin.writeBundle()
|
|
214
|
+
|
|
215
|
+
const content = await readFile(componentsGoPath, 'utf8')
|
|
216
|
+
expect(content).toContain('package main')
|
|
217
|
+
expect(content).toContain('CounterProps')
|
|
218
|
+
expect(content).toContain('func randomID(n int) string {')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('does not write components.go when no discovered component produces `types` (ctx.types.size === 0)', async () => {
|
|
222
|
+
dir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-hooks-notypes-'))
|
|
223
|
+
await mkdir(join(dir, 'src/components'), { recursive: true }) // deliberately empty
|
|
224
|
+
|
|
225
|
+
const templatesDir = join(dir, 'internal/views')
|
|
226
|
+
const plugin: AnyPlugin = barefoot({ components: ['src/components'], templates: templatesDir })[0]
|
|
227
|
+
await plugin.config({ root: dir }, { command: 'build', mode: 'production' })
|
|
228
|
+
plugin.configResolved({ root: dir, base: '/', build: { outDir: 'dist', manifest: true } })
|
|
229
|
+
await mkdir(join(dir, 'dist/.vite'), { recursive: true })
|
|
230
|
+
await writeFile(join(dir, 'dist/.vite/manifest.json'), '{}')
|
|
231
|
+
|
|
232
|
+
await plugin.writeBundle()
|
|
233
|
+
|
|
234
|
+
await expect(readFile(join(dir, 'components.go'), 'utf8')).rejects.toThrow()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('write-if-changed: a second pass with identical output does not rewrite components.go', async () => {
|
|
238
|
+
const { plugin, componentsGoPath } = await setup()
|
|
239
|
+
|
|
240
|
+
await plugin.writeBundle()
|
|
241
|
+
const firstMtime = (await stat(componentsGoPath)).mtimeMs
|
|
242
|
+
|
|
243
|
+
await new Promise(r => setTimeout(r, 20))
|
|
244
|
+
await plugin.writeBundle()
|
|
245
|
+
const secondMtime = (await stat(componentsGoPath)).mtimeMs
|
|
246
|
+
|
|
247
|
+
expect(secondMtime).toBe(firstMtime)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('honors manualTypes and transformTypes', async () => {
|
|
251
|
+
dir = await mkdtemp(join(tmpdir(), 'barefoot-go-vite-hooks-manual-'))
|
|
252
|
+
await mkdir(join(dir, 'src/components'), { recursive: true })
|
|
253
|
+
await writeFile(join(dir, 'src/components/Greeting.tsx'), 'export function Greeting() { return <p>Hi</p> }\n')
|
|
254
|
+
|
|
255
|
+
const templatesDir = join(dir, 'internal/views')
|
|
256
|
+
const plugin: AnyPlugin = barefoot({
|
|
257
|
+
components: ['src/components'],
|
|
258
|
+
templates: templatesDir,
|
|
259
|
+
// manualTypes is appended verbatim, AFTER transformTypes runs on the
|
|
260
|
+
// component-derived content (see `combineGoTypes`), so app-specific
|
|
261
|
+
// hand-written types are never mangled by a transform meant for
|
|
262
|
+
// generated code.
|
|
263
|
+
manualTypes: '// app-specific hand-written type\ntype AppOnly struct{}',
|
|
264
|
+
transformTypes: types => types.replace(/GreetingProps/g, 'GreetingPropsRenamed'),
|
|
265
|
+
})[0]
|
|
266
|
+
await plugin.config({ root: dir }, { command: 'build', mode: 'production' })
|
|
267
|
+
plugin.configResolved({ root: dir, base: '/', build: { outDir: 'dist', manifest: true } })
|
|
268
|
+
await mkdir(join(dir, 'dist/.vite'), { recursive: true })
|
|
269
|
+
await writeFile(join(dir, 'dist/.vite/manifest.json'), '{}')
|
|
270
|
+
|
|
271
|
+
await plugin.writeBundle()
|
|
272
|
+
|
|
273
|
+
const content = await readFile(join(dir, 'components.go'), 'utf8')
|
|
274
|
+
expect(content).toContain('type AppOnly struct{}')
|
|
275
|
+
expect(content).toContain('GreetingPropsRenamed')
|
|
276
|
+
expect(content).not.toMatch(/\btype GreetingProps struct/)
|
|
277
|
+
})
|
|
278
|
+
})
|
|
@@ -189,7 +189,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
189
189
|
// Sentinel marking a parent-scope `bf-s` slot inside a hoisted-JSX children
|
|
190
190
|
// bake (see `extractScopedHtmlChildren`). Can't appear in real HTML text.
|
|
191
191
|
private static readonly SCOPE_SENTINEL = '__BF_SCOPE_SENTINEL__'
|
|
192
|
-
importMapInjection = 'html-snippet' as const
|
|
193
192
|
|
|
194
193
|
// `renderFilterExpr` recursion state. `filterExprDepth` lets the outer call
|
|
195
194
|
// reset `filterExprUnsupported` per independent filter expression; the flag,
|
|
@@ -492,7 +491,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
492
491
|
|
|
493
492
|
const scriptRegistrations = options?.skipScriptRegistration
|
|
494
493
|
? ''
|
|
495
|
-
: this.generateScriptRegistrations(ir, options?.scriptBaseName)
|
|
494
|
+
: this.generateScriptRegistrations(ir, options?.scriptBaseName, options?.scriptAssets, options?.preloadAssets)
|
|
496
495
|
|
|
497
496
|
let template = `{{define "${this.state.componentName}"}}\n${scriptRegistrations}${templateBody}\n{{end}}\n`
|
|
498
497
|
// Companion children defines execute with the parent's data via `bf_tmpl`.
|
|
@@ -611,11 +610,37 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
611
610
|
}
|
|
612
611
|
|
|
613
612
|
/**
|
|
614
|
-
* Script registration code for the template start.
|
|
615
|
-
* Props struct) and guards the registrations
|
|
616
|
-
* collector.
|
|
613
|
+
* Script (and modulepreload) registration code for the template start.
|
|
614
|
+
* Reads `.Scripts` (on every Props struct) and guards the registrations
|
|
615
|
+
* with `{{if .Scripts}}` for a nil collector.
|
|
617
616
|
*/
|
|
618
|
-
private generateScriptRegistrations(
|
|
617
|
+
private generateScriptRegistrations(
|
|
618
|
+
ir: ComponentIR,
|
|
619
|
+
scriptBaseName?: string,
|
|
620
|
+
scriptAssets?: string[],
|
|
621
|
+
preloadAssets?: string[],
|
|
622
|
+
): string {
|
|
623
|
+
// `scriptAssets`, when present (including `[]`), fully supersedes the
|
|
624
|
+
// adapter-computed `barefootJsPath` / `clientJsBasePath` fallback pair
|
|
625
|
+
// below — see `AdapterGenerateOptions.scriptAssets`. The caller (e.g.
|
|
626
|
+
// the Vite plugin) has already decided the exact ordered URL list,
|
|
627
|
+
// including whether any script is needed at all.
|
|
628
|
+
if (scriptAssets) {
|
|
629
|
+
if (scriptAssets.length === 0) return ''
|
|
630
|
+
// `preloadAssets` is only meaningful alongside a non-empty
|
|
631
|
+
// `scriptAssets` (see `AdapterGenerateOptions.preloadAssets`), and
|
|
632
|
+
// every preload registration is emitted BEFORE every script
|
|
633
|
+
// registration — a hint that arrives after the script it describes
|
|
634
|
+
// is useless. `RegisterPreload`, like `Register`, is a no-output
|
|
635
|
+
// statement: the `<link rel="modulepreload">` tag itself is only
|
|
636
|
+
// ever rendered by `BfScripts` (runtime/bf.go), never here.
|
|
637
|
+
const preloadRegistrations = (preloadAssets ?? []).map(
|
|
638
|
+
(url) => `{{.Scripts.RegisterPreload "${url}"}}`,
|
|
639
|
+
)
|
|
640
|
+
const registrations = scriptAssets.map((url) => `{{.Scripts.Register "${url}"}}`)
|
|
641
|
+
return `{{if .Scripts}}${preloadRegistrations.join('')}${registrations.join('')}{{end}}\n`
|
|
642
|
+
}
|
|
643
|
+
|
|
619
644
|
const hasInteractivity = hasClientInteractivity(ir)
|
|
620
645
|
|
|
621
646
|
if (!hasInteractivity) {
|
|
@@ -1,21 +1,5 @@
|
|
|
1
|
-
// Go
|
|
2
|
-
|
|
3
|
-
import type { BuildOptions, PostBuildContext } from '@barefootjs/jsx'
|
|
4
|
-
import { GoTemplateAdapter } from './adapter/index.ts'
|
|
5
|
-
import type { GoTemplateAdapterOptions } from './adapter/index.ts'
|
|
6
|
-
|
|
7
|
-
export interface GoTemplateBuildOptions extends BuildOptions {
|
|
8
|
-
/** Adapter-specific options passed to GoTemplateAdapter */
|
|
9
|
-
adapterOptions?: GoTemplateAdapterOptions
|
|
10
|
-
/** Output path for combined Go types file (relative to projectDir, default: 'components.go') */
|
|
11
|
-
typesOutputFile?: string
|
|
12
|
-
/** Transform the combined types string before writing (for app-specific type fixes) */
|
|
13
|
-
transformTypes?: (types: string) => string
|
|
14
|
-
/** Manual type definitions to append (app-specific types not generated from components) */
|
|
15
|
-
manualTypes?: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// ── Go type helpers ──────────────────────────────────────────────────────
|
|
1
|
+
// Go type-combination helpers shared by the Vite plugin's post-emit hook
|
|
2
|
+
// (`vite.ts`'s `combineGoTypes` call, writing `components.go`).
|
|
19
3
|
|
|
20
4
|
/**
|
|
21
5
|
* Strip Go package header and import block, returning only type definitions.
|
|
@@ -205,71 +189,3 @@ export function combineGoTypes(options: {
|
|
|
205
189
|
|
|
206
190
|
return parts.join('\n') + '\n'
|
|
207
191
|
}
|
|
208
|
-
|
|
209
|
-
// ── Config factory ───────────────────────────────────────────────────────
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Create a BarefootBuildConfig for Go html/template projects.
|
|
213
|
-
*
|
|
214
|
-
* Uses structural typing — does not import BarefootBuildConfig to avoid
|
|
215
|
-
* circular dependency between @barefootjs/go-template and @barefootjs/cli.
|
|
216
|
-
*/
|
|
217
|
-
export function createConfig(options: GoTemplateBuildOptions = {}) {
|
|
218
|
-
const packageName = options.adapterOptions?.packageName ?? 'main'
|
|
219
|
-
const typesOutputFile = options.typesOutputFile ?? 'components.go'
|
|
220
|
-
|
|
221
|
-
const postBuild = async (ctx: PostBuildContext) => {
|
|
222
|
-
if (ctx.types.size === 0) return
|
|
223
|
-
|
|
224
|
-
const content = combineGoTypes({
|
|
225
|
-
types: ctx.types,
|
|
226
|
-
packageName,
|
|
227
|
-
manualTypes: options.manualTypes,
|
|
228
|
-
transformTypes: options.transformTypes,
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
if (content) {
|
|
232
|
-
const { resolve } = await import('node:path')
|
|
233
|
-
const { readFile, writeFile } = await import('node:fs/promises')
|
|
234
|
-
const outPath = resolve(ctx.projectDir, typesOutputFile)
|
|
235
|
-
// Write only when content changed so cache-hit builds don't trip the
|
|
236
|
-
// dev-reload sentinel (ctx.markChanged) and trigger a spurious reload.
|
|
237
|
-
// Use node:fs/promises (not Bun.*) so this hook runs under either
|
|
238
|
-
// runtime — the published `barefoot` CLI bin starts via Node.
|
|
239
|
-
const prev = await readFile(outPath, 'utf-8').catch(() => null)
|
|
240
|
-
if (prev !== content) {
|
|
241
|
-
await writeFile(outPath, content)
|
|
242
|
-
ctx.markChanged?.()
|
|
243
|
-
console.log(`Generated: ${typesOutputFile}`)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Chain user's postBuild with Go types generation
|
|
249
|
-
const userPostBuild = options.postBuild
|
|
250
|
-
const combinedPostBuild = userPostBuild
|
|
251
|
-
? async (ctx: PostBuildContext) => {
|
|
252
|
-
await postBuild(ctx)
|
|
253
|
-
await userPostBuild(ctx)
|
|
254
|
-
}
|
|
255
|
-
: postBuild
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
adapter: new GoTemplateAdapter(options.adapterOptions),
|
|
259
|
-
paths: options.paths,
|
|
260
|
-
components: options.components,
|
|
261
|
-
outDir: options.outDir,
|
|
262
|
-
minify: options.minify,
|
|
263
|
-
contentHash: options.contentHash,
|
|
264
|
-
externals: options.externals,
|
|
265
|
-
externalsBasePath: options.externalsBasePath,
|
|
266
|
-
bundleEntries: options.bundleEntries,
|
|
267
|
-
localImportPrefixes: options.localImportPrefixes,
|
|
268
|
-
outputLayout: options.outputLayout ?? {
|
|
269
|
-
templates: 'templates',
|
|
270
|
-
clientJs: 'client',
|
|
271
|
-
runtime: 'client',
|
|
272
|
-
},
|
|
273
|
-
postBuild: combinedPostBuild,
|
|
274
|
-
}
|
|
275
|
-
}
|