lopata 0.8.3 → 0.8.4
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/package.json
CHANGED
|
@@ -49,7 +49,7 @@ export function configPlugin(envName: string): Plugin {
|
|
|
49
49
|
watch: {
|
|
50
50
|
usePolling: true,
|
|
51
51
|
interval: 500,
|
|
52
|
-
ignored: ['**/.lopata/**', '**/.wrangler/**', '**/.react-router/**'],
|
|
52
|
+
ignored: ['**/.lopata/**', '**/.wrangler/**', '**/.react-router/**', '**/*.tmp.*'],
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
55
|
environments: {
|
|
@@ -130,6 +130,75 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
130
130
|
return currentModule ?? workerModule
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Dispatch a request through the worker's fetch() handler with tracing
|
|
135
|
+
* and generation tracking. Throws on HMR race conditions so the caller
|
|
136
|
+
* can retry.
|
|
137
|
+
*/
|
|
138
|
+
async function handleWorkerFetch(req: IncomingMessage, res: ServerResponse, next: Function): Promise<void> {
|
|
139
|
+
const activeModule = await ensureWorkerModule()
|
|
140
|
+
const genId = currentGenerationId
|
|
141
|
+
genActiveRequests.set(genId, (genActiveRequests.get(genId) ?? 0) + 1)
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const request = nodeReqToRequest(req)
|
|
145
|
+
const parsedUrl = new URL(request.url)
|
|
146
|
+
|
|
147
|
+
const handler = activeModule.default as Record<string, unknown>
|
|
148
|
+
if (!handler || typeof handler.fetch !== 'function') {
|
|
149
|
+
console.error('[lopata:vite] Worker module default export has no fetch() method')
|
|
150
|
+
next()
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Capture caller stack before entering the worker (for async stack stitching)
|
|
155
|
+
const callerStack = new Error()
|
|
156
|
+
|
|
157
|
+
const ctx = new ExecutionContext()
|
|
158
|
+
const response = await (startSpan as Function)({
|
|
159
|
+
name: `${request.method} ${parsedUrl.pathname}`,
|
|
160
|
+
kind: 'server',
|
|
161
|
+
attributes: { 'http.method': request.method, 'http.url': request.url, 'lopata.generation_id': genId },
|
|
162
|
+
}, () =>
|
|
163
|
+
runWithExecutionContext(ctx, async () => {
|
|
164
|
+
try {
|
|
165
|
+
const resp = await (handler.fetch as Function).call(handler, request, env, ctx) as Response
|
|
166
|
+
;(setSpanAttribute as Function)('http.status_code', resp.status)
|
|
167
|
+
|
|
168
|
+
// Intercept React Router error boundary responses with lopata error page
|
|
169
|
+
const routeError = (globalThis as any).__lopata_routeError
|
|
170
|
+
delete (globalThis as any).__lopata_routeError
|
|
171
|
+
if (routeError) {
|
|
172
|
+
if (routeError instanceof Error) {
|
|
173
|
+
stitchAsyncStack(routeError, callerStack)
|
|
174
|
+
}
|
|
175
|
+
console.error('[lopata:vite] Route error:\n' + (routeError instanceof Error ? routeError.stack : String(routeError)))
|
|
176
|
+
return (renderErrorPage as Function)(routeError, request, env, config)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
ctx._awaitAll().catch(() => {})
|
|
180
|
+
return resp
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (isHmrRaceError(err)) {
|
|
183
|
+
currentModule = null
|
|
184
|
+
throw err
|
|
185
|
+
}
|
|
186
|
+
if (err instanceof Error) {
|
|
187
|
+
stitchAsyncStack(err, callerStack)
|
|
188
|
+
}
|
|
189
|
+
console.error('[lopata:vite] Request error:\n' + (err instanceof Error ? err.stack : String(err)))
|
|
190
|
+
return (renderErrorPage as Function)(err, request, env, config)
|
|
191
|
+
}
|
|
192
|
+
})) as Response
|
|
193
|
+
|
|
194
|
+
writeResponse(response, res)
|
|
195
|
+
} finally {
|
|
196
|
+
const count = genActiveRequests.get(genId) ?? 1
|
|
197
|
+
if (count <= 1) genActiveRequests.delete(genId)
|
|
198
|
+
else genActiveRequests.set(genId, count - 1)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
133
202
|
return {
|
|
134
203
|
name: 'lopata:dev-server',
|
|
135
204
|
|
|
@@ -356,67 +425,18 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
356
425
|
}
|
|
357
426
|
|
|
358
427
|
try {
|
|
359
|
-
|
|
360
|
-
const genId = currentGenerationId
|
|
361
|
-
genActiveRequests.set(genId, (genActiveRequests.get(genId) ?? 0) + 1)
|
|
362
|
-
|
|
363
|
-
try {
|
|
364
|
-
const request = nodeReqToRequest(req)
|
|
365
|
-
const parsedUrl = new URL(request.url)
|
|
366
|
-
|
|
367
|
-
const handler = activeModule.default as Record<string, unknown>
|
|
368
|
-
if (!handler || typeof handler.fetch !== 'function') {
|
|
369
|
-
console.error('[lopata:vite] Worker module default export has no fetch() method')
|
|
370
|
-
return next()
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Capture caller stack before entering the worker (for async stack stitching)
|
|
374
|
-
const callerStack = new Error()
|
|
375
|
-
|
|
376
|
-
const ctx = new ExecutionContext()
|
|
377
|
-
const response = await (startSpan as Function)({
|
|
378
|
-
name: `${request.method} ${parsedUrl.pathname}`,
|
|
379
|
-
kind: 'server',
|
|
380
|
-
attributes: { 'http.method': request.method, 'http.url': request.url, 'lopata.generation_id': genId },
|
|
381
|
-
}, () =>
|
|
382
|
-
runWithExecutionContext(ctx, async () => {
|
|
383
|
-
try {
|
|
384
|
-
const resp = await (handler.fetch as Function).call(handler, request, env, ctx) as Response
|
|
385
|
-
;(setSpanAttribute as Function)('http.status_code', resp.status)
|
|
386
|
-
|
|
387
|
-
// Intercept React Router error boundary responses with lopata error page
|
|
388
|
-
const routeError = (globalThis as any).__lopata_routeError
|
|
389
|
-
delete (globalThis as any).__lopata_routeError
|
|
390
|
-
if (routeError) {
|
|
391
|
-
if (routeError instanceof Error) {
|
|
392
|
-
stitchAsyncStack(routeError, callerStack)
|
|
393
|
-
}
|
|
394
|
-
console.error('[lopata:vite] Route error:\n' + (routeError instanceof Error ? routeError.stack : String(routeError)))
|
|
395
|
-
return (renderErrorPage as Function)(routeError, request, env, config)
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
ctx._awaitAll().catch(() => {})
|
|
399
|
-
return resp
|
|
400
|
-
} catch (err) {
|
|
401
|
-
if (err instanceof Error) {
|
|
402
|
-
stitchAsyncStack(err, callerStack)
|
|
403
|
-
}
|
|
404
|
-
console.error('[lopata:vite] Request error:\n' + (err instanceof Error ? err.stack : String(err)))
|
|
405
|
-
return (renderErrorPage as Function)(err, request, env, config)
|
|
406
|
-
}
|
|
407
|
-
})) as Response
|
|
408
|
-
|
|
409
|
-
writeResponse(response, res)
|
|
410
|
-
} finally {
|
|
411
|
-
const count = genActiveRequests.get(genId) ?? 1
|
|
412
|
-
if (count <= 1) genActiveRequests.delete(genId)
|
|
413
|
-
else genActiveRequests.set(genId, count - 1)
|
|
414
|
-
}
|
|
428
|
+
await handleWorkerFetch(req, res, next)
|
|
415
429
|
} catch (err) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
430
|
+
if (!isHmrRaceError(err)) {
|
|
431
|
+
writeRequestError(res, err)
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
// Retry once after a short delay — module graph may be mid-evaluation during HMR
|
|
435
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
436
|
+
try {
|
|
437
|
+
await handleWorkerFetch(req, res, next)
|
|
438
|
+
} catch (retryErr) {
|
|
439
|
+
writeRequestError(res, retryErr)
|
|
420
440
|
}
|
|
421
441
|
}
|
|
422
442
|
})
|
|
@@ -622,6 +642,19 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
622
642
|
}
|
|
623
643
|
}
|
|
624
644
|
|
|
645
|
+
/** Detect transient TypeError from Vite module graph being mid-evaluation during HMR */
|
|
646
|
+
function isHmrRaceError(err: unknown): boolean {
|
|
647
|
+
return err instanceof TypeError && err.message.includes('not be null or undefined')
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function writeRequestError(res: ServerResponse, err: unknown): void {
|
|
651
|
+
console.error('[lopata:vite] Request error:', err)
|
|
652
|
+
if (!res.headersSent) {
|
|
653
|
+
res.writeHead(500, { 'content-type': 'text/plain' })
|
|
654
|
+
res.end(err instanceof Error ? err.stack ?? err.message : String(err))
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
625
658
|
function stitchAsyncStack(err: Error, callerError: Error | null): void {
|
|
626
659
|
if (!callerError) return
|
|
627
660
|
if (!err.stack || !callerError.stack) return
|