@solidjs/start 2.0.0-beta.9 → 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 (126) hide show
  1. package/CHANGELOG.md +149 -1
  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 +13 -10
  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 +226 -33
  10. package/dist/config/index.d.ts +154 -5
  11. package/dist/config/index.js +23 -10
  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 +58 -12
  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/middleware/index.js +4 -1
  27. package/dist/server/StartServer.jsx +1 -1
  28. package/dist/server/assets/render.jsx +1 -1
  29. package/dist/server/collect-styles.d.ts +3 -1
  30. package/dist/server/collect-styles.js +8 -5
  31. package/dist/server/fetchEvent.d.ts +10 -0
  32. package/dist/server/fetchEvent.js +10 -0
  33. package/dist/server/handler.d.ts +3 -4
  34. package/dist/server/handler.js +22 -56
  35. package/dist/server/index.d.ts +3 -1
  36. package/dist/server/index.jsx +1 -0
  37. package/dist/server/manifest/dev-client-manifest.js +1 -1
  38. package/dist/server/manifest/dev-ssr-manifest.js +1 -1
  39. package/dist/server/spa/StartServer.jsx +1 -1
  40. package/dist/server/spa/handler.d.ts +2 -3
  41. package/dist/server/spa/handler.js +2 -2
  42. package/dist/server/strip-path-base.d.ts +1 -0
  43. package/dist/server/strip-path-base.js +10 -0
  44. package/dist/server/types.d.ts +12 -1
  45. package/dist/server/web-stream.d.ts +9 -0
  46. package/dist/server/web-stream.js +29 -0
  47. package/dist/shared/ErrorBoundary.jsx +3 -3
  48. package/dist/shared/clientOnly.js +3 -1
  49. package/dist/shared/{dev-overlay → dev-toolbar/error-viewer}/CodeView.jsx +3 -3
  50. package/dist/shared/dev-toolbar/error-viewer/create-stack-frame.js +90 -0
  51. package/dist/shared/{dev-overlay/DevOverlayDialog.d.ts → dev-toolbar/error-viewer/index.d.ts} +3 -2
  52. package/dist/shared/{dev-overlay/DevOverlayDialog.jsx → dev-toolbar/error-viewer/index.jsx} +66 -77
  53. package/dist/shared/dev-toolbar/error-viewer/styles.css +224 -0
  54. package/dist/shared/dev-toolbar/functions/BlobViewer.css +4 -0
  55. package/dist/shared/dev-toolbar/functions/BlobViewer.d.ts +6 -0
  56. package/dist/shared/dev-toolbar/functions/BlobViewer.jsx +37 -0
  57. package/dist/shared/dev-toolbar/functions/FormDataViewer.d.ts +5 -0
  58. package/dist/shared/dev-toolbar/functions/FormDataViewer.jsx +23 -0
  59. package/dist/shared/dev-toolbar/functions/HeadersViewer.css +8 -0
  60. package/dist/shared/dev-toolbar/functions/HeadersViewer.d.ts +6 -0
  61. package/dist/shared/dev-toolbar/functions/HeadersViewer.jsx +15 -0
  62. package/dist/shared/dev-toolbar/functions/HexViewer.css +34 -0
  63. package/dist/shared/dev-toolbar/functions/HexViewer.d.ts +11 -0
  64. package/dist/shared/dev-toolbar/functions/HexViewer.jsx +70 -0
  65. package/dist/shared/dev-toolbar/functions/SerovalValue.css +5 -0
  66. package/dist/shared/dev-toolbar/functions/SerovalValue.d.ts +7 -0
  67. package/dist/shared/dev-toolbar/functions/SerovalValue.jsx +10 -0
  68. package/dist/shared/dev-toolbar/functions/SerovalViewer.css +70 -0
  69. package/dist/shared/dev-toolbar/functions/SerovalViewer.d.ts +6 -0
  70. package/dist/shared/dev-toolbar/functions/SerovalViewer.jsx +796 -0
  71. package/dist/shared/dev-toolbar/functions/URLSearchParamsViewer.d.ts +5 -0
  72. package/dist/shared/dev-toolbar/functions/URLSearchParamsViewer.jsx +22 -0
  73. package/dist/shared/dev-toolbar/functions/index.d.ts +13 -0
  74. package/dist/shared/dev-toolbar/functions/index.jsx +216 -0
  75. package/dist/shared/dev-toolbar/functions/styles.css +109 -0
  76. package/dist/shared/dev-toolbar/functions/tracker.d.ts +19 -0
  77. package/dist/shared/dev-toolbar/functions/tracker.js +29 -0
  78. package/dist/shared/{dev-overlay → dev-toolbar}/icons.d.ts +9 -0
  79. package/dist/shared/{dev-overlay → dev-toolbar}/icons.jsx +19 -1
  80. package/dist/shared/dev-toolbar/index.css +88 -0
  81. package/dist/shared/dev-toolbar/index.d.ts +6 -0
  82. package/dist/shared/dev-toolbar/index.jsx +179 -0
  83. package/dist/shared/ui/Badge.css +33 -0
  84. package/dist/shared/ui/Badge.d.ts +7 -0
  85. package/dist/shared/ui/Badge.jsx +7 -0
  86. package/dist/shared/ui/Button.css +32 -0
  87. package/dist/shared/ui/Button.d.ts +4 -0
  88. package/dist/shared/ui/Button.jsx +4 -0
  89. package/dist/shared/ui/Cascade.css +29 -0
  90. package/dist/shared/ui/Cascade.d.ts +4 -0
  91. package/dist/shared/ui/Cascade.jsx +4 -0
  92. package/dist/shared/ui/Dialog.css +25 -0
  93. package/dist/shared/ui/Dialog.d.ts +5 -0
  94. package/dist/shared/ui/Dialog.jsx +5 -0
  95. package/dist/shared/ui/IconButton.css +39 -0
  96. package/dist/shared/ui/IconButton.d.ts +4 -0
  97. package/dist/shared/ui/IconButton.jsx +4 -0
  98. package/dist/shared/ui/Placeholder.css +12 -0
  99. package/dist/shared/ui/Placeholder.d.ts +6 -0
  100. package/dist/shared/ui/Placeholder.jsx +4 -0
  101. package/dist/shared/ui/Section.css +18 -0
  102. package/dist/shared/ui/Section.d.ts +9 -0
  103. package/dist/shared/ui/Section.jsx +10 -0
  104. package/dist/shared/ui/Select.css +37 -0
  105. package/dist/shared/ui/Select.d.ts +4 -0
  106. package/dist/shared/ui/Select.jsx +4 -0
  107. package/dist/shared/ui/Table.css +22 -0
  108. package/dist/shared/ui/Table.d.ts +11 -0
  109. package/dist/shared/ui/Table.jsx +13 -0
  110. package/dist/shared/ui/Tabs.css +51 -0
  111. package/dist/shared/ui/Tabs.d.ts +6 -0
  112. package/dist/shared/ui/Tabs.jsx +6 -0
  113. package/dist/shared/ui/Text.css +73 -0
  114. package/dist/shared/ui/Text.d.ts +12 -0
  115. package/dist/shared/ui/Text.jsx +17 -0
  116. package/package.json +36 -29
  117. package/dist/shared/dev-overlay/createStackFrame.js +0 -56
  118. package/dist/shared/dev-overlay/index.d.ts +0 -5
  119. package/dist/shared/dev-overlay/index.jsx +0 -39
  120. package/dist/shared/dev-overlay/styles.css +0 -292
  121. /package/dist/shared/{dev-overlay → dev-toolbar/error-viewer}/CodeView.d.ts +0 -0
  122. /package/dist/shared/{dev-overlay/createStackFrame.d.ts → dev-toolbar/error-viewer/create-stack-frame.d.ts} +0 -0
  123. /package/dist/shared/{dev-overlay → dev-toolbar/error-viewer}/download.d.ts +0 -0
  124. /package/dist/shared/{dev-overlay → dev-toolbar/error-viewer}/download.js +0 -0
  125. /package/dist/shared/{dev-overlay → dev-toolbar/error-viewer}/get-source-map.d.ts +0 -0
  126. /package/dist/shared/{dev-overlay → dev-toolbar/error-viewer}/get-source-map.js +0 -0
