diff --git a/.vscode/settings.json b/.vscode/settings.json index 216a70a8b..2527cbe24 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,8 @@ }, "typescript.tsdk": "node_modules/typescript/lib", "editor.formatOnSave": true, - "editor.defaultFormatter": "oxc.oxc-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode", + "[typescript]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + } } diff --git a/apps/tests/src/e2e/server-function.test.ts b/apps/tests/src/e2e/server-function.test.ts index 7a8131be9..1bcd85d09 100644 --- a/apps/tests/src/e2e/server-function.test.ts +++ b/apps/tests/src/e2e/server-function.test.ts @@ -67,4 +67,9 @@ test.describe("server-function", () => { await page.goto("http://localhost:3000/generator-server-function"); await expect(page.locator("#server-fn-test")).toContainText("¡Hola, Mundo!"); }); + + test("should remove exports for non-function values when top-level use server is used", async ({ page }) => { + await page.goto("http://localhost:3000/server-function-query-toplevel"); + await expect(page.locator("#server-fn-test")).toContainText('false'); + }); }); diff --git a/apps/tests/src/functions/solid-router-query.ts b/apps/tests/src/functions/solid-router-query.ts new file mode 100644 index 000000000..011338e76 --- /dev/null +++ b/apps/tests/src/functions/solid-router-query.ts @@ -0,0 +1,7 @@ +"use server"; + +import { query } from "@solidjs/router"; +import { isServer } from "solid-js/web"; + +export const testQuery = query(() => isServer, 'testQuery'); + diff --git a/apps/tests/src/routes/server-function-query-toplevel.tsx b/apps/tests/src/routes/server-function-query-toplevel.tsx new file mode 100644 index 000000000..2d55e3532 --- /dev/null +++ b/apps/tests/src/routes/server-function-query-toplevel.tsx @@ -0,0 +1,16 @@ +import { createEffect, createSignal } from "solid-js"; +import * as testModule from "~/functions/solid-router-query"; + +export default function App() { + const [output, setOutput] = createSignal(); + + createEffect(() => { + setOutput('testQuery' in testModule); + }); + + return ( +
+ {JSON.stringify(output())} +
+ ); +} diff --git a/apps/tests/test-results/.last-run.json b/apps/tests/test-results/.last-run.json deleted file mode 100644 index cbcc1fbac..000000000 --- a/apps/tests/test-results/.last-run.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "passed", - "failedTests": [] -} \ No newline at end of file diff --git a/packages/start/package.json b/packages/start/package.json index 60bf8269a..29780d946 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -41,7 +41,6 @@ "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.5", "@solidjs/meta": "^0.29.4", - "@tanstack/server-functions-plugin": "1.134.5", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.9", "cookie-es": "^2.0.0", diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 4b1c82179..e445cebb3 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -1,11 +1,9 @@ -import { TanStackServerFnPlugin } from "@tanstack/server-functions-plugin"; import { defu } from "defu"; import { globSync } from "node:fs"; import { extname, isAbsolute, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { normalizePath, type PluginOption } from "vite"; +import { type PluginOption } from "vite"; import solid, { type Options as SolidOptions } from "vite-plugin-solid"; - +import { serverFunctionsPlugin } from "../directives/index.ts"; import { DEFAULT_EXTENSIONS, VIRTUAL_MODULES, VITE_ENVIRONMENTS } from "./constants.ts"; import { devServer } from "./dev-server.ts"; import { SolidStartClientFileRouter, SolidStartServerFileRouter } from "./fs-router.ts"; @@ -163,41 +161,8 @@ export function solidStart(options?: SolidStartOptions): Array { }, }), lazy(), - // Must be placed after fsRoutes, as treeShake will remove the - // server fn exports added in by this plugin - TanStackServerFnPlugin({ - // This is the ID that will be available to look up and import - // our server function manifest and resolve its module - manifestVirtualImportId: VIRTUAL_MODULES.serverFnManifest, - directive: "use server", - callers: [ - { - envConsumer: "client", - envName: VITE_ENVIRONMENTS.client, - getRuntimeCode: () => - `import { createServerReference } from "${normalizePath( - fileURLToPath(new URL("../server/server-runtime", import.meta.url)) - )}"`, - replacer: opts => `createServerReference('${opts.functionId}')`, - }, - { - envConsumer: "server", - envName: VITE_ENVIRONMENTS.server, - getRuntimeCode: () => - `import { createServerReference } from '${normalizePath( - fileURLToPath(new URL("../server/server-fns-runtime", import.meta.url)) - )}'`, - replacer: opts => `createServerReference(${opts.fn}, '${opts.functionId}')`, - }, - ], - provider: { - envName: VITE_ENVIRONMENTS.server, - getRuntimeCode: () => - `import { createServerReference } from '${normalizePath( - fileURLToPath(new URL("../server/server-fns-runtime", import.meta.url)) - )}'`, - replacer: opts => `createServerReference(${opts.fn}, '${opts.functionId}')`, - }, + serverFunctionsPlugin({ + manifest: VIRTUAL_MODULES.serverFnManifest, }), { name: "solid-start:virtual-modules", diff --git a/packages/start/src/directives/bubble-function-declaration.ts b/packages/start/src/directives/bubble-function-declaration.ts new file mode 100644 index 000000000..256f024ac --- /dev/null +++ b/packages/start/src/directives/bubble-function-declaration.ts @@ -0,0 +1,32 @@ +import type * as babel from "@babel/core"; +import * as t from "@babel/types"; + +export function bubbleFunctionDeclaration(path: babel.NodePath): void { + const decl = path.node; + // Check if declaration is FunctionDeclaration + if (decl.id) { + const block = (path.findParent(current => current.isBlockStatement()) || + path.scope.getProgramParent().path) as babel.NodePath; + + const [tmp] = block.unshiftContainer( + "body", + t.variableDeclaration("const", [ + t.variableDeclarator( + decl.id, + t.functionExpression(decl.id, decl.params, decl.body, decl.generator, decl.async), + ), + ]), + ); + path.scope.registerDeclaration(tmp); + // tmp.skip(); + if (path.parentPath.isExportNamedDeclaration()) { + path.parentPath.replaceWith( + t.exportNamedDeclaration(undefined, [t.exportSpecifier(decl.id, decl.id)]), + ); + } else if (path.parentPath.isExportDefaultDeclaration()) { + path.replaceWith(decl.id); + } else { + path.remove(); + } + } +} diff --git a/packages/start/src/directives/compile.ts b/packages/start/src/directives/compile.ts new file mode 100644 index 000000000..774280e44 --- /dev/null +++ b/packages/start/src/directives/compile.ts @@ -0,0 +1,54 @@ +import * as babel from "@babel/core"; +import path from "node:path"; +import { directivesPlugin, type StateContext } from "./plugin.ts"; +import xxHash32 from "./xxhash32.ts"; + +export interface CompileResult { + valid: boolean; + code: string; + map: babel.BabelFileResult["map"]; +} + +export type CompileOptions = Omit; + +export async function compile( + id: string, + code: string, + options: CompileOptions, +): Promise { + const context: StateContext = { + ...options, + valid: false, + hash: xxHash32(id).toString(16), + count: 0, + imports: new Map(), + }; + const pluginOption = [directivesPlugin, context]; + const plugins: NonNullable["plugins"]> = [ + "jsx", + ]; + if (/\.[mc]?tsx?$/i.test(id)) { + plugins.push("typescript"); + } + const result = await babel.transformAsync(code, { + plugins: [pluginOption], + parserOpts: { + plugins, + }, + filename: path.basename(id), + ast: false, + sourceMaps: true, + configFile: false, + babelrc: false, + sourceFileName: id, + }); + + if (result) { + return { + valid: context.valid, + code: result.code || "", + map: result.map, + }; + } + throw new Error("invariant"); +} diff --git a/packages/start/src/directives/generate-unique-name.ts b/packages/start/src/directives/generate-unique-name.ts new file mode 100644 index 000000000..2a85039c2 --- /dev/null +++ b/packages/start/src/directives/generate-unique-name.ts @@ -0,0 +1,22 @@ +import type * as babel from "@babel/core"; +import * as t from "@babel/types"; + +export function generateUniqueName(path: babel.NodePath, name: string): t.Identifier { + let uid: string; + let i = 1; + do { + uid = name + "_" + i; + i++; + } while ( + path.scope.hasLabel(uid) || + path.scope.hasBinding(uid) || + path.scope.hasGlobal(uid) || + path.scope.hasReference(uid) + ); + + const program = path.scope.getProgramParent(); + program.references[uid] = true; + program.uids[uid] = true; + + return t.identifier(uid); +} diff --git a/packages/start/src/directives/get-descriptive-name.ts b/packages/start/src/directives/get-descriptive-name.ts new file mode 100644 index 000000000..9a856a998 --- /dev/null +++ b/packages/start/src/directives/get-descriptive-name.ts @@ -0,0 +1,39 @@ +import type { NodePath } from "@babel/core"; + +export function getDescriptiveName(path: NodePath, defaultName: string): string { + let current: NodePath | null = path; + while (current) { + switch (current.node.type) { + case "FunctionDeclaration": + case "FunctionExpression": { + if (current.node.id) { + return current.node.id.name; + } + break; + } + case "VariableDeclarator": { + if (current.node.id.type === "Identifier") { + return current.node.id.name; + } + break; + } + case "ClassPrivateMethod": + case "ClassMethod": + case "ObjectMethod": { + switch (current.node.key.type) { + case "Identifier": + return current.node.key.name; + case "PrivateName": + return current.node.key.id.name; + default: + break; + } + break; + } + default: + break; + } + current = current.parentPath; + } + return defaultName; +} diff --git a/packages/start/src/directives/get-import-identifier.ts b/packages/start/src/directives/get-import-identifier.ts new file mode 100644 index 000000000..6738affa5 --- /dev/null +++ b/packages/start/src/directives/get-import-identifier.ts @@ -0,0 +1,34 @@ +import type * as babel from "@babel/core"; +import * as t from "@babel/types"; +import { generateUniqueName } from "./generate-unique-name.ts"; +import type { ImportDefinition } from "./types.ts"; + +export function getImportIdentifier( + imports: Map, + path: babel.NodePath, + registration: ImportDefinition, +): t.Identifier { + const name = registration.kind === "named" ? registration.name : "default"; + const target = `${registration.source}[${name}]`; + const current = imports.get(target); + if (current) { + return current; + } + const programParent = path.scope.getProgramParent(); + const uid = generateUniqueName(programParent.path, name); + programParent.registerDeclaration( + (programParent.path as babel.NodePath).unshiftContainer( + "body", + t.importDeclaration( + [ + registration.kind === "named" + ? t.importSpecifier(uid, t.identifier(registration.name)) + : t.importDefaultSpecifier(uid), + ], + t.stringLiteral(registration.source), + ), + )[0], + ); + imports.set(target, uid); + return uid; +} diff --git a/packages/start/src/directives/get-root-statement-path.ts b/packages/start/src/directives/get-root-statement-path.ts new file mode 100644 index 000000000..347e92387 --- /dev/null +++ b/packages/start/src/directives/get-root-statement-path.ts @@ -0,0 +1,14 @@ +import type * as babel from "@babel/core"; +import * as t from "@babel/types"; + +export function getRootStatementPath(path: babel.NodePath): babel.NodePath { + let current = path.parentPath; + while (current) { + const next = current.parentPath; + if (next && t.isProgram(next.node)) { + return current; + } + current = next; + } + return path; +} diff --git a/packages/start/src/directives/index.ts b/packages/start/src/directives/index.ts new file mode 100644 index 000000000..781b62560 --- /dev/null +++ b/packages/start/src/directives/index.ts @@ -0,0 +1,236 @@ +import { fileURLToPath } from "node:url"; +import { + createFilter, + type EnvironmentModuleGraph, + type FilterPattern, + normalizePath, + type Plugin, + type ViteDevServer, +} from "vite"; +import { compile, type CompileOptions } from "./compile.ts"; + +export interface ServerFunctionsFilter { + include?: FilterPattern; + exclude?: FilterPattern; +} + +export interface ServerFunctionsOptions { + manifest: string; + filter?: ServerFunctionsFilter; +} + +const CLIENT_PATH = normalizePath( + fileURLToPath(new URL("../server/server-runtime.ts", import.meta.url)), +); +const SERVER_PATH = normalizePath( + fileURLToPath(new URL("../server/server-fns-runtime.ts", import.meta.url)), +); + +const DEFAULT_INCLUDE = "src/**/*.{jsx,tsx,ts,js,mjs,cjs}"; +const DEFAULT_EXCLUDE = "node_modules/**/*.{jsx,tsx,ts,js,mjs,cjs}"; +const DIRECTIVE = "use server"; + +const CLIENT_OPTIONS: Pick = { + directive: DIRECTIVE, + definitions: { + register: { + kind: "named", + name: "createServerReference", + source: CLIENT_PATH, + }, + clone: { + kind: "named", + name: "cloneServerReference", + source: CLIENT_PATH, + }, + }, +}; +const SERVER_OPTIONS: Pick = { + directive: DIRECTIVE, + definitions: { + register: { + kind: "named", + name: "createServerReference", + source: SERVER_PATH, + }, + clone: { + kind: "named", + name: "cloneServerReference", + source: SERVER_PATH, + }, + }, +}; + +type Manifest = Record>; + +function createManifest(): Manifest { + return { + server: new Set(), + client: new Set(), + }; +} + +interface DeferredPromise { + reference: Promise; + resolve: (value: T) => void; + reject: (value: any) => void; +} + +function createDeferredPromise(): DeferredPromise { + let resolve: DeferredPromise["resolve"]; + let reject: DeferredPromise["reject"]; + + return { + reference: new Promise((res, rej) => { + resolve = res; + reject = rej; + }), + resolve(value) { + resolve(value); + }, + reject(value) { + reject(value); + }, + }; +} + +class Debouncer { + promise: DeferredPromise; + + private timeout: ReturnType | undefined; + + constructor(private source: () => T) { + this.promise = createDeferredPromise(); + this.defer(); + } + + defer(): void { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; + } + this.timeout = setTimeout(() => { + this.promise.resolve(this.source()); + }, 1000); + } +} + +function mergeManifestRecord( + source: Set, + target: Set, +): { invalidPreload: boolean; invalidated: string[] } { + const current = source.size; + for (const entry of target) { + source.add(entry); + } + return { + invalidPreload: current !== source.size, + invalidated: [...source], + }; +} + +function invalidateModule(moduleGraph: EnvironmentModuleGraph, path: string) { + const target = moduleGraph.getModuleById(path); + if (target) { + moduleGraph.invalidateModule(target); + } +} + +function invalidateModules( + server: ViteDevServer | undefined, + result: ReturnType, + manifest: string, +): void { + if (server) { + if (result.invalidPreload) { + invalidateModule(server.environments.client.moduleGraph, manifest); + invalidateModule(server.environments.ssr.moduleGraph, manifest); + } + } +} + +export function serverFunctionsPlugin(options: ServerFunctionsOptions): Plugin[] { + const filter = createFilter( + options.filter?.include || DEFAULT_INCLUDE, + options.filter?.exclude || DEFAULT_EXCLUDE, + ); + + let env: CompileOptions["env"]; + + const manifest = createManifest(); + + const preload: Record | undefined> = { + server: undefined, + client: undefined, + }; + let currentServer: ViteDevServer; + + return [ + { + name: "solid-start:server-functions/setup", + enforce: "pre", + configResolved(config) { + env = config.mode !== "production" ? "development" : "production"; + }, + configureServer(server) { + currentServer = server; + }, + }, + { + name: "solid-start:server-functions/preload", + enforce: "pre", + resolveId(source) { + if (source === options.manifest) { + return { id: options.manifest, moduleSideEffects: true }; + } + return null; + }, + async load(id, opts) { + const mode = opts?.ssr ? "server" : "client"; + if (id === options.manifest) { + const current = new Debouncer(() => + [...manifest[mode]].map(entry => `import "${entry}";`).join("\n"), + ); + preload[mode] = current; + const result = await current.promise.reference; + return result; + } + return null; + }, + }, + { + name: "solid-start:server-functions/compiler", + async transform(code, fileId, opts) { + const mode = opts?.ssr ? "server" : "client"; + const [id] = fileId.split("?"); + if (!filter(id)) { + return null; + } + + const result = await compile(id!, code, { + ...(mode === "server" ? SERVER_OPTIONS : CLIENT_OPTIONS), + mode, + env, + }); + + if (result.valid) { + const preloader = preload[mode]; + if (preloader) { + preloader.defer(); + } + invalidateModules( + currentServer, + mergeManifestRecord(manifest.server, new Set([id!])), + options.manifest, + ); + + return { + code: result.code || "", + map: result.map, + }; + } + return code; + }, + }, + ]; +} diff --git a/packages/start/src/directives/is-statement-top-level.ts b/packages/start/src/directives/is-statement-top-level.ts new file mode 100644 index 000000000..58619e181 --- /dev/null +++ b/packages/start/src/directives/is-statement-top-level.ts @@ -0,0 +1,13 @@ +import type * as babel from "@babel/core"; +import type * as t from "@babel/types"; + +export function isStatementTopLevel(path: babel.NodePath): boolean { + let blockParent = path.scope.getBlockParent(); + const programParent = path.scope.getProgramParent(); + // a FunctionDeclaration binding refers to itself as the block parent + if (blockParent.path === path) { + blockParent = blockParent.parent; + } + + return programParent === blockParent; +} diff --git a/packages/start/src/directives/paths.ts b/packages/start/src/directives/paths.ts new file mode 100644 index 000000000..883f07d08 --- /dev/null +++ b/packages/start/src/directives/paths.ts @@ -0,0 +1,63 @@ +import type * as t from "@babel/types"; + +type TypeFilter = (node: t.Node) => node is V; + +export function isPathValid( + path: unknown, + key: TypeFilter, +): path is babel.NodePath { + const node = (path as babel.NodePath).node; + return node ? key(node) : false; +} + +export type NestedExpression = + | t.ParenthesizedExpression + | t.TypeCastExpression + | t.TSAsExpression + | t.TSSatisfiesExpression + | t.TSNonNullExpression + | t.TSInstantiationExpression + | t.TSTypeAssertion; + +export function isNestedExpression(node: t.Node): node is NestedExpression { + switch (node.type) { + case "ParenthesizedExpression": + case "TypeCastExpression": + case "TSAsExpression": + case "TSSatisfiesExpression": + case "TSNonNullExpression": + case "TSTypeAssertion": + case "TSInstantiationExpression": + return true; + default: + return false; + } +} + +type TypeCheck = K extends TypeFilter ? U : never; + +export function unwrapNode boolean>( + node: t.Node, + key: K, +): TypeCheck | undefined { + if (key(node)) { + return node as TypeCheck; + } + if (isNestedExpression(node)) { + return unwrapNode(node.expression, key); + } + return undefined; +} + +export function unwrapPath( + path: unknown, + key: TypeFilter, +): babel.NodePath | undefined { + if (isPathValid(path, key)) { + return path; + } + if (isPathValid(path, isNestedExpression)) { + return unwrapPath(path.get("expression"), key); + } + return undefined; +} diff --git a/packages/start/src/directives/plugin.ts b/packages/start/src/directives/plugin.ts new file mode 100644 index 000000000..1d2170ad7 --- /dev/null +++ b/packages/start/src/directives/plugin.ts @@ -0,0 +1,389 @@ +import type * as babel from "@babel/core"; +import type { Binding } from "@babel/traverse"; +import * as t from "@babel/types"; +import { bubbleFunctionDeclaration } from "./bubble-function-declaration.ts"; +import { generateUniqueName } from "./generate-unique-name.ts"; +import { getDescriptiveName } from "./get-descriptive-name.ts"; +import { getImportIdentifier } from "./get-import-identifier.ts"; +import { getRootStatementPath } from "./get-root-statement-path.ts"; +import { isStatementTopLevel } from "./is-statement-top-level.ts"; +import { isPathValid, unwrapPath } from "./paths.ts"; +import { removeUnusedVariables } from "./remove-unused-variables.ts"; +import type { ImportDefinition } from "./types.ts"; + +export interface StateContext { + env: "production" | "development"; + mode: "server" | "client"; + directive: string; + hash: string; + count: number; + imports: Map; + valid: boolean; + + definitions: { + register: ImportDefinition; + clone: ImportDefinition; + }; +} + +type ValidFunction = t.ArrowFunctionExpression | t.FunctionExpression; + +function isValidFunction(node: t.Node): node is ValidFunction { + return t.isArrowFunctionExpression(node) || t.isFunctionExpression(node); +} + +function isDirectiveValid(ctx: StateContext, directives: t.Directive[]) { + for (let i = 0, len = directives.length; i < len; i++) { + if (directives[i]!.value.value === ctx.directive) { + return true; + } + } + return false; +} + +function cleanDirectives(path: babel.NodePath, target: string): void { + const newDirectives: t.Directive[] = []; + for (let i = 0, len = path.node.directives.length; i < len; i++) { + const current = path.node.directives[i]!; + if (current.value.value !== target) { + newDirectives.push(current); + } + } + path.node.directives = newDirectives; +} + +function cleanFunctionDirectives( + ctx: StateContext, + path: babel.NodePath, +) { + const body = path.get("body"); + + if (isPathValid(body, t.isBlockStatement)) { + cleanDirectives(body, ctx.directive); + } +} + +function isFunctionDirectiveValid( + ctx: StateContext, + path: babel.NodePath, +) { + const body = path.get("body"); + + if (isPathValid(body, t.isBlockStatement)) { + return isDirectiveValid(ctx, body.node.directives); + } + + return false; +} + +function createID(ctx: StateContext, name: string) { + const base = `${ctx.hash}-${ctx.count++}`; + if (ctx.env === "development") { + return `${base}-${name}`; + } + return base; +} + +function transformFunction( + ctx: StateContext, + path: babel.NodePath, + direct: boolean, +) { + if (!direct) { + if (!isFunctionDirectiveValid(ctx, path)) { + return; + } + cleanFunctionDirectives(ctx, path); + } + // First, get root statement + const rootStatement = getRootStatementPath(path); + + // Create a unique ID for the function + const fnID = createID(ctx, getDescriptiveName(path, "anonymous")); + + if (ctx.mode === "server") { + // Create a "source" function on the root-level + const sourceReference = t.callExpression( + getImportIdentifier(ctx.imports, path, ctx.definitions.register), + [t.stringLiteral(fnID), path.node], + ); + + const sourceID = generateUniqueName(path, "serverFn"); + + rootStatement.insertBefore( + t.variableDeclaration("const", [t.variableDeclarator(sourceID, sourceReference)]), + ); + + // Clone the source function to replace the server function + path.replaceWith( + t.callExpression(getImportIdentifier(ctx.imports, path, ctx.definitions.clone), [sourceID]), + ); + } else { + // Otherwise, clone the function based on its ID + path.replaceWith( + t.callExpression(getImportIdentifier(ctx.imports, path, ctx.definitions.clone), [ + t.stringLiteral(fnID), + ]), + ); + } + + path.scope.crawl(); +} + +function traceBinding(path: babel.NodePath, name: string): Binding | undefined { + const current = path.scope.getBinding(name); + if (!current) { + return undefined; + } + switch (current.kind) { + case "const": + case "let": + case "var": { + if (isPathValid(current.path, t.isVariableDeclarator)) { + // Check if left is identifier + const left = unwrapPath(current.path.get("id"), t.isIdentifier); + if (left) { + const right = unwrapPath(current.path.get("init"), t.isIdentifier); + if (right) { + return traceBinding(path, right.node.name); + } + + // Only valid for functions + const func = unwrapPath(current.path.get('init'), isValidFunction); + if (func) { + return current; + } + } + } + return undefined; + } + case "hoisted": + case "local": + case "module": + case "param": + case "unknown": + return undefined; + } +} + +function transformBindingForServer(ctx: StateContext, binding: Binding) { + if (isPathValid(binding.path, t.isVariableDeclarator)) { + const right = unwrapPath(binding.path.get("init"), isValidFunction); + if (right) { + transformFunction(ctx, right, true); + } + } +} + +interface State extends babel.PluginPass { + opts: StateContext; +} + +function transformModuleLevelDirective(ctx: StateContext, program: babel.NodePath) { + cleanDirectives(program, ctx.directive); + program.traverse({ + FunctionDeclaration(child) { + // We only need to move top-level functions + if (isStatementTopLevel(child)) { + bubbleFunctionDeclaration(child); + } + }, + }); + program.scope.crawl(); + if (ctx.mode === "server") { + // Trace bindings + const bindings = new Set(); + + program.traverse({ + ExportDefaultDeclaration(path) { + const id = unwrapPath(path.get("declaration"), t.isIdentifier); + if (id) { + const binding = traceBinding(path, id.node.name); + if (binding) { + bindings.add(binding); + } + } + }, + ExportNamedDeclaration(path) { + if (path.node.source || path.node.exportKind === "type") { + return; + } + for (const specifier of path.get("specifiers")) { + if (isPathValid(specifier, t.isExportSpecifier)) { + const binding = traceBinding(specifier, specifier.node.local.name); + + if (binding) { + bindings.add(binding); + } + } + } + const declarations = path.get("declaration"); + + if (isPathValid(declarations, t.isVariableDeclaration)) { + for (const declaration of declarations.get("declarations")) { + // Check if left is identifier + const left = unwrapPath(declaration.get("id"), t.isIdentifier); + if (left) { + const binding = traceBinding(left, left.node.name); + if (binding) { + bindings.add(binding); + } + } + } + } + }, + }); + + for (const binding of bindings) { + transformBindingForServer(ctx, binding); + } + } else { + // Trace bindings + const uniqueBindings = new Set(); + const exportedBindings = new Map(); + + program.traverse({ + ExportDefaultDeclaration(path) { + const id = unwrapPath(path.get("declaration"), t.isIdentifier); + if (id) { + const binding = traceBinding(path, id.node.name); + if (binding) { + uniqueBindings.add(binding); + exportedBindings.set("default", binding); + } + } + }, + ExportNamedDeclaration(path) { + if (path.node.source || path.node.exportKind === "type") { + return; + } + for (const specifier of path.get("specifiers")) { + if (isPathValid(specifier, t.isExportSpecifier)) { + const binding = traceBinding(specifier, specifier.node.local.name); + + if (binding) { + const key = t.isIdentifier(specifier.node.exported) + ? specifier.node.exported.name + : specifier.node.exported.value; + uniqueBindings.add(binding); + exportedBindings.set(key, binding); + } + } + } + + const declarations = path.get("declaration"); + + if (isPathValid(declarations, t.isVariableDeclaration)) { + for (const declaration of declarations.get("declarations")) { + // Check if left is identifier + const left = unwrapPath(declaration.get("id"), t.isIdentifier); + if (left) { + const binding = traceBinding(left, left.node.name); + if (binding) { + uniqueBindings.add(binding); + exportedBindings.set(left.node.name, binding); + } + } + } + } + }, + }); + + // generate ids for each unique binding + const sourceIDs = new Map(); + for (const binding of uniqueBindings) { + if (isPathValid(binding.path, t.isVariableDeclarator)) { + const init = unwrapPath(binding.path.get("init"), isValidFunction); + if (init) { + sourceIDs.set(binding, createID(ctx, getDescriptiveName(init, "anonymous"))); + } + } + } + + // clear body + program.node.body = []; + + const declarations: t.VariableDeclarator[] = []; + const specifiers: t.ExportSpecifier[] = []; + + const declarationMap = new Map(); + + // Declare all client functions + for (const [exported, binding] of exportedBindings) { + let currentIdentifier = declarationMap.get(binding); + if (!currentIdentifier) { + currentIdentifier = generateUniqueName(program, "fn"); + + const fnID = sourceIDs.get(binding); + + if (fnID) { + declarations.push( + t.variableDeclarator( + currentIdentifier, + t.callExpression(getImportIdentifier(ctx.imports, program, ctx.definitions.clone), [ + t.stringLiteral(fnID), + ]), + ), + ); + + declarationMap.set(binding, currentIdentifier); + } + } + + if (currentIdentifier) { + specifiers.push(t.exportSpecifier(currentIdentifier, t.stringLiteral(exported))); + } + } + + const body: t.Statement[] = []; + + if (declarations.length > 0) { + body.push(t.variableDeclaration("const", declarations)); + } + if (specifiers.length > 0) { + body.push(t.exportNamedDeclaration(null, specifiers, null)); + } + + program.pushContainer("body", body); + } +} + +export function directivesPlugin(): babel.PluginObj { + return { + name: "solid-start:directives", + visitor: { + Program(program, ctx) { + const isModuleLevel = isDirectiveValid(ctx.opts, program.node.directives); + if (isModuleLevel) { + transformModuleLevelDirective(ctx.opts, program); + ctx.opts.valid = true; + } else { + // First, bubble up function declarations + program.traverse({ + FunctionDeclaration(child) { + if (isFunctionDirectiveValid(ctx.opts, child)) { + bubbleFunctionDeclaration(child); + } + }, + }); + program.scope.crawl(); + // Now we transform each function + program.traverse({ + ArrowFunctionExpression(path) { + transformFunction(ctx.opts, path, false); + }, + FunctionExpression(path) { + transformFunction(ctx.opts, path, false); + }, + }); + program.scope.crawl(); + + if (ctx.opts.count > 0) { + ctx.opts.valid = true; + removeUnusedVariables(program); + } + } + }, + }, + }; +} diff --git a/packages/start/src/directives/remove-unused-variables.ts b/packages/start/src/directives/remove-unused-variables.ts new file mode 100644 index 000000000..6155bfea7 --- /dev/null +++ b/packages/start/src/directives/remove-unused-variables.ts @@ -0,0 +1,55 @@ +import type * as babel from "@babel/core"; +import * as t from "@babel/types"; +import { isPathValid } from "./paths.ts"; + +export function removeUnusedVariables(program: babel.NodePath) { + // TODO(Alexis): + // This implementation is simple but slow + // We repeat removing unused variables from each pass + // until no potential unused variables are left. + // There might be a simpler implementation. + let dirty = true; + + while (dirty) { + dirty = false; + program.traverse({ + BindingIdentifier(path) { + const binding = path.scope.getBinding(path.node.name); + + if (binding) { + switch (binding.kind) { + case "const": + case "let": + case "var": + case "hoisted": + case "module": + if (binding.references === 0 && !binding.path.removed) { + if (isPathValid(binding.path.parentPath, t.isImportDeclaration)) { + const parent = binding.path.parentPath; + if (parent.node.specifiers.length === 1) { + parent.remove(); + } else { + binding.path.remove(); + } + } else { + binding.path.remove(); + } + dirty = true; + } + break; + case "local": + case "param": + case "unknown": + break; + } + } + }, + VariableDeclaration(path) { + if (path.node.declarations.length === 0) { + path.remove(); + } + }, + }); + program.scope.crawl(); + } +} diff --git a/packages/start/src/directives/types.ts b/packages/start/src/directives/types.ts new file mode 100644 index 000000000..4cf5bbc78 --- /dev/null +++ b/packages/start/src/directives/types.ts @@ -0,0 +1,12 @@ +export interface NamedImportDefinition { + kind: "named"; + name: string; + source: string; +} + +export interface DefaultImportDefinition { + kind: "default"; + source: string; +} + +export type ImportDefinition = DefaultImportDefinition | NamedImportDefinition; diff --git a/packages/start/src/directives/xxhash32.ts b/packages/start/src/directives/xxhash32.ts new file mode 100644 index 000000000..21b1ef0d5 --- /dev/null +++ b/packages/start/src/directives/xxhash32.ts @@ -0,0 +1,214 @@ +// @ts-nocheck +/** + * Copyright (c) 2019 Jason Dent + * https://github.com/Jason3S/xxhash + */ +const PRIME32_1 = 2654435761; +const PRIME32_2 = 2246822519; +const PRIME32_3 = 3266489917; +const PRIME32_4 = 668265263; +const PRIME32_5 = 374761393; + +function toUtf8(text: string): Uint8Array { + const bytes: number[] = []; + for (let i = 0, n = text.length; i < n; ++i) { + const c = text.charCodeAt(i); + if (c < 0x80) { + bytes.push(c); + } else if (c < 0x800) { + bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); + } else if (c < 0xd800 || c >= 0xe000) { + bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); + } else { + const cp = + 0x10000 + (((c & 0x3ff) << 10) | (text.charCodeAt(++i) & 0x3ff)); + bytes.push( + 0xf0 | ((cp >> 18) & 0x7), + 0x80 | ((cp >> 12) & 0x3f), + 0x80 | ((cp >> 6) & 0x3f), + 0x80 | (cp & 0x3f), + ); + } + } + return new Uint8Array(bytes); +} +/** + * + * @param buffer - byte array or string + * @param seed - optional seed (32-bit unsigned); + */ +export default function xxHash32( + buffer: Uint8Array | string, + seed = 0, +): number { + buffer = typeof buffer === 'string' ? toUtf8(buffer) : buffer; + const b = buffer; + + /* + Step 1. Initialize internal accumulators + Each accumulator gets an initial value based on optional seed input. + Since the seed is optional, it can be 0. + ``` + u32 acc1 = seed + PRIME32_1 + PRIME32_2; + u32 acc2 = seed + PRIME32_2; + u32 acc3 = seed + 0; + u32 acc4 = seed - PRIME32_1; + ``` + Special case : input is less than 16 bytes + When input is too small (< 16 bytes), the algorithm will not process any stripe. + Consequently, it will not make use of parallel accumulators. + In which case, a simplified initialization is performed, using a single accumulator : + u32 acc = seed + PRIME32_5; + The algorithm then proceeds directly to step 4. + */ + + let acc = (seed + PRIME32_5) & 0xffffffff; + let offset = 0; + + if (b.length >= 16) { + const accN = [ + (seed + PRIME32_1 + PRIME32_2) & 0xffffffff, + (seed + PRIME32_2) & 0xffffffff, + (seed + 0) & 0xffffffff, + (seed - PRIME32_1) & 0xffffffff, + ]; + + /* + Step 2. Process stripes + A stripe is a contiguous segment of 16 bytes. It is evenly divided into 4 lanes, + of 4 bytes each. The first lane is used to update accumulator 1, the second lane + is used to update accumulator 2, and so on. Each lane read its associated 32-bit + value using little-endian convention. For each {lane, accumulator}, the update + process is called a round, and applies the following formula : + ``` + accN = accN + (laneN * PRIME32_2); + accN = accN <<< 13; + accN = accN * PRIME32_1; + ``` + This shuffles the bits so that any bit from input lane impacts several bits in + output accumulator. All operations are performed modulo 2^32. + Input is consumed one full stripe at a time. Step 2 is looped as many times as + necessary to consume the whole input, except the last remaining bytes which cannot + form a stripe (< 16 bytes). When that happens, move to step 3. + */ + + const b = buffer; + const limit = b.length - 16; + let lane = 0; + for (offset = 0; (offset & 0xfffffff0) <= limit; offset += 4) { + const i = offset; + const laneN0 = b[i + 0] + (b[i + 1] << 8); + const laneN1 = b[i + 2] + (b[i + 3] << 8); + const laneNP = laneN0 * PRIME32_2 + ((laneN1 * PRIME32_2) << 16); + let acc = (accN[lane] + laneNP) & 0xffffffff; + acc = (acc << 13) | (acc >>> 19); + const acc0 = acc & 0xffff; + const acc1 = acc >>> 16; + accN[lane] = (acc0 * PRIME32_1 + ((acc1 * PRIME32_1) << 16)) & 0xffffffff; + lane = (lane + 1) & 0x3; + } + + /* + Step 3. Accumulator convergence + All 4 lane accumulators from previous steps are merged to produce a + single remaining accumulator + of same width (32-bit). The associated formula is as follows : + ``` + acc = (acc1 <<< 1) + (acc2 <<< 7) + (acc3 <<< 12) + (acc4 <<< 18); + ``` + */ + acc = + (((accN[0] << 1) | (accN[0] >>> 31)) + + ((accN[1] << 7) | (accN[1] >>> 25)) + + ((accN[2] << 12) | (accN[2] >>> 20)) + + ((accN[3] << 18) | (accN[3] >>> 14))) & + 0xffffffff; + } + + /* + Step 4. Add input length + The input total length is presumed known at this stage. + This step is just about adding the length to + accumulator, so that it participates to final mixing. + ``` + acc = acc + (u32)inputLength; + ``` + */ + acc = (acc + buffer.length) & 0xffffffff; + + /* + Step 5. Consume remaining input + There may be up to 15 bytes remaining to consume from the input. + The final stage will digest them according + to following pseudo-code : + ``` + while (remainingLength >= 4) { + lane = read_32bit_little_endian(input_ptr); + acc = acc + lane * PRIME32_3; + acc = (acc <<< 17) * PRIME32_4; + input_ptr += 4; remainingLength -= 4; + } + ``` + This process ensures that all input bytes are present in the final mix. + */ + + const limit = buffer.length - 4; + for (; offset <= limit; offset += 4) { + const i = offset; + const laneN0 = b[i + 0] + (b[i + 1] << 8); + const laneN1 = b[i + 2] + (b[i + 3] << 8); + const laneP = laneN0 * PRIME32_3 + ((laneN1 * PRIME32_3) << 16); + acc = (acc + laneP) & 0xffffffff; + acc = (acc << 17) | (acc >>> 15); + acc = + ((acc & 0xffff) * PRIME32_4 + (((acc >>> 16) * PRIME32_4) << 16)) & + 0xffffffff; + } + + /* + ``` + while (remainingLength >= 1) { + lane = read_byte(input_ptr); + acc = acc + lane * PRIME32_5; + acc = (acc <<< 11) * PRIME32_1; + input_ptr += 1; remainingLength -= 1; + } + ``` + */ + + for (; offset < b.length; ++offset) { + const lane = b[offset]; + acc += lane * PRIME32_5; + acc = (acc << 11) | (acc >>> 21); + acc = + ((acc & 0xffff) * PRIME32_1 + (((acc >>> 16) * PRIME32_1) << 16)) & + 0xffffffff; + } + + /* + Step 6. Final mix (avalanche) + The final mix ensures that all input bits have a chance to impact any bit in + the output digest, resulting in an unbiased distribution. This is also called + avalanche effect. + ``` + acc = acc xor (acc >> 15); + acc = acc * PRIME32_2; + acc = acc xor (acc >> 13); + acc = acc * PRIME32_3; + acc = acc xor (acc >> 16); + ``` + */ + + acc ^= acc >>> 15; + acc = + (((acc & 0xffff) * PRIME32_2) & 0xffffffff) + + (((acc >>> 16) * PRIME32_2) << 16); + acc ^= acc >>> 13; + acc = + (((acc & 0xffff) * PRIME32_3) & 0xffffffff) + + (((acc >>> 16) * PRIME32_3) << 16); + acc ^= acc >>> 16; + + // turn any negatives back into a positive number; + return acc < 0 ? acc + 4294967296 : acc; +} \ No newline at end of file diff --git a/packages/start/src/server/server-fns-runtime.ts b/packages/start/src/server/server-fns-runtime.ts index a287670b9..a60ce3c7d 100644 --- a/packages/start/src/server/server-fns-runtime.ts +++ b/packages/start/src/server/server-fns-runtime.ts @@ -1,7 +1,22 @@ import { getRequestEvent } from "solid-js/web"; import { provideRequestEvent } from "solid-js/web/storage"; +import { registerServerFunction } from "./server-fns.ts"; -export function createServerReference(fn: Function, id: string) { +interface Registration { + id: string; + fn: (...args: T) => Promise; +} + +export function createServerReference( + id: string, + fn: (...args: T) => Promise, +) { + const registration: Registration = { id, fn }; + registerServerFunction(id, fn); + return registration; +} + +export function cloneServerReference({ id, fn }: Registration) { if (typeof fn !== "function") throw new Error("Export from a 'use server' module must be a function"); let baseURL = import.meta.env.BASE_URL ?? "/"; @@ -24,7 +39,7 @@ export function createServerReference(fn: Function, id: string) { }; evt.serverOnly = true; return provideRequestEvent(evt, () => { - return fn.apply(thisArg, args); + return fn.apply(thisArg, args as T); }); }, }); diff --git a/packages/start/src/server/server-fns.ts b/packages/start/src/server/server-fns.ts new file mode 100644 index 000000000..467719b31 --- /dev/null +++ b/packages/start/src/server/server-fns.ts @@ -0,0 +1,19 @@ +const REGISTRATIONS = new Map(); + +export function registerServerFunction( + id: string, + callback: (...args: T) => Promise, +) { + REGISTRATIONS.set(id, callback); + return callback; +} + +export function getServerFunction( + id: string, +): ((...args: T) => Promise) { + const fn = REGISTRATIONS.get(id) as ((...args: T) => Promise) | undefined; + if (fn) { + return fn; + } + throw new Error('invalid server function: ' + id); +} diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index 160672684..b6ed07e87 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -1,4 +1,3 @@ -import { getServerFnById } from "solidstart:server-fn-manifest"; import { parseSetCookie } from "cookie-es"; import { type H3Event, parseCookies } from "h3"; import { crossSerializeStream, fromJSON, getCrossReferenceHeader } from "seroval"; @@ -17,9 +16,11 @@ import { import { sharedConfig } from "solid-js"; import { renderToString } from "solid-js/web"; import { provideRequestEvent } from "solid-js/web/storage"; +import "solidstart:server-fn-manifest"; import { getFetchEvent, mergeResponseHeaders } from "./fetchEvent.ts"; import { createPageEvent } from "./handler.ts"; +import { getServerFunction } from "./server-fns.ts"; import type { FetchEvent, PageEvent } from "./types.ts"; import { getExpectedRedirectStatus } from "./util.ts"; @@ -91,7 +92,7 @@ export async function handleServerFunction(h3Event: H3Event) { } } - const serverFunction = await getServerFnById(functionId!); + const serverFunction = getServerFunction(functionId!); let parsed: any[] = []; diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index 8bbabe40b..9597cca8b 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -193,7 +193,7 @@ async function fetchServerFunction( return result; } -export function createServerReference(id: string) { +export function cloneServerReference(id: string) { let baseURL = import.meta.env.BASE_URL ?? "/"; if (!baseURL.endsWith("/")) baseURL += "/"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d00c64b91..cb09317c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -322,9 +322,6 @@ importers: '@solidjs/meta': specifier: ^0.29.4 version: 0.29.4(solid-js@1.9.9) - '@tanstack/server-functions-plugin': - specifier: 1.134.5 - version: 1.134.5(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) '@types/babel__traverse': specifier: ^7.28.0 version: 7.28.0 @@ -441,28 +438,14 @@ packages: resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.27.3': - resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} - engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.28.5': - resolution: {integrity: sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-globals@7.28.0': resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} - '@babel/helper-member-expression-to-functions@7.28.5': - resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} - engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.18.6': resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} @@ -477,24 +460,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-optimise-call-expression@7.27.1': - resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} - engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.27.1': resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} - '@babel/helper-replace-supers@7.27.1': - resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': - resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -522,30 +491,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-commonjs@7.27.1': - resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.28.0': - resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-typescript@7.27.1': - resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/runtime@7.26.7': resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==} engines: {node: '>=6.9.0'} @@ -2133,20 +2078,6 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 - '@tanstack/directive-functions-plugin@1.134.5': - resolution: {integrity: sha512-J3oawV8uBRBbPoLgMdyHt+LxzTNuWRKNJJuCLWsm/yq6v0IQSvIVCgfD2+liIiSnDPxGZ8ExduPXy8IzS70eXw==} - engines: {node: '>=12'} - peerDependencies: - vite: '>=6.0.0 || >=7.0.0' - - '@tanstack/router-utils@1.133.19': - resolution: {integrity: sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA==} - engines: {node: '>=12'} - - '@tanstack/server-functions-plugin@1.134.5': - resolution: {integrity: sha512-2sWxq70T+dOEUlE3sHlXjEPhaFZfdPYlWTSkHchWXrFGw2YOAa+hzD6L9wHMjGDQezYd03ue8tQlHG+9Jzbzgw==} - engines: {node: '>=12'} - '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -2471,9 +2402,6 @@ packages: b4a@1.6.6: resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} - babel-dead-code-elimination@1.0.10: - resolution: {integrity: sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==} - babel-plugin-jsx-dom-expressions@0.40.1: resolution: {integrity: sha512-b4iHuirqK7RgaMzB2Lsl7MqrlDgQtVRSSazyrmx7wB3T759ggGjod5Rkok5MfHjQXhR7tRPmdwoeGPqBnW2KfA==} peerDependencies: @@ -2944,10 +2872,6 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - diff@8.0.2: - resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -5172,9 +5096,6 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -5957,10 +5878,6 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-annotate-as-pure@7.27.3': - dependencies: - '@babel/types': 7.28.5 - '@babel/helper-compilation-targets@7.27.2': dependencies: '@babel/compat-data': 7.28.5 @@ -5969,28 +5886,8 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.5 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/helper-globals@7.28.0': {} - '@babel/helper-member-expression-to-functions@7.28.5': - dependencies: - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-imports@7.18.6': dependencies: '@babel/types': 7.28.5 @@ -6011,28 +5908,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-optimise-call-expression@7.27.1': - dependencies: - '@babel/types': 7.28.5 - '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': - dependencies: - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -6053,41 +5930,6 @@ snapshots: '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.3) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) - transitivePeerDependencies: - - supports-color - - '@babel/preset-typescript@7.27.1(@babel/core@7.28.3)': - dependencies: - '@babel/core': 7.28.3 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.3) - '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.3) - transitivePeerDependencies: - - supports-color - '@babel/runtime@7.26.7': dependencies: regenerator-runtime: 0.14.1 @@ -7559,49 +7401,6 @@ snapshots: tailwindcss: 4.1.17 vite: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) - '@tanstack/directive-functions-plugin@1.134.5(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/core': 7.28.3 - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 - '@tanstack/router-utils': 1.133.19 - babel-dead-code-elimination: 1.0.10 - pathe: 2.0.3 - tiny-invariant: 1.3.3 - vite: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) - transitivePeerDependencies: - - supports-color - - '@tanstack/router-utils@1.133.19': - dependencies: - '@babel/core': 7.28.3 - '@babel/generator': 7.28.5 - '@babel/parser': 7.28.5 - '@babel/preset-typescript': 7.27.1(@babel/core@7.28.3) - ansis: 4.2.0 - diff: 8.0.2 - pathe: 2.0.3 - tinyglobby: 0.2.15 - transitivePeerDependencies: - - supports-color - - '@tanstack/server-functions-plugin@1.134.5(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/core': 7.28.3 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 - '@tanstack/directive-functions-plugin': 1.134.5(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) - babel-dead-code-elimination: 1.0.10 - tiny-invariant: 1.3.3 - transitivePeerDependencies: - - supports-color - - vite - '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 @@ -7946,7 +7745,8 @@ snapshots: ansi-styles@6.2.1: {} - ansis@4.2.0: {} + ansis@4.2.0: + optional: true any-promise@1.3.0: {} @@ -8020,15 +7820,6 @@ snapshots: b4a@1.6.6: {} - babel-dead-code-elimination@1.0.10: - dependencies: - '@babel/core': 7.28.3 - '@babel/parser': 7.28.5 - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 - transitivePeerDependencies: - - supports-color - babel-plugin-jsx-dom-expressions@0.40.1(@babel/core@7.28.3): dependencies: '@babel/core': 7.28.3 @@ -8459,8 +8250,6 @@ snapshots: didyoumean@1.2.2: {} - diff@8.0.2: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -10901,8 +10690,6 @@ snapshots: dependencies: any-promise: 1.3.0 - tiny-invariant@1.3.3: {} - tinybench@2.9.0: {} tinyexec@0.3.2: {}