@solidjs/start 2.0.5 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # @solidjs/start
2
2
 
3
+ ## 2.0.6
4
+
5
+ ### Patch Changes
6
+
7
+ - a4ed2a4: Stop `client-only` from failing the build for lazily imported client components.
8
+
9
+ A module that imported `client-only` and was loaded through `clientOnly(() => import(...))` failed the server build, even though it only ever runs in the browser. The server build resolves every dynamic import to emit its chunk, so it resolved the `client-only` module and rejected it, though that module never runs on the server. `client-only` no longer fails the build. `server-only` is unchanged and still fails a client build.
10
+
11
+ - 048f5a9: Stop a no-JS server function POST from returning a 500 when the body is not a form.
12
+
13
+ A POST to a server function without the client runtime, carrying an empty body or a non-form content type, left a value that is not a `FormData` as the last argument. Building the flash cookie called `.entries()` on it and threw, and the error handler rethrew the same way, so the request failed with a 500. The response is now the normal redirect, and the flash cookie is best effort so it can no longer take down the error path.
14
+
15
+ - 59e7dc7: Make reading server function streams safer and faster.
16
+
17
+ - A malformed chunk after the first one is now logged instead of causing an unhandled promise rejection. On hosts that do not catch unhandled rejections, one bad request could stop the server process.
18
+ - The stream is cancelled when a chunk cannot be read or parsed.
19
+ - Chunk headers are now checked strictly, and a server rejects chunks over 64MB that a client sends.
20
+ - Large payloads that arrive in many small pieces are read in linear time. A 16MB chunk read in 16KB pieces took about 2 seconds and now takes about 30ms.
21
+
22
+ - d34acf5: Require seroval and seroval-plugins 1.6.8 or newer.
23
+ - d34acf5: Send `Cache-Control: no-store` on server function responses by default.
24
+
25
+ Server function responses, including calls made with GET, carried no `Cache-Control`, so a shared cache configured to store them could serve one caller's result to another. Every response from the server function handler now defaults to `no-store`: results, errors, redirects, no-JS redirects, raw `Response` passthroughs, and the handler's own refusals. A `Cache-Control` the function sets itself, on a returned `Response` or on the event's response headers, is kept as is, and a `304` is never given one.
26
+
27
+ - d34acf5: Reject cross-site server function requests.
28
+
29
+ A `"use server"` function could be invoked from another site with the visitor's cookies, over a GET or a form POST, because the request's origin was not checked. Requests to server functions are now allowed only from the same origin or same site. The check trusts the `Sec-Fetch-Site` header and falls back to comparing `Origin` against the request host, so a page on another site receives a `403` instead of running the function. Same-origin calls, user-initiated navigations, and no-JS form submissions are unaffected. A separate origin that needs to call your backend should use an API route with explicit CORS.
30
+
31
+ - 9d3cbec: Report server functions that cannot work instead of compiling them into broken output.
32
+
33
+ - A `"use server"` function that reads a variable from an enclosing function now fails the build. The function is moved to the top level of its module, so the variable is not in scope when it runs.
34
+ - The same check covers `this` and `arguments` in an arrow function, `super`, and private class members.
35
+ - A `"use server"` directive in an object or class method now fails the build. It was ignored before, which shipped the method body and the modules it imports to the browser.
36
+ - A `"use server"` string that is not the first statement of a module or a function body now logs a warning. It has no effect there.
37
+ - An export a `"use server"` module cannot serve now logs a warning that names it. These exports are still left out of the client build.
38
+ - A `"use server"` module can now export an anonymous default function. Both `export default async () => {}` and `export default async function () {}` work.
39
+ - Server function ids are now built from the names a function is nested under, such as `Page.load`, instead of the order the functions appear in. An id no longer changes when another server function is added to the same file, and two functions that share a name are told apart by the names around them. Production ids stay opaque.
40
+ - Server functions are now compiled in `.mts` and `.cts` files.
41
+ - Build errors now point at the full path of the file, not just its name.
42
+
43
+ - d34acf5: Treat a server function request with `Origin: null` and no `Sec-Fetch-Site` as cross-site.
44
+
45
+ Browsers send `Origin: null` from opaque origins, such as sandboxed iframes and some cross-origin redirect chains. When `Sec-Fetch-Site` was absent, the handler skipped the `Origin` comparison for that value and allowed the request. It is now refused with a `403`. Requests that carry neither header, such as those from non-browser clients, are still allowed.
46
+
47
+ - d34acf5: Settle server function values that are still waiting on the stream when its body ends.
48
+
49
+ A seroval body can reference a promise or stream that a later frame settles. If the body ended before that frame arrived, the value stayed pending forever, so a server function awaiting such an argument never answered. A malformed later frame also surfaced as an unhandled rejection. When the body ends or fails, every promise still waiting on it now rejects (with the failure, or with "Server function stream ended unexpectedly.") and every open stream errors. Decoded promises that nobody awaits no longer report unhandled rejections; code that awaits them still sees the rejection.
50
+
3
51
  ## 2.0.5
