@solidjs/start 2.0.0-rc.3 → 2.0.0-rc.5

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 (62) hide show
  1. package/CHANGELOG.md +48 -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 +1 -0
  5. package/dist/config/constants.js +1 -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 +46 -1
  10. package/dist/config/index.d.ts +23 -0
  11. package/dist/config/index.js +14 -6
  12. package/dist/config/manifest.js +11 -2
  13. package/dist/directives/remove-unused-variables.js +2 -2
  14. package/dist/directives/xxhash32.js +5 -13
  15. package/dist/fns/handler.js +37 -12
  16. package/dist/fns/plugins.d.ts +34 -0
  17. package/dist/fns/plugins.js +33 -0
  18. package/dist/fns/serialization.js +35 -5
  19. package/dist/server/assets/render.jsx +1 -1
  20. package/dist/server/handler.js +9 -1
  21. package/dist/server/spa/StartServer.jsx +1 -1
  22. package/dist/shared/clientOnly.js +3 -1
  23. package/dist/shared/dev-toolbar/error-viewer/CodeView.jsx +1 -1
  24. package/dist/shared/dev-toolbar/functions/BlobViewer.css +1 -1
  25. package/dist/shared/dev-toolbar/functions/BlobViewer.d.ts +2 -2
  26. package/dist/shared/dev-toolbar/functions/BlobViewer.jsx +7 -11
  27. package/dist/shared/dev-toolbar/functions/FormDataViewer.d.ts +1 -1
  28. package/dist/shared/dev-toolbar/functions/FormDataViewer.jsx +7 -11
  29. package/dist/shared/dev-toolbar/functions/HeadersViewer.d.ts +1 -1
  30. package/dist/shared/dev-toolbar/functions/HeadersViewer.jsx +2 -2
  31. package/dist/shared/dev-toolbar/functions/HexViewer.d.ts +1 -1
  32. package/dist/shared/dev-toolbar/functions/HexViewer.jsx +7 -15
  33. package/dist/shared/dev-toolbar/functions/SerovalValue.css +0 -2
  34. package/dist/shared/dev-toolbar/functions/SerovalValue.d.ts +1 -1
  35. package/dist/shared/dev-toolbar/functions/SerovalValue.jsx +3 -3
  36. package/dist/shared/dev-toolbar/functions/SerovalViewer.css +0 -1
  37. package/dist/shared/dev-toolbar/functions/URLSearchParamsViewer.d.ts +1 -1
  38. package/dist/shared/dev-toolbar/functions/URLSearchParamsViewer.jsx +5 -7
  39. package/dist/shared/dev-toolbar/functions/styles.css +0 -1
  40. package/dist/shared/dev-toolbar/index.jsx +5 -0
  41. package/dist/shared/ui/Badge.css +0 -1
  42. package/dist/shared/ui/Badge.d.ts +3 -3
  43. package/dist/shared/ui/Badge.jsx +3 -3
  44. package/dist/shared/ui/Button.jsx +1 -1
  45. package/dist/shared/ui/Cascade.jsx +3 -3
  46. package/dist/shared/ui/Dialog.css +1 -2
  47. package/dist/shared/ui/Dialog.jsx +3 -3
  48. package/dist/shared/ui/IconButton.jsx +1 -1
  49. package/dist/shared/ui/Placeholder.css +1 -1
  50. package/dist/shared/ui/Placeholder.d.ts +2 -2
  51. package/dist/shared/ui/Placeholder.jsx +2 -4
  52. package/dist/shared/ui/Section.css +0 -1
  53. package/dist/shared/ui/Section.d.ts +2 -2
  54. package/dist/shared/ui/Section.jsx +3 -5
  55. package/dist/shared/ui/Select.jsx +3 -3
  56. package/dist/shared/ui/Table.d.ts +6 -6
  57. package/dist/shared/ui/Table.jsx +5 -5
  58. package/dist/shared/ui/Tabs.jsx +4 -4
  59. package/dist/shared/ui/Text.css +7 -3
  60. package/dist/shared/ui/Text.d.ts +8 -8
  61. package/dist/shared/ui/Text.jsx +9 -9
  62. package/package.json +28 -27
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # @solidjs/start
2
2
 
