@solidjs/start 2.0.0-rc.1 → 2.0.0-rc.10

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.
Files changed (118) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/dist/config/app-root-alias.d.ts +7 -0
  3. package/dist/config/app-root-alias.js +58 -0
  4. package/dist/config/constants.d.ts +2 -0
  5. package/dist/config/constants.js +2 -0
  6. package/dist/config/env.js +2 -2
  7. package/dist/config/fs-routes/index.js +3 -3
  8. package/dist/config/fs-routes/tree-shake.d.ts +27 -0
  9. package/dist/config/fs-routes/tree-shake.js +51 -1
  10. package/dist/config/index.d.ts +154 -5
  11. package/dist/config/index.js +16 -8
  12. package/dist/config/lazy.js +7 -2
  13. package/dist/config/manifest.d.ts +3 -0
  14. package/dist/config/manifest.js +26 -4
  15. package/dist/directives/remove-unused-variables.js +15 -3
  16. package/dist/directives/xxhash32.js +5 -13
  17. package/dist/fns/client.js +14 -5
  18. package/dist/fns/error-handler.d.ts +34 -0
  19. package/dist/fns/error-handler.js +10 -0
  20. package/dist/fns/handler.d.ts +1 -1
  21. package/dist/fns/handler.js +45 -10
  22. package/dist/fns/plugins.d.ts +34 -0
  23. package/dist/fns/plugins.js +33 -0
  24. package/dist/fns/serialization.d.ts +15 -0
  25. package/dist/fns/serialization.js +36 -6
  26. package/dist/server/assets/render.jsx +1 -1
  27. package/dist/server/collect-styles.d.ts +3 -1
  28. package/dist/server/collect-styles.js +8 -5
  29. package/dist/server/handler.d.ts +3 -4
  30. package/dist/server/handler.js +16 -46
  31. package/dist/server/index.d.ts +2 -1
  32. package/dist/server/manifest/dev-client-manifest.js +1 -1
  33. package/dist/server/manifest/dev-ssr-manifest.js +1 -1
  34. package/dist/server/spa/StartServer.jsx +1 -1
  35. package/dist/server/spa/handler.d.ts +2 -3
  36. package/dist/server/spa/handler.js +2 -2
  37. package/dist/server/types.d.ts +12 -1
  38. package/dist/server/web-stream.d.ts +1 -1
  39. package/dist/server/web-stream.js +5 -2
  40. package/dist/shared/ErrorBoundary.jsx +3 -3
  41. package/dist/shared/clientOnly.js +3 -1
  42. package/dist/shared/{dev-overlay → dev-toolbar/error-viewer}/CodeView.jsx +3 -3
  43. package/dist/shared/{dev-overlay/DevOverlayDialog.d.ts → dev-toolbar/error-viewer/index.d.ts} +3 -2
  44. package/dist/shared/{dev-overlay/DevOverlayDialog.jsx → dev-toolbar/error-viewer/index.jsx} +66 -77
  45. package/dist/shared/dev-toolbar/error-viewer/styles.css +224 -0
  46. package/dist/shared/dev-toolbar/functions/BlobViewer.css +4 -0
  47. package/dist/shared/dev-toolbar/functions/BlobViewer.d.ts +6 -0
  48. package/dist/shared/dev-toolbar/functions/BlobViewer.jsx +37 -0
  49. package/dist/shared/dev-toolbar/functions/FormDataViewer.d.ts +5 -0
  50. package/dist/shared/dev-toolbar/functions/FormDataViewer.jsx +23 -0
  51. package/dist/shared/dev-toolbar/functions/HeadersViewer.css +8 -0
  52. package/dist/shared/dev-toolbar/functions/HeadersViewer.d.ts +6 -0
  53. package/dist/shared/dev-toolbar/functions/HeadersViewer.jsx +15 -0
  54. package/dist/shared/dev-toolbar/functions/HexViewer.css +34 -0
  55. package/dist/shared/dev-toolbar/functions/HexViewer.d.ts +11 -0
  56. package/dist/shared/dev-toolbar/functions/HexViewer.jsx +70 -0
  57. package/dist/shared/dev-toolbar/functions/SerovalValue.css +5 -0
  58. package/dist/shared/dev-toolbar/functions/SerovalValue.d.ts +7 -0
  59. package/dist/shared/dev-toolbar/functions/SerovalValue.jsx +10 -0
  60. package/dist/shared/dev-toolbar/functions/SerovalViewer.css +70 -0
  61. package/dist/shared/dev-toolbar/functions/SerovalViewer.d.ts +6 -0
  62. package/dist/shared/dev-toolbar/functions/SerovalViewer.jsx +796 -0
  63. package/dist/shared/dev-toolbar/functions/URLSearchParamsViewer.d.ts +5 -0
  64. package/dist/shared/dev-toolbar/functions/URLSearchParamsViewer.jsx +22 -0
  65. package/dist/shared/dev-toolbar/functions/index.d.ts +13 -0
  66. package/dist/shared/dev-toolbar/functions/index.jsx +216 -0
  67. package/dist/shared/dev-toolbar/functions/styles.css +109 -0
  68. package/dist/shared/dev-toolbar/functions/tracker.d.ts +19 -0
  69. package/dist/shared/dev-toolbar/functions/tracker.js +29 -0
  70. package/dist/shared/{dev-overlay → dev-toolbar}/icons.d.ts +9 -0
  71. package/dist/shared/{dev-overlay → dev-toolbar}/icons.jsx +19 -1
  72. package/dist/shared/dev-toolbar/index.css +88 -0
  73. package/dist/shared/dev-toolbar/index.d.ts +6 -0
  74. package/dist/shared/dev-toolbar/index.jsx +179 -0
  75. package/dist/shared/ui/Badge.css +33 -0
  76. package/dist/shared/ui/Badge.d.ts +7 -0
  77. package/dist/shared/ui/Badge.jsx +7 -0
  78. package/dist/shared/ui/Button.css +32 -0
  79. package/dist/shared/ui/Button.d.ts +4 -0
  80. package/dist/shared/ui/Button.jsx +4 -0
  81. package/dist/shared/ui/Cascade.css +29 -0
  82. package/dist/shared/ui/Cascade.d.ts +4 -0
  83. package/dist/shared/ui/Cascade.jsx +4 -0
  84. package/dist/shared/ui/Dialog.css +25 -0
  85. package/dist/shared/ui/Dialog.d.ts +5 -0
  86. package/dist/shared/ui/Dialog.jsx +5 -0
  87. package/dist/shared/ui/IconButton.css +39 -0
  88. package/dist/shared/ui/IconButton.d.ts +4 -0
  89. package/dist/shared/ui/IconButton.jsx +4 -0
  90. package/dist/shared/ui/Placeholder.css +12 -0
  91. package/dist/shared/ui/Placeholder.d.ts +6 -0
  92. package/dist/shared/ui/Placeholder.jsx +4 -0
  93. package/dist/shared/ui/Section.css +18 -0
  94. package/dist/shared/ui/Section.d.ts +9 -0
  95. package/dist/shared/ui/Section.jsx +10 -0
  96. package/dist/shared/ui/Select.css +37 -0
  97. package/dist/shared/ui/Select.d.ts +4 -0
  98. package/dist/shared/ui/Select.jsx +4 -0
  99. package/dist/shared/ui/Table.css +22 -0
  100. package/dist/shared/ui/Table.d.ts +11 -0
  101. package/dist/shared/ui/Table.jsx +13 -0
  102. package/dist/shared/ui/Tabs.css +51 -0
  103. package/dist/shared/ui/Tabs.d.ts +6 -0
  104. package/dist/shared/ui/Tabs.jsx +6 -0
  105. package/dist/shared/ui/Text.css +73 -0
  106. package/dist/shared/ui/Text.d.ts +12 -0
  107. package/dist/shared/ui/Text.jsx +17 -0
  108. package/package.json +36 -29
  109. package/dist/shared/dev-overlay/index.d.ts +0 -5
  110. package/dist/shared/dev-overlay/index.jsx +0 -39
  111. package/dist/shared/dev-overlay/styles.css +0 -292
  112. /package/dist/shared/{dev-overlay → dev-toolbar/error-viewer}/CodeView.d.ts +0 -0
  113. /package/dist/shared/{dev-overlay/createStackFrame.d.ts → dev-toolbar/error-viewer/create-stack-frame.d.ts} +0 -0
  114. /package/dist/shared/{dev-overlay/createStackFrame.js → dev-toolbar/error-viewer/create-stack-frame.js} +0 -0
  115. /package/dist/shared/{dev-overlay → dev-toolbar/error-viewer}/download.d.ts +0 -0
  116. /package/dist/shared/{dev-overlay → dev-toolbar/error-viewer}/download.js +0 -0
  117. /package/dist/shared/{dev-overlay → dev-toolbar/error-viewer}/get-source-map.d.ts +0 -0
  118. /package/dist/shared/{dev-overlay → dev-toolbar/error-viewer}/get-source-map.js +0 -0
