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
5 changes: 5 additions & 0 deletions .changeset/route-transforms-non-ts-languages.md
Original file line number Diff line number Diff line change
@@ -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
10 changes: 5 additions & 5 deletions packages/start/src/config/fs-router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ExportSpecifier } from "es-module-lexer";
import {
analyzeModule,
analyzeRouteModule,
BaseFileSystemRouter,
cleanPath,
type FileSystemRouterConfig,
Expand All @@ -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")) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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));
Expand Down
50 changes: 42 additions & 8 deletions packages/start/src/config/fs-routes/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;

export type FileSystemRouterConfig = {
dir: string;
extensions: string[];
/** Per-extension (without dot) source transforms for route export analysis. */
routeTransforms?: Record<string, RouteModuleTransform>;
};
type Route = { path: string } & Record<string, any>;

export function cleanPath(src: string, config: FileSystemRouterConfig) {
Expand All @@ -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[];

Expand Down Expand Up @@ -71,12 +105,12 @@ export class BaseFileSystemRouter extends EventTarget {
throw new Error("Not implemented");
}

toRoute(src: string): Route | undefined {
async toRoute(src: string): Promise<Route | undefined> {
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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
22 changes: 21 additions & 1 deletion packages/start/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<string, RouteModuleTransform>;
middleware?: string;
serialization?: {
/**
Expand Down Expand Up @@ -180,10 +198,12 @@ export function solidStart(options?: SolidStartOptions): Array<PluginOption> {
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,
}),
},
Expand Down
Loading