3
+ ## 2.0.0-rc.5
4
+
5
+ ### Minor Changes
6
+
7
+ - 4c803e5: Add `serialization.plugins` to configure custom Seroval plugins for server functions.
8
+
9
+ Values Seroval has no built-in support for (Mongo's `ObjectId`, Prisma's `Decimal`, `Temporal`, and other custom classes) previously threw when returned from or passed to a server function. Point the new option at a module whose default export is an array of plugins:
10
+
11
+ ```ts
12
+ // vite.config.ts
13
+ solidStart({
14
+ serialization: {
15
+ plugins: "src/seroval-plugins.ts",
16
+ },
17
+ });
18
+ ```
19
+
20
+ ```ts
21
+ // src/seroval-plugins.ts
22
+ import { createPlugin } from "@solidjs/start/serialization";
23
+ ```
24
+
25
+ The module is bundled into both the client and the server so both ends of a server function agree on the format, so it must not import server-only code. SolidStart's built-in plugins keep precedence. Only server-function and action payloads are affected; the SSR hydration payload is serialized by `solid-js/web`.
26
+
27
+ Also adds a `@solidjs/start/serialization` entrypoint re-exporting Seroval's `createPlugin`, `OpaqueReference`, and plugin types, so plugin authors stay on the same Seroval version SolidStart serializes with.
28
+
29
+ ### Patch Changes
30
+
31
+ - e117d91: Route module ids now end in the source extension, so ecosystem plugins apply inside `src/routes`.
32
+
33
+ Route files are imported through an id carrying the picked exports in the query (`routes/api.ts?pick=GET`), which left the id ending in the export name. Plugins whose filter is anchored on the file extension (`/\.[cm]?[jt]sx?$/`, the default for `unplugin-auto-import`, `unplugin-macros` and others) silently skipped every route file. The id now ends with a `lang.<ext>` marker, the same convention Vue SFCs use for `?vue&type=script&lang.ts`. Chunk filenames are unchanged.
34
+
35
+ - d8f1ea8: Apply the configured `nonce` to the two script tags that were still missing it, so a strict `script-src` CSP no longer needs `unsafe-inline`:
36
+
37
+ - The client-side redirect that streaming mode emits after the shell has already flushed (`<script>window.location=...</script>`) now carries the nonce.
38
+ - The SPA entry script tag now carries the nonce, matching the SSR entry script.
39
+
40
+ - 27fca88: Fix actions returning `json()` or `reload()` leaving no-JS form submissions stranded on the `/_server` endpoint. These responses carry a value rather than a destination, so the redirect issued for progressive-enhancement submissions had no `Location` header. It now falls back to the submitting page, and the response value is unwrapped into the flash cookie so `useSubmission().result` matches the JS path.
41
+ - 75debc3: Scope the built-in `~` alias to the app package, so files in other workspace packages can map `~` to their own root through an importer-aware plugin such as `vite-tsconfig-paths`. In stylesheets and asset URLs (CSS `@import`, `url()`, `new URL(..., import.meta.url)`) `~` still always means the app root, since Vite resolves those without running plugins.
42
+
43
+ ## 2.0.0-rc.4
44
+
45
+ ### Patch Changes
46
+
47
+ - b6dfaac: Updated dependencies, including `cookie-es` to 3, `shiki` to 4, `srvx` to 0.12.4, `oxc-parser` to 0.141 and `seroval` to 1.5.6. `parseSetCookie` now returns `undefined` for cookies with a forbidden name or an empty name and value, and those cookies are no longer forwarded to nested server function requests.
48
+ - 02cd41e: Stop the dev toolbar from reporting benign `ResizeObserver loop` notifications as application errors. Browsers dispatch these as window `error` events carrying no error object, so the toolbar was capturing the raw `ErrorEvent` and force-opening the error panel over the app on every resize.
49
+ - d3c2af2: Don't send server error stack traces to the client in production builds. When a server function throws, the error is serialized and rethrown on the client, and seroval included `Error.prototype.stack` by default, leaking server file paths and internal function names. Stacks are still serialized in development.
50
+
3
51
  ## 2.0.0-rc.3
4
52
 
5
53
  ### Patch Changes
