@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.
Files changed (81) hide show
  1. package/dist/adapter/hono-adapter.d.ts +141 -0
  2. package/dist/adapter/hono-adapter.d.ts.map +1 -0
  3. package/dist/adapter/index.d.ts +6 -0
  4. package/dist/adapter/index.d.ts.map +1 -0
  5. package/dist/adapter/index.js +632 -0
  6. package/dist/app.d.ts +131 -0
  7. package/dist/app.d.ts.map +1 -0
  8. package/dist/app.js +139 -0
  9. package/dist/async.d.ts +15 -0
  10. package/dist/async.d.ts.map +1 -0
  11. package/dist/async.js +12 -0
  12. package/dist/build.d.ts +65 -0
  13. package/dist/build.d.ts.map +1 -0
  14. package/dist/build.js +785 -0
  15. package/dist/client-shim.d.ts +59 -0
  16. package/dist/client-shim.d.ts.map +1 -0
  17. package/dist/client-shim.js +90 -0
  18. package/dist/dev-worker.d.ts +25 -0
  19. package/dist/dev-worker.d.ts.map +1 -0
  20. package/dist/dev-worker.js +65 -0
  21. package/dist/dev.d.ts +36 -0
  22. package/dist/dev.d.ts.map +1 -0
  23. package/dist/dev.js +418 -0
  24. package/dist/dialog-context.d.ts +13 -0
  25. package/dist/dialog-context.d.ts.map +1 -0
  26. package/dist/dialog-context.js +10 -0
  27. package/dist/index.d.ts +13 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +632 -0
  30. package/dist/jsx/jsx-dev-runtime/index.d.ts +9 -0
  31. package/dist/jsx/jsx-dev-runtime/index.d.ts.map +1 -0
  32. package/dist/jsx/jsx-dev-runtime/index.js +6 -0
  33. package/dist/jsx/jsx-runtime/index.d.ts +32 -0
  34. package/dist/jsx/jsx-runtime/index.d.ts.map +1 -0
  35. package/dist/jsx/jsx-runtime/index.js +10 -0
  36. package/dist/portal-ssr.d.ts +22 -0
  37. package/dist/portal-ssr.d.ts.map +1 -0
  38. package/dist/portal-ssr.js +73 -0
  39. package/dist/portals.d.ts +26 -0
  40. package/dist/portals.d.ts.map +1 -0
  41. package/dist/portals.js +41 -0
  42. package/dist/preload.d.ts +56 -0
  43. package/dist/preload.d.ts.map +1 -0
  44. package/dist/preload.js +51 -0
  45. package/dist/scripts.d.ts +80 -0
  46. package/dist/scripts.d.ts.map +1 -0
  47. package/dist/scripts.js +198 -0
  48. package/dist/test-render.d.ts +28 -0
  49. package/dist/test-render.d.ts.map +1 -0
  50. package/dist/utils.d.ts +16 -0
  51. package/dist/utils.d.ts.map +1 -0
  52. package/dist/utils.js +16 -0
  53. package/package.json +116 -0
  54. package/src/__tests__/async.test.tsx +106 -0
  55. package/src/__tests__/bfscripts-entry-roots.test.tsx +135 -0
  56. package/src/__tests__/build.test.ts +299 -0
  57. package/src/__tests__/dev.test.tsx +123 -0
  58. package/src/__tests__/hydration-props-type.test.ts +141 -0
  59. package/src/__tests__/manifest-scripts.test.ts +87 -0
  60. package/src/__tests__/scaffold.test.ts +209 -0
  61. package/src/__tests__/ssr-context-bridge.test.ts +110 -0
  62. package/src/__tests__/string-literal-css-var-prop.test.ts +84 -0
  63. package/src/__tests__/stub-deps-scripts.test.ts +183 -0
  64. package/src/adapter/hono-adapter.ts +1114 -0
  65. package/src/adapter/index.ts +6 -0
  66. package/src/app.ts +220 -0
  67. package/src/async.tsx +55 -0
  68. package/src/build.ts +230 -0
  69. package/src/client-shim.ts +164 -0
  70. package/src/dev-worker.ts +93 -0
  71. package/src/dev.tsx +146 -0
  72. package/src/dialog-context.tsx +44 -0
  73. package/src/index.ts +26 -0
  74. package/src/jsx/jsx-dev-runtime/index.ts +9 -0
  75. package/src/jsx/jsx-runtime/index.ts +40 -0
  76. package/src/portal-ssr.tsx +92 -0
  77. package/src/portals.tsx +98 -0
  78. package/src/preload.tsx +166 -0
  79. package/src/scripts.tsx +220 -0
  80. package/src/test-render.ts +143 -0
  81. 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
+ })