Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/faster-compiler-transforms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@solidjs/start": patch
---

Speed up the server function and lazy transforms.

- Modules without `"use server"` are no longer parsed by the server function transform.
- The server build no longer parses a module with Babel only to add its lazy id.
- Modules with many server functions compile about 4 times faster.
- A module with a server function no longer fails to compile when it has an unused `for...of` or `for...in` loop variable, or a chain of unused declarations.
43 changes: 43 additions & 0 deletions packages/start/src/config/lazy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";

import lazy from "./lazy.ts";

async function transform(code: string, id = `${process.cwd()}/src/routes/page.tsx`) {
const plugin = lazy() as any;
const context = { environment: { name: "ssr" } };
return plugin.transform.call(context, code, id);
}

describe("lazy", () => {
it("appends the module id without moving existing code", async () => {
const code = `import { A } from "./a.ts";\nexport default function Page() { return A; }`;
const result = await transform(code);

expect(result.code.startsWith(code)).toBe(true);
expect(result.code).toContain(`export const id$$ = "src/routes/page.tsx";`);
expect(result.map).toBeNull();
});

it("imports lazy from the server runtime", async () => {
const result = await transform(
`import { lazy } from "solid-js";\nconst Page = lazy(() => import("./page.tsx"));\nexport default Page;`,
);

expect(result.code).toContain(`import { lazy } from "@solidjs/start/server";`);
expect(result.code).not.toMatch(/import \{ lazy \} from "solid-js"/);
expect(result.code).toContain(`export const id$$ = "src/routes/page.tsx";`);
expect(result.map).toBeTruthy();
});

it("skips a module without a default export or lazy", async () => {
expect(await transform(`import { A } from "./a.ts";\nexport const B = A;`)).toBeUndefined();
});

it("skips the client environment", async () => {
const plugin = lazy() as any;
const context = { environment: { name: "client" } };
expect(
await plugin.transform.call(context, `import A from "./a.ts";\nexport default A;`, "/a.tsx"),
).toBeUndefined();
});
});
42 changes: 11 additions & 31 deletions packages/start/src/config/lazy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,6 @@ import { basename, relative, sep } from "node:path/posix";
import type { PluginOption } from "vite";
import { VITE_ENVIRONMENTS } from "./constants.ts";

const idTransform = (id: string): PluginItem => {
return {
visitor: {
Program(path) {
path.node.body.unshift(
t.exportNamedDeclaration(
t.variableDeclaration("const", [
t.variableDeclarator(t.identifier("id$$"), t.stringLiteral(id)),
]),
),
);
},
},
};
};

