@run0/jiki 0.1.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.
Files changed (152) hide show
  1. package/dist/browser-bundle.d.ts +40 -0
  2. package/dist/builtins.d.ts +22 -0
  3. package/dist/code-transform.d.ts +7 -0
  4. package/dist/config/cdn.d.ts +13 -0
  5. package/dist/container.d.ts +101 -0
  6. package/dist/dev-server.d.ts +69 -0
  7. package/dist/errors.d.ts +19 -0
  8. package/dist/frameworks/code-transforms.d.ts +32 -0
  9. package/dist/frameworks/next-api-handler.d.ts +72 -0
  10. package/dist/frameworks/next-dev-server.d.ts +141 -0
  11. package/dist/frameworks/next-html-generator.d.ts +36 -0
  12. package/dist/frameworks/next-route-resolver.d.ts +19 -0
  13. package/dist/frameworks/next-shims.d.ts +78 -0
  14. package/dist/frameworks/remix-dev-server.d.ts +47 -0
  15. package/dist/frameworks/sveltekit-dev-server.d.ts +43 -0
  16. package/dist/frameworks/vite-dev-server.d.ts +50 -0
  17. package/dist/fs-errors.d.ts +36 -0
  18. package/dist/index.cjs +14916 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.ts +61 -0
  21. package/dist/index.mjs +14898 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/kernel.d.ts +48 -0
  24. package/dist/memfs.d.ts +144 -0
  25. package/dist/metrics.d.ts +78 -0
  26. package/dist/module-resolver.d.ts +60 -0
  27. package/dist/network-interceptor.d.ts +71 -0
  28. package/dist/npm/cache.d.ts +76 -0
  29. package/dist/npm/index.d.ts +60 -0
  30. package/dist/npm/lockfile-reader.d.ts +32 -0
  31. package/dist/npm/pnpm.d.ts +18 -0
  32. package/dist/npm/registry.d.ts +45 -0
  33. package/dist/npm/resolver.d.ts +39 -0
  34. package/dist/npm/sync-installer.d.ts +18 -0
  35. package/dist/npm/tarball.d.ts +4 -0
  36. package/dist/npm/workspaces.d.ts +46 -0
  37. package/dist/persistence.d.ts +94 -0
  38. package/dist/plugin.d.ts +156 -0
  39. package/dist/polyfills/assert.d.ts +30 -0
  40. package/dist/polyfills/child_process.d.ts +116 -0
  41. package/dist/polyfills/chokidar.d.ts +18 -0
  42. package/dist/polyfills/crypto.d.ts +49 -0
  43. package/dist/polyfills/events.d.ts +28 -0
  44. package/dist/polyfills/fs.d.ts +82 -0
  45. package/dist/polyfills/http.d.ts +147 -0
  46. package/dist/polyfills/module.d.ts +29 -0
  47. package/dist/polyfills/net.d.ts +53 -0
  48. package/dist/polyfills/os.d.ts +91 -0
  49. package/dist/polyfills/path.d.ts +96 -0
  50. package/dist/polyfills/perf_hooks.d.ts +21 -0
  51. package/dist/polyfills/process.d.ts +99 -0
  52. package/dist/polyfills/querystring.d.ts +15 -0
  53. package/dist/polyfills/readdirp.d.ts +18 -0
  54. package/dist/polyfills/readline.d.ts +32 -0
  55. package/dist/polyfills/stream.d.ts +106 -0
  56. package/dist/polyfills/stubs.d.ts +737 -0
  57. package/dist/polyfills/tty.d.ts +25 -0
  58. package/dist/polyfills/url.d.ts +41 -0
  59. package/dist/polyfills/util.d.ts +61 -0
  60. package/dist/polyfills/v8.d.ts +43 -0
  61. package/dist/polyfills/vm.d.ts +76 -0
  62. package/dist/polyfills/worker-threads.d.ts +77 -0
  63. package/dist/polyfills/ws.d.ts +32 -0
  64. package/dist/polyfills/zlib.d.ts +87 -0
  65. package/dist/runtime-helpers.d.ts +4 -0
  66. package/dist/runtime-interface.d.ts +39 -0
  67. package/dist/sandbox.d.ts +69 -0
  68. package/dist/server-bridge.d.ts +55 -0
  69. package/dist/shell-commands.d.ts +2 -0
  70. package/dist/shell.d.ts +101 -0
  71. package/dist/transpiler.d.ts +47 -0
  72. package/dist/type-checker.d.ts +57 -0
  73. package/dist/types/package-json.d.ts +17 -0
  74. package/dist/utils/binary-encoding.d.ts +4 -0
  75. package/dist/utils/hash.d.ts +6 -0
  76. package/dist/utils/safe-path.d.ts +6 -0
  77. package/dist/worker-runtime.d.ts +34 -0
  78. package/package.json +59 -0
  79. package/src/browser-bundle.ts +498 -0
  80. package/src/builtins.ts +222 -0
  81. package/src/code-transform.ts +183 -0
  82. package/src/config/cdn.ts +17 -0
  83. package/src/container.ts +343 -0
  84. package/src/dev-server.ts +322 -0
  85. package/src/errors.ts +604 -0
  86. package/src/frameworks/code-transforms.ts +667 -0
  87. package/src/frameworks/next-api-handler.ts +366 -0
  88. package/src/frameworks/next-dev-server.ts +1252 -0
  89. package/src/frameworks/next-html-generator.ts +585 -0
  90. package/src/frameworks/next-route-resolver.ts +521 -0
  91. package/src/frameworks/next-shims.ts +1084 -0
  92. package/src/frameworks/remix-dev-server.ts +163 -0
  93. package/src/frameworks/sveltekit-dev-server.ts +197 -0
  94. package/src/frameworks/vite-dev-server.ts +370 -0
  95. package/src/fs-errors.ts +118 -0
  96. package/src/index.ts +188 -0
  97. package/src/kernel.ts +381 -0
  98. package/src/memfs.ts +1006 -0
  99. package/src/metrics.ts +140 -0
  100. package/src/module-resolver.ts +511 -0
  101. package/src/network-interceptor.ts +143 -0
  102. package/src/npm/cache.ts +172 -0
  103. package/src/npm/index.ts +377 -0
  104. package/src/npm/lockfile-reader.ts +105 -0
  105. package/src/npm/pnpm.ts +108 -0
  106. package/src/npm/registry.ts +120 -0
  107. package/src/npm/resolver.ts +339 -0
  108. package/src/npm/sync-installer.ts +217 -0
  109. package/src/npm/tarball.ts +136 -0
  110. package/src/npm/workspaces.ts +255 -0
  111. package/src/persistence.ts +235 -0
  112. package/src/plugin.ts +293 -0
  113. package/src/polyfills/assert.ts +164 -0
  114. package/src/polyfills/child_process.ts +535 -0
  115. package/src/polyfills/chokidar.ts +52 -0
  116. package/src/polyfills/crypto.ts +433 -0
  117. package/src/polyfills/events.ts +178 -0
  118. package/src/polyfills/fs.ts +297 -0
  119. package/src/polyfills/http.ts +478 -0
  120. package/src/polyfills/module.ts +97 -0
  121. package/src/polyfills/net.ts +123 -0
  122. package/src/polyfills/os.ts +108 -0
  123. package/src/polyfills/path.ts +169 -0
  124. package/src/polyfills/perf_hooks.ts +30 -0
  125. package/src/polyfills/process.ts +349 -0
  126. package/src/polyfills/querystring.ts +66 -0
  127. package/src/polyfills/readdirp.ts +72 -0
  128. package/src/polyfills/readline.ts +80 -0
  129. package/src/polyfills/stream.ts +610 -0
  130. package/src/polyfills/stubs.ts +600 -0
  131. package/src/polyfills/tty.ts +43 -0
  132. package/src/polyfills/url.ts +97 -0
  133. package/src/polyfills/util.ts +173 -0
  134. package/src/polyfills/v8.ts +62 -0
  135. package/src/polyfills/vm.ts +111 -0
  136. package/src/polyfills/worker-threads.ts +189 -0
  137. package/src/polyfills/ws.ts +73 -0
  138. package/src/polyfills/zlib.ts +244 -0
  139. package/src/runtime-helpers.ts +83 -0
  140. package/src/runtime-interface.ts +46 -0
  141. package/src/sandbox.ts +178 -0
  142. package/src/server-bridge.ts +473 -0
  143. package/src/service-worker.ts +153 -0
  144. package/src/shell-commands.ts +708 -0
  145. package/src/shell.ts +795 -0
  146. package/src/transpiler.ts +282 -0
  147. package/src/type-checker.ts +241 -0
  148. package/src/types/package-json.ts +17 -0
  149. package/src/utils/binary-encoding.ts +38 -0
  150. package/src/utils/hash.ts +24 -0
  151. package/src/utils/safe-path.ts +38 -0
  152. package/src/worker-runtime.ts +42 -0
