lopata 0.5.2 → 0.7.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/src/plugin.ts CHANGED
@@ -1,116 +1,18 @@
1
1
  import { plugin } from 'bun'
2
2
  import type { BrowserBinding } from './bindings/browser'
3
- import { SqliteCacheStorage } from './bindings/cache'
4
- import { FixedLengthStream, IdentityTransformStream } from './bindings/cf-streams'
5
3
  import { ContainerBase, getContainer, getRandom } from './bindings/container'
6
- import { patchGlobalCrypto } from './bindings/crypto-extras'
7
4
  import { DurableObjectBase, WebSocketRequestResponsePair } from './bindings/durable-object'
8
5
  import { EmailMessage } from './bindings/email'
9
- import { HTMLRewriter } from './bindings/html-rewriter'
10
6
  import type { ImageTransformOptions, OutputOptions } from './bindings/images'
11
7
  import { WebSocketPair } from './bindings/websocket-pair'
12
8
  import { NonRetryableError, WorkflowEntrypointBase } from './bindings/workflow'
13
- import { getDatabase } from './db'
14
9
  import { globalEnv } from './env'
15
10
  import { getActiveExecutionContext } from './execution-context'
11
+ import { setupCloudflareGlobals } from './setup-globals'
16
12
  import { getActiveContext } from './tracing/context'
17
- import { instrumentBinding } from './tracing/instrument'
18
- import { addSpanEvent, persistError, setSpanAttribute, startSpan } from './tracing/span' // ─── Userland tracing API ────────────────────────────────────────────
19
- // Exposes a lightweight global that user code can call to create custom
20
- // spans visible in the Lopata dashboard. In production (without Lopata)
21
- // the global is simply absent, so the user's thin wrapper becomes a no-op.
22
- ;(globalThis as any).__lopata = {
23
- trace<T>(name: string, attrsOrFn: Record<string, unknown> | (() => T | Promise<T>), maybeFn?: () => T | Promise<T>): Promise<T> {
24
- const fn = typeof attrsOrFn === 'function' ? attrsOrFn : maybeFn!
25
- const attributes = typeof attrsOrFn === 'function' ? undefined : attrsOrFn
26
- return startSpan({ name, attributes }, fn)
27
- },
28
- setAttribute: setSpanAttribute,
29
- addEvent(name: string, message?: string, attrs?: Record<string, unknown>): void {
30
- addSpanEvent(name, 'info', message ?? '', attrs)
31
- },
32
- }
33
-
34
- // Register global `caches` object (CacheStorage) with tracing
35
- const rawCacheStorage = new SqliteCacheStorage(getDatabase())
36
- const cacheMethods = ['match', 'put', 'delete']
37
-
38
- // Instrument the default cache
39
- rawCacheStorage.default = instrumentBinding(rawCacheStorage.default, {
40
- type: 'cache',
41
- name: 'default',
42
- methods: cacheMethods,
43
- }) as typeof rawCacheStorage.default
44
-
45
- // Wrap open() to return instrumented caches
46
- const originalOpen = rawCacheStorage.open.bind(rawCacheStorage)
47
- rawCacheStorage.open = async (cacheName: string) => {
48
- const cache = await originalOpen(cacheName)
49
- return instrumentBinding(cache, {
50
- type: 'cache',
51
- name: cacheName,
52
- methods: cacheMethods,
53
- })
54
- }
55
-
56
- Object.defineProperty(globalThis, 'caches', {
57
- value: rawCacheStorage,
58
- writable: false,
59
- configurable: true,
60
- })
61
-
62
- // Register global `HTMLRewriter` class
63
- Object.defineProperty(globalThis, 'HTMLRewriter', {
64
- value: HTMLRewriter,
65
- writable: false,
66
- configurable: true,
67
- })
13
+ import { addSpanEvent, persistError, setSpanAttribute, startSpan } from './tracing/span'
68
14
 
69
- // Register global `WebSocketPair` class
70
- Object.defineProperty(globalThis, 'WebSocketPair', {
71
- value: WebSocketPair,
72
- writable: false,
73
- configurable: true,
74
- })
75
-
76
- // Register global CF stream classes
77
- Object.defineProperty(globalThis, 'IdentityTransformStream', {
78
- value: IdentityTransformStream,
79
- writable: false,
80
- configurable: true,
81
- })
82
-
83
- Object.defineProperty(globalThis, 'FixedLengthStream', {
84
- value: FixedLengthStream,
85
- writable: false,
86
- configurable: true,
87
- })
88
-
89
- // Patch crypto with CF-specific extensions (timingSafeEqual, DigestStream)
90
- patchGlobalCrypto()
91
-
92
- // Set navigator.userAgent to match Cloudflare Workers
93
- Object.defineProperty(globalThis.navigator, 'userAgent', {
94
- value: 'Cloudflare-Workers',
95
- writable: false,
96
- configurable: true,
97
- })
98
-
99
- // Set navigator.language (behind enable_navigator_language compat flag in CF)
100
- if (!globalThis.navigator.language) {
101
- Object.defineProperty(globalThis.navigator, 'language', {
102
- value: 'en',
103
- writable: false,
104
- configurable: true,
105
- })
106
- }
107
-
108
- // Set performance.timeOrigin to 0 (CF semantics)
109
- Object.defineProperty(globalThis.performance, 'timeOrigin', {
110
- value: 0,
111
- writable: false,
112
- configurable: true,
113
- })
15
+ setupCloudflareGlobals()
114
16
 
