svelte-adapter-uws 0.2.6 → 0.2.7

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
@@ -170,7 +170,10 @@ That's it. This gives you a pub/sub WebSocket server at `/ws` with no authentica
170
170
 
171
171
  ### Step 2: Add the Vite plugin
172
172
 
173
- This makes WebSockets work during `npm run dev`. Without this, `event.platform` won't have WebSocket methods in dev mode.
173
+ The Vite plugin does two things:
174
+
175
+ 1. **Dev mode** - spins up a WebSocket server so `event.platform` works during `npm run dev`
176
+ 2. **Production builds** - runs your `hooks.ws` file through the same Vite pipeline as the rest of your server code, so `$lib`, `$env`, and `$app` imports work correctly
174
177
 
175
178
  **vite.config.js**
176
179
  ```js
@@ -258,6 +261,8 @@ The client store automatically uses `wss://` when the page is served over HTTPS
258
261
 
259
262
  Development works as expected. The Vite plugin (`svelte-adapter-uws/vite`) spins up a `ws` WebSocket server alongside Vite's dev server, so your client store and `event.platform` work identically to production.
260
263
 
264
+ The same plugin also handles production builds - it runs your `hooks.ws` file through the Vite pipeline so that `$lib`, `$env`, and `$app` imports are resolved correctly and shared with the rest of your server code.
265
+
261
266
  **vite.config.js**
