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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lopata",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
- const activeModule = await ensureWorkerModule()
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
- console.error('[lopata:vite] Request error:', err)
417
- if (!res.headersSent) {
418
- res.writeHead(500, { 'content-type': 'text/plain' })
419
- res.end(err instanceof Error ? err.stack ?? err.message : String(err))
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