@@ -0,0 +1,7 @@
1
+ import { type Plugin } from "vite";
2
+ /**
3
+ * Provides SolidStart's `~` app-root alias without claiming module imports made
4
+ * by other workspace packages. Those packages may map `~` to their own root
5
+ * through an importer-aware resolver such as vite-tsconfig-paths.
6
+ */
7
+ export declare function appRootAlias(projectRoot: string, appRoot: string): Plugin;
@@ -0,0 +1,58 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { normalizePath } from "vite";
4
+ /**
5
+ * `~/app.css`, `~/logo.svg`, ... but not `~/lib/api.ts` or `~/components/Counter`.
6
+ *
7
+ * CSS `@import`, `url()`, preprocessor imports and `new URL(..., import.meta.url)`
8
+ * are resolved by internal Vite resolvers that only run the alias plugin, never
9
+ * user plugins, so those ids can never reach `resolveId` below. Keeping non-module
10
+ * ids on a plain alias preserves `~` in stylesheets and asset URLs, where it always
11
+ * means the app root.
12
+ */
13
+ const NON_MODULE_ID = /^~\/([^?#]*\.(?![cm]?[jt]sx?(?:[?#]|$))[^./?#]+(?:[?#].*)?)$/;
14
+ /**
15
+ * Provides SolidStart's `~` app-root alias without claiming module imports made
16
+ * by other workspace packages. Those packages may map `~` to their own root
17
+ * through an importer-aware resolver such as vite-tsconfig-paths.
18
+ */
19
+ export function appRootAlias(projectRoot, appRoot) {
20
+ const appDir = normalizePath(resolve(projectRoot, appRoot));
21
+ const packageRoots = new Map();
22
+ /** Nearest directory at or above `directory` that holds a `package.json`. */
23
+ function packageRoot(directory) {
24
+ if (!packageRoots.has(directory)) {
25
+ const parent = dirname(directory);
26
+ packageRoots.set(directory, existsSync(join(directory, "package.json"))
27
+ ? directory
28
+ : parent === directory
29
+ ? undefined
30
+ : packageRoot(parent));
31
+ }
32
+ return packageRoots.get(directory);
33
+ }
34
+ const appPackage = packageRoot(appDir);
35
+ /** True only when the importer demonstrably belongs to another package. */
36
+ function isForeignImporter(importer) {
37
+ const file = normalizePath(importer.replace(/[?#].*$/s, ""));
38
+ if (!isAbsolute(file))
39
+ return false;
40
+ const owner = packageRoot(dirname(file));
41
+ return owner !== undefined && owner !== appPackage;
42
+ }
43
+ return {
44
+ name: "solid-start:app-root-alias",
45
+ enforce: "pre",
46
+ config() {
47
+ return { resolve: { alias: [{ find: NON_MODULE_ID, replacement: `${appDir}/$1` }] } };
48
+ },
49
+ async resolveId(id, importer, options) {
50
+ if (id !== "~" && !id.startsWith("~/"))
51
+ return null;
52
+ if (importer && isForeignImporter(importer))
53
+ return null;
54
+ const target = join(appDir, id.slice(1));
55
+ return (await this.resolve(target, importer, { ...options, skipSelf: true })) ?? target;
56
+ },
57
+ };
58
+ }
@@ -5,6 +5,7 @@ export declare const VIRTUAL_MODULES: {
5
5
  readonly getClientManifest: "solid-start:get-client-manifest";
6
6
  readonly getManifest: "solid-start:get-manifest";
7
7
  readonly middleware: "solid-start:middleware";
8
+ readonly serovalPlugins: "solid-start:seroval-plugins";
8
9
  readonly serverFnManifest: "solid-start:server-fn-manifest";
9
10
  readonly clientEntry: "solid-start:client-entry";
10
11
  readonly serverEntry: "solid-start:server-entry";
@@ -5,6 +5,7 @@ export const VIRTUAL_MODULES = {
5
5
  getClientManifest: "solid-start:get-client-manifest",
6
6
  getManifest: "solid-start:get-manifest",
7
7
  middleware: "solid-start:middleware",
8
+ serovalPlugins: "solid-start:seroval-plugins",
8
9
  serverFnManifest: "solid-start:server-fn-manifest",
9
10
  clientEntry: "solid-start:client-entry",
10
11
  serverEntry: "solid-start:server-entry",
@@ -58,13 +58,13 @@ export function envPlugin(options) {
58
58
  }
59
59
  const vars = currentOptions.server?.load
60
60
  ? currentOptions.server.load(env)
61
- : loadEnv(env, '.', serverPrefix);
61
+ : loadEnv(env, ".", serverPrefix);
62
62
  return convertObjectToModule(vars);
63
63
  }
64
64
  if (filename === CLIENT_ENV) {
65
65
  const vars = currentOptions.client?.load
66
66
  ? currentOptions.client.load(env)
67
- : loadEnv(env, '.', clientPrefix);
67
+ : loadEnv(env, ".", clientPrefix);
68
68
  return convertObjectToModule(vars);
69
69
  }
70
70
  if (filename === SERVER_RUNTIME_LOADER) {
@@ -1,6 +1,6 @@
1
1
  import { relative } from "node:path";
2
2
  import { fileSystemWatcher } from "./fs-watcher.js";
3
- import { treeShake } from "./tree-shake.js";
3
+ import { toPickId, treeShake } from "./tree-shake.js";
4
4
  export const moduleId = "solid-start:routes";
5
5
  export function fsRoutes({ routers }) {
6
6
  globalThis.ROUTERS = routers;
@@ -24,7 +24,7 @@ export function fsRoutes({ routers }) {
24
24
  if (v === undefined)
25
25
  return undefined;
26
26
  if (k.startsWith("$$")) {
27
- const buildId = `${v.src}?${v.pick.map((p) => `pick=${p}`).join("&")}`;
27
+ const buildId = toPickId(v.src, v.pick);
28
28
  /**
29
29
  * @type {{ [key: string]: string }}
30
30
  */
@@ -40,7 +40,7 @@ export function fsRoutes({ routers }) {
40
40
  };
41
41
  }
42
42
  else if (k.startsWith("$")) {
43
- const buildId = `${v.src}?${v.pick.map((p) => `pick=${p}`).join("&")}`;
43
+ const buildId = toPickId(v.src, v.pick);
44
44
  return {
45
45
  src: relative(root, buildId),
46
46
  build: isBuild ? `_$() => import('${buildId}')$_` : undefined,
@@ -1,2 +1,29 @@
1
1
  import type { Plugin } from "vite";
2
+ /**
3
+ * Builds the module id used to import a subset of a route file's exports.
4
+ *
5
+ * The `pick` list has to live in the query so that the same file can be
6
+ * instantiated once per export subset (the client picks `default`/`$css`,
7
+ * the server picks its HTTP handlers). That query would otherwise leave the
8
+ * id ending in `?pick=GET`, which silently excludes route files from any
9
+ * plugin whose filter is anchored on the file extension -- a very common
10
+ * default, e.g. unplugin-macros' `/\.[cm]?[jt]sx?$/`.
11
+ *
12
+ * The trailing `lang.<ext>` marker is the same convention Vue SFCs use
13
+ * (`?vue&type=script&lang.ts`) and puts a real extension back at the end of
14
+ * the id, so those filters match again.
15
+ *
16
+ * @see https://github.com/solidjs/solid-start/issues/1918
17
+ */
18
+ export declare function toPickId(src: string, pick: string[]): string;
19
+ /**
20
+ * Rolldown's default chunk name sanitizer, plus a pass that drops the
21
+ * `?pick=...&lang` tail so route chunks keep the short, stable filenames they
22
+ * had before the `lang` marker was introduced (`index-<hash>.js`, not
23
+ * `index.tsx_pick_default_pick__css_lang-<hash>.js`). Client chunk filenames
24
+ * are public URLs, so this is not purely cosmetic.
25
+ *
26
+ * @see https://github.com/rollup/rollup/blob/master/src/utils/sanitizeFileName.ts
27
+ */
28
+ export declare function sanitizeChunkFileName(name: string): string;
2
29
  export declare function treeShake(): Plugin;
@@ -1,7 +1,52 @@
1
1
  // All credit for this work goes to the amazing Next.js team.
2
2
  // https://github.com/vercel/next.js/blob/canary/packages/next/build/babel/plugins/next-ssg-transform.ts
3
3
  // This is adapted to work with routeData functions. It can be run in two modes, one which preserves the routeData and the Component in the same file, and one which creates a
4
- import { basename } from "pathe";
4
+ import { basename, extname } from "pathe";
5
+ const PICKABLE_EXTENSIONS = ["js", "jsx", "ts", "tsx"];
6
+ /**
7
+ * Builds the module id used to import a subset of a route file's exports.
8
+ *
9
+ * The `pick` list has to live in the query so that the same file can be
10
+ * instantiated once per export subset (the client picks `default`/`$css`,
11
+ * the server picks its HTTP handlers). That query would otherwise leave the
12
+ * id ending in `?pick=GET`, which silently excludes route files from any
13
+ * plugin whose filter is anchored on the file extension -- a very common
14
+ * default, e.g. unplugin-macros' `/\.[cm]?[jt]sx?$/`.
15
+ *
16
+ * The trailing `lang.<ext>` marker is the same convention Vue SFCs use
17
+ * (`?vue&type=script&lang.ts`) and puts a real extension back at the end of
18
+ * the id, so those filters match again.
19
+ *
20
+ * @see https://github.com/solidjs/solid-start/issues/1918
21
+ */
22
+ export function toPickId(src, pick) {
23
+ const query = pick.map(p => `pick=${p}`).join("&");
24
+ const ext = extname(src).slice(1);
25
+ return PICKABLE_EXTENSIONS.includes(ext) ? `${src}?${query}&lang.${ext}` : `${src}?${query}`;
26
+ }
27
+ /**
28
+ * Matches the tail that {@link toPickId} appends, as it appears in a chunk
29
+ * name. Rolldown derives a chunk name from the module id by taking the
30
+ * basename minus its extension, so `index.tsx?pick=default&pick=$css&lang.tsx`
31
+ * arrives here as `index.tsx?pick=default&pick=$css&lang`.
32
+ */
33
+ const PICK_CHUNK_NAME_RE = /\.[cm]?[jt]sx?\?pick=[^?]*&lang$/;
34
+ const INVALID_CHAR_RE = /[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g;
35
+ const DRIVE_LETTER_RE = /^[a-z]:/i;
36
+ /**
37
+ * Rolldown's default chunk name sanitizer, plus a pass that drops the
38
+ * `?pick=...&lang` tail so route chunks keep the short, stable filenames they
39
+ * had before the `lang` marker was introduced (`index-<hash>.js`, not
40
+ * `index.tsx_pick_default_pick__css_lang-<hash>.js`). Client chunk filenames
41
+ * are public URLs, so this is not purely cosmetic.
42
+ *
43
+ * @see https://github.com/rollup/rollup/blob/master/src/utils/sanitizeFileName.ts
44
+ */
45
+ export function sanitizeChunkFileName(name) {
46
+ const stripped = name.replace(PICK_CHUNK_NAME_RE, "");
47
+ const driveLetter = DRIVE_LETTER_RE.exec(stripped)?.[0] ?? "";
48
+ return driveLetter + stripped.slice(driveLetter.length).replace(INVALID_CHAR_RE, "_");
49
+ }
5
50
  function treeShakeTransform({ types: t }) {
6
51
  function getIdentifier(path) {
7
52
  const parentPath = path.parentPath;
@@ -86,6 +86,29 @@ export interface SolidStartOptions {
86
86
  * @default "json"
87
87
  */
88
88
  mode?: "js" | "json";
89
+ /**
90
+ * Path to a module whose default export is an array of custom Seroval
91
+ * plugins, used to serialize values Seroval doesn't understand natively
92
+ * (ORM id types, decimals, `Temporal`, and other custom classes).
93
+ *
94
+ * Build plugins with `createPlugin` from `seroval`. The module is bundled
95
+ * into both the client and the server so that both ends of a server
96
+ * function agree on the format, so it must not import server-only code.
97
+ *
98
+ * SolidStart's built-in plugins take precedence: Seroval uses the first
99
+ * plugin whose `test()` passes, and these are appended after the built-ins.
100
+ *
101
+ * A plugin's `deserialize` rebuilds the value under {@link mode} `"json"`.
102
+ * `serialize` is only used by `mode: "js"`, where the payload is evaluated
103
+ * on the client and may therefore reference globals only, not the plugin
104
+ * module's own imports.
105
+ *
106
+ * Only applies to server-function and action payloads. The SSR hydration
107
+ * payload is serialized by `solid-js/web` and is unaffected.
108
+ *
109
+ * @example "src/seroval-plugins.ts"
110
+ */
111
+ plugins?: string;
89
112
  };
90
113
  /**
91
114
  * Configures plugin behavior per build environment
@@ -3,12 +3,14 @@ import { globSync } from "node:fs";
3
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";
@@ -71,17 +73,23 @@ export function solidStart(options) {
71
73
  for (const route of await clientRouter.getRoutes()) {
72
74
  for (const [key, value] of Object.entries(route)) {
73
75
  if (value && key.startsWith("$") && !key.startsWith("$$")) {
74
- function toRouteId(route) {
75
- return `${route.src}?${route.pick.map((p) => `pick=${p}`).join("&")}`;
76
- }
77
- clientInput.push(toRouteId(value));
76
+ clientInput.push(toPickId(value.src, value.pick));
78
77
  }
79
78
  }
80
79
  }
81
80
  }
82
81
  return {
83
82
  appType: "custom",
84
- build: { assetsDir: "_build/assets" },
83
+ build: {
84
+ assetsDir: "_build/assets",
85
+ rollupOptions: {
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
+ },
85
93
  optimizeDeps: {
86
94
  // Suppress TS errors from Vite 7 types when configuring Vite 8's Rolldown
87
95
  ...{
@@ -126,7 +134,6 @@ export function solidStart(options) {
126
134
  resolve: {
127
135
  alias: {
128
136
  "@solidjs/start/server/entry": handlers.server,
129
- "~": join(process.cwd(), start.appRoot),
130
137
  ...(!start.ssr
131
138
  ? {
132
139
  "@solidjs/start/server": "@solidjs/start/server/spa",
@@ -175,6 +182,7 @@ export function solidStart(options) {
175
182
  };
176
183
  },
177
184
  },
185
+ appRootAlias(root, start.appRoot),
178
186
  manifest(start),
179
187
  fsRoutes({
180
188
  routers: {
@@ -28,6 +28,12 @@ export function manifest(start) {
28
28
  return await this.resolve(start.middleware);
29
29
  return `\0${VIRTUAL_MODULES.middleware}`;
30
30
  }
31
+ if (id === VIRTUAL_MODULES.serovalPlugins) {
32
+ const plugins = start.serialization?.plugins;
33
+ if (plugins)
34
+ return await this.resolve(plugins);
35
+ return `\0${VIRTUAL_MODULES.serovalPlugins}`;
36
+ }
31
37
  },
32
38
  async load(id) {
33
39
  if (id === `\0${VIRTUAL_MODULES.clientViteManifest}`) {
@@ -40,7 +46,7 @@ export function manifest(start) {
40
46
  if (!entry)
41
47
  throw new Error("No client entry found");
42
48
  let rawManifest;
43
- const viteMajor = parseInt(viteVersion.split('.')[0], 10);
49
+ const viteMajor = parseInt(viteVersion.split(".")[0], 10);
44
50
  const manifestKey = Object.keys(globalThis.START_CLIENT_BUNDLE).find(k => k.endsWith("manifest.json"));
45
51
  if (manifestKey && viteMajor < 8) {
46
52
  const manifestAsset = globalThis.START_CLIENT_BUNDLE[manifestKey];
@@ -53,7 +59,8 @@ export function manifest(start) {
53
59
  if (devServer?.environments?.client?.config?.build?.outDir) {
54
60
  outDir = devServer.environments.client.config.build.outDir;
55
61
  }
56
- else if (this.environment?.config?.build?.outDir && this.environment?.config?.consumer === "client") {
62
+ else if (this.environment?.config?.build?.outDir &&
63
+ this.environment?.config?.consumer === "client") {
57
64
  outDir = this.environment.config.build.outDir;
58
65
  }
59
66
  else if (globalThis.START_CLIENT_OUT_DIR) {
@@ -75,6 +82,8 @@ export function manifest(start) {
75
82
  }
76
83
  else if (id === `\0${VIRTUAL_MODULES.middleware}`)
77
84
  return "export default {};";
85
+ else if (id === `\0${VIRTUAL_MODULES.serovalPlugins}`)
86
+ return "export default [];";
78
87
  else if (id.startsWith("/@manifest")) {
79
88
  if (this.environment.mode !== "dev")
80
89
  throw new Error("@manifest queries are only allowed in dev");
@@ -8,7 +8,7 @@ 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
  }
@@ -42,7 +42,7 @@ export function removeUnusedVariables(program) {
42
42
  }
43
43
  dirty = true;
44
44
  }
45
- else if (!(isInvalidForRemoval(binding.path))) {
45
+ else if (!isInvalidForRemoval(binding.path)) {
46
46
  binding.path.remove();
47
47
  dirty = true;
48
48
  }
@@ -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;
@@ -109,7 +109,7 @@ export async function handleServerFunction(h3Event) {
109
109
  }
110
110
  // handle no JS success case
111
111
  if (!instance)
112
- return handleNoJS(result, request, parsed);
112
+ return await handleNoJS(result, request, parsed);
113
113
  const body = getHeadersAndBody(result);
114
114
  if (body) {
115
115
  return new Response(body.body, {
@@ -132,8 +132,7 @@ export async function handleServerFunction(h3Event) {
132
132
  if (x.headers)
133
133
  mergeResponseHeaders(h3Event, x.headers);
134
134
  // forward non-redirect statuses
135
- if (x.status &&
136
- (!instance || x.status < 300 || x.status >= 400))
135
+ if (x.status && (!instance || x.status < 300 || x.status >= 400))
137
136
  h3Event.res.status = x.status;
138
137
  if (x.customBody) {
139
138
  x = await x.customBody();
@@ -147,7 +146,7 @@ export async function handleServerFunction(h3Event) {
147
146
  h3Event.res.headers.set("X-Error", toHeaderValue(error));
148
147
  }
149
148
  else {
150
- x = handleNoJS(x, request, parsed, true);
149
+ x = await handleNoJS(x, request, parsed, true);
151
150
  }
152
151
  if (instance) {
153
152
  const body = getHeadersAndBody(x);
@@ -182,7 +181,20 @@ function toHeaderValue(value) {
182
181
  return "true";
183
182
  }
184
183
  }
185
- function handleNoJS(result, request, parsed, thrown) {
184
+ function getRefererLocation(request, url) {
185
+ const referer = request.headers.get("referer");
186
+ try {
187
+ if (referer)
188
+ return new URL(referer).toString();
189
+ }
190
+ catch {
191
+ // fall through to the app root below
192
+ }
193
+ // no usable referer (e.g. a no-referrer policy): the app root still beats
194
+ // leaving the browser sitting on the server function endpoint
195
+ return new URL(import.meta.env.BASE_URL, url.origin).toString();
196
+ }
197
+ async function handleNoJS(result, request, parsed, thrown) {
186
198
  const url = new URL(request.url);
187
199
  const isError = result instanceof Error;
188
200
  let statusCode = 302;
@@ -193,10 +205,19 @@ function handleNoJS(result, request, parsed, thrown) {
193
205
  headers.set(`Location`, new URL(result.headers.get("Location"), url.origin + import.meta.env.BASE_URL).toString());
194
206
  statusCode = getExpectedRedirectStatus(result);
195
207
  }
208
+ else {
209
+ // responses that carry a value rather than a destination (json(), reload())
210
+ // still have to send the browser back to the page the form came from
211
+ headers.set("Location", getRefererLocation(request, url));
212
+ }
213
+ // the body is dropped from the redirect, so don't advertise its type
214
+ headers.delete("Content-Type");
215
+ // mirror the JS path: the flash cookie carries the value, not the Response
216
+ result = result.customBody ? await result.customBody() : null;
196
217
  }
197
218
  else
198
219
  headers = new Headers({
199
- Location: new URL(request.headers.get("referer")).toString(),
220
+ Location: getRefererLocation(request, url),
200
221
  });
201
222
  if (result) {
202
223
  headers.append("Set-Cookie", `flash=${encodeURIComponent(JSON.stringify({
@@ -204,10 +225,9 @@ function handleNoJS(result, request, parsed, thrown) {
204
225
  result: isError ? result.message : result,
205
226
  thrown: thrown,
206
227
  error: isError,
207
- input: [
208
- ...parsed.slice(0, -1),
209
- [...parsed[parsed.length - 1].entries()],
210
- ],
228
+ input: parsed.length
229
+ ? [...parsed.slice(0, -1), [...parsed[parsed.length - 1].entries()]]
230
+ : [],
211
231
  }))}; Secure; HttpOnly;`);
212
232
  }
213
233
  return new Response(null, {
@@ -229,10 +249,15 @@ export function createSingleFlightHeaders(sourceEvent) {
229
249
  // useH3Internals = true;
230
250
  // sourceEvent.nativeEvent.node.req.headers.cookie = "";
231
251
  // }
232
- SetCookies.forEach((cookie) => {
252
+ SetCookies.forEach(cookie => {
233
253
  if (!cookie)
234
254
  return;
235
- const { maxAge, expires, name, value } = parseSetCookie(cookie);
255
+ // `parseSetCookie` returns undefined for forbidden cookie names and for
256
+ // cookies with an empty name and value.
257
+ const parsed = parseSetCookie(cookie);
258
+ if (!parsed)
259
+ return;
260
+ const { maxAge, expires, name, value } = parsed;
236
261
  if (maxAge != null && maxAge <= 0) {
237
262
  delete cookies[name];
238
263
  return;
@@ -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";