4
52
 
5
53
  ### Patch Changes
@@ -1,14 +1,20 @@
1
1
  import type { Plugin } from "vite";
2
2
  /**
3
- * Supports `server-only` and `client-only` marker modules (#2162): importing
4
- * `server-only` from a client module (or `client-only` from a server module)
5
- * fails at resolve time; in the allowed environment the marker resolves to an
6
- * empty module.
3
+ * Supports the `server-only` and `client-only` marker modules (#2162).
7
4
  *
8
- * Start's own server-only entry points (`@solidjs/start/http`,
9
- * `@solidjs/start/middleware`) import `server-only` themselves, so pulling
10
- * them into the client bundle fails loudly instead of shipping server code to
11
- * the browser, where it crashed hydration and broke unrelated actions/forms
12
- * with no diagnostic (https://github.com/solidjs/solid-start/issues/2068).
5
+ * `server-only` imported from a client module fails the build at resolve time.
6
+ * A client module that reaches server-only code is always a mistake and must
7
+ * never bundle. Start's own server-only entry points (`@solidjs/start/http`,
8
+ * `@solidjs/start/middleware`) import `server-only` themselves, so pulling them
9
+ * into the client bundle fails loudly instead of shipping server code to the
10
+ * browser (https://github.com/solidjs/solid-start/issues/2068).
11
+ *
12
+ * `client-only` cannot be enforced the same way. The server build resolves
13
+ * every dynamic import to emit its chunk, so a module reached only through
14
+ * `clientOnly(() => import(...))` is resolved by the server build even though
15
+ * it never runs on the server. Failing the build there is a false positive
16
+ * that breaks the client-only lazy pattern, so `client-only` resolves to an
17
+ * empty module in both environments. Client-only code that does reach the
18
+ * server runtime still fails there on its own (e.g. a missing `window`).
13
19
  */
14
20
  export declare function boundaryModules(): Plugin;
@@ -1,15 +1,21 @@
1
- const VIRTUAL_ID = "\0solid-start:boundary-modules:id";
1
+ const EMPTY_ID = "\0solid-start:boundary-modules:empty";
2
2
  /**
3
- * Supports `server-only` and `client-only` marker modules (#2162): importing
4
- * `server-only` from a client module (or `client-only` from a server module)
5
- * fails at resolve time; in the allowed environment the marker resolves to an
6
- * empty module.
3
+ * Supports the `server-only` and `client-only` marker modules (#2162).
7
4
  *
8
- * Start's own server-only entry points (`@solidjs/start/http`,
9
- * `@solidjs/start/middleware`) import `server-only` themselves, so pulling
10
- * them into the client bundle fails loudly instead of shipping server code to
11
- * the browser, where it crashed hydration and broke unrelated actions/forms
12
- * with no diagnostic (https://github.com/solidjs/solid-start/issues/2068).
5
+ * `server-only` imported from a client module fails the build at resolve time.
6
+ * A client module that reaches server-only code is always a mistake and must
7
+ * never bundle. Start's own server-only entry points (`@solidjs/start/http`,
8
+ * `@solidjs/start/middleware`) import `server-only` themselves, so pulling them
9
+ * into the client bundle fails loudly instead of shipping server code to the
10
+ * browser (https://github.com/solidjs/solid-start/issues/2068).
11
+ *
12
+ * `client-only` cannot be enforced the same way. The server build resolves
13
+ * every dynamic import to emit its chunk, so a module reached only through
14
+ * `clientOnly(() => import(...))` is resolved by the server build even though
15
+ * it never runs on the server. Failing the build there is a false positive
16
+ * that breaks the client-only lazy pattern, so `client-only` resolves to an
17
+ * empty module in both environments. Client-only code that does reach the
18
+ * server runtime still fails there on its own (e.g. a missing `window`).
13
19
  */
