@barefootjs/vite 0.30.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/child-marker.d.ts +62 -0
- package/dist/child-marker.d.ts.map +1 -0
- package/dist/compile-cache.d.ts +19 -0
- package/dist/compile-cache.d.ts.map +1 -0
- package/dist/component-manifest.d.ts +69 -0
- package/dist/component-manifest.d.ts.map +1 -0
- package/dist/corpus-program.d.ts +41 -0
- package/dist/corpus-program.d.ts.map +1 -0
- package/dist/debounced-serial-runner.d.ts +27 -0
- package/dist/debounced-serial-runner.d.ts.map +1 -0
- package/dist/dev-server.d.ts +99 -0
- package/dist/dev-server.d.ts.map +1 -0
- package/dist/discover.d.ts +117 -0
- package/dist/discover.d.ts.map +1 -0
- package/dist/emit.d.ts +9 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24626 -0
- package/dist/manifest.d.ts +39 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/paths.d.ts +57 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/resolve-client-js.d.ts +6 -0
- package/dist/resolve-client-js.d.ts.map +1 -0
- package/dist/types.d.ts +141 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/__tests__/child-marker.test.ts +24 -0
- package/src/__tests__/compile-cache.test.ts +73 -0
- package/src/__tests__/component-dir-entry.test.ts +239 -0
- package/src/__tests__/component-manifest.test.ts +124 -0
- package/src/__tests__/corpus-program.test.ts +244 -0
- package/src/__tests__/debounced-serial-runner.test.ts +131 -0
- package/src/__tests__/dev-server.test.ts +138 -0
- package/src/__tests__/discover.test.ts +148 -0
- package/src/__tests__/e2e-vite-build.test.ts +191 -0
- package/src/__tests__/e2e-vite-dev.test.ts +478 -0
- package/src/__tests__/emit.test.ts +73 -0
- package/src/__tests__/manifest.test.ts +146 -0
- package/src/__tests__/paths.test.ts +93 -0
- package/src/__tests__/plugin.test.ts +417 -0
- package/src/__tests__/relative-import-rewrite.test.ts +79 -0
- package/src/__tests__/resolve-client-js.test.ts +55 -0
- package/src/__tests__/templates-optional.test.ts +139 -0
- package/src/child-marker.ts +67 -0
- package/src/compile-cache.ts +63 -0
- package/src/component-manifest.ts +139 -0
- package/src/corpus-program.ts +125 -0
- package/src/debounced-serial-runner.ts +67 -0
- package/src/dev-server.ts +184 -0
- package/src/discover.ts +230 -0
- package/src/emit.ts +66 -0
- package/src/index.ts +25 -0
- package/src/manifest.ts +89 -0
- package/src/paths.ts +114 -0
- package/src/plugin.ts +792 -0
- package/src/resolve-client-js.ts +34 -0
- package/src/types.ts +144 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real end-to-end coverage of `configureServer`: actually starts a Vite dev
|
|
3
|
+
* server (via Vite's Node `createServer` API, the same function the `vite`
|
|
4
|
+
* CLI itself calls) against `../../e2e-fixture-dev`, then drives it over
|
|
5
|
+
* real HTTP/fs, mirroring `e2e-vite-build.test.ts`'s rigor for the build
|
|
6
|
+
* half.
|
|
7
|
+
*
|
|
8
|
+
* The fixture's `components` dir is a SIBLING of the Vite project root
|
|
9
|
+
* (`app/`), not a descendant — mirroring this monorepo's real layouts (an
|
|
10
|
+
* app's `vite.config.ts` root is the backend app dir; components live in a
|
|
11
|
+
* shared `ui/`-style directory next to it).
|
|
12
|
+
*
|
|
13
|
+
* The server-only regression (below) runs against its OWN dedicated server
|
|
14
|
+
* that never fetches any `'use client'` component over HTTP. That isolation
|
|
15
|
+
* matters: Vite's OWN internal `ensureWatchedFile` adds any file it has
|
|
16
|
+
* transformed as a module to the watcher, regardless of this plugin. Fetch
|
|
17
|
+
* Counter.tsx once and Vite itself starts watching THAT SPECIFIC FILE —
|
|
18
|
+
* which would silently paper over a missing `server.watcher.add()` call the
|
|
19
|
+
* moment a later test's "any change → full re-run" pass happens to also
|
|
20
|
+
* pick up Greeting.tsx's already-edited-on-disk content. Keeping the
|
|
21
|
+
* server-only regression on a server that never fetches Counter.tsx (or any
|
|
22
|
+
* other client component) means the ONLY way Greeting.tsx's edit can ever
|
|
23
|
+
* reach the watcher is this plugin's own explicit `server.watcher.add()` —
|
|
24
|
+
* see `plugin.ts`'s `configureServer`.
|
|
25
|
+
*/
|
|
26
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'
|
|
27
|
+
import { createServer, type ViteDevServer } from 'vite'
|
|
28
|
+
import { mkdtemp, rm, readFile, writeFile } from 'node:fs/promises'
|
|
29
|
+
import { tmpdir } from 'node:os'
|
|
30
|
+
import { join, resolve } from 'node:path'
|
|
31
|
+
import { GoTemplateAdapter } from '@barefootjs/go-template/adapter'
|
|
32
|
+
import { barefoot } from '../plugin.ts'
|
|
33
|
+
import { devRequestPath } from '../dev-server.ts'
|
|
34
|
+
|
|
35
|
+
const FIXTURE_ROOT = resolve(import.meta.dirname, '../../e2e-fixture-dev')
|
|
36
|
+
const APP_ROOT = join(FIXTURE_ROOT, 'app')
|
|
37
|
+
const COMPONENTS_DIR = join(FIXTURE_ROOT, 'components')
|
|
38
|
+
const COUNTER_PATH = join(COMPONENTS_DIR, 'Counter.tsx')
|
|
39
|
+
const GREETING_PATH = join(COMPONENTS_DIR, 'Greeting.tsx')
|
|
40
|
+
|
|
41
|
+
async function waitFor(check: () => Promise<boolean> | boolean, timeoutMs = 10_000): Promise<void> {
|
|
42
|
+
const start = Date.now()
|
|
43
|
+
for (;;) {
|
|
44
|
+
if (await check()) return
|
|
45
|
+
if (Date.now() - start > timeoutMs) throw new Error('waitFor: condition not met within timeout')
|
|
46
|
+
await new Promise(r => setTimeout(r, 50))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readIfExists(path: string): Promise<string | null> {
|
|
51
|
+
try {
|
|
52
|
+
return await readFile(path, 'utf8')
|
|
53
|
+
} catch {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function captureWsSends(server: ViteDevServer): { sent: unknown[]; restore: () => void } {
|
|
59
|
+
const sent: unknown[] = []
|
|
60
|
+
const originalSend = server.ws.send.bind(server.ws)
|
|
61
|
+
server.ws.send = ((payload: unknown) => {
|
|
62
|
+
sent.push(payload)
|
|
63
|
+
return originalSend(payload as never)
|
|
64
|
+
}) as typeof server.ws.send
|
|
65
|
+
return { sent, restore: () => { server.ws.send = originalSend } }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function captureLoggerErrors(server: ViteDevServer): { errors: unknown[]; restore: () => void } {
|
|
69
|
+
const errors: unknown[] = []
|
|
70
|
+
const originalError = server.config.logger.error.bind(server.config.logger)
|
|
71
|
+
server.config.logger.error = ((msg: string, opts?: unknown) => {
|
|
72
|
+
errors.push(msg)
|
|
73
|
+
return originalError(msg, opts as never)
|
|
74
|
+
}) as typeof server.config.logger.error
|
|
75
|
+
return { errors, restore: () => { server.config.logger.error = originalError } }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function countFullReloads(sent: unknown[]): number {
|
|
79
|
+
return sent.filter(m => (m as { type?: string }).type === 'full-reload').length
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function startDevServer(
|
|
83
|
+
templatesDir: string,
|
|
84
|
+
extraServerOptions: Record<string, unknown> = {},
|
|
85
|
+
afterEmit?: (ctx: { types: Map<string, string>; projectDir: string; templatesDir: string; outDir: string; mode: string }) => void,
|
|
86
|
+
) {
|
|
87
|
+
const server = await createServer({
|
|
88
|
+
configFile: false,
|
|
89
|
+
root: APP_ROOT,
|
|
90
|
+
logLevel: 'silent',
|
|
91
|
+
server: {
|
|
92
|
+
// Port 0 (OS-assigned) exercises the real requirement: the origin
|
|
93
|
+
// this plugin bakes into templates MUST come from the actually
|
|
94
|
+
// bound port (`httpServer.address()`), not the configured one.
|
|
95
|
+
port: 0,
|
|
96
|
+
strictPort: false,
|
|
97
|
+
fs: { allow: [FIXTURE_ROOT] },
|
|
98
|
+
// Polling sidesteps native fs-event flakiness some sandboxed /
|
|
99
|
+
// containerized filesystems have with inotify.
|
|
100
|
+
watch: { usePolling: true, interval: 30 },
|
|
101
|
+
...extraServerOptions,
|
|
102
|
+
},
|
|
103
|
+
plugins: [
|
|
104
|
+
barefoot({
|
|
105
|
+
adapter: new GoTemplateAdapter({ packageName: 'main' }),
|
|
106
|
+
components: ['../components'],
|
|
107
|
+
templates: templatesDir,
|
|
108
|
+
afterEmit,
|
|
109
|
+
}),
|
|
110
|
+
],
|
|
111
|
+
})
|
|
112
|
+
await server.listen()
|
|
113
|
+
|
|
114
|
+
const address = server.httpServer!.address()
|
|
115
|
+
const port = typeof address === 'object' && address !== null ? address.port : 0
|
|
116
|
+
const baseUrl = `http://localhost:${port}`
|
|
117
|
+
return { server, baseUrl }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe('e2e: vite dev server', () => {
|
|
121
|
+
let server: ViteDevServer
|
|
122
|
+
let templatesDir: string
|
|
123
|
+
let baseUrl: string
|
|
124
|
+
let originalCounterSource: string
|
|
125
|
+
let originalGreetingSource: string
|
|
126
|
+
|
|
127
|
+
beforeAll(async () => {
|
|
128
|
+
templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-vite-dev-views-'))
|
|
129
|
+
originalCounterSource = await readFile(COUNTER_PATH, 'utf8')
|
|
130
|
+
originalGreetingSource = await readFile(GREETING_PATH, 'utf8')
|
|
131
|
+
;({ server, baseUrl } = await startDevServer(templatesDir))
|
|
132
|
+
|
|
133
|
+
// The initial eager pass runs off the httpServer's 'listening' event,
|
|
134
|
+
// asynchronously with respect to `server.listen()` resolving — wait
|
|
135
|
+
// for its output to actually land on disk before asserting on it.
|
|
136
|
+
await waitFor(async () => (await readIfExists(join(templatesDir, 'Counter.tmpl'))) !== null)
|
|
137
|
+
await waitFor(async () => (await readIfExists(join(templatesDir, 'Greeting.tmpl'))) !== null)
|
|
138
|
+
}, 30_000)
|
|
139
|
+
|
|
140
|
+
afterAll(async () => {
|
|
141
|
+
// Always restore fixture sources and tear down the server, even if an
|
|
142
|
+
// assertion above threw — a leaked dev server hangs the whole suite.
|
|
143
|
+
await writeFile(COUNTER_PATH, originalCounterSource).catch(() => {})
|
|
144
|
+
await writeFile(GREETING_PATH, originalGreetingSource).catch(() => {})
|
|
145
|
+
await server?.close()
|
|
146
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('fetching the "use client" component\'s module URL returns compiled client JS', async () => {
|
|
150
|
+
const requestPath = devRequestPath({ root: APP_ROOT }, COUNTER_PATH)
|
|
151
|
+
// Counter lives OUTSIDE the vite root, so this must resolve through
|
|
152
|
+
// Vite's `/@fs/` absolute-path passthrough.
|
|
153
|
+
expect(requestPath.startsWith('@fs/')).toBe(true)
|
|
154
|
+
|
|
155
|
+
const res = await fetch(`${baseUrl}/${requestPath}`)
|
|
156
|
+
expect(res.status).toBe(200)
|
|
157
|
+
|
|
158
|
+
const body = await res.text()
|
|
159
|
+
expect(body).toContain('createSignal')
|
|
160
|
+
expect(body).toContain('hydrate(')
|
|
161
|
+
expect(body).not.toContain('use client')
|
|
162
|
+
expect(body).not.toMatch(/onClick=\{/)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('emitted templates carry dev-origin URLs, including the @vite/client entry', async () => {
|
|
166
|
+
const requestPath = devRequestPath({ root: APP_ROOT }, COUNTER_PATH)
|
|
167
|
+
const template = await readFile(join(templatesDir, 'Counter.tmpl'), 'utf8')
|
|
168
|
+
|
|
169
|
+
expect(template).toContain(`{{.Scripts.Register "${baseUrl}/@vite/client"}}`)
|
|
170
|
+
expect(template).toContain(`{{.Scripts.Register "${baseUrl}/${requestPath}"}}`)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('the server-only component has no script registration, dev or otherwise', async () => {
|
|
174
|
+
const template = await readFile(join(templatesDir, 'Greeting.tmpl'), 'utf8')
|
|
175
|
+
expect(template).not.toContain('Scripts.Register')
|
|
176
|
+
expect(template).toContain('Hello')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('the templates dir carries the dev-artifact marker while the dev server is running', async () => {
|
|
180
|
+
const marker = await readIfExists(join(templatesDir, '.barefootjs-dev-build'))
|
|
181
|
+
expect(marker).not.toBeNull()
|
|
182
|
+
expect(marker).toContain('DEV BUILD OUTPUT')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('writes the cross-language dev-reload sentinel one directory above templates', async () => {
|
|
186
|
+
const sentinel = await readIfExists(resolve(templatesDir, '..', '.dev', 'build-id'))
|
|
187
|
+
expect(sentinel).not.toBeNull()
|
|
188
|
+
expect(sentinel).toMatch(/^\d+$/)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('editing a component updates the dev-reload sentinel value', async () => {
|
|
192
|
+
const sentinelPath = resolve(templatesDir, '..', '.dev', 'build-id')
|
|
193
|
+
const before = await readFile(sentinelPath, 'utf8')
|
|
194
|
+
try {
|
|
195
|
+
const edited = originalGreetingSource.replace('Hello', 'Hello!!!')
|
|
196
|
+
await writeFile(GREETING_PATH, edited)
|
|
197
|
+
|
|
198
|
+
await waitFor(async () => {
|
|
199
|
+
const after = await readIfExists(sentinelPath)
|
|
200
|
+
return after !== null && after !== before
|
|
201
|
+
})
|
|
202
|
+
} finally {
|
|
203
|
+
await writeFile(GREETING_PATH, originalGreetingSource)
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('a request with a cross-origin Origin header gets the localhost-only CORS default', async () => {
|
|
208
|
+
const res = await fetch(`${baseUrl}/@vite/client`, {
|
|
209
|
+
headers: { Origin: 'http://localhost:3010' },
|
|
210
|
+
})
|
|
211
|
+
expect(res.headers.get('access-control-allow-origin')).toBe('http://localhost:3010')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('editing a "use client" component re-emits its template and triggers full-reload', async () => {
|
|
215
|
+
const { sent, restore } = captureWsSends(server)
|
|
216
|
+
try {
|
|
217
|
+
const edited = originalCounterSource.replace(
|
|
218
|
+
'<button onClick=',
|
|
219
|
+
'<button data-edited="counter-marker" onClick=',
|
|
220
|
+
)
|
|
221
|
+
await writeFile(COUNTER_PATH, edited)
|
|
222
|
+
|
|
223
|
+
await waitFor(async () => {
|
|
224
|
+
const tpl = await readIfExists(join(templatesDir, 'Counter.tmpl'))
|
|
225
|
+
return tpl !== null && tpl.includes('data-edited="counter-marker"')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
expect(countFullReloads(sent)).toBeGreaterThanOrEqual(1)
|
|
229
|
+
} finally {
|
|
230
|
+
restore()
|
|
231
|
+
await writeFile(COUNTER_PATH, originalCounterSource)
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// The deterministic proof that passes never overlap and a mid-pass
|
|
236
|
+
// trigger is coalesced into exactly one follow-up (not dropped, not
|
|
237
|
+
// duplicated) is `debounced-serial-runner.test.ts` — it controls task
|
|
238
|
+
// completion directly via manually-resolved promises, which a real e2e
|
|
239
|
+
// test cannot: this fixture's eager pass finishes in low single-digit
|
|
240
|
+
// milliseconds, far too fast to reliably force a real "change arrives
|
|
241
|
+
// while a pass is in flight" race through wall-clock timing alone. This
|
|
242
|
+
// test is the complementary end-to-end confirmation: real rapid disk
|
|
243
|
+
// writes, through the real watcher, into the real debounced runner,
|
|
244
|
+
// converge on the correct final state with no corruption and no crash.
|
|
245
|
+
test('rapid successive edits converge on the final content with no corruption or errors', async () => {
|
|
246
|
+
const { sent, restore: restoreWs } = captureWsSends(server)
|
|
247
|
+
const { errors, restore: restoreLogger } = captureLoggerErrors(server)
|
|
248
|
+
const EDIT_COUNT = 8
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
// Fire edits back-to-back, faster than the 100ms watcher debounce —
|
|
252
|
+
// simulates a save-twice-quickly / multi-file-save / `git checkout`.
|
|
253
|
+
for (let i = 1; i <= EDIT_COUNT; i++) {
|
|
254
|
+
await writeFile(
|
|
255
|
+
COUNTER_PATH,
|
|
256
|
+
originalCounterSource.replace('<button onClick=', `<button data-edit-n="${i}" onClick=`),
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Give the debounce window plus a full eager pass time to settle.
|
|
261
|
+
await waitFor(async () => {
|
|
262
|
+
const tpl = await readIfExists(join(templatesDir, 'Counter.tmpl'))
|
|
263
|
+
return tpl !== null && tpl.includes(`data-edit-n="${EDIT_COUNT}"`)
|
|
264
|
+
})
|
|
265
|
+
// Settle further: if an overlapping/racing pass were still in
|
|
266
|
+
// flight and about to clobber the file with stale content, a short
|
|
267
|
+
// wait would catch it reverting.
|
|
268
|
+
await new Promise(r => setTimeout(r, 300))
|
|
269
|
+
|
|
270
|
+
const finalTemplate = await readFile(join(templatesDir, 'Counter.tmpl'), 'utf8')
|
|
271
|
+
expect(finalTemplate).toContain(`data-edit-n="${EDIT_COUNT}"`)
|
|
272
|
+
// Not just the last edit "eventually" landing — no EARLIER edit's
|
|
273
|
+
// marker should still be present either (that would mean two
|
|
274
|
+
// template-writing passes raced and left mixed output).
|
|
275
|
+
for (let i = 1; i < EDIT_COUNT; i++) {
|
|
276
|
+
expect(finalTemplate).not.toContain(`data-edit-n="${i}"`)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
expect(errors).toEqual([])
|
|
280
|
+
const reloadCount = countFullReloads(sent)
|
|
281
|
+
expect(reloadCount).toBeGreaterThanOrEqual(1)
|
|
282
|
+
// A soft signal, not the proof (see the comment above the test): with
|
|
283
|
+
// 8 back-to-back writes this fast, the underlying watcher's own
|
|
284
|
+
// polling interval typically coalesces most of them into far fewer
|
|
285
|
+
// raw events before this plugin's debounce even runs — so this
|
|
286
|
+
// mainly guards against a REGRESSION to one-reload-per-write (e.g. a
|
|
287
|
+
// watcher config change to non-polling native events), not against
|
|
288
|
+
// debouncing being removed outright.
|
|
289
|
+
expect(reloadCount).toBeLessThan(EDIT_COUNT)
|
|
290
|
+
} finally {
|
|
291
|
+
restoreWs()
|
|
292
|
+
restoreLogger()
|
|
293
|
+
await writeFile(COUNTER_PATH, originalCounterSource)
|
|
294
|
+
await waitFor(async () => {
|
|
295
|
+
const tpl = await readIfExists(join(templatesDir, 'Counter.tmpl'))
|
|
296
|
+
return tpl !== null && !tpl.includes('data-edit-n=')
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
describe('e2e: vite dev server — server-only component watcher regression', () => {
|
|
303
|
+
// Deliberately its OWN server that never fetches ANY component over HTTP
|
|
304
|
+
// (see this file's header comment for why that isolation is required for
|
|
305
|
+
// this specific regression to be meaningful).
|
|
306
|
+
let server: ViteDevServer
|
|
307
|
+
let templatesDir: string
|
|
308
|
+
let originalGreetingSource: string
|
|
309
|
+
|
|
310
|
+
beforeAll(async () => {
|
|
311
|
+
templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-vite-dev-views-serveronly-'))
|
|
312
|
+
originalGreetingSource = await readFile(GREETING_PATH, 'utf8')
|
|
313
|
+
;({ server } = await startDevServer(templatesDir))
|
|
314
|
+
|
|
315
|
+
await waitFor(async () => (await readIfExists(join(templatesDir, 'Greeting.tmpl'))) !== null)
|
|
316
|
+
}, 30_000)
|
|
317
|
+
|
|
318
|
+
afterAll(async () => {
|
|
319
|
+
await writeFile(GREETING_PATH, originalGreetingSource).catch(() => {})
|
|
320
|
+
await server?.close()
|
|
321
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('editing the server-only component (no "use client", never fetched over HTTP) still re-emits and reloads', async () => {
|
|
325
|
+
// Greeting.tsx has no 'use client' directive, so it is NEVER part of
|
|
326
|
+
// Rollup's module graph and Vite never transforms/serves it as a
|
|
327
|
+
// module — the one path (`ensureWatchedFile`) that would otherwise get
|
|
328
|
+
// it onto the watcher for free. It also lives outside the vite `root`,
|
|
329
|
+
// so the default root-only chokidar watch doesn't cover it either. If
|
|
330
|
+
// `plugin.ts` ever drops its `server.watcher.add(componentDirs)` call,
|
|
331
|
+
// this test times out waiting for the reload.
|
|
332
|
+
const { sent, restore } = captureWsSends(server)
|
|
333
|
+
try {
|
|
334
|
+
const edited = originalGreetingSource.replace('<p>', '<p data-edited="greeting-marker">')
|
|
335
|
+
await writeFile(GREETING_PATH, edited)
|
|
336
|
+
|
|
337
|
+
await waitFor(async () => {
|
|
338
|
+
const tpl = await readIfExists(join(templatesDir, 'Greeting.tmpl'))
|
|
339
|
+
return tpl !== null && tpl.includes('data-edited="greeting-marker"')
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
expect(sent.some(m => (m as { type?: string }).type === 'full-reload')).toBe(true)
|
|
343
|
+
} finally {
|
|
344
|
+
restore()
|
|
345
|
+
await writeFile(GREETING_PATH, originalGreetingSource)
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
describe('e2e: vite dev server — afterEmit', () => {
|
|
351
|
+
let server: ViteDevServer
|
|
352
|
+
let templatesDir: string
|
|
353
|
+
|
|
354
|
+
afterAll(async () => {
|
|
355
|
+
await server?.close()
|
|
356
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('fires with mode "dev" on the initial pass, and again on a tracked-file change', async () => {
|
|
360
|
+
templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-vite-dev-views-afteremit-'))
|
|
361
|
+
const calls: Array<{ mode: string; projectDir: string; templatesDir: string; outDir: string; types: Map<string, string> }> = []
|
|
362
|
+
;({ server } = await startDevServer(templatesDir, {}, ctx => { calls.push(ctx as never) }))
|
|
363
|
+
|
|
364
|
+
await waitFor(() => calls.length >= 1)
|
|
365
|
+
expect(calls[0]?.mode).toBe('dev')
|
|
366
|
+
expect(calls[0]?.templatesDir).toBe(templatesDir)
|
|
367
|
+
expect(calls[0]?.projectDir).toBe(APP_ROOT)
|
|
368
|
+
// No component in this fixture produces a `types` output (testAdapter-
|
|
369
|
+
// shaped Go adapter with no Props needing a struct isn't guaranteed
|
|
370
|
+
// either way) — the meaningful assertion is the narrow shape itself,
|
|
371
|
+
// not that it's non-empty. Never carries client JS: the type alone
|
|
372
|
+
// makes that impossible, this just pins the field set at runtime too.
|
|
373
|
+
expect(Object.keys(calls[0] ?? {}).sort()).toEqual(['mode', 'outDir', 'projectDir', 'templatesDir', 'types'])
|
|
374
|
+
|
|
375
|
+
const originalSource = await readFile(COUNTER_PATH, 'utf8')
|
|
376
|
+
const callsBeforeEdit = calls.length
|
|
377
|
+
const edited = originalSource.replace(
|
|
378
|
+
'<button onClick=',
|
|
379
|
+
'<button data-aftertemit-marker="1" onClick=',
|
|
380
|
+
)
|
|
381
|
+
await writeFile(COUNTER_PATH, edited)
|
|
382
|
+
try {
|
|
383
|
+
await waitFor(() => calls.length > callsBeforeEdit)
|
|
384
|
+
expect(calls[calls.length - 1]?.mode).toBe('dev')
|
|
385
|
+
} finally {
|
|
386
|
+
await writeFile(COUNTER_PATH, originalSource)
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
describe('e2e: vite dev server — user-supplied server.cors is not overwritten', () => {
|
|
392
|
+
let server: ViteDevServer
|
|
393
|
+
let templatesDir: string
|
|
394
|
+
|
|
395
|
+
afterAll(async () => {
|
|
396
|
+
await server?.close()
|
|
397
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
test('a user-configured server.cors survives untouched', async () => {
|
|
401
|
+
templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-vite-dev-views-cors-'))
|
|
402
|
+
;({ server } = await startDevServer(templatesDir, {
|
|
403
|
+
cors: { origin: 'https://my-own-cors-policy.example.com' },
|
|
404
|
+
}))
|
|
405
|
+
|
|
406
|
+
expect(server.config.server.cors).toEqual({ origin: 'https://my-own-cors-policy.example.com' })
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
describe('e2e: vite dev server — user-supplied server.cors: false is not overwritten', () => {
|
|
411
|
+
let server: ViteDevServer
|
|
412
|
+
let templatesDir: string
|
|
413
|
+
|
|
414
|
+
afterAll(async () => {
|
|
415
|
+
await server?.close()
|
|
416
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
test('server.cors: false (explicitly disabled) survives untouched, unlike an unset value', async () => {
|
|
420
|
+
templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-vite-dev-views-cors-false-'))
|
|
421
|
+
;({ server } = await startDevServer(templatesDir, { cors: false }))
|
|
422
|
+
|
|
423
|
+
// `!false` is `true` — a naive "fill in if falsy" check would replace
|
|
424
|
+
// this with the localhost default. Only "fill in if unset" is correct.
|
|
425
|
+
expect(server.config.server.cors).toBe(false)
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
describe('e2e: vite dev server — creating and deleting a component file', () => {
|
|
430
|
+
// Its own server + templates dir, and its own component file (never
|
|
431
|
+
// touched by any other describe block), so these tests can freely
|
|
432
|
+
// create/delete `.tsx` files without disturbing shared fixture state.
|
|
433
|
+
let server: ViteDevServer
|
|
434
|
+
let templatesDir: string
|
|
435
|
+
const WIDGET_PATH = join(COMPONENTS_DIR, 'Widget.tsx')
|
|
436
|
+
|
|
437
|
+
beforeAll(async () => {
|
|
438
|
+
templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-vite-dev-views-addunlink-'))
|
|
439
|
+
;({ server } = await startDevServer(templatesDir))
|
|
440
|
+
await waitFor(async () => (await readIfExists(join(templatesDir, 'Counter.tmpl'))) !== null)
|
|
441
|
+
}, 30_000)
|
|
442
|
+
|
|
443
|
+
afterAll(async () => {
|
|
444
|
+
await rm(WIDGET_PATH, { force: true })
|
|
445
|
+
await server?.close()
|
|
446
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
test('creating a new component file mid-session emits a template for it and reloads', async () => {
|
|
450
|
+
const { sent, restore } = captureWsSends(server)
|
|
451
|
+
try {
|
|
452
|
+
await writeFile(WIDGET_PATH, 'export function Widget() { return <p>brand new</p> }\n')
|
|
453
|
+
|
|
454
|
+
await waitFor(async () => (await readIfExists(join(templatesDir, 'Widget.tmpl'))) !== null)
|
|
455
|
+
const template = await readFile(join(templatesDir, 'Widget.tmpl'), 'utf8')
|
|
456
|
+
expect(template).toContain('brand new')
|
|
457
|
+
expect(countFullReloads(sent)).toBeGreaterThanOrEqual(1)
|
|
458
|
+
} finally {
|
|
459
|
+
restore()
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
test('deleting a component file removes its emitted template and reloads', async () => {
|
|
464
|
+
// Widget.tsx and Widget.tmpl both exist already, from the previous
|
|
465
|
+
// test (bun test runs a describe's tests in declaration order).
|
|
466
|
+
expect(await readIfExists(join(templatesDir, 'Widget.tmpl'))).not.toBeNull()
|
|
467
|
+
|
|
468
|
+
const { sent, restore } = captureWsSends(server)
|
|
469
|
+
try {
|
|
470
|
+
await rm(WIDGET_PATH)
|
|
471
|
+
|
|
472
|
+
await waitFor(async () => (await readIfExists(join(templatesDir, 'Widget.tmpl'))) === null)
|
|
473
|
+
expect(countFullReloads(sent)).toBeGreaterThanOrEqual(1)
|
|
474
|
+
} finally {
|
|
475
|
+
restore()
|
|
476
|
+
}
|
|
477
|
+
})
|
|
478
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, test, expect, afterEach } from 'bun:test'
|
|
2
|
+
import { mkdtemp, rm, readFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import type { CompileResult, TemplateAdapter } from '@barefootjs/jsx'
|
|
6
|
+
import { planEmits, writeEmits } from '../emit.ts'
|
|
7
|
+
|
|
8
|
+
const fakeAdapter = { extension: '.tmpl', templatesPerComponent: false } as TemplateAdapter
|
|
9
|
+
const perComponentAdapter = { extension: '.tmpl', templatesPerComponent: true } as TemplateAdapter
|
|
10
|
+
|
|
11
|
+
describe('planEmits', () => {
|
|
12
|
+
test('plans a markedTemplate + ssrDefaults + types output, mirroring source position', () => {
|
|
13
|
+
const result: CompileResult = {
|
|
14
|
+
files: [
|
|
15
|
+
{ path: '/src/components/ui/button/index.html', content: '<button/>', type: 'markedTemplate' },
|
|
16
|
+
{ path: '/src/components/ui/button/index.ssr-defaults.json', content: '{}', type: 'ssrDefaults' },
|
|
17
|
+
{ path: '/src/components/ui/button/index.types', content: 'type ButtonProps struct{}', type: 'types' },
|
|
18
|
+
],
|
|
19
|
+
errors: [],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const targets = planEmits(result, '/src/components/ui/button/index.tsx', ['/src/components'], fakeAdapter)
|
|
23
|
+
const byPath = Object.fromEntries(targets.map(t => [t.relPath, t.content]))
|
|
24
|
+
|
|
25
|
+
expect(byPath['ui/button/index.tmpl']).toBe('<button/>')
|
|
26
|
+
expect(byPath['ui/button/index.ssr-defaults.json']).toBe('{}')
|
|
27
|
+
expect(byPath['ui/button/index.types']).toBe('type ButtonProps struct{}')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('names per-component templates after the component, for templatesPerComponent adapters', () => {
|
|
31
|
+
const result: CompileResult = {
|
|
32
|
+
files: [
|
|
33
|
+
{ path: '/x', content: 'toast body', type: 'markedTemplate', componentName: 'Toast' },
|
|
34
|
+
{ path: '/x', content: 'toaster body', type: 'markedTemplate', componentName: 'Toaster' },
|
|
35
|
+
],
|
|
36
|
+
errors: [],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const targets = planEmits(result, '/src/components/ui/toast/index.tsx', ['/src/components'], perComponentAdapter)
|
|
40
|
+
const byPath = Object.fromEntries(targets.map(t => [t.relPath, t.content]))
|
|
41
|
+
|
|
42
|
+
expect(byPath['ui/toast/Toast.tmpl']).toBe('toast body')
|
|
43
|
+
expect(byPath['ui/toast/Toaster.tmpl']).toBe('toaster body')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('plans nothing for a state-only compile with no markedTemplate output', () => {
|
|
47
|
+
const result: CompileResult = {
|
|
48
|
+
files: [{ path: '/x', content: 'export const x = 1', type: 'clientJs' }],
|
|
49
|
+
errors: [],
|
|
50
|
+
}
|
|
51
|
+
const targets = planEmits(result, '/src/components/state.tsx', ['/src/components'], fakeAdapter)
|
|
52
|
+
expect(targets).toEqual([])
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('writeEmits', () => {
|
|
57
|
+
let dir: string
|
|
58
|
+
|
|
59
|
+
afterEach(async () => {
|
|
60
|
+
if (dir) await rm(dir, { recursive: true, force: true })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('creates nested directories and writes every target', async () => {
|
|
64
|
+
dir = await mkdtemp(join(tmpdir(), 'barefoot-emit-'))
|
|
65
|
+
await writeEmits(dir, [
|
|
66
|
+
{ relPath: 'ui/button/index.tmpl', content: '<button/>' },
|
|
67
|
+
{ relPath: 'top.tmpl', content: 'top' },
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
expect(await readFile(join(dir, 'ui/button/index.tmpl'), 'utf8')).toBe('<button/>')
|
|
71
|
+
expect(await readFile(join(dir, 'top.tmpl'), 'utf8')).toBe('top')
|
|
72
|
+
})
|
|
73
|
+
})
|