From e6091a6f6422ee7170dd811454a8e27d07c61949 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 30 Jul 2026 16:47:28 +0200 Subject: [PATCH 1/2] fix: remove type-only server import remnants --- .changeset/calm-types-vanish.md | 5 ++ packages/start/src/directives/compile.spec.ts | 62 +++++++++++++++++++ .../src/directives/remove-unused-variables.ts | 8 +++ 3 files changed, 75 insertions(+) create mode 100644 .changeset/calm-types-vanish.md create mode 100644 packages/start/src/directives/compile.spec.ts diff --git a/.changeset/calm-types-vanish.md b/.changeset/calm-types-vanish.md new file mode 100644 index 000000000..84bdb53e2 --- /dev/null +++ b/.changeset/calm-types-vanish.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": patch +--- + +Remove type-only import remnants from client server-function transforms so they do not retain server-only dependency chains. diff --git a/packages/start/src/directives/compile.spec.ts b/packages/start/src/directives/compile.spec.ts new file mode 100644 index 000000000..876a0c7b6 --- /dev/null +++ b/packages/start/src/directives/compile.spec.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; +import { compile, type CompileOptions } from "./compile.ts"; + +const clientOptions: CompileOptions = { + env: "development", + mode: "client", + directive: "use server", + definitions: { + register: { + kind: "named", + name: "createServerReference", + source: "virtual:server-runtime", + }, + clone: { + kind: "named", + name: "cloneServerReference", + source: "virtual:server-runtime", + }, + }, +}; + +describe("compile", () => { + it("removes an import when only type specifiers remain", async () => { + const result = await compile( + "/src/server-action.ts", + ` + import { type Session, verify } from "./server-module.ts"; + + export const serverAction = async (): Promise => { + "use server"; + return verify(); + }; + `, + clientOptions, + ); + + expect(result.valid).toBe(true); + expect(result.code).not.toContain("./server-module.ts"); + }); + + it("preserves live value specifiers from a mixed import", async () => { + const result = await compile( + "/src/server-action.ts", + ` + import { type Session, clientValue, verify } from "./server-module.ts"; + + export const value = clientValue; + export const serverAction = async (): Promise => { + "use server"; + return verify(); + }; + `, + clientOptions, + ); + + expect(result.valid).toBe(true); + expect(result.code).toContain( + 'import { type Session, clientValue } from "./server-module.ts";', + ); + expect(result.code).not.toMatch(/\bverify\b/); + }); +}); diff --git a/packages/start/src/directives/remove-unused-variables.ts b/packages/start/src/directives/remove-unused-variables.ts index 312901d53..dd8e9852b 100644 --- a/packages/start/src/directives/remove-unused-variables.ts +++ b/packages/start/src/directives/remove-unused-variables.ts @@ -44,6 +44,14 @@ export function removeUnusedVariables(program: babel.NodePath) { parent.remove(); } else { binding.path.remove(); + if ( + parent.node.specifiers.every( + specifier => + t.isImportSpecifier(specifier) && specifier.importKind === "type", + ) + ) { + parent.remove(); + } } dirty = true; } else if (!isInvalidForRemoval(binding.path)) { From a82b259d34fcebf895595e60429c5685da4e1b1e Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 30 Jul 2026 17:10:10 +0200 Subject: [PATCH 2/2] refactor: count runtime import specifiers --- .../src/directives/remove-unused-variables.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/start/src/directives/remove-unused-variables.ts b/packages/start/src/directives/remove-unused-variables.ts index dd8e9852b..9af4346fb 100644 --- a/packages/start/src/directives/remove-unused-variables.ts +++ b/packages/start/src/directives/remove-unused-variables.ts @@ -16,6 +16,22 @@ function isInvalidForRemoval(path: babel.NodePath) { return isPathValid(target, t.isObjectPattern) || isPathValid(target, t.isArrayPattern); } +function countValidImport(node: t.ImportDeclaration): number { + if (node.importKind === "type") { + return 0; + } + + let count = 0; + + for (const specifier of node.specifiers) { + if (specifier.type !== "ImportSpecifier" || specifier.importKind === "value") { + count += 1; + } + } + + return count; +} + export function removeUnusedVariables(program: babel.NodePath) { // TODO(Alexis): // This implementation is simple but slow @@ -40,18 +56,10 @@ export function removeUnusedVariables(program: babel.NodePath) { if (binding.references === 0 && !binding.path.removed) { const parent = binding.path.parentPath; if (isPathValid(parent, t.isImportDeclaration)) { - if (parent.node.specifiers.length === 1) { + if (countValidImport(parent.node) <= 1) { parent.remove(); } else { binding.path.remove(); - if ( - parent.node.specifiers.every( - specifier => - t.isImportSpecifier(specifier) && specifier.importKind === "type", - ) - ) { - parent.remove(); - } } dirty = true; } else if (!isInvalidForRemoval(binding.path)) {