262
267
  ```js
263
268
  import { sveltekit } from '@sveltejs/kit/vite';
@@ -270,8 +275,9 @@ export default {
270
275
 
271
276
  Without the Vite plugin:
272
277
  - HTTP routes work fine
273
- - `event.platform` is `undefined` - any code calling `platform.publish()` will throw
274
- - The client store will try to connect to `/ws` and fail silently (auto-reconnect will keep trying)
278
+ - `event.platform` is `undefined` in dev - any code calling `platform.publish()` will throw
279
+ - The client store will try to connect to `/ws` in dev and fail silently (auto-reconnect will keep trying)
280
+ - Production builds will fall back to a limited bundler that only resolves `$lib` (not `$env` or `$app`)
275
281
 
276
282
  ### `npm run preview` - WebSockets don't work
277
283
 
@@ -1193,12 +1199,12 @@ If a worker crashes, it is automatically restarted with exponential backoff. On
1193
1199
 
1194
1200
  ### WebSocket + clustering
1195
1201
 
1196
- `platform.publish()` is automatically relayed across all workers via the primary thread, so subscribers on any worker receive the message. This is built in no external pub/sub needed.
1202
+ `platform.publish()` is automatically relayed across all workers via the primary thread, so subscribers on any worker receive the message. This is built in - no external pub/sub needed.
1197
1203
 
1198
1204
  Per-worker limitations (acceptable for most apps):
1199
- - `platform.connections` returns the count for the local worker only
1200
- - `platform.subscribers(topic)` returns the count for the local worker only
1201
- - `platform.sendTo(filter, ...)` only reaches connections on the local worker
1205
+ - `platform.connections` - returns the count for the local worker only
1206
+ - `platform.subscribers(topic)` - returns the count for the local worker only
1207
+ - `platform.sendTo(filter, ...)` - only reaches connections on the local worker
1202
1208
 
1203
1209
  ---
1204
1210
 
@@ -1254,7 +1260,7 @@ The two largest WebSocket costs were `JSON.parse()` on every message for the sub
1254
1260
  | Layer | Before | After | How |
1255
1261
  |---|---|---|---|
1256
1262
  | Subscribe/unsubscribe check | ~15% | ~0% | Byte-prefix discriminator: control messages start with `{"ty` (byte[3]=`y`), user envelopes start with `{"to` (byte[3]=`o`). A single byte comparison skips `JSON.parse` for all regular messages -- from 0.39us to 0.001us per message. |
1257
- | Envelope wrapping | ~8% | ~4.5% | String template with `esc()` validation instead of `JSON.stringify` on a wrapper object. Topic and event names are validated with a fast char scan (~10ns) that throws on quotes, backslashes, or control characters only `data` is stringified. From 0.135us to ~0.085us per publish. |
1263
+ | Envelope wrapping | ~8% | ~4.5% | String template with `esc()` validation instead of `JSON.stringify` on a wrapper object. Topic and event names are validated with a fast char scan (~10ns) that throws on quotes, backslashes, or control characters - only `data` is stringified. From 0.135us to ~0.085us per publish. |
1258
1264
  | Connection tracking | ~2% | ~2% | Unchanged |
1259
1265
  | Origin validation, upgrade headers | ~2% | ~2% | Unchanged |
1260
1266
 
package/files/handler.js CHANGED
@@ -26,7 +26,7 @@ class PayloadTooLargeError extends Error {
26
26
 
27
27
  /**
28
28
  * Safely quote a string for JSON embedding. Topics and events are
29
- * developer-defined identifiers a quote, backslash, or control character
29
+ * developer-defined identifiers - a quote, backslash, or control character
30
30
  * is always a bug, so we throw instead of silently escaping.
31
31
  * @param {string} s
32
32
  * @returns {string} JSON-quoted string, e.g. '"todos"'
package/files/index.js CHANGED
@@ -56,7 +56,7 @@ if (is_primary) {
56
56
  workers.set(worker, msg.descriptor);
57
57
  acceptorApp.addChildAppDescriptor(msg.descriptor);
58
58
  console.log(`Worker thread ${worker.threadId} registered`);
59
- // Worker started successfully reset backoff
59
+ // Worker started successfully - reset backoff
60
60
  restart_delay = 0;
61
61
  if (backoff_reset_timer) { clearTimeout(backoff_reset_timer); backoff_reset_timer = null; }
62
62
  } else if (msg.type === 'publish') {
@@ -137,7 +137,7 @@ if (is_primary) {
137
137
  // Single-process mode (no clustering)
138
138
  start(host, parseInt(port, 10));
139
139
  } else {
140
- // Worker thread register with acceptor, don't listen
140
+ // Worker thread - register with acceptor, don't listen
141
141
  parentPort.postMessage({ type: 'descriptor', descriptor: getDescriptor() });
142
142
 
143
143
  parentPort.on('message', (msg) => {
package/index.js CHANGED
@@ -72,70 +72,53 @@ export default function (opts = {}) {
72
72
 
73
73
  // Write the WebSocket handler module
74
74
  if (websocket) {
75
- // Resolve the handler: explicit path > auto-discovered > built-in default
76
- let handlerFile = websocket.handler;
77
-
78
- if (!handlerFile) {
79
- // Auto-discover src/hooks.ws.{js,ts,mjs}
80
- const candidates = ['src/hooks.ws.js', 'src/hooks.ws.ts', 'src/hooks.ws.mjs'];
81
- for (const candidate of candidates) {
82
- if (existsSync(candidate)) {
83
- handlerFile = candidate;
84
- break;
75
+ // If the Vite plugin was used, ws-handler.js is already in the
76
+ // writeServer output - built through the same Vite pipeline as
77
+ // hooks.server.ts, with $lib/$env/$app resolved and shared modules.
78
+ if (existsSync(`${tmp}/ws-handler.js`)) {
79
+ builder.log.minor('WebSocket handler: built by Vite plugin');
80
+ } else {
81
+ // Vite plugin not installed - resolve handler ourselves
82
+ let handlerFile = websocket.handler;
83
+
84
+ if (!handlerFile) {
85
+ const candidates = ['src/hooks.ws.js', 'src/hooks.ws.ts', 'src/hooks.ws.mjs'];
86
+ for (const candidate of candidates) {
87
+ if (existsSync(candidate)) {
88
+ handlerFile = candidate;
89
+ break;
90
+ }
85
91
  }
86
92
  }
87
- }
88
-
89
- if (handlerFile) {
90
- // Bundle through esbuild to resolve SvelteKit aliases ($lib, $env, $app)
91
- // and handle TypeScript. Without this, these imports survive into
92
- // the Rollup step which doesn't know about SvelteKit virtual modules.
93
- const esbuild = await import('esbuild');
94
- const { loadEnv } = await import('vite');
95
- const libDir = path.resolve(builder.config.kit.files?.lib || 'src/lib');
96
- const publicPrefix = builder.config.kit.env?.publicPrefix ?? 'PUBLIC_';
97
- const allEnv = loadEnv('production', process.cwd(), '');
98
- const version = builder.config.kit.version?.name ?? '';
99
93
 
100
- await esbuild.build({
101
- entryPoints: [path.resolve(handlerFile)],
102
- bundle: true,
103
- format: 'esm',
104
- platform: 'node',
105
- outfile: `${tmp}/ws-handler.js`,
106
- alias: { '$lib': libDir },
107
- packages: 'external',
108
- plugins: [{
109
- name: 'sveltekit-virtual-modules',
110
- setup(build) {
111
- build.onResolve({ filter: /^\$(env|app)\// }, (args) => ({
112
- path: args.path,
113
- namespace: 'sveltekit'
114
- }));
115
- build.onLoad({ filter: /.*/, namespace: 'sveltekit' }, (args) => {
116
- if (args.path === '$app/environment') {
117
- return { contents: `export const dev = false;\nexport const building = false;\nexport const version = ${JSON.stringify(version)};` };
118
- }
119
- const isPublic = args.path.includes('/public');
120
- const isStatic = args.path.includes('/static');
121
- const entries = Object.entries(allEnv).filter(([k]) =>
122
- (isPublic ? k.startsWith(publicPrefix) : !k.startsWith(publicPrefix))
123
- && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k)
124
- );
125
- if (isStatic) {
126
- return { contents: entries.map(([k, v]) => `export const ${k} = ${JSON.stringify(v)};`).join('\n') || 'export {};' };
127
- }
128
- // dynamic: re-export process.env directly
129
- return { contents: 'export const env = process.env;' };
130
- });
131
- }
132
- }]
133
- });
134
- builder.log.minor(`WebSocket handler: ${handlerFile}`);
135
- } else {
136
- // No handler found - use built-in default (subscribe/unsubscribe only)
137
- writeFileSync(`${tmp}/ws-handler.js`, DEFAULT_WS_HANDLER);
138
- builder.log.minor('WebSocket enabled (built-in handler)');
94
+ if (handlerFile) {
95
+ // Bundle through esbuild to resolve $lib and handle TypeScript.
96
+ // This is the fallback path - the Vite plugin approach is preferred
97
+ // because it shares modules with the server bundle and resolves
98
+ // all SvelteKit aliases ($env, $app) natively.
99
+ const esbuild = await import('esbuild');
100
+ const libDir = path.resolve(builder.config.kit.files?.lib || 'src/lib');
101
+
102
+ await esbuild.build({
103
+ entryPoints: [path.resolve(handlerFile)],
104
+ bundle: true,
105
+ format: 'esm',
106
+ platform: 'node',
107
+ outfile: `${tmp}/ws-handler.js`,
108
+ alias: { '$lib': libDir },
109
+ packages: 'external'
110
+ });
111
+ builder.log.minor(`WebSocket handler: ${handlerFile} (esbuild fallback)`);
112
+ builder.log.warn(
113
+ 'Add the Vite plugin for full SvelteKit alias support in hooks.ws:\n' +
114
+ " import uwsDev from 'svelte-adapter-uws/vite';\n" +
115
+ ' export default { plugins: [sveltekit(), uwsDev()] };'
116
+ );
117
+ } else {
118
+ // No handler found - use built-in default (subscribe/unsubscribe only)
119
+ writeFileSync(`${tmp}/ws-handler.js`, DEFAULT_WS_HANDLER);
120
+ builder.log.minor('WebSocket enabled (built-in handler)');
121
+ }
139
122
  }
140
123
  } else {
141
124
  // No WebSocket - empty module
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-adapter-uws",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "SvelteKit adapter for uWebSockets.js - high-performance C++ HTTP server with built-in WebSocket support",
5
5
  "author": "Kevin Radziszewski",
6
6
  "license": "MIT",
package/vite.js CHANGED
@@ -5,7 +5,7 @@ import { parseCookies } from './files/cookies.js';
5
5
 
6
6
  /**
7
7
  * Safely quote a string for JSON embedding. Throws on invalid characters
8
- * (quotes, backslashes, control chars) these are always bugs in topic/event names.
8
+ * (quotes, backslashes, control chars) - these are always bugs in topic/event names.
9
9
  * @param {string} s
10
10
  * @returns {string}
11
11
  */
@@ -187,8 +187,43 @@ export default function uwsDev(options = {}) {
187
187
  };
188
188
  }
189
189
 
190
+ /**
191
+ * Discover the WS handler file path.
192
+ * @param {string} root
193
+ * @returns {string | null}
194
+ */
195
+ function discoverHandler(root) {
196
+ if (options.handler) return path.resolve(root, options.handler);
197
+ const candidates = ['src/hooks.ws.js', 'src/hooks.ws.ts', 'src/hooks.ws.mjs'];
198
+ for (const candidate of candidates) {
199
+ const full = path.resolve(root, candidate);
200
+ if (existsSync(full)) return full;
201
+ }
202
+ return null;
203
+ }
204
+
190
205
  return {
191
206
  name: 'svelte-adapter-uws',
207
+ config(config, env) {
208
+ // During SSR build, inject ws-handler as an additional entry so it
209
+ // goes through the same Vite pipeline as hooks.server.ts - resolving
210
+ // $lib, $env, $app aliases and sharing modules with the server bundle.
211
+ if (env.isSsrBuild) {
212
+ const root = config.root || process.cwd();
213
+ const handlerPath = discoverHandler(root);
214
+ if (handlerPath) {
215
+ return {
216
+ build: {
217
+ rollupOptions: {
218
+ input: {
219
+ 'ws-handler': handlerPath
220
+ }
221
+ }
222
+ }
223
+ };
224
+ }
225
+ }
226
+ },
192
227
  configureServer(server) {
193
228
  wss = new WebSocketServer({ noServer: true });
194
229
  const root = server.config.root;