@@ -0,0 +1,343 @@
1
+ import { MemFS } from "./memfs";
2
+ import { Kernel, RuntimeOptions } from "./kernel";
3
+ import {
4
+ PackageManager,
5
+ InstallOptions,
6
+ InstallResult,
7
+ NpmLayout,
8
+ } from "./npm/index";
9
+ import { PnpmLayout } from "./npm/pnpm";
10
+ import { Shell, ShellOptions, createShell } from "./shell";
11
+ import type { IExecuteResult, VFSSnapshot } from "./runtime-interface";
12
+ import * as pathShim from "./polyfills/path";
13
+ import { base64ToUint8 } from "./utils/binary-encoding";
14
+ import {
15
+ setStreamingCallbacks,
16
+ clearStreamingCallbacks,
17
+ sendStdin,
18
+ } from "./polyfills/child_process";
19
+ import { SyncAutoInstaller } from "./npm/sync-installer";
20
+ import { PluginRegistry, type JikiPlugin } from "./plugin";
21
+ import type { PersistenceAdapter } from "./persistence";
22
+ import {
23
+ registerBuiltin,
24
+ registerBuiltins,
25
+ unregisterBuiltin,
26
+ } from "./builtins";
27
+ import { shouldUseWorker, type WorkerMode } from "./worker-runtime";
28
+ import { SandboxGuard, type SandboxOptions } from "./sandbox";
29
+ import { Metrics } from "./metrics";
30
+ import { NetworkInterceptor, type MockResponse } from "./network-interceptor";
31
+
32
+ export interface ContainerOptions {
33
+ cwd?: string;
34
+ env?: Record<string, string>;
35
+ registry?: string;
36
+ /** `'npm'` (default) or `'pnpm'` */
37
+ packageManager?: "npm" | "pnpm";
38
+ /** Automatically install missing packages on require() */
39
+ autoInstall?: boolean;
40
+ onConsole?: (method: string, args: unknown[]) => void;
41
+ onStdout?: (data: string) => void;
42
+ onStderr?: (data: string) => void;
43
+ /** Plugins to register on this container. */
44
+ plugins?: JikiPlugin[];
45
+ /** Persistence adapter for surviving page refreshes. */
46
+ persistence?: PersistenceAdapter;
47
+ /** Enable inline source maps for TypeScript/JSX transpilation (default: false). */
48
+ sourceMaps?: boolean;
49
+ /**
50
+ * Worker mode for CPU-intensive operations.
51
+ * - `false` (default) — everything on the main thread
52
+ * - `true` — transpilation runs in a Web Worker
53
+ * - `'auto'` — use workers when available (browser), skip in Node.js
54
+ */
55
+ worker?: boolean | "auto";
56
+ /** Resource limits and access controls. */
57
+ sandbox?: SandboxOptions;
58
+ }
59
+
60
+ export interface RunOptions extends ShellOptions {
61
+ signal?: AbortSignal;
62
+ }
63
+
64
+ export interface RunResult {
65
+ stdout: string;
66
+ stderr: string;
67
+ exitCode: number;
68
+ }
69
+
70
+ export class Container {
71
+ readonly vfs: MemFS;
72
+ readonly runtime: Kernel;
73
+ readonly packageManager: PackageManager;
74
+ readonly shell: Shell;
75
+ /** Plugin registry for this container. */
76
+ readonly plugins: PluginRegistry;
77
+ /** Sandbox guard enforcing resource limits. */
78
+ readonly sandbox: SandboxGuard;
79
+ /** Performance metrics for this container. */
80
+ readonly metrics: Metrics;
81
+ /** Network request interceptor for mocking fetch calls. */
82
+ readonly network: NetworkInterceptor;
83
+ private _pnpmPm?: PackageManager;
84
+ private _containerCwd: string;
85
+ private _containerRegistry?: string;
86
+ private _workerMode: WorkerMode;
87
+
88
+ constructor(options: ContainerOptions = {}) {
89
+ const cwd = options.cwd || "/";
90
+ const usePnpm = options.packageManager === "pnpm";
91
+ this._containerCwd = cwd;
92
+ this._containerRegistry = options.registry;
93
+ this._workerMode = options.worker ?? false;
94
+
95
+ // Initialise plugin registry, sandbox, and metrics before other subsystems.
96
+ this.plugins = new PluginRegistry();
97
+ this.sandbox = new SandboxGuard(options.sandbox || {});
98
+ this.metrics = new Metrics();
99
+ this.network = new NetworkInterceptor();
100
+ if (options.plugins) {
101
+ for (const p of options.plugins) this.plugins.register(p);
102
+ }
103
+
104
+ this.vfs = new MemFS({
105
+ persistence: options.persistence,
106
+ sandbox: this.sandbox,
107
+ });
108
+
109
+ const layout = usePnpm ? new PnpmLayout() : new NpmLayout();
110
+ this.packageManager = new PackageManager(this.vfs, {
111
+ cwd,
112
+ registry: options.registry,
113
+ layout,
114
+ });
115
+
116
+ const autoInstallProvider = options.autoInstall
117
+ ? new SyncAutoInstaller(this.vfs, layout, {
118
+ cwd,
119
+ registry: options.registry,
120
+ })
121
+ : undefined;
122
+
123
+ this.runtime = new Kernel(
124
+ this.vfs,
125
+ {
126
+ cwd,
127
+ env: options.env,
128
+ onConsole: options.onConsole,
129
+ onStdout: options.onStdout,
130
+ onStderr: options.onStderr,
131
+ autoInstall: options.autoInstall,
132
+ autoInstallProvider,
133
+ sourceMaps: options.sourceMaps,
134
+ },
135
+ this.plugins,
136
+ );
137
+
138
+ if (usePnpm) {
139
+ this._pnpmPm = this.packageManager;
140
+ }
141
+
142
+ this.shell = createShell(this.vfs, this.runtime, this.packageManager, {
143
+ cwd,
144
+ env: options.env,
145
+ onStdout: options.onStdout,
146
+ onStderr: options.onStderr,
147
+ pnpmPm: usePnpm ? this.packageManager : undefined,
148
+ lazyPnpmPm: usePnpm ? undefined : () => this.lazyPnpmPm,
149
+ });
150
+
151
+ // Register plugin-provided shell commands.
152
+ for (const entry of this.plugins.getCommandHooks()) {
153
+ this.shell.registerCommand(entry.name, entry.handler);
154
+ }
155
+ }
156
+
157
+ private get lazyPnpmPm(): PackageManager {
158
+ if (!this._pnpmPm) {
159
+ this._pnpmPm = new PackageManager(this.vfs, {
160
+ cwd: this._containerCwd,
161
+ registry: this._containerRegistry,
162
+ layout: new PnpmLayout(),
163
+ });
164
+ }
165
+ return this._pnpmPm;
166
+ }
167
+
168
+ async init(options?: { wasmURL?: string | URL }): Promise<void> {
169
+ await this.vfs.hydrate();
170
+ await this.runtime.init({
171
+ ...options,
172
+ useWorker: shouldUseWorker(this._workerMode),
173
+ });
174
+ this.plugins.runBoot();
175
+ }
176
+
177
+ writeFile(path: string, content: string | Uint8Array): void {
178
+ const dir = pathShim.dirname(path);
179
+ if (dir !== "/" && !this.vfs.existsSync(dir)) {
180
+ this.vfs.mkdirSync(dir, { recursive: true });
181
+ }
182
+ // Sandbox checks are enforced at the VFS level (MemFS.putFile)
183
+ // so they apply to all write paths: Container, Shell, executed code.
184
+ this.vfs.writeFileSync(path, content);
185
+ this.metrics.trackWrite();
186
+ }
187
+
188
+ readFile(path: string): string;
189
+ readFile(path: string, encoding: null): Uint8Array;
190
+ readFile(path: string, encoding?: null): string | Uint8Array {
191
+ this.metrics.trackRead();
192
+ return encoding === null
193
+ ? this.vfs.readFileSync(path)
194
+ : this.vfs.readFileSync(path, "utf8");
195
+ }
196
+
197
+ mkdir(path: string): void {
198
+ this.vfs.mkdirSync(path, { recursive: true });
199
+ }
200
+ readdir(path: string): string[] {
201
+ return this.vfs.readdirSync(path);
202
+ }
203
+ exists(path: string): boolean {
204
+ return this.vfs.existsSync(path);
205
+ }
206
+ rm(path: string): void {
207
+ this.vfs.rmSync(path, { recursive: true, force: true });
208
+ }
209
+
210
+ execute(code: string, filename?: string): IExecuteResult {
211
+ return this.runtime.execute(code, filename);
212
+ }
213
+
214
+ runFile(filename: string): IExecuteResult {
215
+ return this.runtime.runFile(filename);
216
+ }
217
+
218
+ async run(command: string, options?: RunOptions): Promise<RunResult> {
219
+ this.metrics.trackCommand();
220
+ if (options?.onStdout || options?.onStderr || options?.signal) {
221
+ setStreamingCallbacks({
222
+ onStdout: options.onStdout,
223
+ onStderr: options.onStderr,
224
+ signal: options.signal,
225
+ });
226
+ }
227
+ try {
228
+ return await this.shell.exec(command, options);
229
+ } finally {
230
+ clearStreamingCallbacks();
231
+ }
232
+ }
233
+
234
+ sendInput(data: string): void {
235
+ sendStdin(data);
236
+ // Also push to process.stdin so readline/inquirer can receive it.
237
+ (this.runtime.process.stdin as any)?._push?.(data);
238
+ }
239
+
240
+ /** Register a custom polyfill for a built-in module. */
241
+ registerPolyfill(name: string, factory: () => unknown): void {
242
+ registerBuiltin(name, factory);
243
+ }
244
+
245
+ /** Register multiple custom polyfills at once. */
246
+ registerPolyfills(map: Record<string, () => unknown>): void {
247
+ registerBuiltins(map);
248
+ }
249
+
250
+ /** Unregister a custom polyfill. */
251
+ unregisterPolyfill(name: string): void {
252
+ unregisterBuiltin(name);
253
+ }
254
+
255
+ /** Mock a fetch URL with a static response. */
256
+ mockFetch(pattern: string | RegExp, response: MockResponse): void {
257
+ this.network.mock(pattern, response);
258
+ }
259
+
260
+ /** Register a dynamic fetch interceptor. */
261
+ onFetch(
262
+ handler: (
263
+ url: string,
264
+ init?: RequestInit,
265
+ ) =>
266
+ | MockResponse
267
+ | null
268
+ | undefined
269
+ | Promise<MockResponse | null | undefined>,
270
+ ): void {
271
+ this.network.onFetch(handler);
272
+ }
273
+
274
+ /** Get a snapshot of performance metrics. */
275
+ getMetrics() {
276
+ return this.metrics.snapshot();
277
+ }
278
+
279
+ async install(
280
+ packages: string | string[],
281
+ options?: InstallOptions,
282
+ ): Promise<InstallResult> {
283
+ const start = Date.now();
284
+ const result = await this.packageManager.install(packages, options);
285
+ this.metrics.trackInstall(Date.now() - start);
286
+ const names = Array.isArray(packages) ? packages : [packages];
287
+ this.plugins.runInstall(names);
288
+ return result;
289
+ }
290
+
291
+ async installDependencies(options?: InstallOptions): Promise<InstallResult> {
292
+ const result = await this.packageManager.installFromPackageJson(options);
293
+ this.plugins.runInstall([]);
294
+ return result;
295
+ }
296
+
297
+ toSnapshot(): VFSSnapshot {
298
+ return this.vfs.toSnapshot();
299
+ }
300
+
301
+ static fromSnapshot(
302
+ snapshot: VFSSnapshot,
303
+ options?: ContainerOptions,
304
+ ): Container {
305
+ const container = new Container(options);
306
+
307
+ for (const entry of snapshot.files) {
308
+ if (entry.path === "/") continue;
309
+ if (entry.type === "directory") {
310
+ container.vfs.mkdirSync(entry.path, { recursive: true });
311
+ } else if (entry.type === "file" && entry.content) {
312
+ const dir = pathShim.dirname(entry.path);
313
+ if (dir !== "/") container.vfs.mkdirSync(dir, { recursive: true });
314
+ container.vfs.writeFileSync(entry.path, base64ToUint8(entry.content));
315
+ } else if (entry.type === "symlink" && entry.target) {
316
+ container.vfs.symlinkSync(entry.target, entry.path);
317
+ }
318
+ }
319
+ return container;
320
+ }
321
+
322
+ export(path = "/"): Record<string, unknown> {
323
+ return this.vfs.export(path);
324
+ }
325
+
326
+ destroy(): void {
327
+ this.runtime.clearCache();
328
+ }
329
+ }
330
+
331
+ /** Register a plugin on an existing container (post-construction). */
332
+ export function registerPlugin(container: Container, plugin: JikiPlugin): void {
333
+ container.plugins.register(plugin);
334
+ for (const entry of container.plugins.getCommandHooks()) {
335
+ container.shell.registerCommand(entry.name, entry.handler);
336
+ }
337
+ }
338
+
339
+ export function boot(options?: ContainerOptions): Container {
340
+ return new Container(options);
341
+ }
342
+
343
+ export { boot as createContainer };
@@ -0,0 +1,322 @@
1
+ import { EventEmitter } from "./polyfills/events";
2
+ import { MemFS } from "./memfs";
3
+ import { BufferImpl as Buffer } from "./polyfills/stream";
4
+ import { safePath } from "./utils/safe-path";
5
+
6
+ export interface DevServerOptions {
7
+ port: number;
8
+ root?: string;
9
+ }
10
+
11
+ export interface ResponseData {
12
+ statusCode: number;
13
+ statusMessage: string;
14
+ headers: Record<string, string>;
15
+ body: Buffer;
16
+ }
17
+
18
+ export interface HMRUpdate {
19
+ type: "update" | "full-reload";
20
+ path: string;
21
+ timestamp?: number;
22
+ }
23
+
24
+ /**
25
+ * Check if a filename contains a content hash (e.g., app.a1b2c3d4.js, chunk-HASH.css).
26
+ * Content-hashed files are safe to cache immutably since the hash changes when content changes.
27
+ */
28
+ function hasContentHash(filepath: string): boolean {
29
+ // Match patterns like: file.abc123ef.js, file-abc123ef.css, file.ABCDEF01.chunk.js
30
+ // The hash segment should be 8+ hex characters between dots or after a dash before the extension
31
+ return /[.\-][a-fA-F0-9]{8,}[.\-]/.test(filepath);
32
+ }
33
+
34
+ /**
35
+ * Determine the appropriate Cache-Control header for a given file path.
36
+ * - Content-hashed assets get immutable caching (1 year)
37
+ * - HTML and non-hashed assets get no-cache for development freshness
38
+ */
39
+ function inferCacheControl(filepath: string): string {
40
+ const ext = filepath.split(".").pop()?.toLowerCase() || "";
41
+ // HTML files should never be cached immutably
42
+ if (ext === "html" || ext === "htm") {
43
+ return "no-cache";
44
+ }
45
+ // Content-hashed files are safe to cache immutably
46
+ if (hasContentHash(filepath)) {
47
+ return "public, max-age=31536000, immutable";
48
+ }
49
+ return "no-cache";
50
+ }
51
+
52
+ function inferMimeType(filepath: string): string {
53
+ const ext = filepath.split(".").pop()?.toLowerCase() || "";
54
+ switch (ext) {
55
+ case "html":
56
+ case "htm":
57
+ return "text/html; charset=utf-8";
58
+ case "css":
59
+ return "text/css; charset=utf-8";
60
+ case "js":
61
+ case "mjs":
62
+ case "cjs":
63
+ case "jsx":
64
+ case "ts":
65
+ case "tsx":
66
+ return "application/javascript; charset=utf-8";
67
+ case "json":
68
+ case "map":
69
+ return "application/json; charset=utf-8";
70
+ case "png":
71
+ return "image/png";
72
+ case "jpg":
73
+ case "jpeg":
74
+ return "image/jpeg";
75
+ case "gif":
76
+ return "image/gif";
77
+ case "svg":
78
+ return "image/svg+xml";
79
+ case "ico":
80
+ return "image/x-icon";
81
+ case "webp":
82
+ return "image/webp";
83
+ case "woff":
84
+ return "font/woff";
85
+ case "woff2":
86
+ return "font/woff2";
87
+ case "ttf":
88
+ return "font/ttf";
89
+ case "otf":
90
+ return "font/otf";
91
+ case "mp3":
92
+ return "audio/mpeg";
93
+ case "wav":
94
+ return "audio/wav";
95
+ case "mp4":
96
+ return "video/mp4";
97
+ case "webm":
98
+ return "video/webm";
99
+ case "pdf":
100
+ return "application/pdf";
101
+ case "xml":
102
+ return "application/xml";
103
+ case "txt":
104
+ return "text/plain; charset=utf-8";
105
+ case "md":
106
+ return "text/markdown; charset=utf-8";
107
+ case "wasm":
108
+ return "application/wasm";
109
+ default:
110
+ return "application/octet-stream";
111
+ }
112
+ }
113
+
114
+ interface FileContent {
115
+ path: string;
116
+ data: Uint8Array;
117
+ mime: string;
118
+ }
119
+
120
+ class HttpResponder {
121
+ static fromContent(fc: FileContent): ResponseData {
122
+ const buf = Buffer.from(fc.data);
123
+ return {
124
+ statusCode: 200,
125
+ statusMessage: "OK",
126
+ headers: {
127
+ "Content-Type": fc.mime,
128
+ "Content-Length": String(buf.length),
129
+ "Cache-Control": inferCacheControl(fc.path),
130
+ },
131
+ body: buf,
132
+ };
133
+ }
134
+
135
+ static text(code: number, statusMsg: string, body: string): ResponseData {
136
+ const buf = Buffer.from(body);
137
+ return {
138
+ statusCode: code,
139
+ statusMessage: statusMsg,
140
+ headers: {
141
+ "Content-Type": "text/plain; charset=utf-8",
142
+ "Content-Length": String(buf.length),
143
+ },
144
+ body: buf,
145
+ };
146
+ }
147
+
148
+ static redirect(
149
+ location: string,
150
+ status: 301 | 302 | 307 | 308 = 302,
151
+ ): ResponseData {
152
+ const labels: Record<number, string> = {
153
+ 301: "Moved Permanently",
154
+ 302: "Found",
155
+ 307: "Temporary Redirect",
156
+ 308: "Permanent Redirect",
157
+ };
158
+ return {
159
+ statusCode: status,
160
+ statusMessage: labels[status] || "Found",
161
+ headers: {
162
+ Location: location,
163
+ "Content-Type": "text/plain; charset=utf-8",
164
+ "Content-Length": "0",
165
+ },
166
+ body: Buffer.from(""),
167
+ };
168
+ }
169
+ }
170
+
171
+ export abstract class DevServer extends EventEmitter {
172
+ protected vfs: MemFS;
173
+ protected port: number;
174
+ protected root: string;
175
+ protected running = false;
176
+
177
+ constructor(vfs: MemFS, options: DevServerOptions) {
178
+ super();
179
+ this.vfs = vfs;
180
+ this.port = options.port;
181
+ this.root = options.root || "/";
182
+ }
183
+
184
+ abstract handleRequest(
185
+ method: string,
186
+ url: string,
187
+ headers: Record<string, string>,
188
+ body?: Buffer,
189
+ ): Promise<ResponseData>;
190
+
191
+ abstract startWatching(): void;
192
+
193
+ /**
194
+ * CORS headers added to all responses for cross-origin iframe support.
195
+ * This is essential since the dev server is typically accessed from
196
+ * a sandboxed iframe on a different origin.
197
+ */
198
+ private static readonly CORS_HEADERS: Record<string, string> = {
199
+ "Access-Control-Allow-Origin": "*",
200
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
201
+ "Access-Control-Allow-Headers":
202
+ "Content-Type, Authorization, X-Requested-With",
203
+ "Access-Control-Max-Age": "86400",
204
+ };
205
+
206
+ /**
207
+ * Add CORS headers to a response.
208
+ */
209
+ protected addCorsHeaders(response: ResponseData): ResponseData {
210
+ return {
211
+ ...response,
212
+ headers: { ...response.headers, ...DevServer.CORS_HEADERS },
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Handle CORS preflight OPTIONS requests.
218
+ */
219
+ protected handleOptionsRequest(): ResponseData {
220
+ return {
221
+ statusCode: 204,
222
+ statusMessage: "No Content",
223
+ headers: {
224
+ ...DevServer.CORS_HEADERS,
225
+ "Content-Length": "0",
226
+ },
227
+ body: Buffer.from(""),
228
+ };
229
+ }
230
+
231
+ stop(): void {
232
+ this.running = false;
233
+ this.emit("close");
234
+ }
235
+
236
+ start(): void {
237
+ this.running = true;
238
+ this.startWatching();
239
+ this.emit("listening", this.port);
240
+ }
241
+
242
+ isRunning(): boolean {
243
+ return this.running;
244
+ }
245
+ getPort(): number {
246
+ return this.port;
247
+ }
248
+
249
+ protected serveFile(filePath: string): ResponseData {
250
+ const resolved = this.resolvePath(filePath);
251
+ let raw: Uint8Array;
252
+ try {
253
+ raw = this.vfs.readFileSync(resolved);
254
+ } catch (err) {
255
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
256
+ return this.notFound(filePath);
257
+ }
258
+ return this.serverError(err);
259
+ }
260
+ return HttpResponder.fromContent({
261
+ path: resolved,
262
+ data: raw,
263
+ mime: this.getMimeType(filePath),
264
+ });
265
+ }
266
+
267
+ protected resolvePath(urlPath: string): string {
268
+ return safePath(this.root, urlPath);
269
+ }
270
+
271
+ protected notFound(path: string): ResponseData {
272
+ return HttpResponder.text(404, "Not Found", `Not found: ${path}`);
273
+ }
274
+
275
+ protected serverError(error: unknown): ResponseData {
276
+ const msg =
277
+ error instanceof Error ? error.message : "Internal Server Error";
278
+ return HttpResponder.text(
279
+ 500,
280
+ "Internal Server Error",
281
+ `Server Error: ${msg}`,
282
+ );
283
+ }
284
+
285
+ protected redirect(
286
+ location: string,
287
+ status: 301 | 302 | 307 | 308 = 302,
288
+ ): ResponseData {
289
+ return HttpResponder.redirect(location, status);
290
+ }
291
+
292
+ protected getMimeType(path: string): string {
293
+ return inferMimeType(path);
294
+ }
295
+
296
+ protected exists(path: string): boolean {
297
+ return this.vfs.existsSync(path);
298
+ }
299
+
300
+ protected isDirectory(path: string): boolean {
301
+ try {
302
+ return this.vfs.statSync(path).isDirectory();
303
+ } catch {
304
+ return false;
305
+ }
306
+ }
307
+
308
+ protected broadcastChange(update: HMRUpdate): void {
309
+ this.emit("hmr-update", {
310
+ ...update,
311
+ timestamp: update.timestamp || Date.now(),
312
+ });
313
+ }
314
+
315
+ /** @deprecated Use broadcastChange instead */
316
+ protected emitHMRUpdate(update: HMRUpdate): void {
317
+ this.broadcastChange(update);
318
+ }
319
+ }
320
+
321
+ export { hasContentHash, inferCacheControl };
322
+ export default DevServer;