diff --git a/apps/tests/src/e2e/server-function.test.ts b/apps/tests/src/e2e/server-function.test.ts
index c77656e9d..ee10f134e 100644
--- a/apps/tests/src/e2e/server-function.test.ts
+++ b/apps/tests/src/e2e/server-function.test.ts
@@ -91,4 +91,33 @@ test.describe("server-function", () => {
await page.goto("http://localhost:3000/server-env");
await expect(page.locator("#server-fn-test")).toContainText('{"result":true}');
});
+
+ test("should build with a server function including an unused try/catch variable", async ({
+ page,
+ }) => {
+ await page.goto("http://localhost:3000/server-function-unused-trycatch");
+ await expect(page.locator("#server-fn-test")).toContainText("false");
+ });
+
+ test("should build with a server function including an unused destructured variable", async ({
+ page,
+ }) => {
+ await page.goto("http://localhost:3000/server-function-unused-destructure");
+ await expect(page.locator("#server-fn-test")).toContainText("false");
+ });
+
+ /**
+ * Makes sure that server function dead code elimination
+ * runs before Solid's SSR transforms.
+ *
+ * Solid's SSR code removes client-only event handler code
+ * such as onClick, but server function's only referenced
+ * in such event handlers still must be registered on
+ * the server.
+ */
+ test("should build with a server function only referenced inside onClick", async ({ page }) => {
+ await page.goto("http://localhost:3000/server-function-onclick");
+ await page.locator("#server-fn-test").click();
+ await expect(page.locator("#server-fn-test")).toContainText("false");
+ });
});
diff --git a/apps/tests/src/routes/server-function-onclick.tsx b/apps/tests/src/routes/server-function-onclick.tsx
new file mode 100644
index 000000000..734fa356d
--- /dev/null
+++ b/apps/tests/src/routes/server-function-onclick.tsx
@@ -0,0 +1,23 @@
+async function serverFnOnClick() {
+ "use server";
+
+ return false;
+}
+
+export default function App() {
+ return (
+
+ {
+ const el = evt.target;
+ serverFnOnClick().then(r => {
+ el.textContent = JSON.stringify(r);
+ });
+ }}
+ >
+ Click me
+
+
+ );
+}
diff --git a/apps/tests/src/routes/server-function-unused-destructure.tsx b/apps/tests/src/routes/server-function-unused-destructure.tsx
new file mode 100644
index 000000000..c1c574767
--- /dev/null
+++ b/apps/tests/src/routes/server-function-unused-destructure.tsx
@@ -0,0 +1,28 @@
+import { createEffect, createSignal } from "solid-js";
+
+function serverFnDestructure() {
+ "use server";
+
+ const rawItems = [{ id: "", age: 42 }];
+ const items: { age: number }[] = [];
+ for (const { id, ...rest } of rawItems) {
+ items.push(rest);
+ }
+
+ return false;
+}
+
+export default function App() {
+ const [output, setOutput] = createSignal();
+
+ createEffect(async () => {
+ const result = await serverFnDestructure();
+ setOutput(result);
+ });
+
+ return (
+
+ {JSON.stringify(output())}
+
+ );
+}
diff --git a/apps/tests/src/routes/server-function-unused-trycatch.tsx b/apps/tests/src/routes/server-function-unused-trycatch.tsx
new file mode 100644
index 000000000..a7a4bfe6e
--- /dev/null
+++ b/apps/tests/src/routes/server-function-unused-trycatch.tsx
@@ -0,0 +1,26 @@
+import { createEffect, createSignal } from "solid-js";
+
+function serverFnTryCatch() {
+ "use server";
+
+ try {
+ throw new Error();
+ } catch (error) {
+ return false;
+ }
+}
+
+export default function App() {
+ const [output, setOutput] = createSignal();
+
+ createEffect(async () => {
+ const result = await serverFnTryCatch();
+ setOutput(result);
+ });
+
+ return (
+
+ {JSON.stringify(output())}
+
+ );
+}
diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts
index 9cdc6830e..447f26ab7 100644
--- a/packages/start/src/config/index.ts
+++ b/packages/start/src/config/index.ts
@@ -63,6 +63,18 @@ export function solidStart(options?: SolidStartOptions): Array {
server: `${start.appRoot}/entry-server${entryExtension}`,
};
return [
+ // TODO (Alexis): check if the comment below is still relevant
+ //
+ // Must be placed after fsRoutes, as treeShake will remove the
+ // server fn exports added in by this plugin
+ serverFunctionsPlugin({
+ manifest: VIRTUAL_MODULES.serverFnManifest,
+ runtime: {
+ server: '@solidjs/start/fns/server',
+ client: '@solidjs/start/fns/client',
+ },
+ filter: options?.serverFunctions?.filter,
+ }),
{
name: "solid-start:config",
enforce: "pre",
@@ -190,16 +202,6 @@ export function solidStart(options?: SolidStartOptions): Array {
}),
lazy(),
envPlugin(options?.env),
- // Must be placed after fsRoutes, as treeShake will remove the
- // server fn exports added in by this plugin
- serverFunctionsPlugin({
- manifest: VIRTUAL_MODULES.serverFnManifest,
- runtime: {
- server: '@solidjs/start/fns/server',
- client: '@solidjs/start/fns/client',
- },
- filter: options?.serverFunctions?.filter,
- }),
{
name: "solid-start:virtual-modules",
async resolveId(id) {
diff --git a/packages/start/src/directives/index.ts b/packages/start/src/directives/index.ts
index 0219fe346..04a30395c 100644
--- a/packages/start/src/directives/index.ts
+++ b/packages/start/src/directives/index.ts
@@ -196,6 +196,7 @@ export function serverFunctionsPlugin(options: ServerFunctionsOptions): Plugin[]
},
{
name: "solid-start:server-functions/compiler",
+ enforce: 'pre',
async transform(code, fileId, opts) {
const mode = opts?.ssr ? "server" : "client";
const [id] = fileId.split("?");
diff --git a/packages/start/src/directives/remove-unused-variables.ts b/packages/start/src/directives/remove-unused-variables.ts
index 6155bfea7..bc7bfdcc0 100644
--- a/packages/start/src/directives/remove-unused-variables.ts
+++ b/packages/start/src/directives/remove-unused-variables.ts
@@ -2,6 +2,20 @@ import type * as babel from "@babel/core";
import * as t from "@babel/types";
import { isPathValid } from "./paths.ts";
+function isInvalidForRemoval(path: babel.NodePath) {
+ if (isPathValid(path, t.isCatchClause)) {
+ // This case is for `catch (error)` blocks
+ return true;
+ }
+
+ // This one is for destructured variables
+ let target = path;
+ if (isPathValid(path, t.isVariableDeclarator)) {
+ target = path.get('id');
+ }
+ return isPathValid(target, t.isObjectPattern) || isPathValid(target, t.isArrayPattern);
+}
+
export function removeUnusedVariables(program: babel.NodePath) {
// TODO(Alexis):
// This implementation is simple but slow
@@ -24,17 +38,18 @@ export function removeUnusedVariables(program: babel.NodePath) {
case "hoisted":
case "module":
if (binding.references === 0 && !binding.path.removed) {
- if (isPathValid(binding.path.parentPath, t.isImportDeclaration)) {
const parent = binding.path.parentPath;
+ if (isPathValid(parent, t.isImportDeclaration)) {
if (parent.node.specifiers.length === 1) {
parent.remove();
} else {
binding.path.remove();
}
- } else {
+ dirty = true;
+ } else if (!(isInvalidForRemoval(binding.path))) {
binding.path.remove();
+ dirty = true;
}
- dirty = true;
}
break;
case "local":