@tanstack/cta-ui-base 0.29.0 → 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/components/file-navigator.js +18 -1
- package/dist/components/file-viewer.js +10 -3
- package/dist/components/web-container-provider.d.ts +31 -0
- package/dist/components/web-container-provider.js +15 -0
- package/dist/components/webcontainer-preview.d.ts +1 -0
- package/dist/components/webcontainer-preview.js +63 -0
- package/dist/hooks/use-web-container.d.ts +24 -0
- package/dist/hooks/use-web-container.js +6 -0
- package/dist/hooks/use-webcontainer-store.d.ts +29 -0
- package/dist/hooks/use-webcontainer-store.js +334 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/lib/als-shim.d.ts +1 -0
- package/dist/lib/als-shim.js +119 -0
- package/package.json +3 -2
- package/src/components/file-navigator.tsx +61 -16
- package/src/components/file-viewer.tsx +18 -4
- package/src/components/web-container-provider.tsx +42 -0
- package/src/components/webcontainer-preview.tsx +241 -0
- package/src/hooks/use-web-container.ts +7 -0
- package/src/hooks/use-webcontainer-store.ts +436 -0
- package/src/index.ts +7 -1
- package/src/lib/als-shim.ts +125 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { WebContainer } from '@webcontainer/api'
|
|
2
|
+
import { createStore } from 'zustand'
|
|
3
|
+
|
|
4
|
+
import shimALS from '../lib/als-shim'
|
|
5
|
+
|
|
6
|
+
import type { FileSystemTree, WebContainerProcess } from '@webcontainer/api'
|
|
7
|
+
|
|
8
|
+
export type SetupStep =
|
|
9
|
+
| 'mounting'
|
|
10
|
+
| 'installing'
|
|
11
|
+
| 'starting'
|
|
12
|
+
| 'ready'
|
|
13
|
+
| 'error'
|
|
14
|
+
|
|
15
|
+
console.log('>>> startup')
|
|
16
|
+
|
|
17
|
+
type WebContainerStore = {
|
|
18
|
+
webContainer: Promise<WebContainer> | null
|
|
19
|
+
ready: boolean
|
|
20
|
+
setupStep: SetupStep
|
|
21
|
+
statusMessage: string
|
|
22
|
+
terminalOutput: Array<string>
|
|
23
|
+
previewUrl: string | null
|
|
24
|
+
error: string | null
|
|
25
|
+
devProcess: WebContainerProcess | null
|
|
26
|
+
projectFiles: Array<{ path: string; content: string }>
|
|
27
|
+
isInstalling: boolean
|
|
28
|
+
|
|
29
|
+
teardown: () => void
|
|
30
|
+
updateProjectFiles: (
|
|
31
|
+
projectFiles: Array<{ path: string; content: string }>,
|
|
32
|
+
) => Promise<void>
|
|
33
|
+
|
|
34
|
+
startDevServer: () => Promise<void>
|
|
35
|
+
addTerminalOutput: (output: string) => void
|
|
36
|
+
installDependencies: () => Promise<void>
|
|
37
|
+
setTerminalOutput: (output: Array<string>) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const processTerminalLine = (data: string): string => {
|
|
41
|
+
// Clean up terminal output - remove ANSI codes and control characters
|
|
42
|
+
let cleaned = data
|
|
43
|
+
|
|
44
|
+
// Remove all ANSI escape sequences (comprehensive)
|
|
45
|
+
cleaned = cleaned.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '') // Standard ANSI sequences
|
|
46
|
+
cleaned = cleaned.replace(/\u001b\][0-9;]*;[^\u0007]*\u0007/g, '') // OSC sequences
|
|
47
|
+
cleaned = cleaned.replace(/\u001b[=>]/g, '') // Other escape codes
|
|
48
|
+
|
|
49
|
+
// Remove carriage returns and other control characters
|
|
50
|
+
cleaned = cleaned.replace(/\r/g, '')
|
|
51
|
+
cleaned = cleaned.replace(
|
|
52
|
+
/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g,
|
|
53
|
+
'',
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// Remove spinner characters and progress bar artifacts
|
|
57
|
+
cleaned = cleaned.replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/g, '')
|
|
58
|
+
cleaned = cleaned.replace(/[▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏]/g, '')
|
|
59
|
+
cleaned = cleaned.replace(/[░▒▓]/g, '')
|
|
60
|
+
|
|
61
|
+
// Trim excessive whitespace
|
|
62
|
+
cleaned = cleaned.trim()
|
|
63
|
+
|
|
64
|
+
// Only return non-empty lines
|
|
65
|
+
return cleaned.length > 0 ? cleaned : ''
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let webContainer: Promise<WebContainer> | null = null
|
|
69
|
+
|
|
70
|
+
export default function createWebContainerStore(shouldShimALS: boolean) {
|
|
71
|
+
if (!webContainer) {
|
|
72
|
+
webContainer = WebContainer.boot()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const store = createStore<WebContainerStore>((set, get) => ({
|
|
76
|
+
webContainer,
|
|
77
|
+
ready: false,
|
|
78
|
+
setupStep: 'mounting',
|
|
79
|
+
statusMessage: '',
|
|
80
|
+
terminalOutput: [],
|
|
81
|
+
previewUrl: null,
|
|
82
|
+
error: null,
|
|
83
|
+
devProcess: null,
|
|
84
|
+
projectFiles: [],
|
|
85
|
+
isInstalling: false,
|
|
86
|
+
|
|
87
|
+
teardown: () => {
|
|
88
|
+
set({ webContainer: null, ready: false })
|
|
89
|
+
},
|
|
90
|
+
addTerminalOutput: (output: string) => {
|
|
91
|
+
set(({ terminalOutput }) => ({
|
|
92
|
+
terminalOutput: [...terminalOutput, output],
|
|
93
|
+
}))
|
|
94
|
+
},
|
|
95
|
+
setTerminalOutput: (output: string[]) => {
|
|
96
|
+
set({ terminalOutput: output })
|
|
97
|
+
},
|
|
98
|
+
startDevServer: async () => {
|
|
99
|
+
const { devProcess, webContainer, addTerminalOutput } = get()
|
|
100
|
+
if (!webContainer) {
|
|
101
|
+
throw new Error('WebContainer not found')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const container = await webContainer
|
|
106
|
+
if (!container) {
|
|
107
|
+
throw new Error('WebContainer not found')
|
|
108
|
+
}
|
|
109
|
+
if (devProcess) {
|
|
110
|
+
console.log('Killing existing dev process...')
|
|
111
|
+
devProcess.kill()
|
|
112
|
+
set({ devProcess: null })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
set({
|
|
116
|
+
setupStep: 'starting',
|
|
117
|
+
statusMessage: 'Starting development server...',
|
|
118
|
+
})
|
|
119
|
+
addTerminalOutput('🚀 Starting dev server...')
|
|
120
|
+
|
|
121
|
+
// Wait for server to be ready (set up listener first)
|
|
122
|
+
container.on('server-ready', (port, url) => {
|
|
123
|
+
console.log('Server ready on port', port, 'at', url)
|
|
124
|
+
const currentState = get()
|
|
125
|
+
set({
|
|
126
|
+
previewUrl: url,
|
|
127
|
+
setupStep: 'ready',
|
|
128
|
+
statusMessage: 'Development server running',
|
|
129
|
+
terminalOutput: [
|
|
130
|
+
...currentState.terminalOutput,
|
|
131
|
+
`✅ Server ready at ${url}`,
|
|
132
|
+
],
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Start the dev server
|
|
137
|
+
const newDevProcess = await container.spawn('pnpm', ['dev'])
|
|
138
|
+
set({ devProcess: newDevProcess })
|
|
139
|
+
|
|
140
|
+
newDevProcess.output.pipeTo(
|
|
141
|
+
new WritableStream({
|
|
142
|
+
write(data) {
|
|
143
|
+
const cleaned = processTerminalLine(data)
|
|
144
|
+
if (cleaned && cleaned.length > 3) {
|
|
145
|
+
console.log('[DEV]', cleaned)
|
|
146
|
+
const currentState = get()
|
|
147
|
+
set({
|
|
148
|
+
terminalOutput: [...currentState.terminalOutput, cleaned],
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
// Check exit code
|
|
156
|
+
const exitCode = await newDevProcess.exit
|
|
157
|
+
if (exitCode !== 0) {
|
|
158
|
+
addTerminalOutput(`❌ Dev server exited with code ${exitCode}`)
|
|
159
|
+
set({ error: `Dev server exited with code ${exitCode}` })
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Dev server start error:', error)
|
|
163
|
+
addTerminalOutput(`❌ Dev server error: ${(error as Error).message}`)
|
|
164
|
+
set({ error: (error as Error).message, setupStep: 'error' })
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
updateProjectFiles: async (
|
|
168
|
+
projectFiles: Array<{ path: string; content: string }>,
|
|
169
|
+
) => {
|
|
170
|
+
const {
|
|
171
|
+
projectFiles: originalProjectFiles,
|
|
172
|
+
addTerminalOutput,
|
|
173
|
+
installDependencies,
|
|
174
|
+
webContainer,
|
|
175
|
+
} = get()
|
|
176
|
+
|
|
177
|
+
if (!webContainer) {
|
|
178
|
+
console.error('WebContainer not found in updateProjectFiles')
|
|
179
|
+
throw new Error('WebContainer not found')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const container = await webContainer
|
|
184
|
+
if (!container) {
|
|
185
|
+
console.error('WebContainer resolved to null')
|
|
186
|
+
throw new Error('WebContainer not found')
|
|
187
|
+
}
|
|
188
|
+
console.log('WebContainer booted successfully!', container)
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error('WebContainer boot failed:', error)
|
|
191
|
+
set({
|
|
192
|
+
error: `WebContainer boot failed: ${(error as Error).message}`,
|
|
193
|
+
setupStep: 'error',
|
|
194
|
+
})
|
|
195
|
+
throw error
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const container = await webContainer
|
|
199
|
+
|
|
200
|
+
let packageJSONChanged = false
|
|
201
|
+
const binaryFiles: Record<string, Uint8Array> = {}
|
|
202
|
+
if (originalProjectFiles.length === 0) {
|
|
203
|
+
const fileSystemTree: FileSystemTree = {}
|
|
204
|
+
let base64FileCount = 0
|
|
205
|
+
|
|
206
|
+
for (const { path, content } of projectFiles) {
|
|
207
|
+
const cleanPath = path.replace(/^\.?\//, '')
|
|
208
|
+
const pathParts = cleanPath.split('/')
|
|
209
|
+
|
|
210
|
+
let current: any = fileSystemTree
|
|
211
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
212
|
+
const part = pathParts[i]
|
|
213
|
+
if (!current[part]) {
|
|
214
|
+
current[part] = { directory: {} }
|
|
215
|
+
}
|
|
216
|
+
current = current[part].directory
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const fileName = pathParts[pathParts.length - 1]
|
|
220
|
+
|
|
221
|
+
const adjustedContent = shouldShimALS
|
|
222
|
+
? shimALS(fileName, content)
|
|
223
|
+
: content
|
|
224
|
+
|
|
225
|
+
if (adjustedContent.startsWith('base64::')) {
|
|
226
|
+
base64FileCount++
|
|
227
|
+
const base64Content = adjustedContent.replace('base64::', '')
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const base64Cleaned = base64Content.replace(/\s/g, '')
|
|
231
|
+
const binaryString = atob(base64Cleaned)
|
|
232
|
+
const bytes = new Uint8Array(binaryString.length)
|
|
233
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
234
|
+
bytes[i] = binaryString.charCodeAt(i) & 0xff
|
|
235
|
+
}
|
|
236
|
+
binaryFiles[cleanPath] = bytes
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error(
|
|
239
|
+
`[BINARY ERROR] Failed to convert ${cleanPath}:`,
|
|
240
|
+
error,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
current[fileName] = {
|
|
245
|
+
file: {
|
|
246
|
+
contents: String(adjustedContent),
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Write the binary files on their own since mount doesn't support binary files correctly
|
|
253
|
+
await container.mount(fileSystemTree)
|
|
254
|
+
for (const [path, bytes] of Object.entries(binaryFiles)) {
|
|
255
|
+
await container.fs.writeFile(path, bytes)
|
|
256
|
+
}
|
|
257
|
+
packageJSONChanged = true
|
|
258
|
+
} else {
|
|
259
|
+
const originalMap = new Map<string, string>()
|
|
260
|
+
for (const { path, content } of originalProjectFiles) {
|
|
261
|
+
originalMap.set(path, content)
|
|
262
|
+
}
|
|
263
|
+
const newMap = new Map<string, string>()
|
|
264
|
+
for (const { path, content } of projectFiles) {
|
|
265
|
+
newMap.set(path, content)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const changedOrNewFiles: Array<{ path: string; content: string }> = []
|
|
269
|
+
for (const { path, content } of projectFiles) {
|
|
270
|
+
if (!originalMap.has(path)) {
|
|
271
|
+
changedOrNewFiles.push({ path, content })
|
|
272
|
+
} else if (originalMap.get(path) !== content) {
|
|
273
|
+
changedOrNewFiles.push({ path, content })
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const deletedFiles: string[] = []
|
|
278
|
+
for (const { path } of originalProjectFiles) {
|
|
279
|
+
if (!newMap.has(path)) {
|
|
280
|
+
deletedFiles.push(path)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (changedOrNewFiles.length > 0 || deletedFiles.length > 0) {
|
|
285
|
+
// Kill dev server before updating files to avoid HMR issues
|
|
286
|
+
const { devProcess } = get()
|
|
287
|
+
if (devProcess) {
|
|
288
|
+
console.log('Stopping dev server before file update...')
|
|
289
|
+
addTerminalOutput(
|
|
290
|
+
'⏸️ Stopping dev server before updating files...',
|
|
291
|
+
)
|
|
292
|
+
devProcess.kill()
|
|
293
|
+
set({ devProcess: null, previewUrl: null })
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const { path, content } of changedOrNewFiles) {
|
|
297
|
+
await container.fs.writeFile(path, content)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const path of deletedFiles) {
|
|
301
|
+
await container.fs.rm(path)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
addTerminalOutput('📁 Files updated successfully')
|
|
305
|
+
|
|
306
|
+
if (changedOrNewFiles.some(({ path }) => path === './package.json')) {
|
|
307
|
+
packageJSONChanged = true
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
set({ projectFiles })
|
|
313
|
+
|
|
314
|
+
if (packageJSONChanged) {
|
|
315
|
+
addTerminalOutput(
|
|
316
|
+
'📦 Package.json changed, reinstalling dependencies...',
|
|
317
|
+
)
|
|
318
|
+
await installDependencies()
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
installDependencies: async () => {
|
|
322
|
+
const { webContainer, addTerminalOutput, startDevServer, isInstalling } =
|
|
323
|
+
get()
|
|
324
|
+
|
|
325
|
+
if (isInstalling) {
|
|
326
|
+
console.log('Install already in progress, skipping')
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!webContainer) {
|
|
331
|
+
throw new Error('WebContainer not found')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
set({ isInstalling: true })
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const container = await webContainer
|
|
338
|
+
if (!container) {
|
|
339
|
+
set({ isInstalling: false })
|
|
340
|
+
throw new Error('WebContainer not found')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
set({
|
|
344
|
+
setupStep: 'installing',
|
|
345
|
+
statusMessage: 'Installing dependencies...',
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
console.log('Starting pnpm install...')
|
|
349
|
+
addTerminalOutput('📦 Running pnpm install...')
|
|
350
|
+
addTerminalOutput('⏳ This may take a minute...')
|
|
351
|
+
|
|
352
|
+
let installProcess
|
|
353
|
+
try {
|
|
354
|
+
installProcess = await container.spawn('pnpm', ['install'])
|
|
355
|
+
console.log('pnpm install process spawned successfully')
|
|
356
|
+
} catch (spawnError) {
|
|
357
|
+
console.error('Failed to spawn pnpm install:', spawnError)
|
|
358
|
+
throw spawnError
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let outputCount = 0
|
|
362
|
+
let lastProgressUpdate = Date.now()
|
|
363
|
+
let allOutput: string[] = []
|
|
364
|
+
let progressInterval = setInterval(() => {
|
|
365
|
+
const elapsed = Math.floor((Date.now() - lastProgressUpdate) / 1000)
|
|
366
|
+
console.log(
|
|
367
|
+
`[INSTALL] Still running... (${elapsed}s, ${outputCount} output chunks)`,
|
|
368
|
+
)
|
|
369
|
+
}, 5000)
|
|
370
|
+
|
|
371
|
+
installProcess.output.pipeTo(
|
|
372
|
+
new WritableStream({
|
|
373
|
+
write(data) {
|
|
374
|
+
outputCount++
|
|
375
|
+
allOutput.push(data)
|
|
376
|
+
|
|
377
|
+
const cleaned = processTerminalLine(data)
|
|
378
|
+
|
|
379
|
+
// Show meaningful output immediately
|
|
380
|
+
if (cleaned && cleaned.length > 3) {
|
|
381
|
+
const isImportant =
|
|
382
|
+
cleaned.includes('added') ||
|
|
383
|
+
cleaned.includes('removed') ||
|
|
384
|
+
cleaned.includes('changed') ||
|
|
385
|
+
cleaned.includes('audited') ||
|
|
386
|
+
cleaned.includes('packages') ||
|
|
387
|
+
cleaned.includes('error') ||
|
|
388
|
+
cleaned.includes('warn') ||
|
|
389
|
+
cleaned.includes('ERR') ||
|
|
390
|
+
cleaned.includes('FAIL')
|
|
391
|
+
|
|
392
|
+
if (isImportant) {
|
|
393
|
+
console.log('[INSTALL]', cleaned)
|
|
394
|
+
addTerminalOutput(cleaned)
|
|
395
|
+
if (isImportant && progressInterval) {
|
|
396
|
+
clearInterval(progressInterval)
|
|
397
|
+
progressInterval = undefined as any
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
}),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
console.log('Waiting for install to complete...')
|
|
406
|
+
const installExitCode = await installProcess.exit
|
|
407
|
+
if (progressInterval) clearInterval(progressInterval)
|
|
408
|
+
console.log('Install exit code:', installExitCode)
|
|
409
|
+
console.log('Total output lines:', outputCount)
|
|
410
|
+
|
|
411
|
+
if (installExitCode !== 0) {
|
|
412
|
+
// Show all output for debugging
|
|
413
|
+
console.error('[INSTALL ERROR] All output:', allOutput.join('\n'))
|
|
414
|
+
const errorMsg = `pnpm install failed with exit code ${installExitCode}`
|
|
415
|
+
addTerminalOutput(`❌ ${errorMsg}`)
|
|
416
|
+
addTerminalOutput('💡 Check console for detailed error output')
|
|
417
|
+
set({ error: errorMsg, setupStep: 'error' })
|
|
418
|
+
throw new Error(errorMsg)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
addTerminalOutput('✅ Dependencies installed successfully')
|
|
422
|
+
|
|
423
|
+
await startDevServer()
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error('Install error:', error)
|
|
426
|
+
addTerminalOutput(`❌ Install error: ${(error as Error).message}`)
|
|
427
|
+
set({ error: (error as Error).message, setupStep: 'error' })
|
|
428
|
+
throw error
|
|
429
|
+
} finally {
|
|
430
|
+
set({ isInstalling: false })
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
}))
|
|
434
|
+
|
|
435
|
+
return store
|
|
436
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -16,8 +16,11 @@ import ModeSelector from './components/sidebar-items/mode-selector'
|
|
|
16
16
|
import TypescriptSwitch from './components/sidebar-items/typescript-switch'
|
|
17
17
|
import StarterDialog from './components/sidebar-items/starter'
|
|
18
18
|
import SidebarGroup from './components/sidebar-items/sidebar-group'
|
|
19
|
+
import WebContainerProvider from './components/web-container-provider'
|
|
20
|
+
import { WebContainerPreview } from './components/webcontainer-preview'
|
|
19
21
|
|
|
20
22
|
import { useApplicationMode, useManager, useReady } from './store/project'
|
|
23
|
+
import { useWebContainer } from './hooks/use-web-container'
|
|
21
24
|
|
|
22
25
|
export {
|
|
23
26
|
FileNavigator,
|
|
@@ -36,9 +39,12 @@ export {
|
|
|
36
39
|
TypescriptSwitch,
|
|
37
40
|
StarterDialog,
|
|
38
41
|
SidebarGroup,
|
|
42
|
+
WebContainerProvider,
|
|
43
|
+
WebContainerPreview,
|
|
39
44
|
useApplicationMode,
|
|
40
45
|
useManager,
|
|
41
46
|
useReady,
|
|
47
|
+
useWebContainer,
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
export default RootComponent
|
|
50
|
+
export default RootComponent
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const ALS_RESOLVER = `resolve: {
|
|
2
|
+
alias: {
|
|
3
|
+
'node:async_hooks': '\\0virtual:async_hooks',
|
|
4
|
+
async_hooks: '\\0virtual:async_hooks',
|
|
5
|
+
},
|
|
6
|
+
},`
|
|
7
|
+
|
|
8
|
+
const ALS_SHIM = `export class AsyncLocalStorage {
|
|
9
|
+
constructor() {
|
|
10
|
+
// queue: array of { store, fn, resolve, reject }
|
|
11
|
+
this._queue = [];
|
|
12
|
+
this._running = false; // true while processing queue
|
|
13
|
+
this._currentStore = undefined; // store visible to getStore() while a run executes
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* run(store, callback, ...args) -> Promise
|
|
18
|
+
* Queues the callback to run with store as the current store. If the callback
|
|
19
|
+
* returns a Promise, the queue waits for it to settle before starting the next run.
|
|
20
|
+
*/
|
|
21
|
+
run(store, callback, ...args) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
this._queue.push({
|
|
24
|
+
store,
|
|
25
|
+
fn: () => callback(...args),
|
|
26
|
+
resolve,
|
|
27
|
+
reject,
|
|
28
|
+
});
|
|
29
|
+
// start processing (if not already)
|
|
30
|
+
this._processQueue().catch((err) => {
|
|
31
|
+
// _processQueue shouldn't throw; but guard anyway.
|
|
32
|
+
console.error('SerialAsyncLocalStorage internal error:', err);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* getStore() -> current store or undefined
|
|
39
|
+
* Returns the store of the currently executing run (or undefined if none).
|
|
40
|
+
*/
|
|
41
|
+
getStore() {
|
|
42
|
+
return this._currentStore;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* enterWith(store)
|
|
47
|
+
* Set the current store for the currently running task synchronously.
|
|
48
|
+
* Throws if there is no active run (this polyfill requires you to be inside a run).
|
|
49
|
+
*/
|
|
50
|
+
enterWith(store) {
|
|
51
|
+
if (!this._running) {
|
|
52
|
+
throw new Error('enterWith() may be used only while a run is active.');
|
|
53
|
+
}
|
|
54
|
+
this._currentStore = store;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// internal: process queue serially
|
|
58
|
+
async _processQueue() {
|
|
59
|
+
if (this._running) return;
|
|
60
|
+
this._running = true;
|
|
61
|
+
|
|
62
|
+
while (this._queue.length) {
|
|
63
|
+
const { store, fn, resolve, reject } = this._queue.shift();
|
|
64
|
+
const prevStore = this._currentStore;
|
|
65
|
+
this._currentStore = store;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = fn();
|
|
69
|
+
// await if callback returned a promise
|
|
70
|
+
const awaited = result instanceof Promise ? await result : result;
|
|
71
|
+
resolve(awaited);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
reject(err);
|
|
74
|
+
} finally {
|
|
75
|
+
// restore previous store (if any)
|
|
76
|
+
this._currentStore = prevStore;
|
|
77
|
+
}
|
|
78
|
+
// loop continues to next queued task
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this._running = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export default AsyncLocalStorage
|
|
85
|
+
`
|
|
86
|
+
|
|
87
|
+
const ALS_SHIM_LOADER = `
|
|
88
|
+
function alsShim(): PluginOption {
|
|
89
|
+
return {
|
|
90
|
+
enforce: 'pre',
|
|
91
|
+
name: 'virtual-async-hooks',
|
|
92
|
+
config() {
|
|
93
|
+
return {
|
|
94
|
+
resolve: {
|
|
95
|
+
alias: {
|
|
96
|
+
// catch both forms
|
|
97
|
+
'node:async_hooks': '\\0virtual:async_hooks',
|
|
98
|
+
async_hooks: '\\0virtual:async_hooks',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
resolveId(id) {
|
|
104
|
+
if (id === '\\0virtual:async_hooks') return id;
|
|
105
|
+
},
|
|
106
|
+
load(id) {
|
|
107
|
+
if (id !== '\\0virtual:async_hooks') return null;
|
|
108
|
+
|
|
109
|
+
return \`${ALS_SHIM}\`;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
`
|
|
114
|
+
|
|
115
|
+
export default function shimALS(fileName: string, content: string) {
|
|
116
|
+
let adjustedContent = content
|
|
117
|
+
if (fileName === 'vite.config.ts') {
|
|
118
|
+
adjustedContent += ALS_SHIM_LOADER
|
|
119
|
+
adjustedContent = adjustedContent.replace(
|
|
120
|
+
'plugins: [',
|
|
121
|
+
`${ALS_RESOLVER}plugins: [alsShim(),`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
return adjustedContent
|
|
125
|
+
}
|