@b9g/platform-node 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.
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@b9g/platform-node",
3
+ "version": "0.1.0",
4
+ "description": "Node.js platform adapter for Shovel with hot reloading and ESBuild integration",
5
+ "keywords": [
6
+ "shovel",
7
+ "platform",
8
+ "nodejs",
9
+ "adapter",
10
+ "hot-reload",
11
+ "esbuild"
12
+ ],
13
+ "dependencies": {
14
+ "@b9g/platform": "workspace:*",
15
+ "@b9g/cache": "workspace:*",
16
+ "@remix-run/node-fetch-server": "^0.11.0",
17
+ "@aws-sdk/client-s3": "^3.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@b9g/libuild": "^0.1.10",
21
+ "bun-types": "latest",
22
+ "@types/node": "^18.0.0"
23
+ },
24
+ "type": "module",
25
+ "types": "src/index.d.ts",
26
+ "module": "src/index.js",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./src/index.d.ts",
30
+ "import": "./src/index.js"
31
+ },
32
+ "./platform": {
33
+ "types": "./src/platform.d.ts",
34
+ "import": "./src/platform.js"
35
+ },
36
+ "./platform.js": {
37
+ "types": "./src/platform.d.ts",
38
+ "import": "./src/platform.js"
39
+ },
40
+ "./package.json": "./package.json",
41
+ "./index": {
42
+ "types": "./src/index.d.ts",
43
+ "import": "./src/index.js"
44
+ },
45
+ "./index.js": {
46
+ "types": "./src/index.d.ts",
47
+ "import": "./src/index.js"
48
+ }
49
+ }
50
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @b9g/platform-node - Node.js platform adapter for Shovel
3
+ *
4
+ * Provides hot reloading, ESBuild integration, and optimized caching for Node.js environments.
5
+ */
6
+ export { NodePlatform, createNodePlatform, type NodePlatformOptions, } from "./platform.js";
7
+ export type { Platform, CacheConfig, StaticConfig, Handler, Server, ServerOptions, } from "@b9g/platform";
package/src/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /// <reference types="./index.d.ts" />
2
+ // src/index.ts
3
+ import {
4
+ NodePlatform,
5
+ createNodePlatform
6
+ } from "./platform.js";
7
+ export {
8
+ NodePlatform,
9
+ createNodePlatform
10
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Node.js platform implementation - ServiceWorker entrypoint loader for Node.js
3
+ *
4
+ * Handles the complex ESBuild VM system, hot reloading, and module linking
5
+ * to make ServiceWorker-style apps run in Node.js environments.
6
+ */
7
+ import { BasePlatform, PlatformConfig, CacheConfig, Handler, Server, ServerOptions, ServiceWorkerOptions, ServiceWorkerInstance } from "@b9g/platform";
8
+ import { CustomCacheStorage } from "@b9g/cache";
9
+ export interface NodePlatformOptions extends PlatformConfig {
10
+ /** Enable hot reloading (default: true in development) */
11
+ hotReload?: boolean;
12
+ /** Port for development server (default: 3000) */
13
+ port?: number;
14
+ /** Host for development server (default: localhost) */
15
+ host?: string;
16
+ /** Working directory for file resolution */
17
+ cwd?: string;
18
+ }
19
+ /**
20
+ * Node.js platform implementation
21
+ * ServiceWorker entrypoint loader for Node.js with ESBuild VM system
22
+ */
23
+ export declare class NodePlatform extends BasePlatform {
24
+ readonly name = "node";
25
+ private options;
26
+ private workerManager?;
27
+ private cacheStorage?;
28
+ private _dist?;
29
+ constructor(options?: NodePlatformOptions);
30
+ /**
31
+ * Build artifacts filesystem (install-time only)
32
+ */
33
+ get distDir(): FileSystemDirectoryHandle;
34
+ /**
35
+ * THE MAIN JOB - Load and run a ServiceWorker-style entrypoint in Node.js
36
+ * Uses Worker threads with coordinated cache storage for isolation and standards compliance
37
+ */
38
+ loadServiceWorker(entrypoint: string, options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
39
+ /**
40
+ * Get platform-specific default cache configuration for Node.js
41
+ */
42
+ protected getDefaultCacheConfig(): CacheConfig;
43
+ /**
44
+ * SUPPORTING UTILITY - Create cache storage optimized for Node.js
45
+ * Now uses the base class implementation with dynamic loading
46
+ */
47
+ createCaches(config?: CacheConfig): Promise<CustomCacheStorage>;
48
+ /**
49
+ * SUPPORTING UTILITY - Create HTTP server for Node.js
50
+ */
51
+ createServer(handler: Handler, options?: ServerOptions): Server;
52
+ /**
53
+ * Get filesystem root for File System Access API
54
+ */
55
+ getFileSystemRoot(name?: string): Promise<FileSystemDirectoryHandle>;
56
+ /**
57
+ * Dispose of platform resources
58
+ */
59
+ dispose(): Promise<void>;
60
+ }
61
+ /**
62
+ * Create a Node.js platform instance
63
+ */
64
+ export declare function createNodePlatform(options?: NodePlatformOptions): NodePlatform;
65
+ /**
66
+ * Default export for easy importing
67
+ */
68
+ export default createNodePlatform;
@@ -0,0 +1,350 @@
1
+ /// <reference types="./platform.d.ts" />
2
+ // src/platform.ts
3
+ import {
4
+ BasePlatform
5
+ } from "@b9g/platform";
6
+ import { CustomCacheStorage, MemoryCacheManager, PostMessageCache } from "@b9g/cache";
7
+ import { FileSystemRegistry, getFileSystemRoot, NodeFileSystemAdapter, NodeFileSystemDirectoryHandle } from "@b9g/filesystem";
8
+ import * as Http from "http";
9
+ import * as Path from "path";
10
+ import { Worker } from "worker_threads";
11
+ import { fileURLToPath } from "url";
12
+ var __filename = fileURLToPath(import.meta.url);
13
+ var __dirname = Path.dirname(__filename);
14
+ var WorkerManager = class {
15
+ constructor(cacheStorage, options, workerCount = 1, entrypoint) {
16
+ this.entrypoint = entrypoint;
17
+ this.memoryCacheManager = new MemoryCacheManager();
18
+ this.options = options;
19
+ console.info(
20
+ "[WorkerManager] Constructor called with entrypoint:",
21
+ entrypoint
22
+ );
23
+ this.initWorkers(workerCount);
24
+ }
25
+ workers = [];
26
+ currentWorker = 0;
27
+ requestId = 0;
28
+ pendingRequests = /* @__PURE__ */ new Map();
29
+ memoryCacheManager;
30
+ options;
31
+ initWorkers(count) {
32
+ for (let i = 0; i < count; i++) {
33
+ this.createWorker();
34
+ }
35
+ }
36
+ createWorker() {
37
+ let workerScript;
38
+ try {
39
+ const workerUrl = import.meta.resolve("@b9g/shovel/worker.js");
40
+ workerScript = fileURLToPath(workerUrl);
41
+ } catch (error) {
42
+ throw new Error(
43
+ `Could not resolve @b9g/shovel/worker.js: ${error.message}`
44
+ );
45
+ }
46
+ const worker = new Worker(workerScript);
47
+ worker.on("message", (message) => {
48
+ if (message.type?.startsWith("cache:")) {
49
+ this.memoryCacheManager.handleMessage(worker, message);
50
+ } else {
51
+ this.handleWorkerMessage(message);
52
+ }
53
+ });
54
+ worker.on("error", (error) => {
55
+ console.error("[Platform-Node] Worker error:", error);
56
+ });
57
+ this.workers.push(worker);
58
+ return worker;
59
+ }
60
+ handleWorkerMessage(message) {
61
+ if (message.type === "response" && message.requestId) {
62
+ const pending = this.pendingRequests.get(message.requestId);
63
+ if (pending) {
64
+ const response = new Response(message.response.body, {
65
+ status: message.response.status,
66
+ statusText: message.response.statusText,
67
+ headers: message.response.headers
68
+ });
69
+ pending.resolve(response);
70
+ this.pendingRequests.delete(message.requestId);
71
+ }
72
+ } else if (message.type === "error" && message.requestId) {
73
+ const pending = this.pendingRequests.get(message.requestId);
74
+ if (pending) {
75
+ pending.reject(new Error(message.error));
76
+ this.pendingRequests.delete(message.requestId);
77
+ }
78
+ } else if (message.type === "ready") {
79
+ console.info(`[Platform-Node] ServiceWorker ready (v${message.version})`);
80
+ } else if (message.type === "worker-ready") {
81
+ console.info("[Platform-Node] Worker initialized");
82
+ }
83
+ }
84
+ /**
85
+ * Handle HTTP request using round-robin Worker selection
86
+ */
87
+ async handleRequest(request) {
88
+ const worker = this.workers[this.currentWorker];
89
+ console.info(
90
+ `[WorkerManager] Dispatching to worker ${this.currentWorker} of ${this.workers.length}`
91
+ );
92
+ this.currentWorker = (this.currentWorker + 1) % this.workers.length;
93
+ const requestId = ++this.requestId;
94
+ return new Promise((resolve2, reject) => {
95
+ this.pendingRequests.set(requestId, { resolve: resolve2, reject });
96
+ worker.postMessage({
97
+ type: "request",
98
+ request: {
99
+ url: request.url,
100
+ method: request.method,
101
+ headers: Object.fromEntries(request.headers.entries()),
102
+ body: request.body
103
+ },
104
+ requestId
105
+ });
106
+ setTimeout(() => {
107
+ if (this.pendingRequests.has(requestId)) {
108
+ this.pendingRequests.delete(requestId);
109
+ reject(new Error("Request timeout"));
110
+ }
111
+ }, 3e4);
112
+ });
113
+ }
114
+ /**
115
+ * Reload ServiceWorker with new version (hot reload simulation)
116
+ */
117
+ async reloadWorkers(version = Date.now()) {
118
+ console.info(`[Platform-Node] Reloading ServiceWorker (v${version})`);
119
+ const loadPromises = this.workers.map((worker) => {
120
+ return new Promise((resolve2) => {
121
+ const handleReady = (message) => {
122
+ if (message.type === "ready" && message.version === version) {
123
+ worker.off("message", handleReady);
124
+ resolve2();
125
+ }
126
+ };
127
+ console.info("[Platform-Node] Sending load message:", {
128
+ version,
129
+ entrypoint: this.entrypoint
130
+ });
131
+ worker.on("message", handleReady);
132
+ worker.postMessage({
133
+ type: "load",
134
+ version,
135
+ entrypoint: this.entrypoint
136
+ });
137
+ });
138
+ });
139
+ await Promise.all(loadPromises);
140
+ console.info(`[Platform-Node] All Workers reloaded (v${version})`);
141
+ }
142
+ /**
143
+ * Graceful shutdown
144
+ */
145
+ async terminate() {
146
+ const terminatePromises = this.workers.map((worker) => worker.terminate());
147
+ await Promise.allSettled(terminatePromises);
148
+ await this.memoryCacheManager.dispose();
149
+ this.workers = [];
150
+ this.pendingRequests.clear();
151
+ }
152
+ };
153
+ var NodePlatform = class extends BasePlatform {
154
+ name = "node";
155
+ options;
156
+ workerManager;
157
+ cacheStorage;
158
+ _dist;
159
+ constructor(options = {}) {
160
+ super(options);
161
+ this.options = {
162
+ hotReload: process.env.NODE_ENV !== "production",
163
+ port: 3e3,
164
+ host: "localhost",
165
+ cwd: process.cwd(),
166
+ ...options
167
+ };
168
+ FileSystemRegistry.register("node", new NodeFileSystemAdapter({
169
+ rootPath: this.options.cwd
170
+ }));
171
+ }
172
+ /**
173
+ * Build artifacts filesystem (install-time only)
174
+ */
175
+ get distDir() {
176
+ if (!this._dist) {
177
+ const distPath = Path.resolve(this.options.cwd, "dist");
178
+ this._dist = new NodeFileSystemDirectoryHandle(distPath);
179
+ }
180
+ return this._dist;
181
+ }
182
+ /**
183
+ * THE MAIN JOB - Load and run a ServiceWorker-style entrypoint in Node.js
184
+ * Uses Worker threads with coordinated cache storage for isolation and standards compliance
185
+ */
186
+ async loadServiceWorker(entrypoint, options = {}) {
187
+ const entryPath = Path.resolve(this.options.cwd, entrypoint);
188
+ if (!this.cacheStorage) {
189
+ this.cacheStorage = await this.createCaches(options.caches);
190
+ }
191
+ if (this.workerManager) {
192
+ await this.workerManager.terminate();
193
+ }
194
+ const workerCount = options.workerCount || 1;
195
+ console.info(
196
+ "[Platform-Node] Creating WorkerManager with entryPath:",
197
+ entryPath
198
+ );
199
+ this.workerManager = new WorkerManager(
200
+ this.cacheStorage,
201
+ this.options,
202
+ workerCount,
203
+ entryPath
204
+ );
205
+ const version = Date.now();
206
+ await this.workerManager.reloadWorkers(version);
207
+ const instance = {
208
+ runtime: this.workerManager,
209
+ handleRequest: async (request) => {
210
+ if (!this.workerManager) {
211
+ throw new Error("WorkerManager not initialized");
212
+ }
213
+ return this.workerManager.handleRequest(request);
214
+ },
215
+ install: async () => {
216
+ console.info(
217
+ "[Platform-Node] ServiceWorker installed via Worker threads"
218
+ );
219
+ },
220
+ activate: async () => {
221
+ console.info(
222
+ "[Platform-Node] ServiceWorker activated via Worker threads"
223
+ );
224
+ },
225
+ collectStaticRoutes: async () => {
226
+ return [];
227
+ },
228
+ get ready() {
229
+ return this.workerManager !== void 0;
230
+ },
231
+ dispose: async () => {
232
+ if (this.workerManager) {
233
+ await this.workerManager.terminate();
234
+ this.workerManager = void 0;
235
+ }
236
+ console.info("[Platform-Node] ServiceWorker disposed");
237
+ }
238
+ };
239
+ console.info(
240
+ "[Platform-Node] ServiceWorker loaded with Worker threads and coordinated caches"
241
+ );
242
+ return instance;
243
+ }
244
+ /**
245
+ * Get platform-specific default cache configuration for Node.js
246
+ */
247
+ getDefaultCacheConfig() {
248
+ return {
249
+ pages: { type: "memory" },
250
+ // PostMessage cache for worker coordination
251
+ api: { type: "memory" },
252
+ static: { type: "memory" }
253
+ };
254
+ }
255
+ /**
256
+ * SUPPORTING UTILITY - Create cache storage optimized for Node.js
257
+ * Now uses the base class implementation with dynamic loading
258
+ */
259
+ async createCaches(config) {
260
+ const cacheStorage = await super.createCaches(config);
261
+ return new CustomCacheStorage((name) => {
262
+ return new PostMessageCache(name, {
263
+ maxEntries: 1e3,
264
+ maxSize: 50 * 1024 * 1024
265
+ // 50MB
266
+ });
267
+ });
268
+ }
269
+ /**
270
+ * SUPPORTING UTILITY - Create HTTP server for Node.js
271
+ */
272
+ createServer(handler, options = {}) {
273
+ const port = options.port ?? this.options.port;
274
+ const host = options.host ?? this.options.host;
275
+ const httpServer = Http.createServer(async (req, res) => {
276
+ try {
277
+ const url = `http://${req.headers.host}${req.url}`;
278
+ const request = new Request(url, {
279
+ method: req.method,
280
+ headers: req.headers,
281
+ body: req.method !== "GET" && req.method !== "HEAD" ? req : void 0
282
+ });
283
+ const response = await handler(request);
284
+ res.statusCode = response.status;
285
+ res.statusMessage = response.statusText;
286
+ response.headers.forEach((value, key) => {
287
+ res.setHeader(key, value);
288
+ });
289
+ if (response.body) {
290
+ const reader = response.body.getReader();
291
+ const pump = async () => {
292
+ const { done, value } = await reader.read();
293
+ if (done) {
294
+ res.end();
295
+ } else {
296
+ res.write(value);
297
+ await pump();
298
+ }
299
+ };
300
+ await pump();
301
+ } else {
302
+ res.end();
303
+ }
304
+ } catch (error) {
305
+ console.error("[Platform-Node] Request error:", error);
306
+ res.statusCode = 500;
307
+ res.setHeader("Content-Type", "text/plain");
308
+ res.end("Internal Server Error");
309
+ }
310
+ });
311
+ return {
312
+ listen: () => {
313
+ return new Promise((resolve2) => {
314
+ httpServer.listen(port, host, () => {
315
+ console.info(`\u{1F680} Server running at http://${host}:${port}`);
316
+ resolve2();
317
+ });
318
+ });
319
+ },
320
+ close: () => new Promise((resolve2) => {
321
+ httpServer.close(() => resolve2());
322
+ }),
323
+ address: () => ({ port, host })
324
+ };
325
+ }
326
+ /**
327
+ * Get filesystem root for File System Access API
328
+ */
329
+ async getFileSystemRoot(name = "default") {
330
+ return await getFileSystemRoot(name);
331
+ }
332
+ /**
333
+ * Dispose of platform resources
334
+ */
335
+ async dispose() {
336
+ if (this.workerManager) {
337
+ await this.workerManager.terminate();
338
+ this.workerManager = void 0;
339
+ }
340
+ }
341
+ };
342
+ function createNodePlatform(options) {
343
+ return new NodePlatform(options);
344
+ }
345
+ var platform_default = createNodePlatform;
346
+ export {
347
+ NodePlatform,
348
+ createNodePlatform,
349
+ platform_default as default
350
+ };