Skip to content

Commit ef30419

Browse files
committed
feat: add new directives plugin
1 parent c8e0697 commit ef30419

23 files changed

Lines changed: 1195 additions & 392 deletions

apps/tests/cypress/e2e/server-function.cy.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,8 @@ describe("server-function", () => {
5252
cy.visit("/generator-server-function");
5353
cy.get("#server-fn-test").contains('¡Hola, Mundo!');
5454
});
55+
it("should remove non-function exports in a module-level use server file", () => {
56+
cy.visit("/server-function-query-toplevel");
57+
cy.get("#server-fn-test").contains("false");
58+
});
5559
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"use server";
2+
3+
import { query } from "@solidjs/router";
4+
import { isServer } from "solid-js/web";
5+
6+
export const testQuery = query(() => isServer, "testQuery");
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createEffect, createSignal } from "solid-js";
2+
import * as testModule from "~/functions/solid-router-query";
3+
4+
export default function App() {
5+
const [output, setOutput] = createSignal<boolean | null>();
6+
7+
createEffect(() => {
8+
setOutput("testQuery" in testModule);
9+
});
10+
11+
return (
12+
<main>
13+
<span id="server-fn-test">{JSON.stringify(output())}</span>
14+
</main>
15+
);
16+
}

packages/start/config/index.js

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { createTanStackServerFnPlugin } from "@tanstack/server-functions-plugin";
21
import defu from "defu";
32
import { existsSync } from "node:fs";
43
import { join } from "node:path";
@@ -7,6 +6,7 @@ import { createApp, resolve } from "vinxi";
76
import { normalize } from "vinxi/lib/path";
87
import { config } from "vinxi/plugins/config";
98
import solid from "vite-plugin-solid";
9+
import { serverFunctionsPlugin } from "../dist/directives/index.js";
1010
import { SolidStartClientFileRouter, SolidStartServerFileRouter } from "./fs-router.js";
1111
import { serverComponents } from "./server-components.js";
1212

@@ -37,36 +37,6 @@ function solidStartServerFsRouter(config) {
3737
);
3838
}
3939

