@b9g/platform-node 0.1.12 → 0.1.13

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/README.md CHANGED
@@ -8,7 +8,7 @@ Node.js platform adapter for Shovel. Runs ServiceWorker applications on Node.js
8
8
  - Hot module reloading for development via VM module system
9
9
  - Worker thread pool for concurrent request handling
10
10
  - Memory and filesystem cache backends
11
- - File System Access API implementation via NodeDirectory
11
+ - File System Access API implementation via NodeFSDirectory
12
12
  - ServiceWorker lifecycle support (install, activate, fetch events)
13
13
 
14
14
  ## Installation
@@ -112,7 +112,7 @@ Loads and runs a ServiceWorker entrypoint.
112
112
  Configured via `caches` option:
113
113
 
114
114
  - `memory`: In-memory caching (MemoryCache)
115
- - `filesystem`: File-based caching (NodeDirectory)
115
+ - `filesystem`: File-based caching (NodeFSDirectory)
116
116
 
117
117
  ## Worker Thread Architecture
118
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/platform-node",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Node.js platform adapter for Shovel with hot reloading and ESBuild integration",
5
5
  "keywords": [
6
6
  "shovel",
@@ -11,14 +11,14 @@
11
11
  "esbuild"
12
12
  ],
13
13
  "dependencies": {
14
- "@b9g/cache": "^0.1.5",
15
- "@b9g/http-errors": "^0.1.5",
16
- "@b9g/platform": "^0.1.12",
14
+ "@b9g/cache": "^0.2.0-beta.0",
15
+ "@b9g/http-errors": "^0.2.0-beta.0",
16
+ "@b9g/node-webworker": "^0.2.0-beta.1",
17
+ "@b9g/platform": "^0.1.13",
17
18
  "@logtape/logtape": "^1.2.0"
18
19
  },
19
20
  "devDependencies": {
20
21
  "@b9g/libuild": "^0.1.18",
21
- "bun-types": "latest",
22
22
  "@types/node": "^18.0.0"
23
23
  },
24
24
  "type": "module",
package/src/index.d.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Provides hot reloading, ESBuild integration, and optimized caching for Node.js environments.
5
5
  */
6
- import { BasePlatform, type PlatformConfig, type Handler, type Server, type ServerOptions, type ServiceWorkerOptions, type ServiceWorkerInstance, type EntryWrapperOptions, type PlatformEsbuildConfig, ServiceWorkerPool } from "@b9g/platform";
6
+ import { BasePlatform, type PlatformConfig, type PlatformDefaults, type Handler, type Server, type ServerOptions, type ServiceWorkerOptions, type ServiceWorkerInstance, type EntryWrapperOptions, type PlatformESBuildConfig, ServiceWorkerPool, CustomLoggerStorage, CustomDatabaseStorage } from "@b9g/platform";
7
+ import { type ShovelConfig } from "@b9g/platform/runtime";
7
8
  import { CustomCacheStorage } from "@b9g/cache";
8
9
  import { CustomDirectoryStorage } from "@b9g/filesystem";