@@ -1,14 +1,16 @@
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 { appRootAlias } from "./app-root-alias.js";
6
7
  import { boundaryModules } from "./boundary-modules.js";
7
8
  import { DEFAULT_EXTENSIONS, VIRTUAL_MODULES, VITE_ENVIRONMENTS } from "./constants.js";
8
9
  import { devServer } from "./dev-server.js";
9
10
  import { envPlugin } from "./env.js";
10
11
  import { SolidStartClientFileRouter, SolidStartServerFileRouter } from "./fs-router.js";
11
12
  import { fsRoutes } from "./fs-routes/index.js";
13
+ import { sanitizeChunkFileName, toPickId } from "./fs-routes/tree-shake.js";
12
14
  import lazy from "./lazy.js";
13
15
  import { manifest } from "./manifest.js";
14
16
  import { parseIdQuery } from "./utils.js";
@@ -61,24 +63,33 @@ export function solidStart(options) {
61
63
  },
62
64
  };
63
65
  },
64
- async config(_, env) {
66
+ async config(config, env) {
65
67
  const clientInput = [handlers.client];
68
+ const clientEntryUrl = env.command === "serve" && config.experimental?.bundledDev
69
+ ? `assets/${basename(handlers.client, entryExtension)}.js`
70
+ : handlers.client;
66
71
  if (env.command === "build") {
67
72
  const clientRouter = globalThis.ROUTERS.client;
68
73
  for (const route of await clientRouter.getRoutes()) {
69
74
  for (const [key, value] of Object.entries(route)) {
70
75
  if (value && key.startsWith("$") && !key.startsWith("$$")) {
71
- function toRouteId(route) {
72
- return `${route.src}?${route.pick.map((p) => `pick=${p}`).join("&")}`;
73
- }
74
- clientInput.push(toRouteId(value));
76
+ clientInput.push(toPickId(value.src, value.pick));
75
77
  }
76
78
  }
77
79
  }
78
80
  }
79
81
  return {
80
82
  appType: "custom",
81
- build: { assetsDir: "_build/assets" },
83
+ build: {
84
+ assetsDir: "_build/assets",
85
+ rolldownOptions: {
86
+ output: {
87
+ // Keeps route chunks named after their file rather than after
88
+ // the `?pick=...` id that addresses them. See toPickId.
89
+ sanitizeFileName: sanitizeChunkFileName,
90
+ },
91
+ },
92
+ },
82
93
  optimizeDeps: {
83
94
  // Suppress TS errors from Vite 7 types when configuring Vite 8's Rolldown
84
95
  ...{
@@ -96,7 +107,7 @@ export function solidStart(options) {
96
107
  write: true,
97
108
  manifest: true,
98
109
  outDir: "dist/client",
99
- rollupOptions: {
110
+ rolldownOptions: {
100
111
  input: clientInput,
101
112
  treeshake: true,
102
113
  preserveEntrySignatures: "exports-only",
@@ -110,7 +121,7 @@ export function solidStart(options) {
110
121
  write: true,
111
122
  manifest: true,
112
123
  copyPublicDir: false,
113
- rollupOptions: {
124
+ rolldownOptions: {
114
125
  input: handlers.server,
115
126
  },
116
127
  outDir: "dist/server",
@@ -123,7 +134,6 @@ export function solidStart(options) {
123
134
  resolve: {
124
135
  alias: {
125
136
  "@solidjs/start/server/entry": handlers.server,
126
- "~": join(process.cwd(), start.appRoot),
127
137
  ...(!start.ssr
128
138
  ? {
129
139
  "@solidjs/start/server": "@solidjs/start/server/spa",
@@ -149,7 +159,9 @@ export function solidStart(options) {
149
159
  // esbuild receives a valid JS string literal for the define value
150
160
  "import.meta.env.START_APP_ENTRY": JSON.stringify(appEntryPath),
151
161
  "import.meta.env.START_CLIENT_ENTRY": JSON.stringify(handlers.client),
162
+ "import.meta.env.START_CLIENT_ENTRY_URL": JSON.stringify(clientEntryUrl),
152
163
  "import.meta.env.START_DEV_OVERLAY": JSON.stringify(start.devOverlay),
164
+ "import.meta.env.SERVER_BASE_URL": JSON.stringify(config.server?.baseURL ?? ""),
153
165
  "import.meta.env.SEROVAL_MODE": JSON.stringify(start.serialization?.mode || "json"),
154
166
  },
155
167
  builder: {
@@ -170,6 +182,7 @@ export function solidStart(options) {
170
182
  };
171
183
  },
172
184
  },
185
+ appRootAlias(root, start.appRoot),
173
186
  manifest(start),
174
187
  fsRoutes({
175
188
  routers: {
@@ -58,12 +58,17 @@ const lazy = () => {
58
58
  if (this.environment.name !== VITE_ENVIRONMENTS.client)
59
59
  return;
60
60
  for (const chunk of Object.values(bundle)) {
61
- if (chunk.type !== "chunk" || !chunk.isDynamicEntry || chunk.facadeModuleId)
61
+ if (chunk.type !== "chunk" || chunk.facadeModuleId)
62
+ continue;
63
+ if (!chunk.viteMetadata?.importedCss.size)
64
+ continue;
65
+ const moduleIds = chunk.moduleIds.filter(id => !id.endsWith("css"));
66
+ if (moduleIds.length <= 1)
62
67
  continue;
63
68
  // Has to follow Vites implementation:
64
69
  // https://github.com/vitejs/vite/blob/4be37a8389c67873880f826b01fe40137e1c29a7/packages/vite/src/node/plugins/manifest.ts#L179
65
70
  const chunkName = `_${basename(chunk.fileName)}`;
66
- for (const id of chunk.moduleIds) {
71
+ for (const id of moduleIds) {
67
72
  sharedChunkNames[id] = chunkName;
68
73
  }
69
74
  }
@@ -1,3 +1,6 @@
1
1
  import { type PluginOption } from "vite";
2
2
  import { type SolidStartOptions } from "./index.ts";
3
+ type StyleFilterOptions = NonNullable<SolidStartOptions["css"]>["filter"];
4
+ export declare function createStyleFilter(options?: StyleFilterOptions): (id: unknown) => boolean;
3
5
  export declare function manifest(start: SolidStartOptions): PluginOption;
6
+ export {};
@@ -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);
@@ -144,10 +145,10 @@ export async function handleServerFunction(h3Event) {
144
145
  }
145
146
  else if (instance) {
146
147
  const error = x instanceof Error ? x.message : typeof x === "string" ? x : "true";
147
- h3Event.res.headers.set("X-Error", error.replace(/[\r\n]+/g, ""));
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);
@@ -158,7 +159,7 @@ export async function handleServerFunction(h3Event) {
158
159
  headers.set("X-Error", errorHeader);
159
160
  }
160
161
  return new Response(body.body, {
161
- headers: body.headers,
162
+ headers,
162
163
  });
163
164
  }
164
165
  h3Event.res.headers.set(BODY_FORMAT_KEY, BodyFormat.Seroval);
@@ -166,13 +167,36 @@ 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;
173
173
  }
174
174
  }
175
- function handleNoJS(result, request, parsed, thrown) {
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
+ }
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) {
176
200
  const url = new URL(request.url);
177
201
  const isError = result instanceof Error;
178
202
  let statusCode = 302;
@@ -183,10 +207,19 @@ function handleNoJS(result, request, parsed, thrown) {
183
207
  headers.set(`Location`, new URL(result.headers.get("Location"), url.origin + import.meta.env.BASE_URL).toString());
184
208
  statusCode = getExpectedRedirectStatus(result);
185
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;
186
219
  }
187
220
  else
188
221
  headers = new Headers({
189
- Location: new URL(request.headers.get("referer")).toString(),
222
+ Location: getRefererLocation(request, url),
190
223
  });
191
224
  if (result) {
192
225
  headers.append("Set-Cookie", `flash=${encodeURIComponent(JSON.stringify({
@@ -194,7 +227,9 @@ function handleNoJS(result, request, parsed, thrown) {
194
227
  result: isError ? result.message : result,
195
228
  thrown: thrown,
196
229
  error: isError,
197
- 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
+ : [],
198
233
  }))}; Secure; HttpOnly;`);
199
234
  }
200
235
  return new Response(null, {
@@ -203,13 +238,19 @@ function handleNoJS(result, request, parsed, thrown) {
203
238
  });
204
239
  }
205
240
  let App;
206
- export function createSingleFlightHeaders(sourceEvent) {
241
+ export function createSingleFlightHeaders(sourceEvent, result) {
207
242
  // cookie handling logic is pretty simplistic so this might be imperfect
208
243
  // unclear if h3 internals are available on all platforms but we need a way to
209
244
  // update request headers on the underlying H3 event.
210
245
  const headers = new Headers(sourceEvent.request.headers);
211
246
  const cookies = parseCookies(sourceEvent.nativeEvent);
212
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());
213
254
  headers.delete("cookie");
214
255
  // let useH3Internals = false;
215
256
  // if (sourceEvent.nativeEvent.node?.req) {
@@ -219,7 +260,12 @@ export function createSingleFlightHeaders(sourceEvent) {
219
260
  SetCookies.forEach(cookie => {
220
261
  if (!cookie)
221
262
  return;
222
- 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;
223
269
  if (maxAge != null && maxAge <= 0) {
224
270
  delete cookies[name];
225
271
  return;
@@ -248,7 +294,7 @@ async function handleSingleFlight(sourceEvent, result) {
248
294
  }
249
295
  const event = { ...sourceEvent };
250
296
  event.request = new Request(url, {
251
- headers: createSingleFlightHeaders(sourceEvent),
297
+ headers: createSingleFlightHeaders(sourceEvent, result),
252
298
  });
253
299
  return await provideRequestEvent(event, async () => {
254
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>;