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
20 changes: 20 additions & 0 deletions docs/1.docs/50.configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,26 @@ export default defineConfig({
> [!NOTE]
> The `srcDir` option is deprecated. Use `serverDir` instead.

## Source extensions

Nitro scans built-in JS and TS server files by default. Use `sourceExtensions` to add other script-like source extensions that your bundler can transform, such as `.civet` or `.res`.

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
sourceExtensions: [".civet", ".res"],
})
```

This affects server file scanning, entry resolution, generated route types, and development reload detection. Nitro still expects your builder or framework to provide the actual transform for the custom extension.

If you also use custom source extensions for Nitro `modules`, those files must be supported by the current JS runtime. For example, with Civet you can register the loader before starting Nitro:

```bash
NODE_OPTIONS="--import @danielx/civet/register" pnpm dev
```

## Environment variables

Certain Nitro behaviors can be configured using environment variables:
Expand Down
11 changes: 8 additions & 3 deletions src/build/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import type { Nitro, NitroImportMeta } from "nitro/types";
import { defineEnv } from "unenv";
import { pkgDir } from "nitro/meta";
import { pathRegExp, toPathRegExp } from "../utils/regex.ts";
import {
getSourceExtensionPattern,
getSourceExtensions,
TS_SOURCE_EXTENSIONS,
} from "../utils/source-extensions.ts";

export type BaseBuildConfig = ReturnType<typeof baseBuildConfig>;

export function baseBuildConfig(nitro: Nitro) {
// prettier-ignore
const extensions: string[] = [".ts", ".mjs", ".js", ".json", ".node", ".tsx", ".jsx" ];
const extensions: string[] = [...getSourceExtensions(nitro.options), ".json", ".node"];

const isNodeless = nitro.options.node === false;

Expand Down Expand Up @@ -66,8 +70,9 @@ export function baseBuildConfig(nitro: Nitro) {
}

function getNoExternals(nitro: Nitro): RegExp[] {
const extensionPattern = getSourceExtensionPattern(nitro.options, TS_SOURCE_EXTENSIONS);
const noExternal: RegExp[] = [
/\.[mc]?tsx?$/,
new RegExp(String.raw`\.(?:${extensionPattern})$`),
/^(?:[\0#~.]|virtual:)/,
new RegExp("^" + pathRegExp(pkgDir) + "(?!.*node_modules)"),
...[
Expand Down
4 changes: 3 additions & 1 deletion src/build/rolldown/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { watch as chokidarWatch } from "chokidar";
import { basename, join } from "pathe";
import { debounce } from "perfect-debounce";
import { scanHandlers } from "../../scan.ts";
import { getSourceExtensionPattern } from "../../utils/source-extensions.ts";
import { writeTypes } from "../types.ts";
import { formatCompatibilityDate } from "compatx";

Expand Down Expand Up @@ -40,7 +41,8 @@ export async function watchDev(nitro: Nitro, config: RolldownOptions) {
}
});

const serverEntryRe = /^server\.[mc]?[jt]sx?$/;
const sourceExtensionPattern = getSourceExtensionPattern(nitro.options);
const serverEntryRe = new RegExp(String.raw`^server(?:\.node)?\.(?:${sourceExtensionPattern})$`);
const rootDirWatcher = chokidarWatch(nitro.options.rootDir, {
ignoreInitial: true,
depth: 0,
Expand Down
4 changes: 3 additions & 1 deletion src/build/rollup/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { defu } from "defu";
import { basename, join } from "pathe";
import { debounce } from "perfect-debounce";
import { scanHandlers } from "../../scan.ts";
import { getSourceExtensionPattern } from "../../utils/source-extensions.ts";
import { formatRollupError } from "./error.ts";
import { writeTypes } from "../types.ts";
import { formatCompatibilityDate } from "compatx";
Expand Down Expand Up @@ -42,7 +43,8 @@ export async function watchDev(nitro: Nitro, rollupConfig: RollupConfig) {
}
});

const serverEntryRe = /^server\.[mc]?[jt]sx?$/;
const sourceExtensionPattern = getSourceExtensionPattern(nitro.options);
const serverEntryRe = new RegExp(String.raw`^server(?:\.node)?\.(?:${sourceExtensionPattern})$`);
const rootDirWatcher = chokidarWatch(nitro.options.rootDir, {
ignoreInitial: true,
depth: 0,
Expand Down
11 changes: 6 additions & 5 deletions src/build/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { TSConfig } from "pkg-types";
import type { JSValue } from "untyped";
import { generateTypes, resolveSchema } from "untyped";
import { toExports } from "unimport";
import { getSourceExtensions, stripSourceExtension } from "../utils/source-extensions.ts";

export async function writeTypes(nitro: Nitro) {
const types: NitroTypes = {
Expand All @@ -29,10 +30,10 @@ export async function writeTypes(nitro: Nitro) {
if (typeof mw.handler !== "string" || !mw.route) {
continue;
}
const relativePath = relative(
generatedTypesDir,
resolveNitroPath(mw.handler, nitro.options)
).replace(/\.(js|mjs|cjs|ts|mts|cts|tsx|jsx)$/, "");
const relativePath = stripSourceExtension(
relative(generatedTypesDir, resolveNitroPath(mw.handler, nitro.options)),
nitro.options
);

const method = mw.method || "default";

Expand Down Expand Up @@ -72,7 +73,7 @@ export async function writeTypes(nitro: Nitro) {
from: nitro.options.rootDir,
conditions: ["type", "node", "import"],
suffixes: ["", "/index"],
extensions: [".mjs", ".cjs", ".js", ".mts", ".cts", ".ts"],
extensions: getSourceExtensions(nitro.options),
});
if (resolvedPath) {
const { dir, name } = parseNodeModulePath(resolvedPath);
Expand Down
5 changes: 4 additions & 1 deletion src/build/vite/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { join } from "pathe";
import { debounce } from "perfect-debounce";
import { withBase } from "ufo";
import { scanHandlers } from "../../scan.ts";
import { getSourceExtensionPattern } from "../../utils/source-extensions.ts";
import { getEnvRunner } from "./env.ts";

// https://vite.dev/guide/api-environment-runtimes.html#modulerunner
Expand Down Expand Up @@ -112,6 +113,8 @@ export class FetchableDevEnvironment extends DevEnvironment {
export async function configureViteDevServer(ctx: NitroPluginContext, server: ViteDevServer) {
const nitro = ctx.nitro!;
const nitroEnv = server.environments.nitro as FetchableDevEnvironment;
const sourceExtensionPattern = getSourceExtensionPattern(nitro.options);
const serverEntryRe = new RegExp(String.raw`^server(?:\.node)?\.(?:${sourceExtensionPattern})$`);

// Restart with nitro.config changes
const nitroConfigFile = nitro.options._c12.configFile;
Expand Down Expand Up @@ -160,7 +163,7 @@ export async function configureViteDevServer(ctx: NitroPluginContext, server: Vi
nitro.options.rootDir,
{ persistent: false },
(_event, filename) => {
if (filename && /^server\.[mc]?[jt]sx?$/.test(filename)) {
if (filename && serverEntryRe.test(filename)) {
reload();
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/build/vite/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { runtimeDependencies, runtimeDir } from "nitro/meta";
import { resolveModulePath } from "exsolve";
import { createFetchableDevEnvironment } from "./dev.ts";
import { isAbsolute } from "pathe";
import { getSourceExtensions } from "../../utils/source-extensions.ts";

export function createNitroEnvironment(ctx: NitroPluginContext): EnvironmentOptions {
const isWorkerdRunner = _isWorkerdRunner(ctx);
Expand Down Expand Up @@ -89,7 +90,7 @@ export function createServiceEnvironment(
},
dev: {
createEnvironment: (envName, envConfig) => {
const entry = tryResolve(serviceConfig.entry);
const entry = tryResolve(serviceConfig.entry, ctx.nitro!.options.sourceExtensions);
(ctx._viteEnvs ??= new Map()).set(envName, entry);
return createFetchableDevEnvironment(envName, envConfig, getEnvRunner(ctx), entry, {
preventExternalize: isWorkerdRunner,
Expand Down Expand Up @@ -237,13 +238,14 @@ function _isWorkerdRunner(ctx: NitroPluginContext): boolean {
return runnerName === "miniflare";
}

function tryResolve(id: string) {
function tryResolve(id: string, sourceExtensions: string[] = []) {
if (/^[~#/\0]/.test(id) || isAbsolute(id)) {
return id;
}
const resolvableSourceExtensions = getSourceExtensions({ sourceExtensions });
const resolved = resolveModulePath(id, {
suffixes: ["", "/index"],
extensions: ["", ".ts", ".mjs", ".cjs", ".js", ".mts", ".cts"],
extensions: ["", ...resolvableSourceExtensions],
try: true,
});
return resolved || id;
Expand Down
9 changes: 4 additions & 5 deletions src/build/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@ import { NitroDevApp } from "../../dev/app.ts";
import { nitroPreviewPlugin } from "./preview.ts";
import assetsPlugin from "@hiogawa/vite-plugin-fullstack/assets";
import type { NitroConfig } from "nitro/types";
import { getSourceExtensions } from "../../utils/source-extensions.ts";

// https://vite.dev/guide/api-environment-plugins
// https://vite.dev/guide/api-environment-frameworks.html

const DEFAULT_EXTENSIONS = [".ts", ".js", ".mts", ".mjs", ".tsx", ".jsx"];

const debug = process.env.NITRO_DEBUG
? (...args: any[]) => console.log("[nitro]", ...args)
: () => {};
Expand Down Expand Up @@ -145,7 +144,7 @@ function nitroEnv(ctx: NitroPluginContext): VitePlugin {
const resolvedEntry =
resolveModulePath(entry, {
from: [ctx.nitro!.options.rootDir, ...ctx.nitro!.options.scanDirs],
extensions: DEFAULT_EXTENSIONS,
extensions: getSourceExtensions(ctx.nitro!.options),
suffixes: ["", "/index"],
try: true,
}) || entry;
Expand Down Expand Up @@ -398,7 +397,7 @@ async function setupNitroContext(
from: ["app", "src", ""].flatMap((d) =>
[ctx.nitro!.options.rootDir, ...ctx.nitro!.options.scanDirs].map((s) => join(s, d) + "/")
),
extensions: DEFAULT_EXTENSIONS,
extensions: getSourceExtensions(ctx.nitro!.options),
try: true,
});
if (ssrEntry) {
Expand All @@ -411,7 +410,7 @@ async function setupNitroContext(
ssrEntry =
resolveModulePath(ssrEntry, {
from: [ctx.nitro.options.rootDir, ...ctx.nitro.options.scanDirs],
extensions: DEFAULT_EXTENSIONS,
extensions: getSourceExtensions(ctx.nitro.options),
suffixes: ["", "/index"],
try: true,
}) || ssrEntry;
Expand Down
1 change: 1 addition & 0 deletions src/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const NitroDefaults: NitroConfig = {
// Dirs
serverDir: false,
scanDirs: [],
sourceExtensions: [],
buildDir: `node_modules/.nitro`,
output: {
dir: "{{ rootDir }}/.output",
Expand Down
8 changes: 4 additions & 4 deletions src/config/resolvers/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { findWorkspaceDir } from "pkg-types";
import { NitroDefaults } from "../defaults.ts";
import { resolveModulePath } from "exsolve";
import consola from "consola";

const RESOLVE_EXTENSIONS = [".ts", ".js", ".mts", ".mjs", ".tsx", ".jsx"];
import { getSourceExtensions, normalizeSourceExtensions } from "../../utils/source-extensions.ts";

export async function resolvePathOptions(options: NitroOptions) {
options.rootDir = resolve(options.rootDir || ".") + "/";
Expand All @@ -30,6 +29,7 @@ export async function resolvePathOptions(options: NitroOptions) {
}

options.alias ??= {};
options.sourceExtensions = normalizeSourceExtensions(options.sourceExtensions);

// Resolve possibly template paths
if (!options.static && !options.entry) {
Expand Down Expand Up @@ -93,7 +93,7 @@ export async function resolvePathOptions(options: NitroOptions) {
const detected = resolveModulePath("./server", {
try: true,
from: options.rootDir,
extensions: RESOLVE_EXTENSIONS.flatMap((ext) => [ext, `.node${ext}`]),
extensions: getSourceExtensions(options).flatMap((ext) => [ext, `.node${ext}`]),
});
if (detected) {
options.serverEntry ??= { handler: "" };
Expand All @@ -118,7 +118,7 @@ export async function resolvePathOptions(options: NitroOptions) {
resolveNitroPath(options.renderer?.handler, options),
{
from: [options.rootDir, ...options.scanDirs],
extensions: RESOLVE_EXTENSIONS,
extensions: getSourceExtensions(options),
}
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Nitro, NitroModule, NitroModuleInput } from "nitro/types";
import { resolveModuleURL } from "exsolve";
import { getSourceExtensions } from "./utils/source-extensions.ts";

export async function installModules(nitro: Nitro) {
const _modules = [...(nitro.options.modules || [])];
Expand All @@ -25,7 +26,7 @@ async function _resolveNitroModule(
if (typeof mod === "string") {
_url = resolveModuleURL(mod, {
from: [nitroOptions.rootDir],
extensions: [".mjs", ".cjs", ".js", ".mts", ".cts", ".ts"],
extensions: getSourceExtensions(nitroOptions),
});
mod = (await import(_url).then((m: any) => m.default || m)) as NitroModule;
}
Expand Down
5 changes: 2 additions & 3 deletions src/presets/cloudflare/entry-exports.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { Nitro } from "nitro/types";
import { resolveModulePath } from "exsolve";
import { prettyPath } from "../../utils/fs.ts";

const RESOLVE_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"];
import { getSourceExtensions } from "../../utils/source-extensions.ts";

export async function setupEntryExports(nitro: Nitro) {
const exportsEntry = resolveExportsEntry(nitro);
Expand All @@ -21,7 +20,7 @@ export async function setupEntryExports(nitro: Nitro) {
function resolveExportsEntry(nitro: Nitro) {
const entry = resolveModulePath(nitro.options.cloudflare?.exports || "./exports.cloudflare.ts", {
from: nitro.options.rootDir,
extensions: RESOLVE_EXTENSIONS,
extensions: getSourceExtensions(nitro.options),
try: true,
});

Expand Down
11 changes: 4 additions & 7 deletions src/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { glob } from "tinyglobby";
import type { Nitro } from "nitro/types";
import { join, relative } from "pathe";
import { withBase, withLeadingSlash, withoutTrailingSlash } from "ufo";

export const GLOB_SCAN_PATTERN = "**/*.{js,mjs,cjs,ts,mts,cts,tsx,jsx}";
import { getScanPattern, stripSourceExtension } from "./utils/source-extensions.ts";
type FileInfo = { path: string; fullPath: string };

const suffixRegex =
Expand Down Expand Up @@ -85,8 +84,7 @@ export async function scanMiddleware(nitro: Nitro) {
export async function scanServerRoutes(nitro: Nitro, dir: string, prefix = "/") {
const files = await scanFiles(nitro, dir);
return files.map((file) => {
let route = file.path
.replace(/\.[A-Za-z]+$/, "")
let route = stripSourceExtension(file.path, nitro.options)
.replace(/\(([^(/\\]+)\)[/\\]/g, "")
.replace(/\[\.{3}]/g, "**")
.replace(/\[\.{3}([^\]]+)]/g, (_, p) => "**:" + p.replace(/[^\w-]/g, "_"))
Expand Down Expand Up @@ -123,9 +121,8 @@ export async function scanPlugins(nitro: Nitro) {
export async function scanTasks(nitro: Nitro) {
const files = await scanFiles(nitro, "tasks");
return files.map((f) => {
const name = f.path
const name = stripSourceExtension(f.path, nitro.options)
.replace(/\/index$/, "")
.replace(/\.[A-Za-z]+$/, "")
.replace(/\//g, ":");
return { name, handler: f.fullPath };
});
Expand All @@ -144,7 +141,7 @@ async function scanFiles(nitro: Nitro, name: string): Promise<FileInfo[]> {
}

async function scanDir(nitro: Nitro, dir: string, name: string): Promise<FileInfo[]> {
const fileNames = await glob(join(name, GLOB_SCAN_PATTERN), {
const fileNames = await glob(join(name, getScanPattern(nitro.options)), {
cwd: dir,
dot: true,
ignore: nitro.options.ignore,
Expand Down
21 changes: 21 additions & 0 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,27 @@ export interface NitroOptions extends PresetOptions {
*/
scanDirs: string[];

/**
* Additional script-like source file extensions Nitro should treat like
* built-in JS/TS server files.
*
* These extensions are appended to Nitro's built-in source extensions and
* are used for file scanning, entry resolution, generated route types, and
* development reload detection.
*
* When used for Nitro `modules`, custom extensions must also be supported
* by the current JS runtime. For example:
* `NODE_OPTIONS="--import @danielx/civet/register" pnpm dev`
*
* @example
* ```ts
* sourceExtensions: [".civet", ".res"]
* ```
*
* @see https://nitro.build/config#sourceextensions
*/
sourceExtensions: string[];

/**
* Directory name to scan for API route handlers.
*
Expand Down
Loading