@solidjs/start 2.0.0-beta.8 → 2.0.0-rc.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @solidjs/start
2
2
 
3
+ ## 2.0.0-rc.1
4
+
5
+ ### Minor Changes
6
+
7
+ - 02e561e: Expose experimental `decorateHandler` and `decorateMiddleware` helpers from
8
+ `@solidjs/start/server` for providing Solid's request context to custom h3 handlers and
9
+ middleware.
10
+
11
+ ### Patch Changes
12
+
13
+ - f66b5a9: fix: keep non-picked route exports that picked exports reference instead of deleting their bindings, so API handlers can call helpers exported from the same file (#2100); also fixes picked exports declared via `export { ... }` specifiers being dropped (#1659)
14
+ - 1763296: fix: run `onBeforeResponse` middleware in declared order (#2131)
15
+
16
+ Applications that reversed their `onBeforeResponse` arrays as a workaround should restore the intended declaration order.
17
+
18
+ - cd98e7d: Fix API route matching when `server.baseURL` is configured.
19
+
20
+ ## 2.0.0-beta.10
21
+
22
+ ### Patch Changes
23
+
24
+ - 8af6304: Support for Vite 8.1 Experimental Bundled Dev Mode
25
+ - 9d91484: fix: don't crash when a server function throws an error whose message contains non-latin1 characters.
26
+ - 068b64c: Return a cancellation-safe web `ReadableStream` for streaming SSR in development. Returning Solid's
27
+ raw stream only rendered on Node; Bun and Deno coerced it to `[object Object]`.
28
+
29
+ ## 2.0.0-beta.9
30
+
31
+ ### Minor Changes
32
+
33
+ - f3e84ec: Add support for `server-only` and `client-only` modules
34
+
35
+ ### Patch Changes
36
+
37
+ - a8a2328: fix: mark `@solidjs/start/http` and `@solidjs/start/middleware` as `server-only` so importing them from client-reachable code fails loudly at dev/build time, instead of silently shipping them to the browser where they crashed hydration and broke unrelated actions/forms with no diagnostic (#2068)
38
+
3
39
  ## 2.0.0-beta.8
4
40
 
5
41
  ### Patch Changes
@@ -108,7 +144,6 @@
108
144
  - 8256190: Rework `@solidjs/start/env`
109
145
  - 6cbba24: Fix multiple Set-Cookie headers being lost on redirect responses
110
146
  - d4cc548: ## Bump Seroval
111
-
112
147
  - version `1.4.1`
113
148
 
114
149
  - dd40610: Handle base url in api routes
@@ -0,0 +1,14 @@
1
+ import type { Plugin } from "vite";
2
+ /**
3
+ * Supports `server-only` and `client-only` marker modules (#2162): importing
4
+ * `server-only` from a client module (or `client-only` from a server module)
5
+ * fails at resolve time; in the allowed environment the marker resolves to an
6
+ * empty module.
7
+ *
8
+ * Start's own server-only entry points (`@solidjs/start/http`,
9
+ * `@solidjs/start/middleware`) import `server-only` themselves, so pulling
10
+ * them into the client bundle fails loudly instead of shipping server code to
11
+ * the browser, where it crashed hydration and broke unrelated actions/forms
12
+ * with no diagnostic (https://github.com/solidjs/solid-start/issues/2068).
13
+ */
14
+ export declare function boundaryModules(): Plugin;
@@ -0,0 +1,41 @@
1
+ const VIRTUAL_ID = "\0solid-start:boundary-modules:id";
2
+ /**
3
+ * Supports `server-only` and `client-only` marker modules (#2162): importing
4
+ * `server-only` from a client module (or `client-only` from a server module)
5
+ * fails at resolve time; in the allowed environment the marker resolves to an
6
+ * empty module.
7
+ *
8
+ * Start's own server-only entry points (`@solidjs/start/http`,
9
+ * `@solidjs/start/middleware`) import `server-only` themselves, so pulling
10
+ * them into the client bundle fails loudly instead of shipping server code to
11
+ * the browser, where it crashed hydration and broke unrelated actions/forms
12
+ * with no diagnostic (https://github.com/solidjs/solid-start/issues/2068).
13
+ */
14
+ export function boundaryModules() {
15
+ return {
16
+ name: "solid-start:boundary-modules",
17
+ enforce: "pre",
18
+ resolveId(id, importer, { ssr }) {
19
+ if (id === "server-only") {
20
+ if (!ssr)
21
+ this.error(`Attempt to import 'server-only' in a client module: ${importer}. ` +
22
+ `Code that uses this module must run only on the server: mark it with ` +
23
+ `"use server", or make sure it is only imported by server code.`);
24
+ }
25
+ else if (id === "client-only") {
26
+ if (ssr)
27
+ this.error(`Attempt to import 'client-only' in a server module: ${importer}. ` +
28
+ `Code that uses this module must run only in the browser: make sure it ` +
29
+ `is only imported by client code (e.g. wrap components with clientOnly()).`);
30
+ }
31
+ else {
32
+ return null;
33
+ }
34
+ return VIRTUAL_ID;
35
+ },
36
+ load(id) {
37
+ if (id === VIRTUAL_ID)
38
+ return "export {}";
39
+ },
40
+ };
41
+ }
@@ -1,4 +1,5 @@
1
1
  import { loadEnv } from "vite";
2
+ import { parseIdQuery } from "./utils.js";
2
3
  const LOADERS = {
3
4
  node: `export default key => process.env[key];`,
4
5
  "cloudflare-workers": `import { env } from 'cloudflare:workers';export default key => env[key];`,
@@ -39,17 +40,19 @@ export function envPlugin(options) {
39
40
  env = config.mode !== "production" ? "development" : "production";
40
41
  },
41
42
  resolveId(id) {
42
- if (id === SERVER_ENV ||
43
- id === CLIENT_ENV ||
44
- id === SERVER_RUNTIME_ENV ||
45
- id === SERVER_RUNTIME_LOADER) {
43
+ const { filename } = parseIdQuery(id);
44
+ if (filename === SERVER_ENV ||
45
+ filename === CLIENT_ENV ||
46
+ filename === SERVER_RUNTIME_ENV ||
47
+ filename === SERVER_RUNTIME_LOADER) {
46
48
  return id;
47
49
  }
48
50
  return undefined;
49
51
  },
50
52
  load(id) {
53
+ const { filename } = parseIdQuery(id);
51
54
  const isServer = this.environment.config.consumer === "server";
52
- if (id === SERVER_ENV) {
55
+ if (filename === SERVER_ENV) {
53
56
  if (!isServer) {
54
57
  return SERVER_ONLY_MODULE;
55
58
  }
@@ -58,19 +61,19 @@ export function envPlugin(options) {
58
61
  : loadEnv(env, '.', serverPrefix);
59
62
  return convertObjectToModule(vars);
60
63
  }
61
- if (id === CLIENT_ENV) {
64
+ if (filename === CLIENT_ENV) {
62
65
  const vars = currentOptions.client?.load
63
66
  ? currentOptions.client.load(env)
64
67
  : loadEnv(env, '.', clientPrefix);
65
68
  return convertObjectToModule(vars);
66
69
  }
67
- if (id === SERVER_RUNTIME_LOADER) {
70
+ if (filename === SERVER_RUNTIME_LOADER) {
68
71
  if (!isServer) {
69
72
  return SERVER_ONLY_MODULE;
70
73
  }
71
74
  return runtimeCode;
72
75
  }
73
- if (id === SERVER_RUNTIME_ENV) {
76
+ if (filename === SERVER_RUNTIME_ENV) {
74
77
  if (!isServer) {
75
78
  return SERVER_ONLY_MODULE;
76
79
  }
@@ -20,13 +20,28 @@ function treeShakeTransform({ types: t }) {
20
20
  }
21
21
  return path.node.id && path.node.id.type === "Identifier" ? path.get("id") : null;
22
22
  }
23
- function isIdentifierReferenced(ident) {
23
+ function isTypeOnlyReference(path) {
24
+ return path.getAncestry().some(parent => {
25
+ if (parent.isTSType())
26
+ return true;
27
+ if (parent.isExportNamedDeclaration())
28
+ return parent.node.exportKind === "type";
29
+ if (parent.isExportSpecifier())
30
+ return parent.node.exportKind === "type";
31
+ return false;
32
+ });
33
+ }
34
+ function runtimeReferences(binding) {
35
+ return binding.referencePaths.filter(ref => !isTypeOnlyReference(ref));
36
+ }
37
+ function isIdentifierReferenced(ident, includeTypes = false) {
24
38
  const b = ident.scope.getBinding(ident.node.name);
25
- if (b?.referenced) {
39
+ if (b) {
40
+ const references = b.constantViolations.concat(includeTypes ? b.referencePaths : runtimeReferences(b));
41
+ if (!references.length)
42
+ return false;
26
43
  if (b.path.type === "FunctionDeclaration") {
27
- return !b.constantViolations
28
- .concat(b.referencePaths)
29
- .every(ref => ref.findParent(p => p === b.path));
44
+ return !references.every(ref => ref.findParent(p => p === b.path));
30
45
  }
31
46
  return true;
32
47
  }
@@ -34,13 +49,13 @@ function treeShakeTransform({ types: t }) {
34
49
  }
35
50
  function markFunction(path, state) {
36
51
  const ident = getIdentifier(path);
37
- if (ident && ident.node && isIdentifierReferenced(ident)) {
52
+ if (ident && ident.node && isIdentifierReferenced(ident, true)) {
38
53
  state.refs.add(ident);
39
54
  }
40
55
  }
41
56
  function markImport(path, state) {
42
57
  const local = path.get("local");
43
- if (isIdentifierReferenced(local)) {
58
+ if (isIdentifierReferenced(local, true)) {
44
59
  state.refs.add(local);
45
60
  }
46
61
  }
@@ -49,12 +64,13 @@ function treeShakeTransform({ types: t }) {
49
64
  Program: {
50
65
  enter(path, state) {
51
66
  state.refs = new Set();
67
+ state.candidates = new Set();
52
68
  state.done = false;
53
69
  path.traverse({
54
70
  VariableDeclarator(variablePath, variableState) {
55
71
  if (variablePath.node.id.type === "Identifier") {
56
72
  const local = variablePath.get("id");
57
- if (isIdentifierReferenced(local)) {
73
+ if (isIdentifierReferenced(local, true)) {
58
74
  variableState.refs.add(local);
59
75
  }
60
76
  }
@@ -69,7 +85,7 @@ function treeShakeTransform({ types: t }) {
69
85
  : (() => {
70
86
  throw new Error("invariant");
71
87
  })());
72
- if (isIdentifierReferenced(local)) {
88
+ if (isIdentifierReferenced(local, true)) {
73
89
  variableState.refs.add(local);
74
90
  }
75
91
  });
@@ -88,29 +104,41 @@ function treeShakeTransform({ types: t }) {
88
104
  else {
89
105
  return;
90
106
  }
91
- if (isIdentifierReferenced(local)) {
107
+ if (isIdentifierReferenced(local, true)) {
92
108
  variableState.refs.add(local);
93
109
  }
94
110
  });
95
111
  }
96
112
  },
97
113
  ExportDefaultDeclaration(exportNamedPath) {
98
- // if opts.keep is true, we don't remove the routeData export
99
114
  if (state.opts.pick && !state.opts.pick.includes("default")) {
100
- exportNamedPath.remove();
115
+ const decl = exportNamedPath.node.declaration;
116
+ // A named function/class declaration creates a module-scope
117
+ // binding that picked exports may reference, so only strip
118
+ // the `export default`; the sweep below removes the
119
+ // declaration again if nothing references it.
120
+ if ((t.isFunctionDeclaration(decl) || t.isClassDeclaration(decl)) && decl.id) {
121
+ const [newPath] = exportNamedPath.replaceWith(decl);
122
+ const id = newPath.get("id");
123
+ state.refs.add(id);
124
+ state.candidates.add(id);
125
+ }
126
+ else {
127
+ exportNamedPath.remove();
128
+ }
101
129
  }
102
130
  },
103
131
  ExportNamedDeclaration(exportNamedPath) {
104
- // if opts.keep is false, we don't remove the routeData export
132
+ // No pick list means nothing gets pruned.
105
133
  if (!state.opts.pick) {
106
134
  return;
107
135
  }
108
136
  const specifiers = exportNamedPath.get("specifiers");
109
137
  if (specifiers.length) {
110
138
  specifiers.forEach(s => {
111
- if (t.isIdentifier(s.node.exported)
112
- ? s.node.exported.name
113
- : state.opts.pick.includes(s.node.exported.value)) {
139
+ const exported = s.node.exported;
140
+ const exportedName = t.isIdentifier(exported) ? exported.name : exported.value;
141
+ if (!state.opts.pick.includes(exportedName)) {
114
142
  s.remove();
115
143
  }
116
144
  });
@@ -124,24 +152,51 @@ function treeShakeTransform({ types: t }) {
124
152
  return;
125
153
  }
126
154
  switch (decl.node.type) {
127
- case "FunctionDeclaration": {
155
+ case "FunctionDeclaration":
156
+ case "ClassDeclaration": {
128
157
  const name = decl.node.id?.name;
129
158
  if (name && state.opts.pick && !state.opts.pick.includes(name)) {
130
- exportNamedPath.remove();
159
+ // Keep the declaration in module scope since picked
160
+ // exports may reference it; the sweep below removes it
161
+ // again if unreferenced.
162
+ const [newPath] = exportNamedPath.replaceWith(decl.node);
163
+ const id = newPath.get("id");
164
+ state.refs.add(id);
165
+ state.candidates.add(id);
131
166
  }
132
167
  break;
133
168
  }
134
169
  case "VariableDeclaration": {
135
- const inner = decl.get("declarations");
136
- inner.forEach(d => {
137
- if (d.node.id.type !== "Identifier") {
138
- return;
139
- }
140
- const name = d.node.id.name;
141
- if (state.opts.pick && !state.opts.pick.includes(name)) {
142
- d.remove();
143
- }
170
+ const declNode = decl.node;
171
+ // Destructuring declarators are conservatively treated as
172
+ // picked (matching the previous behavior of leaving them
173
+ // untouched).
174
+ const isPicked = (d) => d.id.type !== "Identifier" || state.opts.pick.includes(d.id.name);
175
+ if (declNode.declarations.every(isPicked)) {
176
+ break;
177
+ }
178
+ // Split into one declaration per declarator (preserving
179
+ // evaluation order) and export only the picked ones.
180
+ // Unpicked bindings are kept since picked exports may
181
+ // reference them; the sweep below removes them again if
182
+ // unreferenced.
183
+ const statements = declNode.declarations.map(d => {
184
+ const single = t.variableDeclaration(declNode.kind, [d]);
185
+ single.declare = declNode.declare;
186
+ return isPicked(d) ? t.exportNamedDeclaration(single, []) : single;
144
187
  });
188
+ const newPaths = exportNamedPath.replaceWithMultiple(statements);
189
+ for (const p of newPaths) {
190
+ if (!p.isVariableDeclaration())
191
+ continue;
192
+ for (const d of p.get("declarations")) {
193
+ if (d.node.id.type === "Identifier") {
194
+ const id = d.get("id");
195
+ state.refs.add(id);
196
+ state.candidates.add(id);
197
+ }
198
+ }
199
+ }
145
200
  break;
146
201
  }
147
202
  default: {
@@ -152,6 +207,8 @@ function treeShakeTransform({ types: t }) {
152
207
  FunctionDeclaration: markFunction,
153
208
  FunctionExpression: markFunction,
154
209
  ArrowFunctionExpression: markFunction,
210
+ ClassDeclaration: markFunction,
211
+ ClassExpression: markFunction,
155
212
  ImportSpecifier: markImport,
156
213
  ImportDefaultSpecifier: markImport,
157
214
  ImportNamespaceSpecifier: markImport,
@@ -163,11 +220,95 @@ function treeShakeTransform({ types: t }) {
163
220
  }
164
221
  },
165
222
  }, state);
223
+ // References alone cannot distinguish live bindings from dead cycles.
224
+ // Build a graph of localized exports and retain only those reachable
225
+ // from runtime code outside that graph.
226
+ path.scope.crawl();
227
+ const tracked = new Map();
228
+ const candidateOwners = new Map();
229
+ const addOwner = (owners, binding, owner) => {
230
+ const paths = owners.get(binding) ?? new Set();
231
+ paths.add(owner);
232
+ owners.set(binding, paths);
233
+ };
234
+ for (const identifier of state.refs) {
235
+ if (identifier.node?.type !== "Identifier" || !identifier.parentPath)
236
+ continue;
237
+ const binding = identifier.scope.getBinding(identifier.node.name);
238
+ if (binding)
239
+ addOwner(tracked, binding, identifier.parentPath);
240
+ }
241
+ for (const identifier of state.candidates) {
242
+ if (!identifier.parentPath)
243
+ continue;
244
+ const binding = identifier.scope.getBinding(identifier.node.name);
245
+ if (!binding)
246
+ continue;
247
+ addOwner(tracked, binding, identifier.parentPath);
248
+ addOwner(candidateOwners, binding, identifier.parentPath);
249
+ }
250
+ const dependencies = new Map();
251
+ const live = new Set();
252
+ for (const [binding, owners] of tracked) {
253
+ dependencies.set(binding, new Set());
254
+ if ([...owners].some(owner => owner.findParent(parent => parent.isExportNamedDeclaration() || parent.isExportDefaultDeclaration()))) {
255
+ live.add(binding);
256
+ }
257
+ }
258
+ const ownerBindings = new Map();
259
+ for (const [binding, owners] of tracked) {
260
+ for (const owner of owners) {
261
+ ownerBindings.set(owner, binding);
262
+ }
263
+ }
264
+ const owningBinding = (reference) => {
265
+ const owner = reference.find(parent => ownerBindings.has(parent));
266
+ return owner && ownerBindings.get(owner);
267
+ };
268
+ for (const target of tracked.keys()) {
269
+ const references = target.constantViolations.concat(runtimeReferences(target));
270
+ for (const reference of references) {
271
+ const source = owningBinding(reference);
272
+ if (source)
273
+ dependencies.get(source)?.add(target);
274
+ else
275
+ live.add(target);
276
+ }
277
+ }
278
+ const pending = [...live];
279
+ while (pending.length) {
280
+ const source = pending.pop();
281
+ for (const dependency of dependencies.get(source) ?? []) {
282
+ if (live.has(dependency))
283
+ continue;
284
+ live.add(dependency);
285
+ pending.push(dependency);
286
+ }
287
+ }
288
+ const deadCandidates = new Set([...candidateOwners.keys()].filter(candidate => !live.has(candidate)));
289
+ path.traverse({
290
+ VariableDeclarator(variablePath) {
291
+ if (variablePath.node.id.type !== "Identifier")
292
+ return;
293
+ const binding = variablePath.scope.getBinding(variablePath.node.id.name);
294
+ if (binding && deadCandidates.has(binding))
295
+ variablePath.remove();
296
+ },
297
+ });
298
+ for (const [candidate, owners] of candidateOwners) {
299
+ if (!deadCandidates.has(candidate))
300
+ continue;
301
+ for (const owner of owners) {
302
+ if (!owner.removed && !owner.isVariableDeclarator())
303
+ owner.remove();
304
+ }
305
+ }
166
306
  const refs = state.refs;
307
+ const refNodes = new Set([...refs].map(ref => ref.node));
167
308
  let count = 0;
168
309
  const sweepFunction = (sweepPath) => {
169
310
  const ident = getIdentifier(sweepPath);
170
- if (ident && ident.node && refs.has(ident) && !isIdentifierReferenced(ident)) {
311
+ if (ident && ident.node && refNodes.has(ident.node) && !isIdentifierReferenced(ident)) {
171
312
  ++count;
172
313
  if (t.isAssignmentExpression(sweepPath.parentPath) ||
173
314
  t.isVariableDeclarator(sweepPath.parentPath)) {
@@ -180,7 +321,7 @@ function treeShakeTransform({ types: t }) {
180
321
  };
181
322
  function sweepImport(sweepPath) {
182
323
  const local = sweepPath.get("local");
183
- if (refs.has(local) && !isIdentifierReferenced(local)) {
324
+ if (refNodes.has(local.node) && !isIdentifierReferenced(local)) {
184
325
  ++count;
185
326
  sweepPath.remove();
186
327
  if (sweepPath.parent.specifiers.length === 0) {
@@ -195,7 +336,7 @@ function treeShakeTransform({ types: t }) {
195
336
  VariableDeclarator(variablePath) {
196
337
  if (variablePath.node.id.type === "Identifier") {
197
338
  const local = variablePath.get("id");
198
- if (refs.has(local) && !isIdentifierReferenced(local)) {
339
+ if (refNodes.has(local.node) && !isIdentifierReferenced(local)) {
199
340
  ++count;
200
341
  variablePath.remove();
201
342
  }
@@ -212,7 +353,7 @@ function treeShakeTransform({ types: t }) {
212
353
  : (() => {
213
354
  throw new Error("invariant");
214
355
  })());
215
- if (refs.has(local) && !isIdentifierReferenced(local)) {
356
+ if (refNodes.has(local.node) && !isIdentifierReferenced(local)) {
216
357
  ++count;
217
358
  p.remove();
218
359
  }
@@ -236,7 +377,7 @@ function treeShakeTransform({ types: t }) {
236
377
  else {
237
378
  return;
238
379
  }
239
- if (refs.has(local) && !isIdentifierReferenced(local)) {
380
+ if (refNodes.has(local.node) && !isIdentifierReferenced(local)) {
240
381
  ++count;
241
382
  e.remove();
242
383
  }
@@ -249,6 +390,8 @@ function treeShakeTransform({ types: t }) {
249
390
  FunctionDeclaration: sweepFunction,
250
391
  FunctionExpression: sweepFunction,
251
392
  ArrowFunctionExpression: sweepFunction,
393
+ ClassDeclaration: sweepFunction,
394
+ ClassExpression: sweepFunction,
252
395
  ImportSpecifier: sweepImport,
253
396
  ImportDefaultSpecifier: sweepImport,
254
397
  ImportNamespaceSpecifier: sweepImport,
@@ -1,8 +1,9 @@
1
1
  import { defu } from "defu";
2
2
  import { globSync } from "node:fs";
3
- import { extname, isAbsolute, join } from "node:path";
3
+ import { basename, extname, isAbsolute, join } from "node:path";
4
4
  import solid from "vite-plugin-solid";
5
5
  import { serverFunctionsPlugin } from "../directives/index.js";
6
+ import { boundaryModules } from "./boundary-modules.js";
6
7
  import { DEFAULT_EXTENSIONS, VIRTUAL_MODULES, VITE_ENVIRONMENTS } from "./constants.js";
7
8
  import { devServer } from "./dev-server.js";
8
9
  import { envPlugin } from "./env.js";
@@ -60,8 +61,11 @@ export function solidStart(options) {
60
61
  },
61
62
  };
62
63
  },
63
- async config(_, env) {
64
+ async config(config, env) {
64
65
  const clientInput = [handlers.client];
66
+ const clientEntryUrl = env.command === "serve" && config.experimental?.bundledDev
67
+ ? `assets/${basename(handlers.client, entryExtension)}.js`
68
+ : handlers.client;
65
69
  if (env.command === "build") {
66
70
  const clientRouter = globalThis.ROUTERS.client;
67
71
  for (const route of await clientRouter.getRoutes()) {
@@ -148,7 +152,9 @@ export function solidStart(options) {
148
152
  // esbuild receives a valid JS string literal for the define value
149
153
  "import.meta.env.START_APP_ENTRY": JSON.stringify(appEntryPath),
150
154
  "import.meta.env.START_CLIENT_ENTRY": JSON.stringify(handlers.client),
155
+ "import.meta.env.START_CLIENT_ENTRY_URL": JSON.stringify(clientEntryUrl),
151
156
  "import.meta.env.START_DEV_OVERLAY": JSON.stringify(start.devOverlay),
157
+ "import.meta.env.SERVER_BASE_URL": JSON.stringify(config.server?.baseURL ?? ""),
152
158
  "import.meta.env.SEROVAL_MODE": JSON.stringify(start.serialization?.mode || "json"),
153
159
  },
154
160
  builder: {
@@ -185,6 +191,29 @@ export function solidStart(options) {
185
191
  }),
186
192
  lazy(),
187
193
  envPlugin(options?.env),
194
+ boundaryModules(),
195
+ {
196
+ name: "solid-start:boundary-modules",
197
+ enforce: "pre",
198
+ resolveId(id, importer, { ssr }) {
199
+ if (id === "server-only") {
200
+ if (!ssr)
201
+ this.error(`Attempt to import 'server-only' in a client module: ${importer}`);
202
+ }
203
+ else if (id === "client-only") {
204
+ if (ssr)
205
+ this.error(`Attempt to import 'client-only' in a server module: ${importer}`);
206
+ }
207
+ else {
208
+ return null;
209
+ }
210
+ return "\0solid-start:boundary-modules:id";
211
+ },
212
+ load(id) {
213
+ if (id === "\0solid-start:boundary-modules:id")
214
+ return "export {}";
215
+ },
216
+ },
188
217
  {
189
218
  name: "solid-start:virtual-modules",
190
219
  async resolveId(id) {
@@ -144,7 +144,7 @@ export async function handleServerFunction(h3Event) {
144
144
  }
145
145
  else if (instance) {
146
146
  const error = x instanceof Error ? x.message : typeof x === "string" ? x : "true";
147
- h3Event.res.headers.set("X-Error", error.replace(/[\r\n]+/g, ""));
147
+ h3Event.res.headers.set("X-Error", toHeaderValue(error));
148
148
  }
149
149
  else {
150
150
  x = handleNoJS(x, request, parsed, true);
@@ -158,7 +158,7 @@ export async function handleServerFunction(h3Event) {
158
158
  headers.set("X-Error", errorHeader);
159
159
  }
160
160
  return new Response(body.body, {
161
- headers: body.headers,
161
+ headers,
162
162
  });
163
163
  }
164
164
  h3Event.res.headers.set(BODY_FORMAT_KEY, BodyFormat.Seroval);
@@ -172,6 +172,17 @@ export async function handleServerFunction(h3Event) {
172
172
  return x;
173
173
  }
174
174
  }
175
+ // header values must be ByteString-safe (no chars > 0xFF), otherwise Headers.set throws
176
+ function toHeaderValue(value) {
177
+ const stripped = value.replace(/[\r\n]+/g, "");
178
+ try {
179
+ return /[^\x00-\xFF]/.test(stripped) ? encodeURIComponent(stripped) : stripped;
180
+ }
181
+ catch {
182
+ // encodeURIComponent throws on lone surrogates
183
+ return "true";
184
+ }
185
+ }
175
186
  function handleNoJS(result, request, parsed, thrown) {
176
187
  const url = new URL(request.url);
177
188
  const isError = result instanceof Error;