@barefootjs/hono 0.1.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/hono-adapter.d.ts +141 -0
- package/dist/adapter/hono-adapter.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +632 -0
- package/dist/app.d.ts +131 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +139 -0
- package/dist/async.d.ts +15 -0
- package/dist/async.d.ts.map +1 -0
- package/dist/async.js +12 -0
- package/dist/build.d.ts +65 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +785 -0
- package/dist/client-shim.d.ts +59 -0
- package/dist/client-shim.d.ts.map +1 -0
- package/dist/client-shim.js +90 -0
- package/dist/dev-worker.d.ts +25 -0
- package/dist/dev-worker.d.ts.map +1 -0
- package/dist/dev-worker.js +65 -0
- package/dist/dev.d.ts +36 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +418 -0
- package/dist/dialog-context.d.ts +13 -0
- package/dist/dialog-context.d.ts.map +1 -0
- package/dist/dialog-context.js +10 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +632 -0
- package/dist/jsx/jsx-dev-runtime/index.d.ts +9 -0
- package/dist/jsx/jsx-dev-runtime/index.d.ts.map +1 -0
- package/dist/jsx/jsx-dev-runtime/index.js +6 -0
- package/dist/jsx/jsx-runtime/index.d.ts +32 -0
- package/dist/jsx/jsx-runtime/index.d.ts.map +1 -0
- package/dist/jsx/jsx-runtime/index.js +10 -0
- package/dist/portal-ssr.d.ts +22 -0
- package/dist/portal-ssr.d.ts.map +1 -0
- package/dist/portal-ssr.js +73 -0
- package/dist/portals.d.ts +26 -0
- package/dist/portals.d.ts.map +1 -0
- package/dist/portals.js +41 -0
- package/dist/preload.d.ts +56 -0
- package/dist/preload.d.ts.map +1 -0
- package/dist/preload.js +51 -0
- package/dist/scripts.d.ts +80 -0
- package/dist/scripts.d.ts.map +1 -0
- package/dist/scripts.js +198 -0
- package/dist/test-render.d.ts +28 -0
- package/dist/test-render.d.ts.map +1 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +16 -0
- package/package.json +116 -0
- package/src/__tests__/async.test.tsx +106 -0
- package/src/__tests__/bfscripts-entry-roots.test.tsx +135 -0
- package/src/__tests__/build.test.ts +299 -0
- package/src/__tests__/dev.test.tsx +123 -0
- package/src/__tests__/hydration-props-type.test.ts +141 -0
- package/src/__tests__/manifest-scripts.test.ts +87 -0
- package/src/__tests__/scaffold.test.ts +209 -0
- package/src/__tests__/ssr-context-bridge.test.ts +110 -0
- package/src/__tests__/string-literal-css-var-prop.test.ts +84 -0
- package/src/__tests__/stub-deps-scripts.test.ts +183 -0
- package/src/adapter/hono-adapter.ts +1114 -0
- package/src/adapter/index.ts +6 -0
- package/src/app.ts +220 -0
- package/src/async.tsx +55 -0
- package/src/build.ts +230 -0
- package/src/client-shim.ts +164 -0
- package/src/dev-worker.ts +93 -0
- package/src/dev.tsx +146 -0
- package/src/dialog-context.tsx +44 -0
- package/src/index.ts +26 -0
- package/src/jsx/jsx-dev-runtime/index.ts +9 -0
- package/src/jsx/jsx-runtime/index.ts +40 -0
- package/src/portal-ssr.tsx +92 -0
- package/src/portals.tsx +98 -0
- package/src/preload.tsx +166 -0
- package/src/scripts.tsx +220 -0
- package/src/test-render.ts +143 -0
- package/src/utils.ts +26 -0
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Hydration-only synthetic props (`__bfParent`, `__bfMount`,
|
|
2
|
+
// `__bfParentProps`, `data-key`) are destructured into every generated
|
|
3
|
+
// SSR template, so the function parameter's type annotation has to
|
|
4
|
+
// declare them — otherwise tsc reports TS2339 ("Property '__bfParent'
|
|
5
|
+
// does not exist on type {…}") across the emitted SSR templates the
|
|
6
|
+
// moment a user runs `tsc --noEmit` or opens the project in an IDE.
|
|
7
|
+
//
|
|
8
|
+
// Pins the codegen contract that:
|
|
9
|
+
//
|
|
10
|
+
// - The annotation always carries the full hydration-props set,
|
|
11
|
+
// regardless of whether the user declared a Props type, used
|
|
12
|
+
// destructured-props, or wrote `function X()` with no parameters.
|
|
13
|
+
//
|
|
14
|
+
// Round 5 / PR #1450: before this fix the destructured-no-Props branch
|
|
15
|
+
// declared only `{ __instanceId?; __bfScope?; __bfChild? }`, and any
|
|
16
|
+
// scaffolded `function Counter()`-style component produced ~6 tsc
|
|
17
|
+
// errors per emitted file.
|
|
18
|
+
|
|
19
|
+
import { describe, test, expect } from 'bun:test'
|
|
20
|
+
import { compileJSX } from '@barefootjs/jsx'
|
|
21
|
+
import { HonoAdapter } from '../adapter'
|
|
22
|
+
|
|
23
|
+
const adapter = new HonoAdapter()
|
|
24
|
+
|
|
25
|
+
// The four hydration fields the generated body destructures BEYOND the
|
|
26
|
+
// pre-existing `__instanceId / __bfScope / __bfChild`.
|
|
27
|
+
const HYDRATION_FIELDS = [
|
|
28
|
+
'__bfParentProps?: string',
|
|
29
|
+
'__bfParent?: string',
|
|
30
|
+
'__bfMount?: string',
|
|
31
|
+
'"data-key"?: string | number',
|
|
32
|
+
] as const
|
|
33
|
+
|
|
34
|
+
function compileMarkedTemplate(source: string, file = 'Demo.tsx'): string {
|
|
35
|
+
const result = compileJSX(source, file, { adapter })
|
|
36
|
+
const errors = result.errors.filter((e) => e.severity === 'error')
|
|
37
|
+
expect(errors).toEqual([])
|
|
38
|
+
const tmpl = result.files.find((f) => f.type === 'markedTemplate')
|
|
39
|
+
expect(tmpl).toBeDefined()
|
|
40
|
+
return tmpl!.content
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('Hono adapter — type alias preservation (#1453)', () => {
|
|
44
|
+
test('seeds reachability from `propsTypeName` so the destructured-props alias carries it', () => {
|
|
45
|
+
// `${Name}PropsWithHydration = ${propsTypeName} & {…}` is emitted
|
|
46
|
+
// AFTER the component body is scanned for typedef references, so a
|
|
47
|
+
// body that only mentions `ButtonPropsWithHydration` would not pull
|
|
48
|
+
// in `ButtonProps`. Without the alias-aware seed, every emitted
|
|
49
|
+
// Button-shape (the scaffold's onboarding component) raises TS2304
|
|
50
|
+
// for the very type it documents.
|
|
51
|
+
const source = `'use client'
|
|
52
|
+
interface ButtonProps { variant?: 'a' | 'b' }
|
|
53
|
+
export function Button({ variant }: ButtonProps) {
|
|
54
|
+
return <button>{variant}</button>
|
|
55
|
+
}`
|
|
56
|
+
const tmpl = compileMarkedTemplate(source, 'Button.tsx')
|
|
57
|
+
expect(tmpl).toContain('interface ButtonProps')
|
|
58
|
+
expect(tmpl).toContain('type ButtonPropsWithHydration = ButtonProps &')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('pulls in types reached transitively from the seed', () => {
|
|
62
|
+
// `ButtonProps` references `ButtonVariant`; once `ButtonProps` is
|
|
63
|
+
// seeded by `propsTypeName`, the transitive closure must include
|
|
64
|
+
// `ButtonVariant` too — otherwise `[variant]` lookups raise TS7053
|
|
65
|
+
// because `variant` widens to `any`.
|
|
66
|
+
const source = `'use client'
|
|
67
|
+
type ButtonVariant = 'default' | 'destructive'
|
|
68
|
+
interface ButtonProps { variant?: ButtonVariant }
|
|
69
|
+
export function Button({ variant = 'default' }: ButtonProps) {
|
|
70
|
+
return <button>{variant}</button>
|
|
71
|
+
}`
|
|
72
|
+
const tmpl = compileMarkedTemplate(source, 'Button.tsx')
|
|
73
|
+
expect(tmpl).toContain("type ButtonVariant = 'default' | 'destructive'")
|
|
74
|
+
expect(tmpl).toContain('interface ButtonProps')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('carries forward declarations referenced by named re-exports', () => {
|
|
78
|
+
// `export type { ButtonVariant }` requires `ButtonVariant` to be
|
|
79
|
+
// declared locally. Even if the component body never mentions
|
|
80
|
+
// `ButtonVariant`, the re-export ties it to the public surface.
|
|
81
|
+
const source = `'use client'
|
|
82
|
+
type ButtonVariant = 'default' | 'destructive'
|
|
83
|
+
export function Button() {
|
|
84
|
+
return <button />
|
|
85
|
+
}
|
|
86
|
+
export type { ButtonVariant }`
|
|
87
|
+
const tmpl = compileMarkedTemplate(source, 'Button.tsx')
|
|
88
|
+
expect(tmpl).toContain("type ButtonVariant = 'default' | 'destructive'")
|
|
89
|
+
expect(tmpl).toContain('export type { ButtonVariant }')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('Hono adapter — hydration-props type annotation', () => {
|
|
94
|
+
test('parameterless client component declares the full hydration-props type', () => {
|
|
95
|
+
// The scaffolded TodoList / "no props" shape — most common case for
|
|
96
|
+
// a brand-new user. This is the path that used to slip through the
|
|
97
|
+
// narrow `{ __instanceId; __bfScope; __bfChild }` fallback.
|
|
98
|
+
const source = `'use client'
|
|
99
|
+
import { createSignal } from '@barefootjs/client'
|
|
100
|
+
export function NoProps() {
|
|
101
|
+
const [v, setV] = createSignal(0)
|
|
102
|
+
return <button onClick={() => setV(v() + 1)}>{v()}</button>
|
|
103
|
+
}`
|
|
104
|
+
const tmpl = compileMarkedTemplate(source, 'NoProps.tsx')
|
|
105
|
+
for (const field of HYDRATION_FIELDS) {
|
|
106
|
+
expect(tmpl).toContain(field)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('destructured-props pattern with a Props type uses the `<Name>PropsWithHydration` alias', () => {
|
|
111
|
+
// The alias is declared at the top of the file with every hydration
|
|
112
|
+
// field. Pin its presence so a regression in `generateTypeDeclarations`
|
|
113
|
+
// doesn't silently strip the synth fields.
|
|
114
|
+
const source = `'use client'
|
|
115
|
+
import { createSignal } from '@barefootjs/client'
|
|
116
|
+
interface Props { initial: number }
|
|
117
|
+
export function Counter({ initial }: Props) {
|
|
118
|
+
const [v, setV] = createSignal(initial)
|
|
119
|
+
return <button onClick={() => setV(v() + 1)}>{v()}</button>
|
|
120
|
+
}`
|
|
121
|
+
const tmpl = compileMarkedTemplate(source, 'Counter.tsx')
|
|
122
|
+
expect(tmpl).toContain('type CounterPropsWithHydration = Props & {')
|
|
123
|
+
for (const field of HYDRATION_FIELDS) {
|
|
124
|
+
expect(tmpl).toContain(field)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('SolidJS-style props=Props pattern carries hydration fields inline', () => {
|
|
129
|
+
const source = `'use client'
|
|
130
|
+
import { createSignal } from '@barefootjs/client'
|
|
131
|
+
interface Props { initial: number }
|
|
132
|
+
export function Counter(props: Props) {
|
|
133
|
+
const [v, setV] = createSignal(props.initial)
|
|
134
|
+
return <button onClick={() => setV(v() + 1)}>{v()}</button>
|
|
135
|
+
}`
|
|
136
|
+
const tmpl = compileMarkedTemplate(source, 'Counter.tsx')
|
|
137
|
+
for (const field of HYDRATION_FIELDS) {
|
|
138
|
+
expect(tmpl).toContain(field)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { manifestToScriptUrls } from '../app'
|
|
3
|
+
|
|
4
|
+
describe('manifestToScriptUrls', () => {
|
|
5
|
+
test('empty manifest returns empty array', () => {
|
|
6
|
+
expect(manifestToScriptUrls({}, '/static/components')).toEqual([])
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test('runtime entry is emitted first', () => {
|
|
10
|
+
const out = manifestToScriptUrls(
|
|
11
|
+
{
|
|
12
|
+
Counter: { clientJs: 'components/Counter.client.js' },
|
|
13
|
+
__barefoot__: { clientJs: 'components/barefoot.js' },
|
|
14
|
+
},
|
|
15
|
+
'/static/components',
|
|
16
|
+
)
|
|
17
|
+
expect(out[0]).toBe('/static/components/barefoot.js')
|
|
18
|
+
expect(out).toContain('/static/components/Counter.client.js')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('drops "components/" prefix from manifest paths', () => {
|
|
22
|
+
expect(
|
|
23
|
+
manifestToScriptUrls(
|
|
24
|
+
{ Counter: { clientJs: 'components/Counter.client.js' } },
|
|
25
|
+
'/static/components',
|
|
26
|
+
),
|
|
27
|
+
).toEqual(['/static/components/Counter.client.js'])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('preserves nested subdirs from manifest', () => {
|
|
31
|
+
expect(
|
|
32
|
+
manifestToScriptUrls(
|
|
33
|
+
{ 'ui/button/index': { clientJs: 'components/ui/button/index.client.js' } },
|
|
34
|
+
'/static/components',
|
|
35
|
+
),
|
|
36
|
+
).toEqual(['/static/components/ui/button/index.client.js'])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('honors custom base URL', () => {
|
|
40
|
+
expect(
|
|
41
|
+
manifestToScriptUrls(
|
|
42
|
+
{
|
|
43
|
+
__barefoot__: { clientJs: 'components/barefoot.js' },
|
|
44
|
+
Counter: { clientJs: 'components/Counter.client.js' },
|
|
45
|
+
},
|
|
46
|
+
'/assets/bf',
|
|
47
|
+
),
|
|
48
|
+
).toEqual([
|
|
49
|
+
'/assets/bf/barefoot.js',
|
|
50
|
+
'/assets/bf/Counter.client.js',
|
|
51
|
+
])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('strips trailing slash from base URL', () => {
|
|
55
|
+
expect(
|
|
56
|
+
manifestToScriptUrls(
|
|
57
|
+
{ Counter: { clientJs: 'components/Counter.client.js' } },
|
|
58
|
+
'/static/components/',
|
|
59
|
+
),
|
|
60
|
+
).toEqual(['/static/components/Counter.client.js'])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('skips entries without clientJs', () => {
|
|
64
|
+
expect(
|
|
65
|
+
manifestToScriptUrls(
|
|
66
|
+
{
|
|
67
|
+
__barefoot__: { clientJs: 'components/barefoot.js' },
|
|
68
|
+
ServerOnly: {},
|
|
69
|
+
Counter: { clientJs: 'components/Counter.client.js' },
|
|
70
|
+
},
|
|
71
|
+
'/static/components',
|
|
72
|
+
),
|
|
73
|
+
).toEqual([
|
|
74
|
+
'/static/components/barefoot.js',
|
|
75
|
+
'/static/components/Counter.client.js',
|
|
76
|
+
])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('passthrough for clientJs paths not under "components/"', () => {
|
|
80
|
+
expect(
|
|
81
|
+
manifestToScriptUrls(
|
|
82
|
+
{ Counter: { clientJs: 'Counter.client.js' } },
|
|
83
|
+
'/static/components',
|
|
84
|
+
),
|
|
85
|
+
).toEqual(['/static/components/Counter.client.js'])
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Specification-as-test for `bun create barefootjs@latest <project-name>`
|
|
2
|
+
// with the default Hono (Cloudflare Workers) adapter.
|
|
3
|
+
//
|
|
4
|
+
// This file verifies that the Hono scaffold satisfies both the
|
|
5
|
+
// cross-adapter contract defined in `create-barefootjs` and the
|
|
6
|
+
// Hono-specific wiring (wrangler.jsonc, CF Workers deploy, script shapes).
|
|
7
|
+
//
|
|
8
|
+
// The happy-path tests are gated by the same network flag as the
|
|
9
|
+
// companion mojo scaffold test, because `barefoot init` probes the live
|
|
10
|
+
// UI registry over the network:
|
|
11
|
+
//
|
|
12
|
+
// BAREFOOT_CREATE_INTEGRATION=1 bun test src/__tests__/scaffold.test.ts
|
|
13
|
+
|
|
14
|
+
import { describe, test, expect, beforeAll } from 'bun:test'
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
16
|
+
import path from 'node:path'
|
|
17
|
+
import { spawnSync } from 'node:child_process'
|
|
18
|
+
import { mkdtempSync } from 'node:fs'
|
|
19
|
+
import { tmpdir } from 'node:os'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
import {
|
|
22
|
+
assertScaffoldContract,
|
|
23
|
+
ensureCreateCli,
|
|
24
|
+
type ScaffoldFacts,
|
|
25
|
+
} from '@barefootjs/adapter-tests'
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers (thin wrappers around the compiled create-barefootjs CLI)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
const CREATE_PKG_DIR = path.join(
|
|
32
|
+
fileURLToPath(new URL('.', import.meta.url)),
|
|
33
|
+
'../../../create-barefootjs',
|
|
34
|
+
)
|
|
35
|
+
const CREATE_CLI = path.join(CREATE_PKG_DIR, 'dist', 'index.js')
|
|
36
|
+
|
|
37
|
+
function mktmp(): string {
|
|
38
|
+
return mkdtempSync(path.join(tmpdir(), 'bf-hono-scaffold-test-'))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface RunResult {
|
|
42
|
+
exitCode: number | null
|
|
43
|
+
stdout: string
|
|
44
|
+
stderr: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function runCreate(
|
|
48
|
+
args: string[],
|
|
49
|
+
opts: { cwd: string; env?: Record<string, string> },
|
|
50
|
+
): RunResult {
|
|
51
|
+
ensureCreateCli(CREATE_PKG_DIR)
|
|
52
|
+
const result = spawnSync('node', [CREATE_CLI, ...args], {
|
|
53
|
+
cwd: opts.cwd,
|
|
54
|
+
env: {
|
|
55
|
+
...process.env,
|
|
56
|
+
...opts.env,
|
|
57
|
+
// Remove PM detection signal so commands are deterministic.
|
|
58
|
+
npm_config_user_agent: undefined,
|
|
59
|
+
},
|
|
60
|
+
encoding: 'utf-8',
|
|
61
|
+
})
|
|
62
|
+
return {
|
|
63
|
+
exitCode: result.status,
|
|
64
|
+
stdout: result.stdout ?? '',
|
|
65
|
+
stderr: result.stderr ?? '',
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const INTEGRATION = process.env.BAREFOOT_CREATE_INTEGRATION === '1'
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Happy-path scenario — `bun create barefootjs@latest demo-app` (Hono default)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
describe.skipIf(!INTEGRATION)(
|
|
76
|
+
'Scenario: bun create barefootjs@latest <project-name> (Hono adapter)',
|
|
77
|
+
() => {
|
|
78
|
+
let result: RunResult
|
|
79
|
+
let projectDir: string
|
|
80
|
+
|
|
81
|
+
beforeAll(() => {
|
|
82
|
+
const cwd = mktmp()
|
|
83
|
+
result = runCreate(['demo-app'], { cwd })
|
|
84
|
+
projectDir = path.join(cwd, 'demo-app')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('satisfies the cross-adapter scaffold contract', () => {
|
|
88
|
+
assertScaffoldContract({
|
|
89
|
+
exitCode: result.exitCode,
|
|
90
|
+
stdout: result.stdout,
|
|
91
|
+
projectDir,
|
|
92
|
+
adapterPackageName: '@barefootjs/hono',
|
|
93
|
+
devReload: {
|
|
94
|
+
// Hono CF uses `wrangler dev --live-reload`; wrangler injects
|
|
95
|
+
// its own reload client, so no scaffold-side snippet is needed.
|
|
96
|
+
subscribesBrowserInDev: true,
|
|
97
|
+
gatedToDev: true,
|
|
98
|
+
// No shared SSE protocol — wrangler manages the reload channel.
|
|
99
|
+
sentinelSseEndpoint: null,
|
|
100
|
+
},
|
|
101
|
+
} satisfies ScaffoldFacts)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// -------------------------------------------------------------------------
|
|
105
|
+
// Hono-specific wiring
|
|
106
|
+
// -------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
test('hono runtime dep is present alongside the adapter', () => {
|
|
109
|
+
const pkg = JSON.parse(
|
|
110
|
+
readFileSync(path.join(projectDir, 'package.json'), 'utf-8'),
|
|
111
|
+
) as { dependencies?: Record<string, string> }
|
|
112
|
+
expect(pkg.dependencies?.['hono']).toBeDefined()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('wrangler.jsonc is present (CF Workers target)', () => {
|
|
116
|
+
expect(existsSync(path.join(projectDir, 'wrangler.jsonc'))).toBe(true)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('wrangler.jsonc name matches the target directory', () => {
|
|
120
|
+
// Without this, every scaffold would deploy as the generic
|
|
121
|
+
// "my-app" Worker and overwrite each other on shared CF accounts.
|
|
122
|
+
const raw = readFileSync(path.join(projectDir, 'wrangler.jsonc'), 'utf-8')
|
|
123
|
+
const wrangler = JSON.parse(raw.replace(/^\s*\/\/.*$/gm, ''))
|
|
124
|
+
expect(wrangler.name).toBe('demo-app')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('dev script wires bf build --watch + unocss + wrangler dev --live-reload', () => {
|
|
128
|
+
const pkg = JSON.parse(
|
|
129
|
+
readFileSync(path.join(projectDir, 'package.json'), 'utf-8'),
|
|
130
|
+
) as { scripts?: Record<string, string> }
|
|
131
|
+
expect(pkg.scripts?.dev).toContain('bf build --watch')
|
|
132
|
+
expect(pkg.scripts?.dev).toContain('unocss --watch')
|
|
133
|
+
expect(pkg.scripts?.dev).toContain('wrangler dev --live-reload')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('deploy script targets Cloudflare Workers', () => {
|
|
137
|
+
expect(result.stdout).toContain('Deploy:')
|
|
138
|
+
expect(result.stdout).toMatch(/npm run deploy\s+# deploy to Cloudflare Workers/)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('multi-segment positional path sanitizes name + cd uses full path', () => {
|
|
142
|
+
const sandbox = mktmp()
|
|
143
|
+
const r = runCreate(['foo/bar/bazz'], { cwd: sandbox })
|
|
144
|
+
expect(r.exitCode).toBe(0)
|
|
145
|
+
const nested = path.join(sandbox, 'foo', 'bar', 'bazz')
|
|
146
|
+
const nestedPkg = JSON.parse(
|
|
147
|
+
readFileSync(path.join(nested, 'package.json'), 'utf-8'),
|
|
148
|
+
) as { name: string }
|
|
149
|
+
expect(nestedPkg.name).toBe('bazz')
|
|
150
|
+
const nestedRaw = readFileSync(path.join(nested, 'wrangler.jsonc'), 'utf-8')
|
|
151
|
+
const nestedWrangler = JSON.parse(nestedRaw.replace(/^\s*\/\/.*$/gm, ''))
|
|
152
|
+
expect(nestedWrangler.name).toBe('bazz')
|
|
153
|
+
expect(r.stdout).toMatch(/cd foo\/bar\/bazz/)
|
|
154
|
+
})
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Package-manager scenario — the detected PM dictates the post-scaffold guide
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
describe.skipIf(!INTEGRATION)(
|
|
163
|
+
'Scenario: the invoking package manager dictates the next-step commands',
|
|
164
|
+
() => {
|
|
165
|
+
interface PmCase {
|
|
166
|
+
pm: 'npm' | 'bun' | 'pnpm' | 'yarn'
|
|
167
|
+
env: Record<string, string>
|
|
168
|
+
install: string
|
|
169
|
+
run: string
|
|
170
|
+
}
|
|
171
|
+
const cases: PmCase[] = [
|
|
172
|
+
{
|
|
173
|
+
pm: 'npm',
|
|
174
|
+
env: { npm_config_user_agent: 'npm/10.0.0 node/v22.0.0 darwin arm64' },
|
|
175
|
+
install: 'npm install',
|
|
176
|
+
run: 'npm run dev',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
pm: 'bun',
|
|
180
|
+
env: { npm_config_user_agent: 'bun/1.3.0' },
|
|
181
|
+
install: 'bun install',
|
|
182
|
+
run: 'bun run dev',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
pm: 'pnpm',
|
|
186
|
+
env: { npm_config_user_agent: 'pnpm/9.0.0' },
|
|
187
|
+
install: 'pnpm install',
|
|
188
|
+
run: 'pnpm dev',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
pm: 'yarn',
|
|
192
|
+
env: { npm_config_user_agent: 'yarn/4.0.0' },
|
|
193
|
+
install: 'yarn',
|
|
194
|
+
run: 'yarn dev',
|
|
195
|
+
},
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
test.each(cases)(
|
|
199
|
+
'when invoked via $pm, the post-scaffold guide uses $pm commands',
|
|
200
|
+
({ env, install, run }) => {
|
|
201
|
+
const cwd = mktmp()
|
|
202
|
+
const r = runCreate(['demo-app'], { cwd, env })
|
|
203
|
+
expect(r.exitCode).toBe(0)
|
|
204
|
+
expect(r.stdout).toContain(install)
|
|
205
|
+
expect(r.stdout).toContain(run)
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
},
|
|
209
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR context-bridge end-to-end (#1084).
|
|
3
|
+
*
|
|
4
|
+
* Verifies that BarefootJS components using `<Context.Provider>` plus
|
|
5
|
+
* `useContext` actually flow values through Hono's per-render context stack
|
|
6
|
+
* at SSR time. This is the bonus path of Option B: not only does the SSR
|
|
7
|
+
* template no longer crash with `ReferenceError: useContext is not defined`,
|
|
8
|
+
* the rendered HTML reflects the provided value rather than the context
|
|
9
|
+
* default.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, test, expect } from 'bun:test'
|
|
13
|
+
import { renderHonoComponent } from '../test-render'
|
|
14
|
+
import { HonoAdapter } from '../adapter/hono-adapter'
|
|
15
|
+
|
|
16
|
+
describe('SSR context bridge (#1084 / Option B)', () => {
|
|
17
|
+
test('useContext returns the value provided by an enclosing Context.Provider at SSR', async () => {
|
|
18
|
+
const html = await renderHonoComponent({
|
|
19
|
+
adapter: new HonoAdapter(),
|
|
20
|
+
source: `
|
|
21
|
+
'use client'
|
|
22
|
+
import { createContext, useContext } from '@barefootjs/client'
|
|
23
|
+
|
|
24
|
+
const ThemeContext = createContext('light')
|
|
25
|
+
|
|
26
|
+
function ThemeLabel() {
|
|
27
|
+
const theme = useContext(ThemeContext)
|
|
28
|
+
return <span class="theme">{theme}</span>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ThemeRoot() {
|
|
32
|
+
return (
|
|
33
|
+
<div class="root">
|
|
34
|
+
<ThemeContext.Provider value="dark">
|
|
35
|
+
<ThemeLabel />
|
|
36
|
+
</ThemeContext.Provider>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
`,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
expect(html).toContain('class="theme"')
|
|
44
|
+
// The provided value flows through to the consumer at SSR.
|
|
45
|
+
expect(html).toContain('>dark<')
|
|
46
|
+
expect(html).not.toContain('>light<')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('useContext falls back to defaultValue when no provider is in scope', async () => {
|
|
50
|
+
const html = await renderHonoComponent({
|
|
51
|
+
adapter: new HonoAdapter(),
|
|
52
|
+
source: `
|
|
53
|
+
'use client'
|
|
54
|
+
import { createContext, useContext } from '@barefootjs/client'
|
|
55
|
+
|
|
56
|
+
const LocaleContext = createContext('en')
|
|
57
|
+
|
|
58
|
+
export function LocaleLabel() {
|
|
59
|
+
const locale = useContext(LocaleContext)
|
|
60
|
+
return <span>{locale}</span>
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
})
|
|
64
|
+
expect(html).toContain('>en<')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('reads a memo body that depends on a context value at SSR', async () => {
|
|
68
|
+
// Mirrors the pattern blocking step 2 of #1080: a primitive consumes the
|
|
69
|
+
// chart container's scales via useContext and computes geometry inside a
|
|
70
|
+
// createMemo. The memo body runs at SSR and used to throw because
|
|
71
|
+
// useContext was undefined; now it produces real markup.
|
|
72
|
+
const html = await renderHonoComponent({
|
|
73
|
+
adapter: new HonoAdapter(),
|
|
74
|
+
source: `
|
|
75
|
+
'use client'
|
|
76
|
+
import { createContext, createMemo, useContext } from '@barefootjs/client'
|
|
77
|
+
|
|
78
|
+
const GridContext = createContext({ ticks: [] })
|
|
79
|
+
|
|
80
|
+
function GridLines() {
|
|
81
|
+
const ctx = useContext(GridContext)
|
|
82
|
+
const lines = createMemo(() => ctx.ticks.map((y) => ({ y })))
|
|
83
|
+
return (
|
|
84
|
+
<g class="grid">
|
|
85
|
+
{lines().map((l) => <line key={l.y} y1={l.y} y2={l.y} />)}
|
|
86
|
+
</g>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function Chart() {
|
|
91
|
+
return (
|
|
92
|
+
<svg>
|
|
93
|
+
<GridContext.Provider value={{ ticks: [10, 20, 30] }}>
|
|
94
|
+
<GridLines />
|
|
95
|
+
</GridContext.Provider>
|
|
96
|
+
</svg>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
`,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Three <line> elements emitted at SSR — proving the memo body executed
|
|
103
|
+
// with a populated context (vs. the empty default).
|
|
104
|
+
const lineMatches = html.match(/<line\b/g) ?? []
|
|
105
|
+
expect(lineMatches.length).toBe(3)
|
|
106
|
+
expect(html).toMatch(/y1="10"/)
|
|
107
|
+
expect(html).toMatch(/y1="20"/)
|
|
108
|
+
expect(html).toMatch(/y1="30"/)
|
|
109
|
+
})
|
|
110
|
+
})
|