@webjsdev/server 0.8.27 → 0.8.29
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 +4 -1
- package/src/action-seed-bun.js +67 -0
- package/src/action-seed.js +89 -24
- package/src/dev.js +113 -151
- package/src/file-storage.js +13 -6
- package/src/listener-bun.js +359 -0
- package/src/listener-core.js +245 -0
- package/src/listener-types.js +19 -0
- package/src/node-version.js +9 -0
- package/src/ts-strip.js +157 -0
- package/src/websocket.js +2 -12
package/src/node-version.js
CHANGED
|
@@ -89,6 +89,15 @@ export function requiredNodeMajor() {
|
|
|
89
89
|
* @returns {void}
|
|
90
90
|
*/
|
|
91
91
|
export function assertNodeVersion(opts = {}) {
|
|
92
|
+
// Bun (#508) satisfies webjs's requirements through a different mechanism: the
|
|
93
|
+
// TS strip comes from `amaro` (resolved by ts-strip.js), and `fs.watch` /
|
|
94
|
+
// `node:crypto` are provided by its node-compat layer, even though Bun reports
|
|
95
|
+
// a Node version string. The Node-major gate is a proxy for "the Node built-ins
|
|
96
|
+
// exist", which does not hold on Bun, so skip it there. Only when no explicit
|
|
97
|
+
// `current` is passed (real runtime detection, not a unit test override).
|
|
98
|
+
if (opts.current === undefined && typeof process !== 'undefined' && process.versions && process.versions.bun) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
92
101
|
const current = opts.current ?? process.versions.node;
|
|
93
102
|
const requiredMajor = opts.requiredMajor ?? requiredNodeMajor();
|
|
94
103
|
const onFail = opts.onFail ?? 'throw';
|
package/src/ts-strip.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable TypeScript stripper (#508), the one seam that lets webjs run on both
|
|
3
|
+
* Node and Bun.
|
|
4
|
+
*
|
|
5
|
+
* webjs serves `.ts` / `.mts` source to the browser as JavaScript by ERASING the
|
|
6
|
+
* type syntax in place (position-preserving whitespace replacement, so no
|
|
7
|
+
* sourcemap is shipped and stack traces stay byte-exact). On Node 24+ that is the
|
|
8
|
+
* built-in `module.stripTypeScriptTypes`. Bun has no such built-in, so this
|
|
9
|
+
* module picks the backend at boot:
|
|
10
|
+
*
|
|
11
|
+
* - **Node 24+**: the built-in `module.stripTypeScriptTypes` (zero deps, the
|
|
12
|
+
* default; nothing about the Node path changes).
|
|
13
|
+
* - **Bun (or any runtime lacking the built-in)**: `amaro`, lazily imported.
|
|
14
|
+
* Node's built-in is itself a thin wrapper over `amaro`'s `strip-only` mode,
|
|
15
|
+
* so the output is BYTE-IDENTICAL and equally position-preserving. `amaro` is
|
|
16
|
+
* an `optionalDependency` of `@webjsdev/server`: a Node-only install that
|
|
17
|
+
* prunes optionals still runs (it never loads amaro), while a Bun install
|
|
18
|
+
* gets it.
|
|
19
|
+
*
|
|
20
|
+
* `WEBJS_TS_STRIPPER=builtin|amaro` forces a backend (used by the tests to
|
|
21
|
+
* exercise the amaro path under Node, and an escape hatch).
|
|
22
|
+
*
|
|
23
|
+
* Erasable TypeScript only, either way: non-erasable syntax (`enum`, value
|
|
24
|
+
* `namespace`, parameter properties, legacy decorators, `import = require`)
|
|
25
|
+
* throws at strip time, which the `erasable-typescript-only` /
|
|
26
|
+
* `no-non-erasable-typescript` lint rules catch at edit time.
|
|
27
|
+
*/
|
|
28
|
+
// Namespace import, NOT `import { stripTypeScriptTypes }`. On Node < 22.13 the
|
|
29
|
+
// named export does not exist, and a NAMED import of a missing builtin export is
|
|
30
|
+
// a LINK-TIME SyntaxError that fires before any module body runs, which would
|
|
31
|
+
// defeat the Node-version preflight by crashing the import of @webjsdev/server
|
|
32
|
+
// itself. A namespace import links on every runtime (the property is just
|
|
33
|
+
// `undefined` where absent, e.g. Bun) and we branch on it at runtime instead.
|
|
34
|
+
import * as nodeModule from 'node:module';
|
|
35
|
+
|
|
36
|
+
// Suppress the one-shot ExperimentalWarning Node prints the first time
|
|
37
|
+
// `stripTypeScriptTypes` is called (the API is committed per Node 24's release
|
|
38
|
+
// notes; the warning is a holdover). Installed here, co-located with the strip
|
|
39
|
+
// call, so it covers every consumer of this module (dev.js, the CLI, tests),
|
|
40
|
+
// not just the dev server. Every other warning passes through untouched.
|
|
41
|
+
const _origEmitWarning = process.emitWarning.bind(process);
|
|
42
|
+
process.emitWarning = function (warning, type, code, ctor) {
|
|
43
|
+
const msg = warning && warning.message ? warning.message : String(warning);
|
|
44
|
+
if (
|
|
45
|
+
(type === 'ExperimentalWarning' || (warning && warning.name === 'ExperimentalWarning')) &&
|
|
46
|
+
msg.includes('stripTypeScriptTypes')
|
|
47
|
+
) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
return _origEmitWarning(warning, type, code, ctor);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** @typedef {{ fn: (source: string) => string, name: 'builtin' | 'amaro' }} Stripper */
|
|
54
|
+
|
|
55
|
+
/** @type {Stripper | null} */
|
|
56
|
+
let _strip = null;
|
|
57
|
+
/** @type {Promise<Stripper> | null} */
|
|
58
|
+
let _resolving = null;
|
|
59
|
+
|
|
60
|
+
/** Forced backend from the env, or '' for auto-detect. */
|
|
61
|
+
function forcedBackend() {
|
|
62
|
+
const v = String(process.env.WEBJS_TS_STRIPPER || '').toLowerCase().trim();
|
|
63
|
+
return v === 'builtin' || v === 'amaro' ? v : '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the active stripper backend once. Auto-detects (built-in when present,
|
|
68
|
+
* else amaro); honors a `WEBJS_TS_STRIPPER` override.
|
|
69
|
+
* @returns {Promise<Stripper>}
|
|
70
|
+
*/
|
|
71
|
+
async function resolve() {
|
|
72
|
+
const forced = forcedBackend();
|
|
73
|
+
const hasBuiltin = typeof nodeModule.stripTypeScriptTypes === 'function';
|
|
74
|
+
if (forced !== 'amaro' && (forced === 'builtin' || hasBuiltin)) {
|
|
75
|
+
if (forced === 'builtin' && !hasBuiltin) {
|
|
76
|
+
throw new Error('WEBJS_TS_STRIPPER=builtin but module.stripTypeScriptTypes is unavailable on this runtime.');
|
|
77
|
+
}
|
|
78
|
+
return { fn: (source) => nodeModule.stripTypeScriptTypes(source), name: 'builtin' };
|
|
79
|
+
}
|
|
80
|
+
// amaro backend (Bun, a Node without the built-in, or forced).
|
|
81
|
+
let amaro;
|
|
82
|
+
try {
|
|
83
|
+
amaro = await import('amaro');
|
|
84
|
+
} catch (e) {
|
|
85
|
+
const why = e && e.message ? e.message : String(e);
|
|
86
|
+
throw new Error(
|
|
87
|
+
"webjs needs a TypeScript stripper: this runtime has no built-in " +
|
|
88
|
+
"`module.stripTypeScriptTypes` (e.g. Bun) and the `amaro` fallback failed " +
|
|
89
|
+
"to load. Install `amaro` (it is an optionalDependency of @webjsdev/server) " +
|
|
90
|
+
"to run webjs here. Underlying: " + why,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const tx = amaro.transformSync || (amaro.default && amaro.default.transformSync);
|
|
94
|
+
if (typeof tx !== 'function') {
|
|
95
|
+
throw new Error('webjs: the resolved `amaro` module has no `transformSync` export.');
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
fn: (source) => {
|
|
99
|
+
try {
|
|
100
|
+
return tx(source, { mode: 'strip-only' }).code;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// Normalize amaro's non-erasable-syntax error code to Node's, so a
|
|
103
|
+
// single downstream check (`err.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX'`
|
|
104
|
+
// in dev.js's tsResponse) classifies the failure identically on both
|
|
105
|
+
// runtimes (amaro reports `code: 'UnsupportedSyntax'` for an enum /
|
|
106
|
+
// namespace-with-values / parameter-property / legacy-decorator). The
|
|
107
|
+
// strip-error contract is part of this seam, like the strip itself.
|
|
108
|
+
if (e && e.code === 'UnsupportedSyntax') {
|
|
109
|
+
try { e.code = 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX'; } catch {}
|
|
110
|
+
}
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
name: 'amaro',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve (once, memoized) and return the active stripper. Call at boot so the
|
|
120
|
+
* backend is ready and a missing-amaro error surfaces early rather than on the
|
|
121
|
+
* first `.ts` request.
|
|
122
|
+
* @returns {Promise<Stripper>}
|
|
123
|
+
*/
|
|
124
|
+
export async function ensureStripper() {
|
|
125
|
+
if (_strip) return _strip;
|
|
126
|
+
// Clear `_resolving` on BOTH settle paths so a failed resolve (e.g. a missing
|
|
127
|
+
// amaro on a runtime lacking the built-in) re-detects on the next call instead
|
|
128
|
+
// of caching the rejection forever (self-healing, matching `__resetStripper`).
|
|
129
|
+
if (!_resolving) {
|
|
130
|
+
_resolving = resolve().then(
|
|
131
|
+
(s) => { _strip = s; _resolving = null; return s; },
|
|
132
|
+
(e) => { _resolving = null; throw e; },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return _resolving;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Strip TypeScript types from `source`, resolving the backend on first use.
|
|
140
|
+
* @param {string} source
|
|
141
|
+
* @returns {Promise<string>}
|
|
142
|
+
*/
|
|
143
|
+
export async function stripTypeScript(source) {
|
|
144
|
+
const s = _strip || await ensureStripper();
|
|
145
|
+
return s.fn(source);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The active backend name, or null before resolution (diagnostics / tests). */
|
|
149
|
+
export function stripperName() {
|
|
150
|
+
return _strip ? _strip.name : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Test seam: forget the resolved backend so the next call re-detects. */
|
|
154
|
+
export function __resetStripper() {
|
|
155
|
+
_strip = null;
|
|
156
|
+
_resolving = null;
|
|
157
|
+
}
|
package/src/websocket.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
|
-
import { pathToFileURL } from 'node:url';
|
|
3
2
|
import { matchApi } from './router.js';
|
|
4
3
|
import { urlFromRequest } from './forwarded.js';
|
|
5
4
|
import { registerClient } from './broadcast.js';
|
|
5
|
+
import { loadWsModule } from './listener-core.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* WebSocket support.
|
|
@@ -48,7 +48,7 @@ export function attachWebSocket(server, getRouteTable, opts) {
|
|
|
48
48
|
return reject(socket, 404, 'Not Found');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const mod = await
|
|
51
|
+
const mod = await loadWsModule(match.route.file, opts.dev);
|
|
52
52
|
if (typeof mod.WS !== 'function') {
|
|
53
53
|
return reject(socket, 426, 'Upgrade not supported at this route');
|
|
54
54
|
}
|
|
@@ -109,13 +109,3 @@ function buildRequestFromUpgrade(req, url) {
|
|
|
109
109
|
}
|
|
110
110
|
return new Request(url, { method: 'GET', headers });
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* @param {string} file
|
|
115
|
-
* @param {boolean} dev
|
|
116
|
-
*/
|
|
117
|
-
async function loadModule(file, dev) {
|
|
118
|
-
const url = pathToFileURL(file).toString();
|
|
119
|
-
const bust = dev ? `?t=${Date.now()}-${Math.random().toString(36).slice(2)}` : '';
|
|
120
|
-
return import(url + bust);
|
|
121
|
-
}
|