const importTransform = (): PluginItem => {
return {
visitor: {
Expand Down Expand Up @@ -96,24 +80,21 @@ const lazy = (): PluginOption => {
if (src.indexOf("import") === -1) return;
if (id.includes("entry-server")) return;

const plugins: PluginItem[] = [];

const hasDefaultExport = src.indexOf("export default") !== -1;
if (hasDefaultExport) {
const localId = relative(cwd, id);
const chunkName = sharedChunkNames[id];
plugins.push(idTransform(chunkName ?? localId));
// Appended rather than prepended, so no existing code moves and the
// source map of the input still applies to the output.
let idExport = "";
if (src.indexOf("export default") !== -1) {
const chunkName = sharedChunkNames[id] ?? relative(cwd, id);
idExport = `\nexport const id$$ = ${JSON.stringify(chunkName)};\n`;
}

const hasLazy = src.indexOf("lazy(") !== -1;
if (hasLazy) plugins.push(importTransform());

if (!plugins.length) {
return;
if (src.indexOf("lazy(") === -1) {
if (!idExport) return;
return { code: src + idExport, map: null };
}

const transformed = await babel.transformAsync(src, {
plugins,
plugins: [importTransform()],
parserOpts: {
plugins: ["jsx", "typescript"],
},
Expand All @@ -127,8 +108,7 @@ const lazy = (): PluginOption => {

if (!transformed?.code) return;

const { code, map } = transformed;
return { code, map };
return { code: transformed.code + idExport, map: transformed.map };
},
};
};
Expand Down
59 changes: 59 additions & 0 deletions packages/start/src/directives/compile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,65 @@ describe("compile", () => {
expect(result.code).not.toContain("./server-module.ts");
});

it("removes declarations that only unused declarations read", async () => {
const result = await compile(
"/src/server-action.ts",
`
import { db } from "./db.ts";

const first = db;
const second = first;
const third = second;

export const serverAction = async () => {
"use server";
return third;
};
`,
clientOptions,
);

expect(result.code).not.toContain("./db.ts");
expect(result.code).not.toMatch(/first|second|third/);
});

it("removes every declaration of an unused var", async () => {
const result = await compile(
"/src/server-action.ts",
`
var value = 1;
var value = 2;

export const serverAction = async () => {
"use server";
return 1;
};
`,
clientOptions,
);

expect(result.code).not.toContain("value");
});

it("keeps an unused loop variable", async () => {
const result = await compile(
"/src/server-action.ts",
`
for (const item of []) {}
for (const key in {}) {}

export const serverAction = async () => {
"use server";
return 1;
};
`,
clientOptions,
);

expect(result.code).toContain("for (const item of [])");
expect(result.code).toContain("for (const key in {})");
});

it("preserves live value specifiers from a mixed import", async () => {
const result = await compile(
"/src/server-action.ts",
Expand Down
5 changes: 5 additions & 0 deletions packages/start/src/directives/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ export function serverFunctionsPlugin(options: ServerFunctionsOptions): Plugin[]
if (!filter(id)) {
return null;
}
// Most modules have no directive. Skipping them avoids a Babel parse
// and print whose output would be thrown away.
if (!code.includes(DIRECTIVE)) {
return null;
}

const result = await compile(id!, code, {
...(mode === "server" ? serverOptions : clientOptions),
Expand Down
17 changes: 6 additions & 11 deletions packages/start/src/directives/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ import { isPathValid, unwrapPath } from "./paths.ts";
import { removeUnusedVariables } from "./remove-unused-variables.ts";
import type { ImportDefinition } from "./types.ts";
import xxHash32 from "./xxhash32.ts";
import {
assertHoistable,
assertNoMethodDirectives,
collectMisplacedDirectives,
} from "./validate.ts";
import { assertHoistable, validateDirectivePlacement } from "./validate.ts";

export interface StateContext {
env: "production" | "development";
Expand Down Expand Up @@ -129,9 +125,12 @@ function transformFunction(

const sourceID = generateUniqueName(path, "serverFn");

rootStatement.insertBefore(
const [source] = rootStatement.insertBefore(
t.variableDeclaration("const", [t.variableDeclarator(sourceID, sourceReference)]),
);
// Registering only the new declaration keeps the scope current without a
// full crawl per function. References are recounted once at the end.
path.scope.getProgramParent().registerDeclaration(source!);

// Clone the source function to replace the server function
path.replaceWith(
Expand All @@ -145,8 +144,6 @@ function transformFunction(
]),
);
}

path.scope.crawl();
}

function traceBinding(path: babel.NodePath, name: string): Binding | undefined {
Expand Down Expand Up @@ -412,8 +409,7 @@ export function directivesPlugin(): babel.PluginObj<State> {
name: "solid-start:directives",
visitor: {
Program(program, ctx) {
assertNoMethodDirectives(program, ctx.opts.directive);
ctx.opts.warnings.push(...collectMisplacedDirectives(program, ctx.opts.directive));
ctx.opts.warnings.push(...validateDirectivePlacement(program, ctx.opts.directive));

const isModuleLevel = isDirectiveValid(ctx.opts, program.node.directives);
if (isModuleLevel) {
Expand All @@ -438,7 +434,6 @@ export function directivesPlugin(): babel.PluginObj<State> {
transformFunction(ctx.opts, path, false);
},
});
program.scope.crawl();

if (ctx.opts.count > 0) {
ctx.opts.valid = true;
Expand Down
Loading
Loading