@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.
@@ -30,14 +30,18 @@ export const renderDivergences: RenderDivergences = {
30
30
  // `test-render.ts`) now replicates that documented contract for a
31
31
  // signal-backed dynamic child-component loop.
32
32
 
33
- // Onboarding TSX-fidelity fixtures (PR #2461): `expectedHtml` is
34
- // hand-authored to the CORRECT output because the emission bug lives in
33
+ // Onboarding TSX-fidelity fixtures (PR #2461): `expectedHtml` was
34
+ // hand-authored to the CORRECT output while the emission bug lived in
35
35
  // the shared compiler layer — every adapter, including the Hono
36
- // reference, currently emits the broken form (verified against this
37
- // adapter's emitted template; see each fixture's docstring). Graduate
38
- // by fixing the shared emission, regenerating `expectedHtml` from the
39
- // fixed reference, and deleting these lines (and the matching hono
40
- // `skipJsx` entries).
36
+ // reference, used to emit the broken form. That shared-layer defect
37
+ // (#2460) is now FIXED (b4f5075): `expectedHtml` is generated from the
38
+ // Hono reference like any other fixture. The remaining gap is
39
+ // Go-specific the Input struct field stays keyed by the local
40
+ // binding instead of `sourceName ?? name`, so the caller-side struct
41
+ // literal fails `go run` outright — tracked by #2525 (go-template's
42
+ // `go run` exit-1 failure). Graduate by keying the Input struct field
43
+ // by `sourceName ?? name` and deleting this line (and the matching
44
+ // hono `skipJsx` entry, already gone).
41
45
  'aliased-destructured-prop':
42
- 'aliased destructured prop `{ n: count }` loses its rename — the Input struct field is Count `json:"count"`, so the caller-side struct literal keyed by the real prop name fails `go run` outright (unknown field N, exit 1) (https://github.com/piconic-ai/barefootjs/issues/2460)',
46
+ 'aliased destructured prop `{ n: count }` loses its rename — the Input struct field is Count `json:"count"`, so the caller-side struct literal keyed by the real prop name fails `go run` outright (unknown field N, exit 1) (https://github.com/piconic-ai/barefootjs/issues/2525)',
43
47
  }
@@ -8,7 +8,7 @@
8
8
  import { compileJSX } from '@barefootjs/jsx'
9
9
  import type { TemplateAdapter, ComponentIR, ParsedExpr } from '@barefootjs/jsx'
10
10
  import { GoTemplateAdapter } from './adapter/go-template-adapter.ts'
11
- import { deduplicateGoTypes } from './build.ts'
11
+ import { deduplicateGoTypes } from './go-types.ts'
12
12
  import { capitalizeFieldName, goFieldNameForKey, loopKeyToGoFieldPath } from './adapter/lib/go-naming.ts'
13
13
  import { findNestedComponents } from './adapter/analysis/component-tree.ts'
14
14
  import type { NestedComponentInfo } from './adapter/lib/types.ts'
@@ -258,7 +258,7 @@ export async function renderGoTemplateComponent(options: RenderOptions): Promise
258
258
  }
259
259
 
260
260
  // Merge entry + sibling + child type blocks through the same
261
- // `deduplicateGoTypes` helper `bf build` uses. Duplicates arise in two
261
+ // `deduplicateGoTypes` helper `vite.ts`'s `combineGoTypes` uses. Duplicates arise in two
262
262
  // ways (#1896): a multi-component file emits its module-scope shared
263
263
  // types (a context-value struct, a data `type Payment = …`) once per
264
264
  // component IR — both across the entry source's own sibling exports