@@ -1,12 +1,17 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { version as viteVersion } from "vite";
3
+ import { createFilter, version as viteVersion } from "vite";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { findStylesInModuleGraph } from "../server/collect-styles.js";
6
6
  import { VIRTUAL_MODULES } from "./constants.js";
7
7
  import { wrapId } from "./vite-utils.js";
8
+ const DEFAULT_STYLE_EXCLUDE = /node_modules/;
9
+ export function createStyleFilter(options) {
10
+ return createFilter(options?.include || [], options?.exclude || DEFAULT_STYLE_EXCLUDE);
11
+ }
8
12
  export function manifest(start) {
9
13
  let devServer = undefined;
14
+ const styleFilter = createStyleFilter(start.css?.filter);
10
15
  return {
11
16
  name: "solid-start:manifest-plugin",
12
17
  enforce: "pre",
@@ -28,6 +33,18 @@ export function manifest(start) {
28
33
  return await this.resolve(start.middleware);
29
34
  return `\0${VIRTUAL_MODULES.middleware}`;
30
35
  }
36
+ if (id === VIRTUAL_MODULES.serovalPlugins) {
37
+ const plugins = start.serialization?.plugins;
38
+ if (plugins)
39
+ return await this.resolve(plugins);
40
+ return `\0${VIRTUAL_MODULES.serovalPlugins}`;
41
+ }
42
+ if (id === VIRTUAL_MODULES.serverFnErrorHandler) {
43
+ const onError = start.serverFunctions?.onError;
44
+ if (onError)
45
+ return await this.resolve(onError);
46
+ return `\0${VIRTUAL_MODULES.serverFnErrorHandler}`;
47
+ }
31
48
  },
32
49
  async load(id) {
33
50
  if (id === `\0${VIRTUAL_MODULES.clientViteManifest}`) {
@@ -40,7 +57,7 @@ export function manifest(start) {
40
57
  if (!entry)
41
58
  throw new Error("No client entry found");
42
59
  let rawManifest;
43
- const viteMajor = parseInt(viteVersion.split('.')[0], 10);
60
+ const viteMajor = parseInt(viteVersion.split(".")[0], 10);
44
61
  const manifestKey = Object.keys(globalThis.START_CLIENT_BUNDLE).find(k => k.endsWith("manifest.json"));
45
62
  if (manifestKey && viteMajor < 8) {
46
63
  const manifestAsset = globalThis.START_CLIENT_BUNDLE[manifestKey];
@@ -53,7 +70,8 @@ export function manifest(start) {
53
70
  if (devServer?.environments?.client?.config?.build?.outDir) {
54
71
  outDir = devServer.environments.client.config.build.outDir;
55
72
  }
56
- else if (this.environment?.config?.build?.outDir && this.environment?.config?.consumer === "client") {
73
+ else if (this.environment?.config?.build?.outDir &&
74
+ this.environment?.config?.consumer === "client") {
57
75
  outDir = this.environment.config.build.outDir;
58
76
  }
59
77
  else if (globalThis.START_CLIENT_OUT_DIR) {
@@ -75,6 +93,10 @@ export function manifest(start) {
75
93
  }
76
94
  else if (id === `\0${VIRTUAL_MODULES.middleware}`)
77
95
  return "export default {};";
96
+ else if (id === `\0${VIRTUAL_MODULES.serovalPlugins}`)
97
+ return "export default [];";
98
+ else if (id === `\0${VIRTUAL_MODULES.serverFnErrorHandler}`)
99
+ return "export default undefined;";
78
100
  else if (id.startsWith("/@manifest")) {
79
101
  if (this.environment.mode !== "dev")
80
102
  throw new Error("@manifest queries are only allowed in dev");
@@ -91,7 +113,7 @@ export function manifest(start) {
91
113
  // Client env does not have css dependencies in mod.transformResult
92
114
  // Aalways use ssr env instead, to prevent hydration mismatches
93
115
  const env = devServer.environments["ssr"];
94
- const styles = await findStylesInModuleGraph(env, id);
116
+ const styles = await findStylesInModuleGraph(env, id, styleFilter);
95
117
  const cssAssets = Object.entries(styles).map(([key, value]) => `{
96
118
  tag: "style",
97
119
  attrs: {
@@ -8,10 +8,22 @@ function isInvalidForRemoval(path) {
8
8
  // This one is for destructured variables
9
9
  let target = path;
10
10
  if (isPathValid(path, t.isVariableDeclarator)) {
11
- target = path.get('id');
11
+ target = path.get("id");
12
12
  }
13
13
  return isPathValid(target, t.isObjectPattern) || isPathValid(target, t.isArrayPattern);
14
14
  }
15
+ function countValidImport(node) {
16
+ if (node.importKind === "type") {
17
+ return 0;
18
+ }
19
+ let count = 0;
20
+ for (const specifier of node.specifiers) {
21
+ if (specifier.type !== "ImportSpecifier" || specifier.importKind === "value") {
22
+ count += 1;
23
+ }
24
+ }
25
+ return count;
26
+ }
15
27
  export function removeUnusedVariables(program) {
16
28
  // TODO(Alexis):
17
29
  // This implementation is simple but slow
@@ -34,7 +46,7 @@ export function removeUnusedVariables(program) {
34
46
  if (binding.references === 0 && !binding.path.removed) {
35
47
  const parent = binding.path.parentPath;
36
48
  if (isPathValid(parent, t.isImportDeclaration)) {
37
- if (parent.node.specifiers.length === 1) {
49
+ if (countValidImport(parent.node) <= 1) {
38
50
  parent.remove();
39
51
  }
40
52
  else {
@@ -42,7 +54,7 @@ export function removeUnusedVariables(program) {
42
54
  }
43
55
  dirty = true;
44
56
  }
45
- else if (!(isInvalidForRemoval(binding.path))) {
57
+ else if (!isInvalidForRemoval(binding.path)) {
46
58
  binding.path.remove();
47
59
  dirty = true;
48
60
  }
@@ -34,7 +34,7 @@ function toUtf8(text) {
34
34
  * @param seed - optional seed (32-bit unsigned);
35
35
  */
36
36
  export default function xxHash32(buffer, seed = 0) {
37
- buffer = typeof buffer === 'string' ? toUtf8(buffer) : buffer;
37
+ buffer = typeof buffer === "string" ? toUtf8(buffer) : buffer;
38
38
  const b = buffer;
39
39
  /*
40
40
  Step 1. Initialize internal accumulators
@@ -144,9 +144,7 @@ export default function xxHash32(buffer, seed = 0) {
144
144
  const laneP = laneN0 * PRIME32_3 + ((laneN1 * PRIME32_3) << 16);
145
145
  acc = (acc + laneP) & 0xffffffff;
146
146
  acc = (acc << 17) | (acc >>> 15);
147
- acc =
148
- ((acc & 0xffff) * PRIME32_4 + (((acc >>> 16) * PRIME32_4) << 16)) &
149
- 0xffffffff;
147
+ acc = ((acc & 0xffff) * PRIME32_4 + (((acc >>> 16) * PRIME32_4) << 16)) & 0xffffffff;
150
148
  }
151
149
  /*
152
150
  ```
@@ -162,9 +160,7 @@ export default function xxHash32(buffer, seed = 0) {
162
160
  const lane = b[offset];
163
161
  acc += lane * PRIME32_5;
164
162
  acc = (acc << 11) | (acc >>> 21);
165
- acc =
166
- ((acc & 0xffff) * PRIME32_1 + (((acc >>> 16) * PRIME32_1) << 16)) &
167
- 0xffffffff;
163
+ acc = ((acc & 0xffff) * PRIME32_1 + (((acc >>> 16) * PRIME32_1) << 16)) & 0xffffffff;
168
164
  }
169
165
  /*
170
166
  Step 6. Final mix (avalanche)
@@ -180,13 +176,9 @@ export default function xxHash32(buffer, seed = 0) {
180
176
  ```
181
177
  */
182
178
  acc ^= acc >>> 15;
183
- acc =
184
- (((acc & 0xffff) * PRIME32_2) & 0xffffffff) +
185
- (((acc >>> 16) * PRIME32_2) << 16);
179
+ acc = (((acc & 0xffff) * PRIME32_2) & 0xffffffff) + (((acc >>> 16) * PRIME32_2) << 16);
186
180
  acc ^= acc >>> 13;
187
- acc =
188
- (((acc & 0xffff) * PRIME32_3) & 0xffffffff) +
189
- (((acc >>> 16) * PRIME32_3) << 16);
181
+ acc = (((acc & 0xffff) * PRIME32_3) & 0xffffffff) + (((acc >>> 16) * PRIME32_3) << 16);
190
182
  acc ^= acc >>> 16;
191
183
  // turn any negatives back into a positive number;
192
184
  return acc < 0 ? acc + 4294967296 : acc;
@@ -1,10 +1,11 @@
1
+ import { pushRequest, pushResponse } from "../shared/dev-toolbar/functions/tracker.js";
1
2
  import {
2
3
  // serializeToJSONStream,
3
4
  serializeToJSONString, } from "./serialization.js";
4
- import { BODY_FORMAT_KEY, BodyFormat, extractBody, getHeadersAndBody, } from "./shared.js";
5
+ import { BODY_FORMAT_KEY, BodyFormat, extractBody, getHeadersAndBody } from "./shared.js";
5
6
  let INSTANCE = 0;
6
- function createRequest(base, id, instance, options) {
7
- return fetch(base, {
7
+ async function createRequest(base, id, instance, options) {
8
+ const request = new Request(base, {
8
9
  method: "POST",
9
10
  ...options,
10
11
  headers: {
@@ -13,6 +14,14 @@ function createRequest(base, id, instance, options) {
13
14
  "X-Server-Instance": instance,
14
15
  },
15
16
  });
17
+ if (import.meta.env.DEV) {
18
+ pushRequest(id, instance, request.clone());
19
+ }
20
+ const response = await fetch(request);
21
+ if (import.meta.env.DEV) {
22
+ pushResponse(id, instance, response.clone());
23
+ }
24
+ return response;
16
25
  }
17
26
  async function initializeResponse(base, id, instance, options, args) {
18
27
  // No args, skip serialization
@@ -63,8 +72,8 @@ async function fetchServerFunction(base, id, options, args) {
63
72
  return response;
64
73
  }
65
74
  const result = await extractBody(instance, true, response.clone());
66
- if (response.headers.has("X-Error")) {
67
- throw result;
75
+ if (response.headers.has("X-Error") || response.status >= 500) {
76
+ throw result ?? new Error(`Server function call failed with status ${response.status}`);
68
77
  }
69
78
  return result;
70
79
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Handles whatever a server function threw, before it reaches the client. Use
3
+ * it to type the default export of the module named by the
4
+ * `serverFunctions.onError` option in `vite.config.ts`.
5
+ *
6
+ * The handler may be asynchronous. SolidStart awaits its return value before
7
+ * sending the error to the client. If the handler throws or rejects, SolidStart
8
+ * sends the original thrown value instead.
9
+ *
10
+ * @param thrown The value the server function threw. This is a `Response` when
11
+ * the server function threw control flow such as a `redirect()`.
12
+ * @returns `undefined` (or `null`) to send `thrown` unchanged, a `Response` to
13
+ * preserve control flow, any other replacement value, or a promise resolving
14
+ * to any of those values.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * // src/server-fn-error.ts
19
+ * import type { ServerFunctionErrorHandler } from "@solidjs/start/server";
20
+ * import { captureException } from "./your-monitoring-client";
21
+ *
22
+ * const onServerFunctionError: ServerFunctionErrorHandler = thrown => {
23
+ * // redirect() throws a Response, so returning it preserves that control flow.
24
+ * if (thrown instanceof Response) return thrown;
25
+ *
26
+ * captureException(thrown); // or console.error, or any reporter
27
+ * return new Error("Something went wrong");
28
+ * };
29
+ *
30
+ * export default onServerFunctionError;
31
+ * ```
32
+ */
33
+ export type ServerFunctionErrorHandler = (thrown: unknown) => unknown;
34
+ export declare function applyServerFunctionErrorHandler(thrown: unknown): Promise<unknown>;
@@ -0,0 +1,10 @@
1
+ import onServerFunctionError from "solid-start:server-fn-error-handler";
2
+ export async function applyServerFunctionErrorHandler(thrown) {
3
+ try {
4
+ const replacement = await onServerFunctionError?.(thrown);
5
+ return replacement ?? thrown;
6
+ }
7
+ catch {
8
+ return thrown;
9
+ }
10
+ }
@@ -2,4 +2,4 @@ import { type H3Event } from "h3";
2
2
  import "solid-start:server-fn-manifest";
3
3
  import type { FetchEvent } from "../server/types.ts";
4
4
  export declare function handleServerFunction(h3Event: H3Event): Promise<unknown>;
5
- export declare function createSingleFlightHeaders(sourceEvent: FetchEvent): Headers;
5
+ export declare function createSingleFlightHeaders(sourceEvent: FetchEvent, result?: unknown): Headers;
@@ -17,6 +17,7 @@ import { deserializeFromJSONString, serializeToJSONStream, serializeToJSStream,
17
17
  import { BODY_FORMAT_KEY, BodyFormat, extractBody, getHeadersAndBody } from "./shared.js";
18
18
  import "solid-start:server-fn-manifest";
19
19
  import { getServerFunction, hasServerFunction } from "./registration.js";
20
+ import { applyServerFunctionErrorHandler } from "./error-handler.js";
20
21
  import { getExpectedRedirectStatus } from "../server/util.js";
21
22
  export async function handleServerFunction(h3Event) {
22
23
  const event = getFetchEvent(h3Event);
@@ -109,7 +110,7 @@ export async function handleServerFunction(h3Event) {
109
110
  }
110
111
  // handle no JS success case
111
112
  if (!instance)
112
- return handleNoJS(result, request, parsed);
113
+ return await handleNoJS(result, request, parsed);
113
114
  const body = getHeadersAndBody(result);
114
115
  if (body) {
115
116
  return new Response(body.body, {
@@ -121,10 +122,10 @@ export async function handleServerFunction(h3Event) {
121
122
  h3Event.res.headers.set("content-type", "text/javascript");
122
123
  return serializeToJSStream(instance, result);
123
124
  }
124
- h3Event.res.headers.set("content-type", "text/plain");
125
125
  return serializeToJSONStream(result);
126
126
  }
127
127
  catch (x) {
128
+ x = await applyServerFunctionErrorHandler(x);
128
129
  if (x instanceof Response) {
129
130
  if (singleFlight && instance) {
130
131
  x = await handleSingleFlight(event, x);
@@ -147,7 +148,7 @@ export async function handleServerFunction(h3Event) {
147
148
  h3Event.res.headers.set("X-Error", toHeaderValue(error));
148
149
  }
149
150
  else {
150
- x = handleNoJS(x, request, parsed, true);
151
+ x = await handleNoJS(x, request, parsed, true);
151
152
  }
152
153
  if (instance) {
153
154
  const body = getHeadersAndBody(x);
@@ -166,7 +167,6 @@ export async function handleServerFunction(h3Event) {
166
167
  h3Event.res.headers.set("content-type", "text/javascript");
167
168
  return serializeToJSStream(instance, x);
168
169
  }
169
- h3Event.res.headers.set("content-type", "text/plain");
170
170
  return serializeToJSONStream(x);
171
171
  }
172
172
  return x;
@@ -183,7 +183,20 @@ function toHeaderValue(value) {
183
183
  return "true";
184
184
  }
185
185
  }
186
- function handleNoJS(result, request, parsed, thrown) {
186
+ function getRefererLocation(request, url) {
187
+ const referer = request.headers.get("referer");
188
+ try {
189
+ if (referer)
190
+ return new URL(referer).toString();
191
+ }
192
+ catch {
193
+ // fall through to the app root below
194
+ }
195
+ // no usable referer (e.g. a no-referrer policy): the app root still beats
196
+ // leaving the browser sitting on the server function endpoint
197
+ return new URL(import.meta.env.BASE_URL, url.origin).toString();
198
+ }
199
+ async function handleNoJS(result, request, parsed, thrown) {
187
200
  const url = new URL(request.url);
188
201
  const isError = result instanceof Error;
189
202
  let statusCode = 302;
@@ -194,10 +207,19 @@ function handleNoJS(result, request, parsed, thrown) {
194
207
  headers.set(`Location`, new URL(result.headers.get("Location"), url.origin + import.meta.env.BASE_URL).toString());
195
208
  statusCode = getExpectedRedirectStatus(result);
196
209
  }
210
+ else {
211
+ // responses that carry a value rather than a destination (json(), reload())
212
+ // still have to send the browser back to the page the form came from
213
+ headers.set("Location", getRefererLocation(request, url));
214
+ }
215
+ // the body is dropped from the redirect, so don't advertise its type
216
+ headers.delete("Content-Type");
217
+ // mirror the JS path: the flash cookie carries the value, not the Response
218
+ result = result.customBody ? await result.customBody() : null;
197
219
  }
198
220
  else
199
221
  headers = new Headers({
200
- Location: new URL(request.headers.get("referer")).toString(),
222
+ Location: getRefererLocation(request, url),
201
223
  });
202
224
  if (result) {
203
225
  headers.append("Set-Cookie", `flash=${encodeURIComponent(JSON.stringify({
@@ -205,7 +227,9 @@ function handleNoJS(result, request, parsed, thrown) {
205
227
  result: isError ? result.message : result,
206
228
  thrown: thrown,
207
229
  error: isError,
208
- input: [...parsed.slice(0, -1), [...parsed[parsed.length - 1].entries()]],
230
+ input: parsed.length
231
+ ? [...parsed.slice(0, -1), [...parsed[parsed.length - 1].entries()]]
232
+ : [],
209
233
  }))}; Secure; HttpOnly;`);
210
234
  }
211
235
  return new Response(null, {
@@ -214,13 +238,19 @@ function handleNoJS(result, request, parsed, thrown) {
214
238
  });
215
239
  }
216
240
  let App;
217
- export function createSingleFlightHeaders(sourceEvent) {
241
+ export function createSingleFlightHeaders(sourceEvent, result) {
218
242
  // cookie handling logic is pretty simplistic so this might be imperfect
219
243
  // unclear if h3 internals are available on all platforms but we need a way to
220
244
  // update request headers on the underlying H3 event.
221
245
  const headers = new Headers(sourceEvent.request.headers);
222
246
  const cookies = parseCookies(sourceEvent.nativeEvent);
223
247
  const SetCookies = sourceEvent.response.headers.getSetCookie();
248
+ // cookies attached to the returned/thrown response (eg. `redirect(to, { headers })`)
249
+ // haven't been merged onto the event response yet, but a browser round trip would
250
+ // have sent them back with the next request, so apply them here too. They come after
251
+ // the ones on the event response, so they win on conflict.
252
+ if (result instanceof Response)
253
+ SetCookies.push(...result.headers.getSetCookie());
224
254
  headers.delete("cookie");
225
255
  // let useH3Internals = false;
226
256
  // if (sourceEvent.nativeEvent.node?.req) {
@@ -230,7 +260,12 @@ export function createSingleFlightHeaders(sourceEvent) {
230
260
  SetCookies.forEach(cookie => {
231
261
  if (!cookie)
232
262
  return;
233
- const { maxAge, expires, name, value } = parseSetCookie(cookie);
263
+ // `parseSetCookie` returns undefined for forbidden cookie names and for
264
+ // cookies with an empty name and value.
265
+ const parsed = parseSetCookie(cookie);
266
+ if (!parsed)
267
+ return;
268
+ const { maxAge, expires, name, value } = parsed;
234
269
  if (maxAge != null && maxAge <= 0) {
235
270
  delete cookies[name];
236
271
  return;
@@ -259,7 +294,7 @@ async function handleSingleFlight(sourceEvent, result) {
259
294
  }
260
295
  const event = { ...sourceEvent };
261
296
  event.request = new Request(url, {
262
- headers: createSingleFlightHeaders(sourceEvent),
297
+ headers: createSingleFlightHeaders(sourceEvent, result),
263
298
  });
264
299
  return await provideRequestEvent(event, async () => {
265
300
  await createPageEvent(event);
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Re-exports Seroval's plugin authoring API for use with the
3
+ * `serialization.plugins` option in `vite.config.ts`.
4
+ *
5
+ * Importing from here rather than depending on `seroval` directly keeps plugin
6
+ * authors on the exact Seroval version SolidStart serializes with. A mismatched
7
+ * version would not fail the build: it would produce plugin nodes the other end
8
+ * of the wire can't interpret.
9
+ *
10
+ * @see https://github.com/solidjs/solid-start/issues/1474
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // src/seroval-plugins.ts
15
+ * import { createPlugin } from "@solidjs/start/serialization";
16
+ * import { ObjectId } from "mongodb";
17
+ *
18
+ * const ObjectIdPlugin = createPlugin<ObjectId, { hex: any }>({
19
+ * tag: "app/ObjectId",
20
+ * test: value => value instanceof ObjectId,
21
+ * parse: {
22
+ * sync: (value, ctx) => ({ hex: ctx.parse(value.toHexString()) }),
23
+ * async: async (value, ctx) => ({ hex: await ctx.parse(value.toHexString()) }),
24
+ * stream: (value, ctx) => ({ hex: ctx.parse(value.toHexString()) }),
25
+ * },
26
+ * serialize: (node, ctx) => `globalThis.ObjectId(${ctx.serialize(node.hex)})`,
27
+ * deserialize: (node, ctx) => new ObjectId(ctx.deserialize(node.hex) as string),
28
+ * });
29
+ *
30
+ * export default [ObjectIdPlugin];
31
+ * ```
32
+ */
33
+ export { createPlugin, OpaqueReference } from "seroval";
34
+ export type { AsyncParsePluginContext, DeserializePluginContext, Plugin, PluginData, PluginInfo, SerializePluginContext, SerovalNode, StreamParsePluginContext, SyncParsePluginContext, } from "seroval";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Re-exports Seroval's plugin authoring API for use with the
3
+ * `serialization.plugins` option in `vite.config.ts`.
4
+ *
5
+ * Importing from here rather than depending on `seroval` directly keeps plugin
6
+ * authors on the exact Seroval version SolidStart serializes with. A mismatched
7
+ * version would not fail the build: it would produce plugin nodes the other end
8
+ * of the wire can't interpret.
9
+ *
10
+ * @see https://github.com/solidjs/solid-start/issues/1474
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // src/seroval-plugins.ts
15
+ * import { createPlugin } from "@solidjs/start/serialization";
16
+ * import { ObjectId } from "mongodb";
17
+ *
18
+ * const ObjectIdPlugin = createPlugin<ObjectId, { hex: any }>({
19
+ * tag: "app/ObjectId",
20
+ * test: value => value instanceof ObjectId,
21
+ * parse: {
22
+ * sync: (value, ctx) => ({ hex: ctx.parse(value.toHexString()) }),
23
+ * async: async (value, ctx) => ({ hex: await ctx.parse(value.toHexString()) }),
24
+ * stream: (value, ctx) => ({ hex: ctx.parse(value.toHexString()) }),
25
+ * },
26
+ * serialize: (node, ctx) => `globalThis.ObjectId(${ctx.serialize(node.hex)})`,
27
+ * deserialize: (node, ctx) => new ObjectId(ctx.deserialize(node.hex) as string),
28
+ * });
29
+ *
30
+ * export default [ObjectIdPlugin];
31
+ * ```
32
+ */
33
+ export { createPlugin, OpaqueReference } from "seroval";
@@ -1,5 +1,20 @@
1
1
  export declare function serializeToJSStream(id: string, value: any): ReadableStream<any>;
2
2
  export declare function serializeToJSONStream(value: any): ReadableStream<any>;
3
+ export declare class SerovalChunkReader {
4
+ reader: ReadableStreamDefaultReader<Uint8Array>;
5
+ buffer: Uint8Array;
6
+ done: boolean;
7
+ constructor(stream: ReadableStream<Uint8Array>);
8
+ readChunk(): Promise<void>;
9
+ next(): Promise<{
10
+ done: true;
11
+ value: undefined;
12
+ } | {
13
+ done: false;
14
+ value: string;
15
+ }>;
16
+ drain(interpret: (chunk: string) => void): Promise<void>;
17
+ }
3
18
  export declare function serializeToJSONString(value: any): Promise<string>;
4
19
  export declare function deserializeFromJSONString(json: string): Promise<unknown>;
5
20
  export declare function deserializeJSONStream(response: Response | Request): Promise<unknown>;
@@ -1,6 +1,6 @@
1
1
  import { crossSerializeStream, deserialize, Feature, fromCrossJSON, getCrossReferenceHeader, toCrossJSONStream, } from "seroval";
2
2
  import { AbortSignalPlugin, CustomEventPlugin, DOMExceptionPlugin, EventPlugin, FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLPlugin, URLSearchParamsPlugin, } from "seroval-plugins/web";
3
- // TODO(Alexis): if we can, allow providing an option to extend these.
3
+ import userPlugins from "solid-start:seroval-plugins";
4
4
  const DEFAULT_PLUGINS = [
5
5
  AbortSignalPlugin,
6
6
  CustomEventPlugin,
@@ -14,8 +14,37 @@ const DEFAULT_PLUGINS = [
14
14
  URLSearchParamsPlugin,
15
15
  URLPlugin,
16
16
  ];
17
+ /**
18
+ * Plugins from `serialization.plugins` are appended rather than prepended:
19
+ * seroval picks the first plugin whose `test()` passes, so the built-ins win
20
+ * and a loose user `test()` can't take over `Request`/`FormData`/`URL`.
21
+ *
22
+ * The same list has to be used on both ends of a server function, which is why
23
+ * this comes from a virtual module bundled into the client and the server
24
+ * rather than a runtime option.
25
+ */
26
+ const PLUGINS = [...DEFAULT_PLUGINS, ...userPlugins];
17
27
  const MAX_SERIALIZATION_DEPTH_LIMIT = 64;
18
28
  const DISABLED_FEATURES = Feature.RegExp;
29
+ /**
30
+ * An error thrown by a server function is serialized and rethrown on the
31
+ * client, and seroval includes `Error.prototype.stack` by default. In
32
+ * production that leaks server file paths, internal function names and the
33
+ * shape of the deployment to anyone who can trigger a throw, so strip it.
34
+ * Development keeps the stack: that's where it's actually useful, and the
35
+ * paths it exposes are the developer's own.
36
+ *
37
+ * Only applied when writing; parsing leaves `DISABLED_FEATURES` alone so an
38
+ * incoming payload that does carry a stack still deserializes.
39
+ */
40
+ const SERIALIZE_DISABLED_FEATURES = import.meta.env.PROD
41
+ ? DISABLED_FEATURES | Feature.ErrorPrototypeStack
42
+ : DISABLED_FEATURES;
43
+ /**
44
+ * `crossSerializeStream` historically ran with every feature enabled, so only
45
+ * add the stack removal here rather than the full serialize set.
46
+ */
47
+ const JS_SERIALIZE_DISABLED_FEATURES = import.meta.env.PROD ? Feature.ErrorPrototypeStack : 0;
19
48
  /**
20
49
  * Alexis:
21
50
  *
@@ -47,7 +76,8 @@ export function serializeToJSStream(id, value) {
47
76
  start(controller) {
48
77
  crossSerializeStream(value, {
49
78
  scopeId: id,
50
- plugins: DEFAULT_PLUGINS,
79
+ disabledFeatures: JS_SERIALIZE_DISABLED_FEATURES,
80
+ plugins: PLUGINS,
51
81
  onSerialize(data, initial) {
52
82
  controller.enqueue(createChunk(initial ? `(${getCrossReferenceHeader(id)},${data})` : data));
53
83
  },
@@ -65,9 +95,9 @@ export function serializeToJSONStream(value) {
65
95
  return new ReadableStream({
66
96
  start(controller) {
67
97
  toCrossJSONStream(value, {
68
- disabledFeatures: DISABLED_FEATURES,
98
+ disabledFeatures: SERIALIZE_DISABLED_FEATURES,
69
99
  depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT,
70
- plugins: DEFAULT_PLUGINS,
100
+ plugins: PLUGINS,
71
101
  onParse(node) {
72
102
  controller.enqueue(createChunk(JSON.stringify(node)));
73
103
  },
@@ -81,7 +111,7 @@ export function serializeToJSONStream(value) {
81
111
  },
82
112
  });
83
113
  }
84
- class SerovalChunkReader {
114
+ export class SerovalChunkReader {
85
115
  reader;
86
116
  buffer;
87
117
  done;
@@ -180,7 +210,7 @@ export async function deserializeJSONStream(response) {
180
210
  refs,
181
211
  disabledFeatures: DISABLED_FEATURES,
182
212
  depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT,
183
- plugins: DEFAULT_PLUGINS,
213
+ plugins: PLUGINS,
184
214
  });
185
215
  return value;
186
216
  }
@@ -9,6 +9,6 @@ const assetMap = {
9
9
  noscript: (props) => (<noscript {...props.attrs}>{props.children}</noscript>),
10
10
  };
11
11
  export function renderAsset(asset, nonce) {
12
- let { tag, attrs: { key, ...attrs } = { key: undefined }, children, } = asset;
12
+ let { tag, attrs: { key, ...attrs } = { key: undefined }, children } = asset;
13
13
  return assetMap[tag]({ attrs: { ...attrs, nonce }, key, children });
14
14
  }
@@ -1,3 +1,5 @@
1
1
  import type { DevEnvironment } from "vite";
2
+ type StyleFilter = (id: string) => boolean;
2
3
  export declare const isCssModulesFile: (file: string) => boolean;
3
- export declare function findStylesInModuleGraph(vite: DevEnvironment, id: string): Promise<Record<string, any>>;
4
+ export declare function findStylesInModuleGraph(vite: DevEnvironment, id: string, filter: StyleFilter): Promise<Record<string, any>>;
5
+ export {};
@@ -8,13 +8,16 @@ async function getViteModuleNode(vite, file, importer) {
8
8
  }
9
9
  catch (err) { }
10
10
  }
11
- async function findModuleDependencies(vite, file, deps, crawledFiles = new Set(), importer) {
11
+ async function findModuleDependencies(vite, file, deps, filter, crawledFiles = new Set(), importer) {
12
12
  crawledFiles.add(file);
13
13
  const module = await getViteModuleNode(vite, file, importer);
14
14
  if (!module?.id || deps.has(module))
15
15
  return;
16
16
  deps.add(module);
17
- if (module.url.endsWith(".css") || module.url.includes("node_modules"))
17
+ if (module.url.endsWith(".css"))
18
+ return;
19
+ // Apply user-config file filters only to real files (virtual modules should always be included)
20
+ if (module.file && !module.id.startsWith("\0") && !filter(module.file))
18
21
  return;
19
22
  if (!module.transformResult) {
20
23
  await vite.transformRequest(module.id).catch(() => { });
@@ -28,7 +31,7 @@ async function findModuleDependencies(vite, file, deps, crawledFiles = new Set()
28
31
  if (crawledFiles.has(dep)) {
29
32
  continue;
30
33
  }
31
- await findModuleDependencies(vite, dep, deps, crawledFiles, module.id);
34
+ await findModuleDependencies(vite, dep, deps, filter, crawledFiles, module.id);
32
35
  }
33
36
  }
34
37
  // Vite doesn't expose these so we just copy the list for now
@@ -38,11 +41,11 @@ const cssFileRegExp = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)$/;
38
41
  const cssModulesRegExp = new RegExp(`\\.module${cssFileRegExp.source}`);
39
42
  const isCssFile = (file) => cssFileRegExp.test(file);
40
43
  export const isCssModulesFile = (file) => cssModulesRegExp.test(file);
41
- export async function findStylesInModuleGraph(vite, id) {
44
+ export async function findStylesInModuleGraph(vite, id, filter) {
42
45
  const absolute = path.resolve(process.cwd(), id);
43
46
  const dependencies = new Set();
44
47
  try {
45
- await findModuleDependencies(vite, absolute, dependencies);
48
+ await findModuleDependencies(vite, absolute, dependencies, filter);
46
49
  }
47
50
  catch (e) {
48
51
  console.error(e);
@@ -1,6 +1,5 @@
1
- import { H3 } from "h3/generic";
2
1
  import type { JSX } from "solid-js";
3
- import type { FetchEvent, HandlerOptions, PageEvent } from "./types.ts";
4
- export declare function createBaseHandler(createPageEvent: (e: FetchEvent) => Promise<PageEvent>, fn: (context: PageEvent) => JSX.Element, options?: HandlerOptions | ((context: PageEvent) => HandlerOptions | Promise<HandlerOptions>)): H3;
5
- export declare function createHandler(fn: (context: PageEvent) => JSX.Element, options?: HandlerOptions | ((context: PageEvent) => HandlerOptions | Promise<HandlerOptions>)): H3;
2
+ import type { FetchEvent, HandlerOptions, PageEvent, StartHandler } from "./types.ts";
3
+ export declare function createBaseHandler(createPageEvent: (e: FetchEvent) => Promise<PageEvent>, fn: (context: PageEvent) => JSX.Element, options?: HandlerOptions | ((context: PageEvent) => HandlerOptions | Promise<HandlerOptions>), routerLoad?: (event: FetchEvent) => Promise<void>): StartHandler;
4
+ export declare function createHandler(fn: (context: PageEvent) => JSX.Element, options?: HandlerOptions | ((context: PageEvent) => HandlerOptions | Promise<HandlerOptions>), routerLoad?: (event: FetchEvent) => Promise<void>): StartHandler;
6
5
  export declare function createPageEvent(ctx: FetchEvent): Promise<PageEvent>;