9
10
  export interface NodePlatformOptions extends PlatformConfig {
@@ -15,6 +16,8 @@ export interface NodePlatformOptions extends PlatformConfig {
15
16
  cwd?: string;
16
17
  /** Number of worker threads (default: 1) */
17
18
  workers?: number;
19
+ /** Shovel configuration (caches, directories, etc.) */
20
+ config?: ShovelConfig;
18
21
  }
19
22
  /**
20
23
  * Node.js platform implementation
@@ -27,7 +30,13 @@ export declare class NodePlatform extends BasePlatform {
27
30
  /**
28
31
  * Get options for testing
29
32
  */
30
- get options(): Required<NodePlatformOptions>;
33
+ get options(): {
34
+ port: number;
35
+ host: string;
36
+ cwd: string;
37
+ workers: number;
38
+ config?: ShovelConfig;
39
+ };
31
40
  /**
32
41
  * Get/set worker pool for testing
33
42
  */
@@ -39,13 +48,25 @@ export declare class NodePlatform extends BasePlatform {
39
48
  */
40
49
  loadServiceWorker(entrypoint: string, options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
41
50
  /**
42
- * Create cache storage (in-memory by default)
51
+ * Create cache storage using config from shovel.json
52
+ * Used for testing - production uses the generated config module
53
+ * Merges with runtime defaults (actual class references) for fallback behavior
43
54
  */
44
55
  createCaches(): Promise<CustomCacheStorage>;
45
56
  /**
46
- * Create directory storage for the given base directory
57
+ * Create directory storage using config from shovel.json
58
+ * Used for testing - production uses the generated config module
59
+ * Merges with runtime defaults (actual class references) for fallback behavior
47
60
  */
48
- createDirectories(baseDir: string): CustomDirectoryStorage;
61
+ createDirectories(): Promise<CustomDirectoryStorage>;
62
+ /**
63
+ * Create logger storage using config from shovel.json
64
+ */
65
+ createLoggers(): Promise<CustomLoggerStorage>;
66
+ /**
67
+ * Create database storage from declarative config in shovel.json
68
+ */
69
+ createDatabases(configOverride?: NodePlatformOptions["config"]): CustomDatabaseStorage | undefined;
49
70
  /**
50
71
  * SUPPORTING UTILITY - Create HTTP server for Node.js
51
72
  */
@@ -58,28 +79,44 @@ export declare class NodePlatform extends BasePlatform {
58
79
  /**
59
80
  * Get virtual entry wrapper for Node.js
60
81
  *
61
- * Returns production server entry template that uses:
62
- * - shovel:config virtual module for configuration
63
- * - Worker threads via ServiceWorkerPool for multi-worker scaling
64
- * - Platform's loadServiceWorker and createServer methods
82
+ * @param entryPath - Absolute path to user's entrypoint file
83
+ * @param options - Entry wrapper options
84
+ * @param options.type - "production" (default) or "worker"
65
85
  *
66
- * The template is a real .ts file (entry-template.ts) for better
67
- * IDE support and linting. It's imported with {type: "text"}.
86
+ * Returns:
87
+ * - "production": Server entry that loads ServiceWorkerPool
88
+ * - "worker": Worker entry that sets up runtime and message loop
68
89
  */
69
- getEntryWrapper(_entryPath: string, _options?: EntryWrapperOptions): string;
90
+ getEntryWrapper(entryPath: string, options?: EntryWrapperOptions): string;
70
91
  /**
71
92
  * Get Node.js-specific esbuild configuration
72
93
  *
73
94
  * Note: Node.js doesn't support import.meta.env natively, so we alias it
74
95
  * to process.env for compatibility with code that uses Vite-style env access.
75
96
  */
76
- getEsbuildConfig(): PlatformEsbuildConfig;
97
+ getESBuildConfig(): PlatformESBuildConfig;
98
+ /**
99
+ * Get Node.js-specific defaults for config generation
100
+ *
101
+ * Provides default directories (server, public, tmp) that work
102
+ * out of the box for Node.js deployments.
103
+ */
104
+ getDefaults(): PlatformDefaults;
77
105
  /**
78
106
  * Dispose of platform resources
79
107
  */
80
108
  dispose(): Promise<void>;
109
+ /**
110
+ * Get the OS temp directory (Node.js-specific implementation)
111
+ */
112
+ tmpdir(): string;
81
113
  }
82
114
  /**
83
115
  * Default export for easy importing
84
116
  */
85
117
  export default NodePlatform;
118
+ /**
119
+ * Platform's default cache implementation.
120
+ * Re-exported so config can reference: { module: "@b9g/platform-node", export: "DefaultCache" }
121
+ */
122
+ export { MemoryCache as DefaultCache } from "@b9g/cache/memory";
package/src/index.js CHANGED
@@ -1,21 +1,33 @@
1
1
  /// <reference types="./index.d.ts" />
2
2
  // src/index.ts
3
+ import { builtinModules } from "node:module";
4
+ import { tmpdir } from "node:os";
3
5
  import {
4
6
  BasePlatform,
5
7
  ServiceWorkerPool,
6
8
  SingleThreadedRuntime,
7
- CustomLoggerStorage
9
+ CustomLoggerStorage,
10
+ CustomDatabaseStorage,
11
+ createDatabaseFactory
8
12
  } from "@b9g/platform";
13
+ import {
14
+ createCacheFactory,
15
+ createDirectoryFactory
16
+ } from "@b9g/platform/runtime";
9
17
  import { CustomCacheStorage } from "@b9g/cache";
10
- import { CustomDirectoryStorage } from "@b9g/filesystem";
11
18
  import { MemoryCache } from "@b9g/cache/memory";
12
- import { NodeDirectory } from "@b9g/filesystem/node";
19
+ import { CustomDirectoryStorage } from "@b9g/filesystem";
20
+ import { NodeFSDirectory } from "@b9g/filesystem/node-fs";
13
21
  import { InternalServerError, isHTTPError } from "@b9g/http-errors";
14
22
  import * as HTTP from "http";
15
23
  import * as Path from "path";
16
24
  import { getLogger } from "@logtape/logtape";
25
+ import { MemoryCache as MemoryCache2 } from "@b9g/cache/memory";
17
26
  var entryTemplate = `// Node.js Production Server Entry
18
- // This file is imported as text and used as the entry wrapper template
27
+ import {tmpdir} from "os"; // For [tmpdir] config expressions
28
+ import * as HTTP from "http";
29
+ import {parentPort} from "worker_threads";
30
+ import {Worker} from "@b9g/node-webworker";
19
31
  import {getLogger} from "@logtape/logtape";
20
32
  import {configureLogging} from "@b9g/platform/runtime";
21
33
  import {config} from "shovel:config"; // Virtual module - resolved at build time
@@ -24,52 +36,177 @@ import Platform from "@b9g/platform-node";
24
36
  // Configure logging before anything else
25
37
  await configureLogging(config.logging);
26
38
 
27
- const logger = getLogger(["platform"]);
39
+ const logger = getLogger(["shovel", "platform"]);
28
40
 
29
- // Configuration from shovel:config (with process.env fallbacks baked in)
41
+ // Configuration from shovel:config
30
42
  const PORT = config.port;
31
43
  const HOST = config.host;
32
- const WORKER_COUNT = config.workers;
44
+ const WORKERS = config.workers;
33
45
 
34
- logger.info("Starting production server", {});
35
- logger.info("Workers", {count: WORKER_COUNT});
46
+ // Use explicit marker instead of isMainThread
47
+ // This handles the edge case where Shovel's build output is embedded in another worker
48
+ const isShovelWorker = process.env.SHOVEL_SPAWNED_WORKER === "1";
36
49
 
37
- // Create platform instance
38
- const platform = new Platform();
50
+ if (WORKERS === 1) {
51
+ // Single worker mode: worker owns server (same as Bun)
52
+ // No reusePort needed since there's only one listener
53
+ if (isShovelWorker) {
54
+ // Worker thread: runs BOTH server AND ServiceWorker
55
+ const platform = new Platform({port: PORT, host: HOST, workers: 1});
39
56
 
40
- // Get the path to the user's ServiceWorker code
41
- const userCodeURL = new URL("./server.js", import.meta.url);
42
- const userCodePath = userCodeURL.pathname;
57
+ // Track resources for shutdown - these get assigned during startup
58
+ let server;
59
+ let serviceWorker;
43
60
 
44
- // Load ServiceWorker with worker pool
45
- const serviceWorker = await platform.loadServiceWorker(userCodePath, {
46
- workerCount: WORKER_COUNT,
47
- });
61
+ // Register shutdown handler BEFORE async startup to prevent race condition
62
+ // where SIGINT arrives during startup and the shutdown message is dropped
63
+ self.onmessage = async (event) => {
64
+ if (event.data.type === "shutdown") {
65
+ logger.info("Worker shutting down");
66
+ if (server) await server.close();
67
+ if (serviceWorker) await serviceWorker.dispose();
68
+ await platform.dispose();
69
+ postMessage({type: "shutdown-complete"});
70
+ }
71
+ };
48
72
 
49
- // Create HTTP server
50
- const server = platform.createServer(serviceWorker.handleRequest, {
51
- port: PORT,
52
- host: HOST,
53
- });
73
+ const userCodePath = new URL("./server.js", import.meta.url).pathname;
74
+ serviceWorker = await platform.loadServiceWorker(userCodePath);
54
75
 
55
- await server.listen();
56
- logger.info("Server running", {url: \`http://\${HOST}:\${PORT}\`});
57
- logger.info("Load balancing", {workers: WORKER_COUNT});
76
+ server = platform.createServer(serviceWorker.handleRequest, {
77
+ port: PORT,
78
+ host: HOST,
79
+ });
80
+ await server.listen();
58
81
 
59
- // Graceful shutdown
60
- const shutdown = async () => {
61
- logger.info("Shutting down server", {});
62
- await serviceWorker.dispose();
63
- await platform.dispose();
64
- await server.close();
65
- logger.info("Server stopped", {});
66
- process.exit(0);
67
- };
82
+ // Signal ready to main thread
83
+ postMessage({type: "ready"});
84
+ logger.info("Worker started with server", {port: PORT});
85
+ } else {
86
+ // Main thread: supervisor only - spawn single worker
87
+ logger.info("Starting production server (single worker)", {port: PORT});
88
+
89
+ // Port availability check - fail fast if port is in use
90
+ const checkPort = () => new Promise((resolve, reject) => {
91
+ const testServer = HTTP.createServer();
92
+ testServer.once("error", reject);
93
+ testServer.listen(PORT, HOST, () => {
94
+ testServer.close(() => resolve(undefined));
95
+ });
96
+ });
97
+ try {
98
+ await checkPort();
99
+ } catch (err) {
100
+ logger.error("Port unavailable", {port: PORT, host: HOST, error: err});
101
+ process.exit(1);
102
+ }
103
+
104
+ let shuttingDown = false;
105
+
106
+ const worker = new Worker(new URL(import.meta.url), {
107
+ env: {SHOVEL_SPAWNED_WORKER: "1"},
108
+ });
109
+
110
+ worker.onmessage = (event) => {
111
+ if (event.data.type === "ready") {
112
+ logger.info("Worker ready", {port: PORT});
113
+ } else if (event.data.type === "shutdown-complete") {
114
+ logger.info("Worker shutdown complete");
115
+ process.exit(0);
116
+ }
117
+ };
118
+
119
+ worker.onerror = (event) => {
120
+ logger.error("Worker error", {error: event.error});
121
+ };
122
+
123
+ // If a worker crashes, fail fast - let process supervisor handle restarts
124
+ worker.addEventListener("close", (event) => {
125
+ if (shuttingDown) return;
126
+ if (event.code === 0) return; // Clean exit
127
+ logger.error("Worker crashed", {exitCode: event.code});
128
+ process.exit(1);
129
+ });
130
+
131
+ // Graceful shutdown
132
+ const shutdown = () => {
133
+ shuttingDown = true;
134
+ logger.info("Shutting down");
135
+ worker.postMessage({type: "shutdown"});
136
+ };
137
+
138
+ process.on("SIGINT", shutdown);
139
+ process.on("SIGTERM", shutdown);
140
+ }
141
+ } else {
142
+ // Multi-worker mode: main thread owns server (no reusePort in Node.js)
143
+ // Workers handle ServiceWorker code via postMessage
144
+ if (isShovelWorker) {
145
+ // Worker: ServiceWorker only, respond via message loop
146
+ // This path uses the workerEntryTemplate, not this template
147
+ throw new Error("Multi-worker mode uses workerEntryTemplate, not entryTemplate");
148
+ } else {
149
+ // Main thread: HTTP server + dispatch to worker pool
150
+ const platform = new Platform({port: PORT, host: HOST, workers: WORKERS});
151
+ const userCodePath = new URL("./server.js", import.meta.url).pathname;
152
+
153
+ logger.info("Starting production server (multi-worker)", {workers: WORKERS, port: PORT});
154
+
155
+ // Load ServiceWorker with worker pool
156
+ const serviceWorker = await platform.loadServiceWorker(userCodePath, {
157
+ workerCount: WORKERS,
158
+ });
68
159
 
69
- process.on("SIGINT", shutdown);
70
- process.on("SIGTERM", shutdown);
160
+ // Create HTTP server
161
+ const server = platform.createServer(serviceWorker.handleRequest, {
162
+ port: PORT,
163
+ host: HOST,
164
+ });
165
+
166
+ await server.listen();
167
+ logger.info("Server running", {url: \`http://\${HOST}:\${PORT}\`, workers: WORKERS});
168
+
169
+ // Graceful shutdown
170
+ const shutdown = async () => {
171
+ logger.info("Shutting down server");
172
+ await serviceWorker.dispose();
173
+ await platform.dispose();
174
+ await server.close();
175
+ logger.info("Server stopped");
176
+ process.exit(0);
177
+ };
178
+
179
+ process.on("SIGINT", shutdown);
180
+ process.on("SIGTERM", shutdown);
181
+ }
182
+ }
71
183
  `;
72
- var logger = getLogger(["platform"]);
184
+ var workerEntryTemplate = `// Worker Entry for ServiceWorkerPool
185
+ // This file sets up the ServiceWorker runtime and message loop
186
+ import {tmpdir} from "os"; // For [tmpdir] config expressions
187
+ import {config} from "shovel:config";
188
+ import {initWorkerRuntime, startWorkerMessageLoop, configureLogging} from "@b9g/platform/runtime";
189
+
190
+ // Configure logging before anything else
191
+ await configureLogging(config.logging);
192
+
193
+ // Initialize the worker runtime (installs ServiceWorker globals)
194
+ // Platform defaults and paths are already resolved at build time
195
+ const {registration, databases} = await initWorkerRuntime({config});
196
+
197
+ // Import user code (registers event handlers via addEventListener)
198
+ // Must use dynamic import to ensure globals are installed first
199
+ await import("__USER_ENTRY__");
200
+
201
+ // Run ServiceWorker lifecycle
202
+ await registration.install();
203
+ await registration.activate();
204
+
205
+ // Start the message loop (handles request/response messages from main thread)
206
+ // Pass databases for graceful shutdown (close connections before termination)
207
+ startWorkerMessageLoop({registration, databases});
208
+ `;
209
+ var logger = getLogger(["shovel", "platform"]);
73
210
  var NodePlatform = class extends BasePlatform {
74
211
  name;
75
212
  #options;
@@ -77,6 +214,7 @@ var NodePlatform = class extends BasePlatform {
77
214
  #singleThreadedRuntime;
78
215
  #cacheStorage;
79
216
  #directoryStorage;
217
+ #databaseStorage;
80
218
  constructor(options = {}) {
81
219
  super(options);
82
220
  this.name = "node";
@@ -86,7 +224,7 @@ var NodePlatform = class extends BasePlatform {
86
224
  host: options.host ?? "localhost",
87
225
  workers: options.workers ?? 1,
88
226
  cwd,
89
- ...options
227
+ config: options.config
90
228
  };
91
229
  }
92
230
  /**
@@ -121,12 +259,55 @@ var NodePlatform = class extends BasePlatform {
121
259
  */
122
260
  async #loadServiceWorkerDirect(entrypoint, _options) {
123
261
  const entryPath = Path.resolve(this.#options.cwd, entrypoint);
124
- const entryDir = Path.dirname(entryPath);
262
+ let config = this.#options.config;
263
+ const configPath = Path.join(Path.dirname(entryPath), "config.js");
264
+ try {
265
+ const configModule = await import(configPath);
266
+ config = configModule.config ?? config;
267
+ } catch (err) {
268
+ logger.debug`Using platform config (no config.js): ${err}`;
269
+ }
125
270
  if (!this.#cacheStorage) {
126
- this.#cacheStorage = await this.createCaches();
271
+ const runtimeCacheDefaults = {
272
+ default: { impl: MemoryCache }
273
+ };
274
+ const userCaches = config?.caches ?? {};
275
+ const cacheConfigs = {};
276
+ const allCacheNames = /* @__PURE__ */ new Set([
277
+ ...Object.keys(runtimeCacheDefaults),
278
+ ...Object.keys(userCaches)
279
+ ]);
280
+ for (const name of allCacheNames) {
281
+ cacheConfigs[name] = {
282
+ ...runtimeCacheDefaults[name],
283
+ ...userCaches[name]
284
+ };
285
+ }
286
+ this.#cacheStorage = new CustomCacheStorage(
287
+ createCacheFactory({ configs: cacheConfigs })
288
+ );
127
289
  }
128
290
  if (!this.#directoryStorage) {
129
- this.#directoryStorage = this.createDirectories(entryDir);
291
+ const runtimeDirDefaults = {
292
+ server: { impl: NodeFSDirectory },
293
+ public: { impl: NodeFSDirectory },
294
+ tmp: { impl: NodeFSDirectory }
295
+ };
296
+ const userDirs = config?.directories ?? {};
297
+ const dirConfigs = {};
298
+ const allDirNames = /* @__PURE__ */ new Set([
299
+ ...Object.keys(runtimeDirDefaults),
300
+ ...Object.keys(userDirs)
301
+ ]);
302
+ for (const name of allDirNames) {
303
+ dirConfigs[name] = { ...runtimeDirDefaults[name], ...userDirs[name] };
304
+ }
305
+ this.#directoryStorage = new CustomDirectoryStorage(
306
+ createDirectoryFactory(dirConfigs)
307
+ );
308
+ }
309
+ if (!this.#databaseStorage) {
310
+ this.#databaseStorage = this.createDatabases(config);
130
311
  }
131
312
  if (this.#singleThreadedRuntime) {
132
313
  await this.#singleThreadedRuntime.terminate();
@@ -139,7 +320,8 @@ var NodePlatform = class extends BasePlatform {
139
320
  this.#singleThreadedRuntime = new SingleThreadedRuntime({
140
321
  caches: this.#cacheStorage,
141
322
  directories: this.#directoryStorage,
142
- loggers: new CustomLoggerStorage((...cats) => getLogger(cats))
323
+ databases: this.#databaseStorage,
324
+ loggers: new CustomLoggerStorage((cats) => getLogger(cats))
143
325
  });
144
326
  await this.#singleThreadedRuntime.init();
145
327
  await this.#singleThreadedRuntime.load(entryPath);
@@ -180,8 +362,20 @@ var NodePlatform = class extends BasePlatform {
180
362
  */
181
363
  async #loadServiceWorkerWithPool(entrypoint, _options, workerCount) {
182
364
  const entryPath = Path.resolve(this.#options.cwd, entrypoint);
365
+ let config = this.#options.config;
366
+ const configPath = Path.join(Path.dirname(entryPath), "config.js");
367
+ try {
368
+ const configModule = await import(configPath);
369
+ config = configModule.config ?? config;
370
+ } catch (err) {
371
+ logger.debug`Using platform config (no config.js): ${err}`;
372
+ }
183
373
  if (!this.#cacheStorage) {
184
- this.#cacheStorage = await this.createCaches();
374
+ this.#cacheStorage = new CustomCacheStorage(
375
+ createCacheFactory({
376
+ configs: config?.caches ?? {}
377
+ })
378
+ );
185
379
  }
186
380
  if (this.#singleThreadedRuntime) {
187
381
  await this.#singleThreadedRuntime.terminate();
@@ -201,12 +395,9 @@ var NodePlatform = class extends BasePlatform {
201
395
  cwd: this.#options.cwd
202
396
  },
203
397
  entryPath,
204
- this.#cacheStorage,
205
- {}
206
- // Empty config - use defaults
398
+ this.#cacheStorage
207
399
  );
208
400
  await this.#workerPool.init();
209
- await this.#workerPool.reloadWorkers(entryPath);
210
401
  const workerPool = this.#workerPool;
211
402
  const platform = this;
212
403
  const instance = {
@@ -244,26 +435,63 @@ var NodePlatform = class extends BasePlatform {
244
435
  return instance;
245
436
  }
246
437
  /**
247
- * Create cache storage (in-memory by default)
438
+ * Create cache storage using config from shovel.json
439
+ * Used for testing - production uses the generated config module
440
+ * Merges with runtime defaults (actual class references) for fallback behavior
248
441
  */
249
442
  async createCaches() {
250
- return new CustomCacheStorage((name) => new MemoryCache(name));
443
+ const runtimeDefaults = {
444
+ default: { impl: MemoryCache }
445
+ };
446
+ const userCaches = this.#options.config?.caches ?? {};
447
+ const configs = {};
448
+ const allNames = /* @__PURE__ */ new Set([
449
+ ...Object.keys(runtimeDefaults),
450
+ ...Object.keys(userCaches)
451
+ ]);
452
+ for (const name of allNames) {
453
+ configs[name] = { ...runtimeDefaults[name], ...userCaches[name] };
454
+ }
455
+ return new CustomCacheStorage(createCacheFactory({ configs }));
251
456
  }
252
457
  /**
253
- * Create directory storage for the given base directory
458
+ * Create directory storage using config from shovel.json
459
+ * Used for testing - production uses the generated config module
460
+ * Merges with runtime defaults (actual class references) for fallback behavior
254
461
  */
255
- createDirectories(baseDir) {
256
- return new CustomDirectoryStorage((name) => {
257
- let dirPath;
258
- if (name === "static") {
259
- dirPath = Path.resolve(baseDir, "../static");
260
- } else if (name === "server") {
261
- dirPath = baseDir;
262
- } else {
263
- dirPath = Path.resolve(baseDir, `../${name}`);
264
- }
265
- return Promise.resolve(new NodeDirectory(dirPath));
266
- });
462
+ async createDirectories() {
463
+ const runtimeDefaults = {
464
+ server: { impl: NodeFSDirectory, path: this.#options.cwd },
465
+ public: { impl: NodeFSDirectory, path: this.#options.cwd },
466
+ tmp: { impl: NodeFSDirectory, path: tmpdir() }
467
+ };
468
+ const userDirs = this.#options.config?.directories ?? {};
469
+ const configs = {};
470
+ const allNames = /* @__PURE__ */ new Set([
471
+ ...Object.keys(runtimeDefaults),
472
+ ...Object.keys(userDirs)
473
+ ]);
474
+ for (const name of allNames) {
475
+ configs[name] = { ...runtimeDefaults[name], ...userDirs[name] };
476
+ }
477
+ return new CustomDirectoryStorage(createDirectoryFactory(configs));
478
+ }
479
+ /**
480
+ * Create logger storage using config from shovel.json
481
+ */
482
+ async createLoggers() {
483
+ return new CustomLoggerStorage((categories) => getLogger(categories));
484
+ }
485
+ /**
486
+ * Create database storage from declarative config in shovel.json
487
+ */
488
+ createDatabases(configOverride) {
489
+ const config = configOverride ?? this.#options.config;
490
+ if (config?.databases && Object.keys(config.databases).length > 0) {
491
+ const factory = createDatabaseFactory(config.databases);
492
+ return new CustomDatabaseStorage(factory);
493
+ }
494
+ return void 0;
267
495
  }
268
496
  /**
269
497
  * SUPPORTING UTILITY - Create HTTP server for Node.js
@@ -368,15 +596,18 @@ var NodePlatform = class extends BasePlatform {
368
596
  /**
369
597
  * Get virtual entry wrapper for Node.js
370
598
  *
371
- * Returns production server entry template that uses:
372
- * - shovel:config virtual module for configuration
373
- * - Worker threads via ServiceWorkerPool for multi-worker scaling
374
- * - Platform's loadServiceWorker and createServer methods
599
+ * @param entryPath - Absolute path to user's entrypoint file
600
+ * @param options - Entry wrapper options
601
+ * @param options.type - "production" (default) or "worker"
375
602
  *
376
- * The template is a real .ts file (entry-template.ts) for better
377
- * IDE support and linting. It's imported with {type: "text"}.
603
+ * Returns:
604
+ * - "production": Server entry that loads ServiceWorkerPool
605
+ * - "worker": Worker entry that sets up runtime and message loop
378
606
  */
379
- getEntryWrapper(_entryPath, _options) {
607
+ getEntryWrapper(entryPath, options) {
608
+ if (options?.type === "worker") {
609
+ return workerEntryTemplate.replace("__USER_ENTRY__", entryPath);
610
+ }
380
611
  return entryTemplate;
381
612
  }
382
613
  /**
@@ -385,16 +616,49 @@ var NodePlatform = class extends BasePlatform {
385
616
  * Note: Node.js doesn't support import.meta.env natively, so we alias it
386
617
  * to process.env for compatibility with code that uses Vite-style env access.
387
618
  */
388
- getEsbuildConfig() {
619
+ getESBuildConfig() {
389
620
  return {
390
621
  platform: "node",
391
- external: ["node:*"],
622
+ external: ["node:*", ...builtinModules],
392
623
  define: {
393
624
  // Node.js doesn't support import.meta.env, alias to process.env
394
625
  "import.meta.env": "process.env"
395
626
  }
396
627
  };
397
628
  }
629
+ /**
630
+ * Get Node.js-specific defaults for config generation
631
+ *
632
+ * Provides default directories (server, public, tmp) that work
633
+ * out of the box for Node.js deployments.
634
+ */
635
+ getDefaults() {
636
+ return {
637
+ caches: {
638
+ default: {
639
+ module: "@b9g/cache/memory",
640
+ export: "MemoryCache"
641
+ }
642
+ },
643
+ directories: {
644
+ server: {
645
+ module: "@b9g/filesystem/node-fs",
646
+ export: "NodeFSDirectory",
647
+ path: "[outdir]/server"
648
+ },
649
+ public: {
650
+ module: "@b9g/filesystem/node-fs",
651
+ export: "NodeFSDirectory",
652
+ path: "[outdir]/public"
653
+ },
654
+ tmp: {
655
+ module: "@b9g/filesystem/node-fs",
656
+ export: "NodeFSDirectory",
657
+ path: "[tmpdir]"
658
+ }
659
+ }
660
+ };
661
+ }
398
662
  /**
399
663
  * Dispose of platform resources
400
664
  */
@@ -411,10 +675,24 @@ var NodePlatform = class extends BasePlatform {
411
675
  await this.#cacheStorage.dispose();
412
676
  this.#cacheStorage = void 0;
413
677
  }
678
+ if (this.#databaseStorage) {
679
+ await this.#databaseStorage.closeAll();
680
+ this.#databaseStorage = void 0;
681
+ }
682
+ }
683
+ // =========================================================================
684
+ // Config Expression Method Overrides
685
+ // =========================================================================
686
+ /**
687
+ * Get the OS temp directory (Node.js-specific implementation)
688
+ */
689
+ tmpdir() {
690
+ return tmpdir();
414
691
  }
415
692
  };
416
693
  var src_default = NodePlatform;
417
694
  export {
695
+ MemoryCache2 as DefaultCache,
418
696
  NodePlatform,
419
697
  src_default as default
420
698
  };