14
20
  export function boundaryModules() {
15
21
  return {
@@ -17,25 +23,22 @@ export function boundaryModules() {
17
23
  enforce: "pre",
18
24
  resolveId(id, importer, { ssr }) {
19
25
  if (id === "server-only") {
20
- if (!ssr)
26
+ if (!ssr) {
21
27
  this.error(`Attempt to import 'server-only' in a client module: ${importer}. ` +
22
28
  `Code that uses this module must run only on the server: mark it with ` +
23
29
  `"use server", or make sure it is only imported by server code.`);
30
+ }
31
+ return EMPTY_ID;
24
32
  }
25
- else if (id === "client-only") {
26
- if (ssr)
27
- this.error(`Attempt to import 'client-only' in a server module: ${importer}. ` +
28
- `Code that uses this module must run only in the browser: make sure it ` +
29
- `is only imported by client code (e.g. wrap components with clientOnly()).`);
30
- }
31
- else {
32
- return null;
33
+ if (id === "client-only") {
34
+ return EMPTY_ID;
33
35
  }
34
- return VIRTUAL_ID;
36
+ return null;
35
37
  },
36
38
  load(id) {
37
- if (id === VIRTUAL_ID)
39
+ if (id === EMPTY_ID) {
38
40
  return "export {}";
41
+ }
39
42
  },
40
43
  };
41
44
  }
@@ -4,6 +4,8 @@ export interface CompileResult {
4
4
  valid: boolean;
5
5
  code: string;
6
6
  map: babel.BabelFileResult["map"];
7
+ /** Problems that do not stop the compile, reported to the caller. */
8
+ warnings: string[];
7
9
  }
8
- export type CompileOptions = Omit<StateContext, "count" | "hash" | "imports" | "valid">;
10
+ export type CompileOptions = Omit<StateContext, "count" | "names" | "hash" | "imports" | "valid" | "warnings">;
9
11
  export declare function compile(id: string, code: string, options: CompileOptions): Promise<CompileResult>;
@@ -1,13 +1,14 @@
1
1
  import * as babel from "@babel/core";
2
- import path from "node:path";
3
2
  import { directivesPlugin } from "./plugin.js";
4
3
  import xxHash32 from "./xxhash32.js";
5
4
  export async function compile(id, code, options) {
6
5
  const context = {
7
6
  ...options,
8
7
  valid: false,
8
+ warnings: [],
9
9
  hash: xxHash32(id).toString(16),
10
10
  count: 0,
11
+ names: new Map(),
11
12
  imports: new Map(),
12
13
  };
13
14
  const pluginOption = [directivesPlugin, context];
@@ -22,7 +23,8 @@ export async function compile(id, code, options) {
22
23
  parserOpts: {
23
24
  plugins,
24
25
  },
25
- filename: path.basename(id),
26
+ // The full path, so diagnostics point at the file the user edits.
27
+ filename: id,
26
28
  ast: false,
27
29
  sourceMaps: true,
28
30
  configFile: false,
@@ -34,6 +36,7 @@ export async function compile(id, code, options) {
34
36
  valid: context.valid,
35
37
  code: result.code || "",
36
38
  map: result.map,
39
+ warnings: context.warnings,
37
40
  };
38
41
  }
39
42
  throw new Error("invariant");
@@ -0,0 +1,2 @@
1
+ import type { NodePath } from "@babel/core";
2
+ export declare function getHierarchicalName(path: NodePath): string;
@@ -0,0 +1,104 @@
1
+ import * as t from "@babel/types";
2
+ /**
3
+ * The names a function is nested under, from the top of the module down, joined
4
+ * with a dot. `Page.load` for a `load` declared inside `Page`.
5
+ *
6
+ * Server function ids are built from this instead of the order the functions
7
+ * appear in. An id then stays the same when another function is added to the
8
+ * file, and two functions that share a name are still told apart by the names
9
+ * around them.
10
+ */
11
+ const ANONYMOUS = "anonymous";
12
+ function getKeyName(node, computed) {
13
+ if (computed) {
14
+ return undefined;
15
+ }
16
+ switch (node.type) {
17
+ case "Identifier":
18
+ return node.name;
19
+ case "PrivateName":
20
+ return node.id.name;
21
+ case "StringLiteral":
22
+ return node.value;
23
+ case "NumericLiteral":
24
+ return String(node.value);
25
+ default:
26
+ return undefined;
27
+ }
28
+ }
29
+ function getSegment(path, child) {
30
+ const node = path.node;
31
+ switch (node.type) {
32
+ case "VariableDeclarator":
33
+ return t.isIdentifier(node.id) ? node.id.name : undefined;
34
+ case "FunctionDeclaration":
35
+ case "ClassDeclaration":
36
+ case "ClassExpression":
37
+ return node.id?.name;
38
+ case "FunctionExpression":
39
+ if (node.id) {
40
+ return node.id.name;
41
+ }
42
+ return path.parentPath && getSegment(path.parentPath, path) ? undefined : ANONYMOUS;
43
+ case "ObjectProperty":
44
+ return getKeyName(node.key, node.computed);
45
+ case "ObjectMethod":
46
+ case "ClassMethod":
47
+ return getKeyName(node.key, node.computed ?? false);
48
+ case "ClassPrivateMethod":
49
+ return getKeyName(node.key, false);
50
+ case "ClassProperty":
51
+ // Only the initializer belongs to the property.
52
+ return child.node === node.value ? getKeyName(node.key, node.computed ?? false) : undefined;
53
+ case "ClassPrivateProperty":
54
+ return child.node === node.value ? getKeyName(node.key, false) : undefined;
55
+ case "ExportDefaultDeclaration":
56
+ return "default";
57
+ case "AssignmentExpression":
58
+ return t.isIdentifier(node.left) ? node.left.name : undefined;
59
+ case "ArrowFunctionExpression":
60
+ // An enclosing function that nothing names still has to separate what is
61
+ // inside it from what is beside it.
62
+ return path.parentPath && getSegment(path.parentPath, path) ? undefined : ANONYMOUS;
63
+ default:
64
+ return undefined;
65
+ }
66
+ }
67
+ function isFunctionBoundary(node) {
68
+ switch (node.type) {
69
+ case "ArrowFunctionExpression":
70
+ case "FunctionExpression":
71
+ case "FunctionDeclaration":
72
+ case "ObjectMethod":
73
+ case "ClassMethod":
74
+ case "ClassPrivateMethod":
75
+ case "StaticBlock":
76
+ return true;
77
+ default:
78
+ return false;
79
+ }
80
+ }
81
+ export function getHierarchicalName(path) {
82
+ const segments = [];
83
+ let child = path;
84
+ let current = path.parentPath;
85
+ while (current && !t.isProgram(current.node)) {
86
+ // Nothing named this function before another function encloses it, so it
87
+ // is an anonymous function inside that one.
88
+ if (segments.length === 0 && isFunctionBoundary(current.node)) {
89
+ segments.push(ANONYMOUS);
90
+ }
91
+ const segment = getSegment(current, child);
92
+ // A named function expression assigned to a variable of the same name
93
+ // would otherwise repeat itself.
94
+ if (segment && segment !== segments[segments.length - 1]) {
95
+ segments.push(segment);
96
+ }
97
+ child = current;
98
+ current = current.parentPath;
99
+ }
100
+ if (segments.length === 0) {
101
+ return ANONYMOUS;
102
+ }
103
+ return segments.reverse().join(".");
104
+ }
@@ -2,8 +2,9 @@ import { createFilter, } from "vite";
2
2
  import fg from "fast-glob";
3
3
  import { compile } from "./compile.js";
4
4
  import xxHash32 from "./xxhash32.js";
5
- const DEFAULT_INCLUDE = "src/**/*.{jsx,tsx,ts,js,mjs,cjs}";
6
- const DEFAULT_EXCLUDE = "node_modules/**/*.{jsx,tsx,ts,js,mjs,cjs}";
5
+ const EXTENSIONS = "{jsx,tsx,ts,js,mjs,cjs,mts,cts}";
6
+ const DEFAULT_INCLUDE = `src/**/*.${EXTENSIONS}`;
7
+ const DEFAULT_EXCLUDE = `node_modules/**/*.${EXTENSIONS}`;
7
8
  const DIRECTIVE = "use server";
8
9
  // Dev-only virtual module used by fns/handler.ts to lazily resolve a server
9
10
  // function id back to its owning module when the function was never evaluated
@@ -184,6 +185,9 @@ export function serverFunctionsPlugin(options) {
184
185
  mode,
185
186
  env,
186
187
  });
188
+ for (const warning of result.warnings) {
189
+ this.warn(warning);
190
+ }
187
191
  if (result.valid) {
188
192
  const preloader = preload[mode];
189
193
  if (preloader) {
@@ -7,8 +7,11 @@ export interface StateContext {
7
7
  directive: string;
8
8
  hash: string;
9
9
  count: number;
10
+ /** How many times each name path has been used, to keep ids unique. */
11
+ names: Map<string, number>;
10
12
  imports: Map<string, t.Identifier>;
11
13
  valid: boolean;
14
+ warnings: string[];
12
15
  definitions: {
13
16
  register: ImportDefinition;
14
17
  clone: ImportDefinition;