@barefootjs/hono 0.5.1 → 0.5.3

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.
@@ -14,6 +14,16 @@ export interface RenderOptions {
14
14
  props?: Record<string, unknown>;
15
15
  /** Additional component files (filename → source) */
16
16
  components?: Record<string, string>;
17
+ /**
18
+ * Pre-compiled child component modules (import specifier → absolute
19
+ * module path) — #1467 Phase 2a. When the parent imports one of these
20
+ * specifiers, the import is *re-anchored* to the given module path
21
+ * (kept as a real ESM import) instead of having the child inlined via
22
+ * `components`. The module is a committed, export-intact marked
23
+ * template, so SSR loads it through the module system — no export
24
+ * stripping. Takes precedence over `components` for the same key.
25
+ */
26
+ componentModules?: Record<string, string>;
17
27
  /**
18
28
  * Explicit component to render when the source declares multiple
19
29
  * exports. When omitted, the first function-valued export in
@@ -1 +1 @@
1
- {"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAQtD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,eAAe,CAAA;IACxB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA0GjF"}
1
+ {"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAStD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,eAAe,CAAA;IACxB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACzC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AA2BD,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA0KjF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/hono",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Hono integration for BarefootJS",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,6 +8,7 @@
8
8
  import { compileJSX } from '@barefootjs/jsx'
9
9
  import type { TemplateAdapter } from '@barefootjs/jsx'
10
10
  import { Hono } from 'hono'
11
+ import { readFileSync } from 'node:fs'
11
12
  import { mkdir, rm } from 'node:fs/promises'
12
13
  import { resolve } from 'node:path'
13
14
 
@@ -23,6 +24,16 @@ export interface RenderOptions {
23
24
  props?: Record<string, unknown>
24
25
  /** Additional component files (filename → source) */
25
26
  components?: Record<string, string>
27
+ /**
28
+ * Pre-compiled child component modules (import specifier → absolute
29
+ * module path) — #1467 Phase 2a. When the parent imports one of these
30
+ * specifiers, the import is *re-anchored* to the given module path
31
+ * (kept as a real ESM import) instead of having the child inlined via
32
+ * `components`. The module is a committed, export-intact marked
33
+ * template, so SSR loads it through the module system — no export
34
+ * stripping. Takes precedence over `components` for the same key.
35
+ */
36
+ componentModules?: Record<string, string>
26
37
  /**
27
38
  * Explicit component to render when the source declares multiple
28
39
  * exports. When omitted, the first function-valued export in
@@ -34,14 +45,46 @@ export interface RenderOptions {
34
45
  componentName?: string
35
46
  }
36
47
 
48
+ /**
49
+ * Drop module-level exports from a compiled marked template so it can be
50
+ * inlined as plain declarations alongside other components. Specifier
51
+ * blocks (`export { … }`, `export type { … }`, with or without a
52
+ * trailing `from '…'` re-export source) are removed whole; declaration
53
+ * forms (`export function/const/let/type/interface`, `export default`)
54
+ * keep their body with only the leading keyword stripped.
55
+ *
56
+ * The set of forms is bounded by `generateModuleExports` in
57
+ * @barefootjs/jsx — see the caller for the enumeration. This stays a
58
+ * line-oriented text pass (rather than a real parse) because the input
59
+ * is compiler-generated with a stable, single-line-per-export shape.
60
+ */
61
+ function stripModuleExports(code: string): string {
62
+ return code
63
+ // `export [type] { … } [from '…']` specifier / re-export blocks.
64
+ .replace(
65
+ /^[ \t]*export\s+(?:type\s+)?\{[^}]*\}(?:[ \t]*from[ \t]*['"][^'"]*['"])?[ \t]*;?[ \t]*$/gm,
66
+ '',
67
+ )
68
+ // Leading keyword on declaration forms (`export function`,
69
+ // `export const X = …`, `export default …`, etc.).
70
+ .replace(/\bexport\s+(default\s+)?/g, '')
71
+ }
72
+
37
73
  export async function renderHonoComponent(options: RenderOptions): Promise<string> {
38
- const { source, adapter, props, components, componentName: requestedName } = options
74
+ const { source, adapter, props, components, componentModules, componentName: requestedName } = options
39
75
 
40
- // Compile child components first
76
+ // Child imports re-anchored to a pre-compiled module (#1467 Phase 2a):
77
+ // import specifier → absolute path. These are NOT inlined; the parent's
78
+ // matching import is rewritten to the path and loaded as a real module.
79
+ const moduleMap = new Map<string, string>(Object.entries(componentModules ?? {}))
80
+
81
+ // Compile child components first (inline path). Keys also present in
82
+ // `moduleMap` are skipped here — they load as real modules instead.
41
83
  const childCodes: string[] = []
42
84
  const componentKeys = new Set<string>()
43
85
  if (components) {
44
86
  for (const [filename, childSource] of Object.entries(components)) {
87
+ if (moduleMap.has(filename)) continue
45
88
  componentKeys.add(filename)
46
89
  const childResult = compileJSX(childSource, filename, { adapter })
47
90
  const childErrors = childResult.errors.filter(e => e.severity === 'error')
@@ -50,8 +93,23 @@ export async function renderHonoComponent(options: RenderOptions): Promise<strin
50
93
  }
51
94
  const childTemplate = childResult.files.find(f => f.type === 'markedTemplate')
52
95
  if (!childTemplate) throw new Error(`No marked template for ${filename}`)
53
- // Strip export keywords so only the parent component is exported
54
- const localCode = childTemplate.content.replace(/\bexport\s+(default\s+)?/g, '')
96
+ // Strip exports so only the parent component is exported, inlining
97
+ // the child as plain top-level declarations. The marked template's
98
+ // export forms are fixed by `generateModuleExports` (+ the
99
+ // component's own `export function`) in @barefootjs/jsx, each on
100
+ // its own line:
101
+ //
102
+ // export const/let X = … export function / async function …
103
+ // export type X = … export interface X { … }
104
+ // export { A, B } [from '…'] export type { A } [from '…']
105
+ //
106
+ // The `export { … }` / `export type { … }` *specifier* blocks
107
+ // (with or without a trailing `from '…'`) must be dropped whole —
108
+ // their bindings are already declared inline, and naively removing
109
+ // just the `export ` keyword leaves a bare `{ A }` / `type { A }`
110
+ // (the latter a syntax error). Declaration forms keep their body;
111
+ // only the leading `export `/`export default ` is removed.
112
+ const localCode = stripModuleExports(childTemplate.content)
55
113
  childCodes.push(localCode)
56
114
  }
57
115
  }