40-
const SolidStartServerFnsPlugin = createTanStackServerFnPlugin({
41-
// This is the ID that will be available to look up and import
42-
// our server function manifest and resolve its module
43-
manifestVirtualImportId: "solidstart:server-fn-manifest",
44-
client: {
45-
getRuntimeCode: () =>
46-
`import { createServerReference } from "${normalize(
47-
fileURLToPath(new URL("../dist/runtime/server-runtime.js", import.meta.url))
48-
)}"`,
49-
replacer: opts =>
50-
`createServerReference(${() => {}}, '${opts.functionId}', '${opts.extractedFilename}')`
51-
},
52-
ssr: {
53-
getRuntimeCode: () =>
54-
`import { createServerReference } from '${normalize(
55-
fileURLToPath(new URL("../dist/runtime/server-fns-runtime.js", import.meta.url))
56-
)}'`,
57-
replacer: opts =>
58-
`createServerReference(${opts.fn}, '${opts.functionId}', '${opts.extractedFilename}')`
59-
},
60-
server: {
61-
getRuntimeCode: () =>
62-
`import { createServerReference } from '${normalize(
63-
fileURLToPath(new URL("../dist/runtime/server-fns-runtime.js", import.meta.url))
64-
)}'`,
65-
replacer: opts =>
66-
`createServerReference(${opts.fn}, '${opts.functionId}', '${opts.extractedFilename}')`
67-
}
68-
});
69-
7040
export function defineConfig(baseConfig = {}) {
7141
let { vite = {}, ...start } = baseConfig;
7242
const extensions = [...DEFAULT_EXTENSIONS, ...(start.extensions || [])];
@@ -145,7 +115,9 @@ export function defineConfig(baseConfig = {}) {
145115
}
146116
}),
147117
...plugins,
148-
SolidStartServerFnsPlugin.ssr,
118+
...serverFunctionsPlugin({
119+
manifest: "solidstart:server-fn-manifest"
120+
}),
149121
start.experimental.islands ? serverComponents.server() : null,
150122
solid({ ...start.solid, ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
151123
config("app-server", {
@@ -207,7 +179,9 @@ export function defineConfig(baseConfig = {}) {
207179
}
208180
}),
209181
...plugins,
210-
SolidStartServerFnsPlugin.client,
182+
...serverFunctionsPlugin({
183+
manifest: "solidstart:server-fn-manifest"
184+
}),
211185
start.experimental.islands ? serverComponents.client() : null,
212186
solid({ ...start.solid, ssr: start.ssr, extensions: extensions.map(ext => `.${ext}`) }),
213187
config("app-client", {
@@ -274,7 +248,9 @@ export function defineConfig(baseConfig = {}) {
274248
cacheDir: "node_modules/.vinxi/server-fns"
275249
}),
276250
...plugins,
277-
SolidStartServerFnsPlugin.server,
251+
...serverFunctionsPlugin({
252+
manifest: "solidstart:server-fn-manifest"
253+
}),
278254
start.experimental.islands ? serverComponents.server() : null,
279255
solid({ ...start.solid, ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
280256
config("app-server", {

packages/start/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,17 @@
6161
}
6262
},
6363
"devDependencies": {
64+
"@types/babel__core": "^7.20.5",
65+
"@types/babel__traverse": "^7.28.0",
6466
"solid-js": "^1.9.11",
67+
"vite": "^6.3.6",
6568
"vinxi": "^0.5.7",
6669
"vitest": "3.0.5"
6770
},
6871
"dependencies": {
69-
"@tanstack/server-functions-plugin": "1.121.21",
72+
"@babel/core": "^7.28.3",
73+
"@babel/traverse": "^7.28.3",
74+
"@babel/types": "^7.28.5",
7075
"@vinxi/plugin-directives": "^0.5.0",
7176
"@vinxi/server-components": "^0.5.0",
7277
"cookie-es": "^2.0.0",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type * as babel from "@babel/core";
2+
import * as t from "@babel/types";
3+
4+
export function bubbleFunctionDeclaration(path: babel.NodePath<t.FunctionDeclaration>): void {
5+
const decl = path.node;
6+
if (decl.id) {
7+
const block = (path.findParent(current => current.isBlockStatement()) ||
8+
path.scope.getProgramParent().path) as babel.NodePath<t.BlockStatement>;
9+
10+
const [tmp] = block.unshiftContainer(
11+
"body",
12+
t.variableDeclaration("const", [
13+
t.variableDeclarator(
14+
decl.id,
15+
t.functionExpression(decl.id, decl.params, decl.body, decl.generator, decl.async),
16+
),
17+
]),
18+
);
19+
path.scope.registerDeclaration(tmp);
20+
if (path.parentPath.isExportNamedDeclaration()) {
21+
path.parentPath.replaceWith(
22+
t.exportNamedDeclaration(undefined, [t.exportSpecifier(decl.id, decl.id)]),
23+
);
24+
} else if (path.parentPath.isExportDefaultDeclaration()) {
25+
path.replaceWith(decl.id);
26+
} else {
27+
path.remove();
28+
}
29+
}
30+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as babel from "@babel/core";
2+
import path from "node:path";
3+
import { directivesPlugin, type StateContext } from "./plugin.js";
4+
import xxHash32 from "./xxhash32.js";
5+
6+
export interface CompileResult {
7+
valid: boolean;
8+
code: string;
9+
map: babel.BabelFileResult["map"];
10+
}
11+
12+
export type CompileOptions = Omit<StateContext, "count" | "file" | "hash" | "imports" | "valid">;
13+
14+
export async function compile(
15+
id: string,
16+
code: string,
17+
options: CompileOptions,
18+
): Promise<CompileResult> {
19+
const context: StateContext = {
20+
...options,
21+
file: id,
22+
valid: false,
23+
hash: xxHash32(id).toString(16),
24+
count: 0,
25+
imports: new Map(),
26+
};
27+
const pluginOption = [directivesPlugin, context];
28+
const plugins: NonNullable<NonNullable<babel.TransformOptions["parserOpts"]>["plugins"]> = [
29+
"jsx",
30+
];
31+
if (/\.[mc]?tsx?$/i.test(id)) {
32+
plugins.push("typescript");
33+
}
34+
const result = await babel.transformAsync(code, {
35+
plugins: [pluginOption],
36+
parserOpts: {
37+
plugins,
38+
},
39+
filename: path.basename(id),
40+
ast: false,
41+
sourceMaps: true,
42+
configFile: false,
43+
babelrc: false,
44+
sourceFileName: id,
45+
});
46+
47+
if (result) {
48+
return {
49+
valid: context.valid,
50+
code: result.code || "",
51+
map: result.map,
52+
};
53+
}
54+
throw new Error("invariant");
55+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type * as babel from "@babel/core";
2+
import * as t from "@babel/types";
3+
4+
export function generateUniqueName(path: babel.NodePath, name: string): t.Identifier {
5+
let uid: string;
6+
let i = 1;
7+
do {
8+
uid = name + "_" + i;
9+
i++;
10+
} while (
11+
path.scope.hasLabel(uid) ||
12+
path.scope.hasBinding(uid) ||
13+
path.scope.hasGlobal(uid) ||
14+
path.scope.hasReference(uid)
15+
);
16+
17+
const program = path.scope.getProgramParent();
18+
program.references[uid] = true;
19+
program.uids[uid] = true;
20+
21+
return t.identifier(uid);
22+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { NodePath } from "@babel/core";
2+
3+
export function getDescriptiveName(path: NodePath, defaultName: string): string {
4+
let current: NodePath | null = path;
5+
while (current) {
6+
switch (current.node.type) {
7+
case "FunctionDeclaration":
8+
case "FunctionExpression": {
9+
if (current.node.id) {
10+
return current.node.id.name;
11+
}
12+
break;
13+
}
14+
case "VariableDeclarator": {
15+
if (current.node.id.type === "Identifier") {
16+
return current.node.id.name;
17+
}
18+
break;
19+
}
20+
case "ClassPrivateMethod":
21+
case "ClassMethod":
22+
case "ObjectMethod": {
23+
switch (current.node.key.type) {
24+
case "Identifier":
25+
return current.node.key.name;
26+
case "PrivateName":
27+
return current.node.key.id.name;
28+
default:
29+
break;
30+
}
31+
break;
32+
}
33+
default:
34+
break;
35+
}
36+
current = current.parentPath;
37+
}
38+
return defaultName;
39+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type * as babel from "@babel/core";
2+
import * as t from "@babel/types";
3+
import { generateUniqueName } from "./generate-unique-name.js";
4+
import type { ImportDefinition } from "./types.js";
5+
6+
export function getImportIdentifier(
7+
imports: Map<string, t.Identifier>,
8+
path: babel.NodePath,
9+
registration: ImportDefinition,
10+
): t.Identifier {
11+
const name = registration.kind === "named" ? registration.name : "default";
12+
const target = `${registration.source}[${name}]`;
13+
const current = imports.get(target);
14+
if (current) {
15+
return current;
16+
}
17+
const programParent = path.scope.getProgramParent();
18+
const uid = generateUniqueName(programParent.path, name);
19+
programParent.registerDeclaration(
20+
(programParent.path as babel.NodePath<t.Program>).unshiftContainer(
21+
"body",
22+
t.importDeclaration(
23+
[
24+
registration.kind === "named"
25+
? t.importSpecifier(uid, t.identifier(registration.name))
26+
: t.importDefaultSpecifier(uid),
27+
],
28+
t.stringLiteral(registration.source),
29+
),
30+
)[0],
31+
);
32+
imports.set(target, uid);
33+
return uid;
34+
}

0 commit comments

Comments
 (0)