From 4d7850d4fa5f7f1f6d943206fd9b673f70477117 Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Thu, 11 Jun 2026 09:07:37 -0700 Subject: [PATCH 1/2] feat: routeTransforms option for non-TS route languages Route modules are analyzed for their exports with esbuild's tsx loader, which fails on languages that compile to JS/TS (Civet, CoffeeScript, ...), silently dropping the route. Let bundler plugins/users register a per-extension source transform that runs before analysis. Co-Authored-By: Claude Fable 5 --- packages/start/src/config/fs-router.ts | 10 ++-- packages/start/src/config/fs-routes/router.ts | 50 ++++++++++++++++--- packages/start/src/config/index.ts | 22 +++++++- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/packages/start/src/config/fs-router.ts b/packages/start/src/config/fs-router.ts index fdc90bd0c..4b79891b9 100644 --- a/packages/start/src/config/fs-router.ts +++ b/packages/start/src/config/fs-router.ts @@ -1,6 +1,6 @@ import type { ExportSpecifier } from "es-module-lexer"; import { - analyzeModule, + analyzeRouteModule, BaseFileSystemRouter, cleanPath, type FileSystemRouterConfig, @@ -25,7 +25,7 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter { return routePath?.length > 0 ? `/${routePath}` : "/"; } - toRoute(src: string) { + async toRoute(src: string) { const path = this.toPath(src); if (src.endsWith(".md") || src.endsWith(".mdx")) { @@ -41,7 +41,7 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter { }; } - const [_, exports] = analyzeModule(src); + const [_, exports] = await analyzeRouteModule(src, this.config); const hasDefault = !!exports.find(e => e.n === "default"); const hasRouteConfig = !!exports.find(e => e.n === "route"); if (hasDefault) { @@ -114,7 +114,7 @@ export class SolidStartServerFileRouter extends BaseFileSystemRouter { return routePath?.length > 0 ? `/${routePath}` : "/"; } - toRoute(src: string) { + async toRoute(src: string) { const path = this.toPath(src); if (src.endsWith(".md") || src.endsWith(".mdx")) { return { @@ -128,7 +128,7 @@ export class SolidStartServerFileRouter extends BaseFileSystemRouter { }; } - const [_, exports] = analyzeModule(src); + const [_, exports] = await analyzeRouteModule(src, this.config); const hasRouteConfig = exports.find(e => e.n === "route"); const hasDefault = !!exports.find(e => e.n === "default"); const hasAPIRoutes = !!exports.find(exp => HTTP_METHODS.includes(exp.n)); diff --git a/packages/start/src/config/fs-routes/router.ts b/packages/start/src/config/fs-routes/router.ts index 470eb95d5..52fd963c5 100644 --- a/packages/start/src/config/fs-routes/router.ts +++ b/packages/start/src/config/fs-routes/router.ts @@ -12,7 +12,24 @@ export { pathToRegexp }; export const glob = (path: string) => fg.sync(path, { absolute: true }); -export type FileSystemRouterConfig = { dir: string; extensions: string[] }; +/** + * Transforms a route module's source into JS/TS/TSX before export analysis. + * Route modules are parsed with esbuild's tsx loader to enumerate their + * exports (for `?pick=` tree-shaking); languages that *compile to* JS/TS — + * Civet, CoffeeScript, etc. — can't be parsed directly, so their bundler + * plugin (or the user) supplies a transform keyed by extension. + */ +export type RouteModuleTransform = ( + code: string, + sourcePath: string, +) => string | Promise; + +export type FileSystemRouterConfig = { + dir: string; + extensions: string[]; + /** Per-extension (without dot) source transforms for route export analysis. */ + routeTransforms?: Record; +}; type Route = { path: string } & Record; export function cleanPath(src: string, config: FileSystemRouterConfig) { @@ -21,17 +38,34 @@ export function cleanPath(src: string, config: FileSystemRouterConfig) { .replace(new RegExp(`\.(${(config.extensions ?? []).join("|")})$`), ""); } -export function analyzeModule(src: string) { +export function analyzeCode(code: string, sourcePath: string) { return parse( - esbuild.transformSync(fs.readFileSync(src, "utf-8"), { + esbuild.transformSync(code, { jsx: "transform", format: "esm", loader: "tsx", }).code, - src, + sourcePath, ); } +export function analyzeModule(src: string) { + return analyzeCode(fs.readFileSync(src, "utf-8"), src); +} + +/** + * Like analyzeModule, but applies the configured per-extension transform + * first so non-TS route languages can be analyzed. + */ +export async function analyzeRouteModule(sourcePath: string, config: FileSystemRouterConfig) { + const transform = config.routeTransforms?.[sourcePath.slice(sourcePath.lastIndexOf(".") + 1)]; + if (!transform) return analyzeModule(sourcePath); + // parse() requires the lexer wasm to be initialized; buildRoutes awaits + // this before the initial scan, but watcher updates can arrive earlier. + await init; + return analyzeCode(await transform(fs.readFileSync(sourcePath, "utf-8"), sourcePath), sourcePath); +} + export class BaseFileSystemRouter extends EventTarget { routes: Route[]; @@ -71,12 +105,12 @@ export class BaseFileSystemRouter extends EventTarget { throw new Error("Not implemented"); } - toRoute(src: string): Route | undefined { + async toRoute(src: string): Promise { let path = this.toPath(src); if (path === undefined) return; - const [_, exports] = analyzeModule(src); + const [_, exports] = await analyzeRouteModule(src, this.config); if (!exports.find(e => e.n === "default")) { console.warn("No default export", src); @@ -106,7 +140,7 @@ export class BaseFileSystemRouter extends EventTarget { src = normalizePath(src); if (this.isRoute(src)) { try { - const route = this.toRoute(src); + const route = await this.toRoute(src); if (route) { this._addRoute(route); this.reload(route.path); @@ -132,7 +166,7 @@ export class BaseFileSystemRouter extends EventTarget { src = normalizePath(src); if (this.isRoute(src)) { try { - const route = this.toRoute(src); + const route = await this.toRoute(src); if (route) { this._addRoute(route); this.reload(route.path); diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 9cdc6830e..8f1d5e220 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -9,7 +9,7 @@ import { devServer } from "./dev-server.ts"; import { envPlugin, type EnvPluginOptions } from "./env.ts"; import { SolidStartClientFileRouter, SolidStartServerFileRouter } from "./fs-router.ts"; import { fsRoutes } from "./fs-routes/index.ts"; -import type { BaseFileSystemRouter } from "./fs-routes/router.ts"; +import type { BaseFileSystemRouter, RouteModuleTransform } from "./fs-routes/router.ts"; import lazy from "./lazy.ts"; import { manifest } from "./manifest.ts"; import { parseIdQuery } from "./utils.ts"; @@ -19,6 +19,24 @@ export interface SolidStartOptions { ssr?: boolean; routeDir?: string; extensions?: string[]; + /** + * Per-extension (without dot) source transforms applied before route + * modules are analyzed for their exports. The analyzer parses route + * files with esbuild's tsx loader; extensions registered here are + * transformed to JS/TS/TSX first, so compile-to-JS languages can be + * used in the route directory: + * + * ```ts + * import civet from "@danielx/civet"; + * solidStart({ + * extensions: ["civet"], + * routeTransforms: { + * civet: (code, sourcePath) => civet.compile(code, { filename: sourcePath, sync: true }), + * }, + * }) + * ``` + */ + routeTransforms?: Record; middleware?: string; serialization?: { /** @@ -180,10 +198,12 @@ export function solidStart(options?: SolidStartOptions): Array { client: new SolidStartClientFileRouter({ dir: absolute(routeDir, root), extensions, + routeTransforms: options?.routeTransforms, }), ssr: new SolidStartServerFileRouter({ dir: absolute(routeDir, root), extensions, + routeTransforms: options?.routeTransforms, dataOnly: !start.ssr, }), }, From 103fb5e108884f92c962372bd9f8fc69f20bbb5d Mon Sep 17 00:00:00 2001 From: Daniel X Moore Date: Thu, 11 Jun 2026 09:39:32 -0700 Subject: [PATCH 2/2] chore: changeset for routeTransforms Co-Authored-By: Claude Fable 5 --- .changeset/route-transforms-non-ts-languages.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/route-transforms-non-ts-languages.md diff --git a/.changeset/route-transforms-non-ts-languages.md b/.changeset/route-transforms-non-ts-languages.md new file mode 100644 index 000000000..0e73d38b8 --- /dev/null +++ b/.changeset/route-transforms-non-ts-languages.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": minor +--- + +Add `routeTransforms` option: per-extension source transforms applied before route modules are analyzed for their exports, so compile-to-JS languages (Civet, CoffeeScript, ...) can be used in the route directory