115
17
  // Register addEventListener shim for legacy service worker syntax
116
18
  // Workers that use addEventListener("fetch", handler) instead of export default { fetch }
@@ -127,17 +29,6 @@ Object.defineProperty(globalThis, 'addEventListener', {
127
29
  }) /** @internal Get the registered service worker fetch handler */
128
30
  ;(globalThis as any).__lopata_sw_handlers = _serviceWorkerHandlers
129
31
 
130
- // Register scheduler.wait(ms) — await-able setTimeout alternative
131
- Object.defineProperty(globalThis, 'scheduler', {
132
- value: {
133
- wait(ms: number): Promise<void> {
134
- return new Promise((resolve) => setTimeout(resolve, ms))
135
- },
136
- },
137
- writable: false,
138
- configurable: true,
139
- })
140
-
141
32
  // ─── Console instrumentation ─────────────────────────────────────────
142
33
  // Captures console.log/info/warn/error/debug as span events when inside a trace context.
143
34
 
@@ -0,0 +1,150 @@
1
+ import { SqliteCacheStorage } from './bindings/cache'
2
+ import { FixedLengthStream, IdentityTransformStream } from './bindings/cf-streams'
3
+ import { patchGlobalCrypto } from './bindings/crypto-extras'
4
+ import { WebSocketRequestResponsePair } from './bindings/durable-object'
5
+ import { HTMLRewriter } from './bindings/html-rewriter'
6
+ import { WebSocketPair } from './bindings/websocket-pair'
7
+ import { getDatabase } from './db'
8
+ import { instrumentBinding } from './tracing/instrument'
9
+ import { addSpanEvent, setSpanAttribute, startSpan } from './tracing/span'
10
+
11
+ let initialized = false
12
+
13
+ /**
14
+ * Sets up global Cloudflare-compatible APIs:
15
+ * caches, HTMLRewriter, WebSocketPair, IdentityTransformStream, FixedLengthStream,
16
+ * navigator.userAgent, navigator.language, performance.timeOrigin, scheduler.wait(),
17
+ * crypto extensions, and __lopata userland tracing API.
18
+ *
19
+ * Idempotent — safe to call multiple times.
20
+ */
21
+ export function setupCloudflareGlobals() {
22
+ if (initialized) return
23
+ initialized = true // ─── Userland tracing API ────────────────────────────────────────────
24
+ // Exposes a lightweight global that user code can call to create custom
25
+ // spans visible in the Lopata dashboard. In production (without Lopata)
26
+ // the global is simply absent, so the user's thin wrapper becomes a no-op.
27
+ ;(globalThis as any).__lopata = {
28
+ trace<T>(name: string, attrsOrFn: Record<string, unknown> | (() => T | Promise<T>), maybeFn?: () => T | Promise<T>): Promise<T> {
29
+ const fn = typeof attrsOrFn === 'function' ? attrsOrFn : maybeFn!
30
+ const attributes = typeof attrsOrFn === 'function' ? undefined : attrsOrFn
31
+ return startSpan({ name, attributes }, fn)
32
+ },
33
+ setAttribute: setSpanAttribute,
34
+ addEvent(name: string, message?: string, attrs?: Record<string, unknown>): void {
35
+ addSpanEvent(name, 'info', message ?? '', attrs)
36
+ },
37
+ }
38
+
39
+ // Register global `caches` object (CacheStorage) with tracing
40
+ const rawCacheStorage = new SqliteCacheStorage(getDatabase())
41
+ const cacheMethods = ['match', 'put', 'delete']
42
+
43
+ // Instrument the default cache
44
+ rawCacheStorage.default = instrumentBinding(rawCacheStorage.default, {
45
+ type: 'cache',
46
+ name: 'default',
47
+ methods: cacheMethods,
48
+ }) as typeof rawCacheStorage.default
49
+
50
+ // Wrap open() to return instrumented caches
51
+ const originalOpen = rawCacheStorage.open.bind(rawCacheStorage)
52
+ rawCacheStorage.open = async (cacheName: string) => {
53
+ const cache = await originalOpen(cacheName)
54
+ return instrumentBinding(cache, {
55
+ type: 'cache',
56
+ name: cacheName,
57
+ methods: cacheMethods,
58
+ })
59
+ }
60
+
61
+ Object.defineProperty(globalThis, 'caches', {
62
+ value: rawCacheStorage,
63
+ writable: false,
64
+ configurable: true,
65
+ })
66
+
67
+ // Register global `HTMLRewriter` class
68
+ Object.defineProperty(globalThis, 'HTMLRewriter', {
69
+ value: HTMLRewriter,
70
+ writable: false,
71
+ configurable: true,
72
+ })
73
+
74
+ // Register global `WebSocketPair` class
75
+ Object.defineProperty(globalThis, 'WebSocketPair', {
76
+ value: WebSocketPair,
77
+ writable: false,
78
+ configurable: true,
79
+ })
80
+
81
+ // Register global `WebSocketRequestResponsePair` class (used by DO hibernation API)
82
+ Object.defineProperty(globalThis, 'WebSocketRequestResponsePair', {
83
+ value: WebSocketRequestResponsePair,
84
+ writable: false,
85
+ configurable: true,
86
+ })
87
+
88
+ // Patch Response to preserve CF-specific `webSocket` property from init
89
+ const OriginalResponse = globalThis.Response
90
+ globalThis.Response = class extends OriginalResponse {
91
+ webSocket?: InstanceType<typeof WebSocketPair>[0]
92
+
93
+ constructor(body?: any, init?: ResponseInit & { webSocket?: InstanceType<typeof WebSocketPair>[0] }) {
94
+ super(body, init)
95
+ if (init && 'webSocket' in init) {
96
+ this.webSocket = init.webSocket
97
+ }
98
+ }
99
+ }
100
+
101
+ // Register global CF stream classes
102
+ Object.defineProperty(globalThis, 'IdentityTransformStream', {
103
+ value: IdentityTransformStream,
104
+ writable: false,
105
+ configurable: true,
106
+ })
107
+
108
+ Object.defineProperty(globalThis, 'FixedLengthStream', {
109
+ value: FixedLengthStream,
110
+ writable: false,
111
+ configurable: true,
112
+ })
113
+
114
+ // Patch crypto with CF-specific extensions (timingSafeEqual, DigestStream)
115
+ patchGlobalCrypto()
116
+
117
+ // Set navigator.userAgent to match Cloudflare Workers
118
+ Object.defineProperty(globalThis.navigator, 'userAgent', {
119
+ value: 'Cloudflare-Workers',
120
+ writable: false,
121
+ configurable: true,
122
+ })
123
+
124
+ // Set navigator.language (behind enable_navigator_language compat flag in CF)
125
+ if (!globalThis.navigator.language) {
126
+ Object.defineProperty(globalThis.navigator, 'language', {
127
+ value: 'en',
128
+ writable: false,
129
+ configurable: true,
130
+ })
131
+ }
132
+
133
+ // Set performance.timeOrigin to 0 (CF semantics)
134
+ Object.defineProperty(globalThis.performance, 'timeOrigin', {
135
+ value: 0,
136
+ writable: false,
137
+ configurable: true,
138
+ })
139
+
140
+ // Register scheduler.wait(ms) — await-able setTimeout alternative
141
+ Object.defineProperty(globalThis, 'scheduler', {
142
+ value: {
143
+ wait(ms: number): Promise<void> {
144
+ return new Promise((resolve) => setTimeout(resolve, ms))
145
+ },
146
+ },
147
+ writable: false,
148
+ configurable: true,
149
+ })
150
+ }
@@ -19,7 +19,7 @@ class LopataDevEnvironment extends DevEnvironment {
19
19
 
20
20
  get runner(): ModuleRunner {
21
21
  if (!this._runner) {
22
- this._runner = createServerModuleRunner(this, { hmr: false })
22
+ this._runner = createServerModuleRunner(this, { hmr: {} })
23
23
  }
24
24
  return this._runner
25
25
  }
@@ -42,15 +42,19 @@ export function configPlugin(envName: string): Plugin {
42
42
  name: 'lopata:config',
43
43
  config() {
44
44
  return {
45
+ // Disable Vite's SPA fallback (history API fallback) — Lopata handles
46
+ // all requests via worker.fetch(), so no index.html rewriting is needed.
47
+ appType: 'custom',
45
48
  server: {
46
49
  watch: {
47
- ignored: ['**/.lopata/**'],
50
+ ignored: ['**/.lopata/**', '**/.wrangler/**', '**/.react-router/**'],
48
51
  },
49
52
  },
50
53
  environments: {
51
54
  [envName]: {
52
55
  resolve: {
53
56
  externalConditions: ['workerd', 'worker'],
57
+ dedupe: ['react', 'react-dom', 'react-router', 'react-router-dom'],
54
58
  },
55
59
  dev: {
56
60
  createEnvironment(name, config) {