@barefootjs/hono 0.30.6 → 0.31.1

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.
@@ -1,299 +0,0 @@
1
- import { describe, test, expect } from 'bun:test'
2
- import { addScriptCollection, createConfig, maskComments } from '../build'
3
-
4
- // ── addScriptCollection ──────────────────────────────────────────────
5
-
6
- describe('addScriptCollection', () => {
7
- test('injects imports and script collector into exported function', () => {
8
- const input = `import { jsx } from 'hono/jsx'
9
-
10
- export function Counter(props: CounterProps) {
11
- return (<div>hello</div>)
12
- }`
13
-
14
- const result = addScriptCollection(input, 'Counter', 'Counter.client.js')
15
-
16
- expect(result).toContain("import { useRequestContext } from 'hono/jsx-renderer'")
17
- expect(result).toContain("import { Fragment } from 'hono/jsx'")
18
- expect(result).toContain('__bfWrap')
19
- expect(result).toContain('bfCollectedScripts')
20
- expect(result).toContain("'Counter'")
21
- expect(result).toContain('Counter.client.js')
22
- })
23
-
24
- test('preserves content when no import match', () => {
25
- const input = 'const x = 1'
26
- // Should not throw, returns unchanged or minimally modified
27
- const result = addScriptCollection(input, 'Test', 'Test.client.js')
28
- expect(result).toBeDefined()
29
- })
30
-
31
- test('uses custom scriptBasePath', () => {
32
- const input = `import { jsx } from 'hono/jsx'
33
-
34
- export function Counter() {
35
- return (<div>hello</div>)
36
- }`
37
-
38
- const result = addScriptCollection(input, 'Counter', 'Counter.client.js', '/assets/js/')
39
- expect(result).toContain('/assets/js/barefoot.js')
40
- expect(result).toContain('/assets/js/Counter.client.js')
41
- expect(result).not.toContain('/static/components/')
42
- })
43
-
44
- test('normalizes scriptBasePath without trailing slash', () => {
45
- const input = `import { jsx } from 'hono/jsx'
46
-
47
- export function Counter() {
48
- return (<div>hello</div>)
49
- }`
50
-
51
- const result = addScriptCollection(input, 'Counter', 'Counter.client.js', '/assets/js')
52
- expect(result).toContain('/assets/js/barefoot.js')
53
- expect(result).toContain('/assets/js/Counter.client.js')
54
- })
55
-
56
- test('ignores `function PascalCase(` text inside JSDoc / inline comments (#1236)', () => {
57
- // A docstring example previously triggered a bogus insertion when
58
- // the function-pattern regex matched inside the comment, after
59
- // which the paren counter walked into the wrong `{` and corrupted
60
- // a real function further down.
61
- const input = `import { jsx } from 'hono/jsx'
62
-
63
- export interface MyProps {
64
- /**
65
- * Example imperative signature for the docs:
66
- * function MyNode(this: HTMLElement, props): void
67
- */
68
- nodeTypes?: Record<string, unknown>
69
- }
70
-
71
- // also: function FakeFromLineComment(this: any) {} should not match
72
-
73
- export function Counter(props: MyProps) {
74
- return (<div>hello</div>)
75
- }`
76
-
77
- const result = addScriptCollection(input, 'Counter', 'Counter.client.js')
78
-
79
- // The real Counter must be wrapped exactly once.
80
- const collectorCount = (result.match(/let __bfInlineScripts/g) || []).length
81
- expect(collectorCount).toBe(1)
82
-
83
- // And the collector must land inside Counter's body, immediately after
84
- // its opening brace — the same shape the destructured-params test
85
- // above verifies. If the comment matches had fired, the collector
86
- // would be misplaced inside the interface or the JSDoc.
87
- const counterBodyMatch = result.match(/function Counter\(props: MyProps\)\s*\{/)
88
- expect(counterBodyMatch).not.toBeNull()
89
- if (counterBodyMatch) {
90
- const after = result.slice(result.indexOf(counterBodyMatch[0]) + counterBodyMatch[0].length)
91
- expect(after.trimStart().startsWith('let __bfInlineScripts')).toBe(true)
92
- }
93
- })
94
-
95
- test('still finds function declarations after JSX text with apostrophes (#1236)', () => {
96
- // Defensive: an unbalanced `'` inside JSX text content (e.g.
97
- // `How's it going`) used to cause an over-aggressive string-mask
98
- // to blank everything until the next stray `'`, hiding later
99
- // function declarations from the regex. Keep apostrophe-containing
100
- // JSX text unmasked so subsequent functions still get instrumented.
101
- const input = `import { jsx } from 'hono/jsx'
102
-
103
- export function Greeting() {
104
- return (<div>Hey! How's it going?</div>)
105
- }
106
-
107
- export function Footer() {
108
- return (<div>Bye</div>)
109
- }`
110
-
111
- const result = addScriptCollection(input, 'page', 'page-abc.js')
112
-
113
- // BOTH functions must be wrapped.
114
- const collectorCount = (result.match(/let __bfInlineScripts/g) || []).length
115
- expect(collectorCount).toBe(2)
116
- expect(result).toMatch(/function Greeting\(\)\s*\{\s*\n?\s*let __bfInlineScripts/)
117
- expect(result).toMatch(/function Footer\(\)\s*\{\s*\n?\s*let __bfInlineScripts/)
118
- })
119
-
120
- test('handles destructured params with arrow function defaults', () => {
121
- const input = `import { jsx } from 'hono/jsx'
122
-
123
- export function Textarea({ className = '', onInput = () => {}, onChange = () => {}, ...props }: TextareaProps) {
124
- return (<textarea class={className} {...props} />)
125
- }`
126
-
127
- const result = addScriptCollection(input, 'textarea', 'textarea-abc123.js')
128
-
129
- // Script collector must be inside the Textarea function body, NOT inside a default param
130
- expect(result).toContain('__bfInlineScripts')
131
- expect(result).toContain('__bfWrap')
132
-
133
- // Verify __bfInlineScripts is declared AFTER the function opening brace,
134
- // not inside an arrow function default value
135
- const funcBodyMatch = result.match(/\.\.\.props\s*\}\s*:\s*TextareaProps\)\s*\{/)
136
- expect(funcBodyMatch).not.toBeNull()
137
- // After the function body opening, the next thing should be the script collector
138
- if (funcBodyMatch) {
139
- const afterFuncBody = result.slice(result.indexOf(funcBodyMatch[0]) + funcBodyMatch[0].length)
140
- expect(afterFuncBody.trimStart().startsWith('let __bfInlineScripts')).toBe(true)
141
- }
142
- })
143
- })
144
-
145
- // ── createConfig() factory ──────────────────────────────────────────
146
-
147
- describe('createConfig()', () => {
148
- test('creates config with HonoAdapter', () => {
149
- const config = createConfig()
150
- expect(config.adapter.name).toBe('hono')
151
- })
152
-
153
- test('sets transformMarkedTemplate by default', () => {
154
- const config = createConfig()
155
- expect(typeof config.transformMarkedTemplate).toBe('function')
156
- })
157
-
158
- test('disables transformMarkedTemplate when scriptCollection is false', () => {
159
- const config = createConfig({ scriptCollection: false })
160
- expect(config.transformMarkedTemplate).toBeUndefined()
161
- })
162
-
163
- test('uses custom scriptBasePath in transformMarkedTemplate', () => {
164
- const config = createConfig({ scriptBasePath: '/assets/js/' })
165
- const input = `import { jsx } from 'hono/jsx'
166
-
167
- export function Counter() {
168
- return (<div>hello</div>)
169
- }`
170
- const result = config.transformMarkedTemplate!(input, 'Counter', 'Counter.client.js')
171
- expect(result).toContain('/assets/js/barefoot.js')
172
- expect(result).toContain('/assets/js/Counter.client.js')
173
- expect(result).not.toContain('/static/components/')
174
- })
175
-
176
- test('uses default scriptBasePath in transformMarkedTemplate', () => {
177
- const config = createConfig()
178
- const input = `import { jsx } from 'hono/jsx'
179
-
180
- export function Counter() {
181
- return (<div>hello</div>)
182
- }`
183
- const result = config.transformMarkedTemplate!(input, 'Counter', 'Counter.client.js')
184
- expect(result).toContain('/static/components/barefoot.js')
185
- expect(result).toContain('/static/components/Counter.client.js')
186
- })
187
-
188
- test('passes through build options', () => {
189
- const config = createConfig({
190
- components: ['src'],
191
- outDir: 'build',
192
- minify: true,
193
- contentHash: true,
194
- })
195
- expect(config.components).toEqual(['src'])
196
- expect(config.outDir).toBe('build')
197
- expect(config.minify).toBe(true)
198
- expect(config.contentHash).toBe(true)
199
- })
200
-
201
- test('passes through externals and externalsBasePath', () => {
202
- const externals = { react: { url: 'https://cdn.example.com/react.js' } }
203
- const config = createConfig({
204
- externals,
205
- externalsBasePath: '/cdn/',
206
- })
207
- expect(config.externals).toBe(externals)
208
- expect(config.externalsBasePath).toBe('/cdn/')
209
- })
210
-
211
- test('externals and externalsBasePath default to undefined', () => {
212
- const config = createConfig()
213
- expect(config.externals).toBeUndefined()
214
- expect(config.externalsBasePath).toBeUndefined()
215
- })
216
-
217
- test('passes through localImportPrefixes', () => {
218
- const config = createConfig({ localImportPrefixes: ['@/', '@ui/'] })
219
- expect(config.localImportPrefixes).toEqual(['@/', '@ui/'])
220
- })
221
-
222
- test('localImportPrefixes defaults to undefined', () => {
223
- const config = createConfig()
224
- expect(config.localImportPrefixes).toBeUndefined()
225
- })
226
- })
227
-
228
- // ── maskComments ────────────────────────────────────────────────────
229
-
230
- describe('maskComments', () => {
231
- test('preserves length and newlines so indices stay valid in the original', () => {
232
- const src = '/* foo */ x // bar\ny\n/* multi\nline */ z'
233
- const masked = maskComments(src)
234
- expect(masked).toHaveLength(src.length)
235
- // Every newline position in the original is preserved in the masked
236
- // copy, so line counts (and therefore line:column error reporting
237
- // from downstream tools) line up.
238
- const newlinePositions = (s: string) => [...s].flatMap((c, i) => c === '\n' ? [i] : [])
239
- expect(newlinePositions(masked)).toEqual(newlinePositions(src))
240
- })
241
-
242
- test('blanks JSDoc / block comments including any quotes inside', () => {
243
- const comment = "/** has 'apostrophe' inside */"
244
- const tail = ' const x = 1'
245
- const src = comment + tail
246
- const masked = maskComments(src)
247
- // The whole `/** ... */` is replaced with spaces; the apostrophes
248
- // inside cannot re-open as strings later.
249
- expect(masked).toBe(' '.repeat(comment.length) + tail)
250
- })
251
-
252
- test('blanks `//` line comments up to (but not including) the newline', () => {
253
- const comment = '// ignored'
254
- const tail = '\nconst x = 1'
255
- const src = comment + tail
256
- const masked = maskComments(src)
257
- expect(masked).toBe(' '.repeat(comment.length) + tail)
258
- })
259
-
260
- test('handles unclosed block comment by masking through end of input', () => {
261
- const src = 'a /* never closed\nfunction Real() {}'
262
- const masked = maskComments(src)
263
- // Without `*/`, the masker blanks to EOF. `function Real()` is
264
- // hidden — this matches the JS lexer's behaviour for unterminated
265
- // comments and is the conservative thing to do for the
266
- // function-pattern regex.
267
- expect(masked.startsWith('a ')).toBe(true)
268
- expect(masked).not.toContain('function Real')
269
- expect(masked).toHaveLength(src.length)
270
- })
271
-
272
- test('leaves comment-free code untouched (no false positives on JSX text)', () => {
273
- // Plain code with no comment delimiters round-trips identically.
274
- // JSX text with apostrophes (`How's`) is the hot path: a
275
- // string-aware masker would treat the `'` as an open quote and
276
- // blank the rest of the file, hiding later function declarations
277
- // (#1236 follow-up).
278
- const src = `export function Greeting() {\n return (<div>Hey! How's it going?</div>)\n}\nexport function Footer() {}`
279
- expect(maskComments(src)).toBe(src)
280
- })
281
-
282
- test('KNOWN LIMITATION: `//` inside a string is still treated as a line comment', () => {
283
- // Documented in `maskComments` jsdoc: this helper does not track
284
- // string boundaries, so a `//` appearing inside a string literal
285
- // is still treated as a comment delimiter. SSR template output
286
- // (the only current caller) does not produce such cases, so the
287
- // simpler implementation is acceptable. If a future caller can
288
- // produce them, swap in a real lexer — this test will start to
289
- // fail and force the conversation.
290
- const prefix = `const u = "https:`
291
- const blanked = `//example.com" ; const x = 1`
292
- const src = prefix + blanked
293
- const masked = maskComments(src)
294
- // The `//` in `https://` is treated as a line-comment delimiter
295
- // and the rest of the line gets blanked.
296
- expect(masked).toBe(prefix + ' '.repeat(blanked.length))
297
- expect(masked).toHaveLength(src.length)
298
- })
299
- })
@@ -1,123 +0,0 @@
1
- /** @jsxImportSource hono/jsx */
2
- /**
3
- * BfDevReload / createDevReloader tests
4
- *
5
- * Verifies the dev-gate (no leak into production) and the basic SSE wire
6
- * format so a regression in the build-id watcher is caught before E2E.
7
- */
8
- import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
9
- import { Hono } from 'hono'
10
- import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs'
11
- import { tmpdir } from 'node:os'
12
- import { join } from 'node:path'
13
- import { renderToString } from 'hono/jsx/dom/server'
14
- // `BfDevReload` lives in `app.ts` (runtime-agnostic, html-tagged-template
15
- // based); `createDevReloader` lives in `dev.tsx` (Node fs-watch based).
16
- import { BfDevReload } from '../app'
17
- import { createDevReloader } from '../dev'
18
-
19
- describe('BfDevReload', () => {
20
- // The runtime gate lives in `barefootDevReload` (middleware). When
21
- // it's mounted with `enabled: false` it never publishes the endpoint
22
- // to the request context, so <BfDevReload /> falls back to `null`.
23
- // Tests below exercise the component directly, which means the
24
- // "endpoint provided" branch is the snippet branch and the
25
- // "no endpoint, no context" branch is the null branch.
26
-
27
- it('renders the EventSource snippet when an endpoint is provided', () => {
28
- const html = renderToString(<BfDevReload endpoint="/_bf/reload" />)
29
- expect(html).toContain('<script>')
30
- expect(html).toContain('new EventSource(\"/_bf/reload\")')
31
- expect(html).toContain("addEventListener('reload'")
32
- })
33
-
34
- it('renders nothing when no endpoint is available (no context, no prop)', () => {
35
- const html = renderToString(<BfDevReload />)
36
- expect(html).toBe('')
37
- })
38
-
39
- it('respects a custom endpoint passed as a prop', () => {
40
- const html = renderToString(<BfDevReload endpoint="/__reload" />)
41
- expect(html).toContain('new EventSource(\"/__reload\")')
42
- })
43
- })
44
-
45
- describe('createDevReloader', () => {
46
- let dir: string
47
-
48
- beforeEach(() => {
49
- dir = mkdtempSync(join(tmpdir(), 'bf-dev-reloader-'))
50
- mkdirSync(join(dir, '.dev'), { recursive: true })
51
- writeFileSync(join(dir, '.dev', 'build-id'), '1000')
52
- })
53
-
54
- afterEach(() => {
55
- rmSync(dir, { recursive: true, force: true })
56
- })
57
-
58
- it('returns 404 when disabled', async () => {
59
- const app = new Hono()
60
- app.get('/_bf/reload', createDevReloader({ distDir: dir, enabled: false }))
61
-
62
- const res = await app.request('/_bf/reload')
63
- expect(res.status).toBe(404)
64
- })
65
-
66
- it('streams initial hello with current build-id', async () => {
67
- const app = new Hono()
68
- app.get('/_bf/reload', createDevReloader({ distDir: dir, enabled: true }))
69
-
70
- const ctrl = new AbortController()
71
- const res = await app.request(new Request('http://localhost/_bf/reload', { signal: ctrl.signal }))
72
- expect(res.status).toBe(200)
73
- expect(res.headers.get('Content-Type')).toBe('text/event-stream')
74
-
75
- const reader = res.body!.getReader()
76
- const decoder = new TextDecoder()
77
- let received = ''
78
- // Accumulate until the hello event lands (first two chunks should suffice).
79
- for (let i = 0; i < 4 && !received.includes('event: hello'); i++) {
80
- const { value, done } = await reader.read()
81
- if (done) break
82
- received += decoder.decode(value)
83
- }
84
-
85
- expect(received).toContain('retry: 1000')
86
- expect(received).toContain('event: hello')
87
- expect(received).toContain('data: 1000')
88
-
89
- ctrl.abort()
90
- try { await reader.cancel() } catch { /* already closed */ }
91
- })
92
-
93
- // Regression: when a client reconnects after a build happened during its
94
- // disconnected window, it must see `reload` (not `hello`), otherwise the
95
- // missed rebuild silently stays unpainted until the next change.
96
- it('emits reload on reconnect when Last-Event-ID is stale', async () => {
97
- const app = new Hono()
98
- app.get('/_bf/reload', createDevReloader({ distDir: dir, enabled: true }))
99
-
100
- const ctrl = new AbortController()
101
- const req = new Request('http://localhost/_bf/reload', {
102
- headers: { 'Last-Event-ID': '999' },
103
- signal: ctrl.signal,
104
- })
105
- const res = await app.request(req)
106
-
107
- const reader = res.body!.getReader()
108
- const decoder = new TextDecoder()
109
- let received = ''
110
- for (let i = 0; i < 4 && !received.includes('event: '); i++) {
111
- const { value, done } = await reader.read()
112
- if (done) break
113
- received += decoder.decode(value)
114
- }
115
-
116
- expect(received).toContain('event: reload')
117
- expect(received).not.toContain('event: hello')
118
- expect(received).toContain('data: 1000')
119
-
120
- ctrl.abort()
121
- try { await reader.cancel() } catch { /* already closed */ }
122
- })
123
- })
@@ -1,98 +0,0 @@
1
- /**
2
- * BfImportMap tests
3
- *
4
- * Verifies the importmap merges configured externals from
5
- * `barefoot-externals.json` (issue #1639) and emits modulepreload
6
- * links, while preserving the pre-#1639 `@barefootjs/client*` defaults
7
- * when no externals are passed.
8
- */
9
- import { describe, test, expect } from 'bun:test'
10
- import { BfImportMap } from '../app'
11
- import type { ImportMapManifest } from '@barefootjs/jsx'
12
-
13
- function parseImportMap(html: string): Record<string, string> {
14
- const match = html.match(/<script type="importmap">(.*?)<\/script>/s)
15
- if (!match) throw new Error(`no importmap in: ${html}`)
16
- return JSON.parse(match[1]).imports
17
- }
18
-
19
- describe('BfImportMap', () => {
20
- test('emits @barefootjs/client defaults when no externals passed', () => {
21
- const html = String(BfImportMap({ base: '/components' }))
22
- expect(parseImportMap(html)).toEqual({
23
- '@barefootjs/client': '/components/barefoot.js',
24
- '@barefootjs/client/runtime': '/components/barefoot.js',
25
- })
26
- expect(html).not.toContain('modulepreload')
27
- })
28
-
29
- test('strips trailing slash from base', () => {
30
- const html = String(BfImportMap({ base: '/components/' }))
31
- expect(parseImportMap(html)['@barefootjs/client']).toBe('/components/barefoot.js')
32
- })
33
-
34
- test('merges externals importmap on top of the client defaults', () => {
35
- const externals: ImportMapManifest = {
36
- importmap: {
37
- imports: {
38
- zod: 'https://esm.sh/zod@4.4.3',
39
- '@barefootjs/form': '/components/form.js',
40
- },
41
- },
42
- preloads: [],
43
- }
44
- const imports = parseImportMap(String(BfImportMap({ base: '/components', externals })))
45
- expect(imports).toEqual({
46
- '@barefootjs/client': '/components/barefoot.js',
47
- '@barefootjs/client/runtime': '/components/barefoot.js',
48
- zod: 'https://esm.sh/zod@4.4.3',
49
- '@barefootjs/form': '/components/form.js',
50
- })
51
- })
52
-
53
- test('manifest @barefootjs/client mapping wins over the prop-derived one', () => {
54
- const externals: ImportMapManifest = {
55
- importmap: { imports: { '@barefootjs/client': '/vendor/barefoot.js' } },
56
- }
57
- const imports = parseImportMap(String(BfImportMap({ base: '/components', externals })))
58
- expect(imports['@barefootjs/client']).toBe('/vendor/barefoot.js')
59
- })
60
-
61
- test('emits modulepreload links for manifest preloads', () => {
62
- const externals: ImportMapManifest = {
63
- importmap: { imports: {} },
64
- preloads: ['/components/form.js', 'https://esm.sh/zod@4.4.3'],
65
- }
66
- const html = String(BfImportMap({ base: '/components', externals }))
67
- expect(html).toContain('<link rel="modulepreload" href="/components/form.js" crossorigin>')
68
- expect(html).toContain('<link rel="modulepreload" href="https://esm.sh/zod@4.4.3" crossorigin>')
69
- })
70
-
71
- test('emits crossorigin on modulepreload so cross-origin CDN preloads are reused', () => {
72
- const externals: ImportMapManifest = {
73
- preloads: ['https://esm.sh/zod@4.4.3'],
74
- }
75
- const html = String(BfImportMap({ base: '/components', externals }))
76
- const match = html.match(/<link rel="modulepreload"[^>]*>/)
77
- expect(match?.[0]).toContain('crossorigin')
78
- })
79
-
80
- test('preload=false suppresses modulepreload links', () => {
81
- const externals: ImportMapManifest = {
82
- preloads: ['/components/form.js'],
83
- }
84
- const html = String(BfImportMap({ base: '/components', externals, preload: false }))
85
- expect(html).not.toContain('modulepreload')
86
- // importmap still emitted
87
- expect(parseImportMap(html)['@barefootjs/client']).toBe('/components/barefoot.js')
88
- })
89
-
90
- test('escapes double quotes in preload hrefs', () => {
91
- const externals: ImportMapManifest = {
92
- preloads: ['/components/"onerror=alert(1).js'],
93
- }
94
- const html = String(BfImportMap({ base: '/components', externals }))
95
- expect(html).not.toContain('"onerror=alert(1)')
96
- expect(html).toContain('&quot;onerror=alert(1)')
97
- })
98
- })