package/src/vite.ts ADDED
@@ -0,0 +1,303 @@
1
+ /**
2
+ * `@barefootjs/go-template/vite` — a Go-specific COMPOSITION of core
3
+ * `@barefootjs/vite`'s `barefoot()`, not a new plugin implementation. Per
4
+ * the design brief: the generic escape hatch (`afterEmit`) lives in core;
5
+ * everything Go-specific (constructing `GoTemplateAdapter`, combining
6
+ * per-file `types` output into one compilable `components.go`, and — for
7
+ * apps with a hand-written non-component script entry — a generated Go
8
+ * asset map) is confined here so core never needs to know Go exists.
9
+ *
10
+ * The asset map is split across TWO build-tagged files declaring the same
11
+ * `Assets` symbol (see `writeAssetMap`/`renderAssetMapFile` below): a
12
+ * `!production`-tagged dev file (committed — dev-server URLs never change)
13
+ * and a `production`-tagged sibling (gitignored — hashed build URLs churn
14
+ * every `vite build`). The untagged default is DEV, so `go run .` on a
15
+ * fresh clone with no prior build just works; a production build/deploy
16
+ * must pass `-tags production` or it silently compiles the dev file's
17
+ * localhost URLs — `bfdev.GuardAssets` (Go runtime) is the loud failure for
18
+ * that mistake.
19
+ *
20
+ * A user never passes `adapter` — this wraps core's `barefoot()` and
21
+ * constructs `GoTemplateAdapter` itself:
22
+ *
23
+ * import { barefoot } from '@barefootjs/go-template/vite'
24
+ *
25
+ * export default defineConfig({
26
+ * base: '/static/build/',
27
+ * build: { outDir: 'static/build' },
28
+ * plugins: [barefoot({
29
+ * components: ['src/components'],
30
+ * templates: 'internal/views',
31
+ * packageName: 'main',
32
+ * typesOutputFile: 'components.go',
33
+ * })],
34
+ * })
35
+ *
36
+ * Named `barefoot` (not `barefootGo`) both as a named AND default export,
37
+ * matching core's `packages/vite/src/index.ts` exactly: the import
38
+ * specifier already names the adapter, so the identifier doesn't need to
39
+ * repeat it — and swapping adapters later only changes the specifier, not
40
+ * every call site's identifier.
41
+ *
42
+ * ## Why this returns `Plugin[]`, not a single `Plugin`
43
+ *
44
+ * `afterEmit`'s context (per core's `types.ts`) is DELIBERATELY narrow:
45
+ * `types` / `projectDir` / `templatesDir` / `outDir` / `mode` — no
46
+ * `ResolvedConfig`, no dev origin, no manifest. Combining `types` into
47
+ * `components.go` needs nothing more than that. But the OPTIONAL `assets`
48
+ * map (see below) needs to resolve a Vite-bundled URL for a file core's
49
+ * own component discovery never sees, which needs the SAME machinery core
50
+ * itself uses internally (`ResolvedConfig`, the dev origin, the build
51
+ * manifest) — none of which `afterEmit` carries, on purpose. A tiny
52
+ * companion plugin (`goAssetsConfigCapture` below) exists SOLELY to
53
+ * capture that context via its own `configResolved`/`configureServer`
54
+ * hooks into closure variables `afterEmit` (defined in the SAME closure)
55
+ * reads when it fires. It contributes no `types`/template logic of its
56
+ * own — the actual `components.go`/asset-map WRITES both still happen
57
+ * inside the ONE `afterEmit` callback, per the design brief's
58
+ * recommendation. Returning `[core]` alone when `assets` is empty (the
59
+ * common case) keeps the single-plugin shape for anyone inspecting the
60
+ * array's length; Vite flattens either way.
61
+ */
62
+ import { readFile, writeFile } from 'node:fs/promises'
63
+ import { resolve } from 'node:path'
64
+ import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
65
+ import { barefoot as coreBarefoot } from '@barefootjs/vite'
66
+ import type { AfterEmitContext } from '@barefootjs/vite'
67
+ import { devModuleUrl, loadManifest, resolveDevOrigin, resolveScriptAssets, toPosixRelative } from '@barefootjs/vite'
68
+ import { GoTemplateAdapter } from './adapter/index.ts'
69
+ import { combineGoTypes } from './go-types.ts'
70
+
71
+ export interface GoTemplateViteOptions {
72
+ /** Source directories to scan for `.tsx` components, relative to the
73
+ * Vite project root (or absolute). */
74
+ components: string[]
75
+ /** Where compiled templates, `ssrDefaults`, and `components.go` land —
76
+ * relative to the Vite project root (or absolute). */
77
+ templates: string
78
+ /** Go package name for generated types (default: 'main'). */
79
+ packageName?: string
80
+ /** Output path for the combined Go types file, relative to the Vite
81
+ * project root (default: 'components.go'). */
82
+ typesOutputFile?: string
83
+ /** Manual type definitions to append (app-specific types not generated
84
+ * from components). */
85
+ manualTypes?: string
86
+ /** Transform the combined types string before writing (for app-specific
87
+ * type fixes). */
88
+ transformTypes?: (types: string) => string
89
+ /**
90
+ * Extra, non-component script entries whose Vite-resolved URL (dev:
91
+ * origin-based; production: content-hashed manifest path) should be
92
+ * exposed to Go code as a generated asset map — e.g. a hand-written
93
+ * client bootstrap script (a router entry point, say) that isn't a
94
+ * `.tsx` component, so it never goes through core's discovery/
95
+ * `scriptAssets` machinery, but still needs a `<script src="...">` URL
96
+ * only knowable after bundling.
97
+ *
98
+ * Keyed by the Go identifier the resolved URL should appear under in
99
+ * the generated map; values are entry paths relative to the Vite
100
+ * project root. You must ALSO register the same path as a Rollup entry
101
+ * yourself via stock `build.rollupOptions.input` — this plugin never
102
+ * adds bundling configuration on your behalf (per the design's
103
+ * "everything except adapter/components/templates is stock Vite
104
+ * config"); this option only resolves the URL Vite already bundled it
105
+ * to, it doesn't request the bundling.
106
+ */
107
+ assets?: Record<string, string>
108
+ /**
109
+ * Output path for the DEV asset-map file, relative to the Vite project
110
+ * root. Default: 'bf_assets.go'. Ignored when `assets` is empty.
111
+ *
112
+ * Two build-tagged files declare the SAME `Assets` symbol so exactly one
113
+ * compiles: this one carries `//go:build !production` (the untagged
114
+ * default — a fresh clone with no prior `vite build` still compiles) and
115
+ * holds dev-server-origin URLs, which never change between rebuilds, so
116
+ * it's safe — and meant — to commit. Its sibling (this path with `.go`
117
+ * replaced by `_prod.go`, e.g. `bf_assets_prod.go`) carries
118
+ * `//go:build production`, holds content-hashed build URLs that change
119
+ * every `vite build`, and is gitignored; compiling against it needs
120
+ * `-tags production`.
121
+ */
122
+ assetsOutputFile?: string
123
+ }
124
+
125
+ /** write-if-changed: writes `content` to `absPath` only if it differs from
126
+ * what's already there, logging `label` when it actually wrote. Shared by
127
+ * both `components.go` and the asset map: an unrelated eager pass
128
+ * (triggered by editing a DIFFERENT component) touching a generated file's
129
+ * mtime would falsely trip a Go-side file watcher (`air`, etc.) into a
130
+ * no-op rebuild. */
131
+ async function writeIfChanged(absPath: string, content: string, label: string): Promise<void> {
132
+ const prev = await readFile(absPath, 'utf-8').catch(() => null)
133
+ if (prev === content) return
134
+ await writeFile(absPath, content)
135
+ console.log(`Generated: ${label}`)
136
+ }
137
+
138
+ /**
139
+ * Combines this pass's `types` fragments into `components.go`. Shared by
140
+ * both build and dev — Go's generated file has to exist for `go run .` to
141
+ * even compile in dev, which is exactly why `afterEmit` fires from both
142
+ * passes (see core's docstring).
143
+ */
144
+ async function writeCombinedTypes(
145
+ ctx: AfterEmitContext,
146
+ packageName: string,
147
+ typesOutputFile: string,
148
+ manualTypes: string | undefined,
149
+ transformTypes: ((types: string) => string) | undefined,
150
+ ): Promise<void> {
151
+ if (ctx.types.size === 0) return
152
+
153
+ const content = combineGoTypes({ types: ctx.types, packageName, manualTypes, transformTypes })
154
+ if (!content) return
155
+
156
+ await writeIfChanged(resolve(ctx.projectDir, typesOutputFile), content, typesOutputFile)
157
+ }
158
+
159
+ /** Resolves ONE asset entry's URL for the current pass: manifest-hashed for
160
+ * `mode: 'build'`, dev-origin-based for `mode: 'dev'`. Throws with an
161
+ * actionable message when the entry isn't in the build manifest — almost
162
+ * always means the caller forgot to also add it to
163
+ * `build.rollupOptions.input`, and a silently-empty/broken URL baked into
164
+ * generated Go source is a much worse failure mode to debug than a build-
165
+ * time error naming exactly what's missing. */
166
+ function resolveAssetUrl(
167
+ ctx: AfterEmitContext,
168
+ config: ResolvedConfig,
169
+ devServer: ViteDevServer | undefined,
170
+ entryRelPath: string,
171
+ manifest: Record<string, { file: string }> | undefined,
172
+ ): string {
173
+ const absPath = resolve(ctx.projectDir, entryRelPath)
174
+ if (ctx.mode === 'dev') {
175
+ if (!devServer) throw new Error(`[go-template/vite] asset "${entryRelPath}": dev server not ready`)
176
+ return devModuleUrl(config, resolveDevOrigin(devServer), absPath)
177
+ }
178
+
179
+ const manifestKey = toPosixRelative(config.root, absPath)
180
+ const [url] = resolveScriptAssets(manifest ?? {}, manifestKey, config.base)
181
+ if (!url) {
182
+ throw new Error(
183
+ `[go-template/vite] asset "${entryRelPath}" was not found in the build manifest. ` +
184
+ `Did you also add it to build.rollupOptions.input?`,
185
+ )
186
+ }
187
+ return url
188
+ }
189
+
190
+ /** Derives the gitignored production sibling of a dev `assetsOutputFile`
191
+ * (`bf_assets.go` → `bf_assets_prod.go`). See `GoTemplateViteOptions.
192
+ * assetsOutputFile`'s docstring for why there are two files. */
193
+ function prodAssetsOutputFile(devAssetsOutputFile: string): string {
194
+ return devAssetsOutputFile.replace(/\.go$/, '_prod.go')
195
+ }
196
+
197
+ /** Renders the generated Go asset-map file for ONE side of the dev/prod
198
+ * split: `buildTag` is the `//go:build` constraint that gates it, `entries`
199
+ * the already-rendered map body. Both call sites (dev and build passes)
200
+ * share this so the two files stay textually identical apart from the tag,
201
+ * the URLs, and which sibling they name in the doc comment. */
202
+ function renderAssetMapFile(
203
+ packageName: string,
204
+ buildTag: string,
205
+ entries: string,
206
+ devFile: string,
207
+ prodFile: string,
208
+ ): string {
209
+ return [
210
+ `// Code generated by BarefootJS. DO NOT EDIT.`,
211
+ '',
212
+ `//go:build ${buildTag}`,
213
+ '',
214
+ `package ${packageName}`,
215
+ '',
216
+ `// Assets maps a logical asset name (this map's key) to its resolved URL.`,
217
+ `// Two build-tagged files declare this SAME symbol so exactly one`,
218
+ `// compiles: ${devFile} (tag !production, the untagged default) holds`,
219
+ `// Vite dev-server-origin URLs and is committed since they're stable`,
220
+ `// across rebuilds; ${prodFile} (tag production, gitignored) holds`,
221
+ `// content-hashed build URLs that change every \`vite build\`. Compile`,
222
+ `// against the production build with \`-tags production\`. Regenerated`,
223
+ `// by @barefootjs/go-template/vite's afterEmit hook every time templates`,
224
+ `// are (re)emitted.`,
225
+ `var Assets = map[string]string{`,
226
+ entries,
227
+ `}`,
228
+ ].join('\n') + '\n'
229
+ }
230
+
231
+ /** Builds and write-if-changed's the generated Go asset map for the CURRENT
232
+ * pass only: `ctx.mode === 'dev'` writes the `!production`-tagged
233
+ * `assetsOutputFile` (dev URLs, committed); `ctx.mode === 'build'` writes
234
+ * the `production`-tagged sibling (hashed URLs, gitignored). No-op when
235
+ * `assets` is empty. */
236
+ async function writeAssetMap(
237
+ ctx: AfterEmitContext,
238
+ config: ResolvedConfig,
239
+ devServer: ViteDevServer | undefined,
240
+ assets: Record<string, string>,
241
+ packageName: string,
242
+ assetsOutputFile: string,
243
+ ): Promise<void> {
244
+ const keys = Object.keys(assets)
245
+ if (keys.length === 0) return
246
+
247
+ const manifest = ctx.mode === 'build' ? await loadManifest(ctx.outDir, config.build.manifest) : undefined
248
+
249
+ const entries = keys
250
+ .map(goName => `\t${JSON.stringify(goName)}: ${JSON.stringify(resolveAssetUrl(ctx, config, devServer, assets[goName]!, manifest))},`)
251
+ .join('\n')
252
+
253
+ const prodFile = prodAssetsOutputFile(assetsOutputFile)
254
+ const [buildTag, outputFile] = ctx.mode === 'dev' ? ['!production', assetsOutputFile] : ['production', prodFile]
255
+ const content = renderAssetMapFile(packageName, buildTag, entries, assetsOutputFile, prodFile)
256
+
257
+ await writeIfChanged(resolve(ctx.projectDir, outputFile), content, outputFile)
258
+ }
259
+
260
+ export function barefoot(options: GoTemplateViteOptions): Plugin[] {
261
+ const packageName = options.packageName ?? 'main'
262
+ const typesOutputFile = options.typesOutputFile ?? 'components.go'
263
+ const assets = options.assets ?? {}
264
+ const assetsOutputFile = options.assetsOutputFile ?? 'bf_assets.go'
265
+
266
+ // Populated by `goAssetsConfigCapture` below (only added to the returned
267
+ // array when `assets` is non-empty). Read by `afterEmit`, which always
268
+ // fires strictly after both `configResolved` (build AND dev — Vite/
269
+ // Rollup run every plugin's `configResolved` before any `writeBundle`)
270
+ // and `configureServer` (dev — `configureServer` itself is what
271
+ // SCHEDULES core's dev pass, so it has always already run by the time
272
+ // that pass, and hence `afterEmit`, fires).
273
+ let resolvedConfig: ResolvedConfig | undefined
274
+ let devServer: ViteDevServer | undefined
275
+
276
+ const core = coreBarefoot({
277
+ adapter: new GoTemplateAdapter({ packageName }),
278
+ components: options.components,
279
+ templates: options.templates,
280
+ async afterEmit(ctx) {
281
+ await writeCombinedTypes(ctx, packageName, typesOutputFile, options.manualTypes, options.transformTypes)
282
+ if (Object.keys(assets).length > 0 && resolvedConfig) {
283
+ await writeAssetMap(ctx, resolvedConfig, devServer, assets, packageName, assetsOutputFile)
284
+ }
285
+ },
286
+ })
287
+
288
+ if (Object.keys(assets).length === 0) return [core]
289
+
290
+ const goAssetsConfigCapture: Plugin = {
291
+ name: 'barefoot-go-assets-config-capture',
292
+ configResolved(config) {
293
+ resolvedConfig = config
294
+ },
295
+ configureServer(server) {
296
+ devServer = server
297
+ },
298
+ }
299
+
300
+ return [core, goAssetsConfigCapture]
301
+ }
302
+
303
+ export { barefoot as default }
package/dist/build.d.ts DELETED
@@ -1,53 +0,0 @@
1
- import type { BuildOptions, PostBuildContext } from '@barefootjs/jsx';
2
- import { GoTemplateAdapter } from './adapter/index.ts';
3
- import type { GoTemplateAdapterOptions } from './adapter/index.ts';
4
- export interface GoTemplateBuildOptions extends BuildOptions {
5
- /** Adapter-specific options passed to GoTemplateAdapter */
6
- adapterOptions?: GoTemplateAdapterOptions;
7
- /** Output path for combined Go types file (relative to projectDir, default: 'components.go') */
8
- typesOutputFile?: string;
9
- /** Transform the combined types string before writing (for app-specific type fixes) */
10
- transformTypes?: (types: string) => string;
11
- /** Manual type definitions to append (app-specific types not generated from components) */
12
- manualTypes?: string;
13
- }
14
- /**
15
- * Strip Go package header and import block, returning only type definitions.
16
- */
17
- export declare function stripGoPackageHeader(types: string): string;
18
- /**
19
- * Deduplicate Go type definitions and NewXxxProps constructor functions.
20
- * When duplicates exist, prefer the version that contains ScopeID (the complete Props struct
21
- * from generatePropsStruct) over the simplified version from typeDefinitions.
22
- */
23
- export declare function deduplicateGoTypes(combined: string): string;
24
- /**
25
- * Combine Go types from multiple components into a single .go file.
26
- */
27
- export declare function combineGoTypes(options: {
28
- types: Map<string, string>;
29
- packageName: string;
30
- manualTypes?: string;
31
- transformTypes?: (types: string) => string;
32
- }): string;
33
- /**
34
- * Create a BarefootBuildConfig for Go html/template projects.
35
- *
36
- * Uses structural typing — does not import BarefootBuildConfig to avoid
37
- * circular dependency between @barefootjs/go-template and @barefootjs/cli.
38
- */
39
- export declare function createConfig(options?: GoTemplateBuildOptions): {
40
- adapter: GoTemplateAdapter;
41
- paths: import("@barefootjs/jsx").BarefootPaths | undefined;
42
- components: string[] | undefined;
43
- outDir: string | undefined;
44
- minify: boolean | undefined;
45
- contentHash: boolean | undefined;
46
- externals: Record<string, import("@barefootjs/jsx").ExternalSpec> | undefined;
47
- externalsBasePath: string | undefined;
48
- bundleEntries: import("@barefootjs/jsx").BundleEntry[] | undefined;
49
- localImportPrefixes: string[] | undefined;
50
- outputLayout: import("@barefootjs/jsx").OutputLayout;
51
- postBuild: (ctx: PostBuildContext) => Promise<void>;
52
- };
53
- //# sourceMappingURL=build.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAA;AAElE,MAAM,WAAW,sBAAuB,SAAQ,YAAY;IAC1D,2DAA2D;IAC3D,cAAc,CAAC,EAAE,wBAAwB,CAAA;IACzC,gGAAgG;IAChG,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,uFAAuF;IACvF,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAC1C,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAID;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA0C1D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAmD3D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE;IACtC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;CAC3C,GAAG,MAAM,CA0ET;AAID;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,sBAA2B;IAyC7D,OAAO;IACP,KAAK;IACL,UAAU;IACV,MAAM;IACN,MAAM;IACN,WAAW;IACX,SAAS;IACT,iBAAiB;IACjB,aAAa;IACb,mBAAmB;IACnB,YAAY;IAKZ,SAAS,QApDmB,gBAAgB;EAsD/C"}