@@ -67,22 +125,56 @@ export async function renderHonoComponent(options: RenderOptions): Promise<strin
67
125
  const templateFile = result.files.find(f => f.type === 'markedTemplate')
68
126
  if (!templateFile) throw new Error('No marked template in compile output')
69
127
 
128
+ // Pre-compiled child modules are committed under the adapter-tests
129
+ // fixtures tree, where `hono/jsx` is NOT resolvable (hono lives in
130
+ // this package's node_modules — the very reason render temp files go
131
+ // here). Copy each committed module verbatim into the render temp dir
132
+ // and re-anchor the parent import there. The committed file stays the
133
+ // reviewable source of truth; this is a byte copy, not export surgery.
134
+ const childModuleWrites: Array<{ path: string; content: string }> = []
135
+ const moduleTempPaths = new Map<string, string>()
136
+ for (const [key, modPath] of moduleMap) {
137
+ const safe = key.replace(/[^a-zA-Z0-9]+/g, '_')
138
+ const tempPath = resolve(
139
+ RENDER_TEMP_DIR,
140
+ `child-${safe}-${Date.now()}-${Math.random().toString(36).slice(2)}.tsx`,
141
+ )
142
+ moduleTempPaths.set(key, tempPath)
143
+ childModuleWrites.push({ path: tempPath, content: readFileSync(modPath, 'utf8') })
144
+ }
145
+
70
146
  let parentCode = templateFile.content
71
- // Strip import lines that reference component files
72
- if (componentKeys.size > 0) {
147
+ // Resolve each child import: re-anchor to a pre-compiled module's temp
148
+ // copy (`moduleTempPaths`), strip it (inlined via `components`), or
149
+ // leave it. Both maps key on the import specifier; match the parent's
150
+ // import path with or without a `.tsx` extension (`./badge` ↔
151
+ // `./badge.tsx`).
152
+ //
153
+ // Assumes one import statement per line — the marked-template adapter
154
+ // emits single-line imports (`import { Slot } from '../slot'`), so the
155
+ // per-line scan is sufficient. A multi-line import would not match
156
+ // here; the unrewritten `../slot` then fails loudly at module
157
+ // resolution rather than rendering wrong output.
158
+ if (componentKeys.size > 0 || moduleTempPaths.size > 0) {
159
+ const matchKey = (importPath: string, keys: Iterable<string>): string | undefined => {
160
+ for (const key of keys) {
161
+ const keyWithoutExt = key.replace(/\.tsx?$/, '')
162
+ if (importPath === keyWithoutExt || importPath === key) return key
163
+ }
164
+ return undefined
165
+ }
73
166
  parentCode = parentCode
74
167
  .split('\n')
75
- .filter(line => {
76
- const importMatch = line.match(/^\s*import\s+.*from\s+['"](.+?)['"]/)
77
- if (!importMatch) return true
78
- const importPath = importMatch[1]
79
- // Match against component keys: './badge' matches './badge.tsx'
80
- for (const key of componentKeys) {
81
- const keyWithoutExt = key.replace(/\.tsx?$/, '')
82
- if (importPath === keyWithoutExt || importPath === key) return false
83
- }
84
- return true
168
+ .map(line => {
169
+ const importMatch = line.match(/^(\s*import\s+.*from\s+['"])(.+?)(['"].*)$/)
170
+ if (!importMatch) return line
171
+ const [, prefix, importPath, suffix] = importMatch
172
+ const moduleKey = matchKey(importPath, moduleTempPaths.keys())
173
+ if (moduleKey) return `${prefix}${moduleTempPaths.get(moduleKey)}${suffix}`
174
+ if (matchKey(importPath, componentKeys)) return null
175
+ return line
85
176
  })
177
+ .filter((line): line is string => line !== null)
86
178
  .join('\n')
87
179
  }
88
180
 
@@ -95,6 +187,11 @@ export async function renderHonoComponent(options: RenderOptions): Promise<strin
95
187
  const code = codeParts.join('\n')
96
188
 
97
189
  await mkdir(RENDER_TEMP_DIR, { recursive: true })
190
+ // Materialise the verbatim child-module copies next to the parent so
191
+ // their `hono/jsx` pragma resolves.
192
+ for (const { path, content } of childModuleWrites) {
193
+ await Bun.write(path, content)
194
+ }
98
195
  // Unique filename per render to avoid Bun's process-level module cache
99
196
  // (bun#12371: re-importing the same path returns stale module)
100
197
  const tempFile = resolve(
@@ -139,5 +236,8 @@ export async function renderHonoComponent(options: RenderOptions): Promise<strin
139
236
  return await res.text()
140
237
  } finally {
141
238
  await rm(tempFile, { force: true }).catch(() => {})
239
+ for (const { path } of childModuleWrites) {
240
+ await rm(path, { force: true }).catch(() => {})
241
+ }
142
242
  }
143
243
  }