-
virtual files
+
`;
- const id = decodeURIComponent(event.req.url?.slice(1) || "");
-
- let file = "";
- if (id in vfsEntries) {
- let contents = vfsEntries[id];
- if (typeof contents === "function") {
- contents = await contents();
- }
- file = editorTemplate({
- readOnly: true,
- language: id.endsWith("html") ? "html" : "javascript",
- theme: "vs-dark",
- value: contents,
- wordWrap: "wordWrapColumn",
- wordWrapColumn: 80,
- });
- } else if (id) {
- throw createError({ message: "File not found", statusCode: 404 });
- } else {
- file = `
-
-
Select a virtual file to inspect
+ const file = id
+ ? editorTemplate({
+ readOnly: true,
+ language: id.endsWith("html") ? "html" : "javascript",
+ theme: "vs-dark",
+ value: content,
+ wordWrap: "wordWrapColumn",
+ wordWrapColumn: 80,
+ })
+ : `
+
+
Select a virtual file to inspect
`;
- }
+
return `
@@ -61,9 +83,18 @@ export function createVFSHandler(nitro: Nitro) {
+
-
+
${files}
${file}
@@ -77,7 +108,7 @@ const monacoUrl = `https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/${monaco
const vsUrl = `${monacoUrl}/vs`;
const editorTemplate = (options: Record
) => `
-
+
`;
diff --git a/src/nitro.ts b/src/nitro.ts
index 773a390b0b..9414efee82 100644
--- a/src/nitro.ts
+++ b/src/nitro.ts
@@ -34,6 +34,10 @@ export async function createNitro(config: NitroConfig = {}): Promise {
nitro.options.plugins.push("#internal/nitro/debug");
}
+ if (nitro.options.timing) {
+ nitro.options.plugins.push("#internal/nitro/timing");
+ }
+
// Logger config
if (nitro.options.logLevel !== undefined) {
nitro.logger.level = nitro.options.logLevel;
diff --git a/src/options.ts b/src/options.ts
index 6cd1bbc139..ec69f5af40 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -1,11 +1,10 @@
-import { pathToFileURL } from "node:url";
-import { resolve, join, isAbsolute } from "pathe";
+import { resolve, join, normalize } from "pathe";
import { loadConfig } from "c12";
import { klona } from "klona/full";
import { camelCase } from "scule";
import { defu } from "defu";
import { resolveModuleExportNames, resolvePath as resolveModule } from "mlly";
-// import escapeRE from 'escape-string-regexp'
+import escapeRE from "escape-string-regexp";
import { withLeadingSlash, withoutTrailingSlash, withTrailingSlash } from "ufo";
import { isTest, isDebug } from "std-env";
import { findWorkspaceDir } from "pkg-types";
@@ -35,7 +34,7 @@ const NitroDefaults: NitroConfig = {
publicDir: "{{ output.dir }}/public",
},
- // Featueres
+ // Features
experimental: {},
storage: {},
devStorage: {},
@@ -44,7 +43,7 @@ const NitroDefaults: NitroConfig = {
serverAssets: [],
plugins: [],
imports: {
- exclude: [/[/\\]node_modules[/\\]/, /[/\\]\.git[/\\]/],
+ exclude: [],
presets: nitroImports,
},
virtual: {},
@@ -190,19 +189,32 @@ export async function loadOptions(
if (options.scanDirs.length === 0) {
options.scanDirs = [options.srcDir];
}
+ options.scanDirs = options.scanDirs.map((dir) =>
+ resolve(options.srcDir, dir)
+ );
- if (options.imports && Array.isArray(options.imports.exclude)) {
+ if (
+ options.imports &&
+ Array.isArray(options.imports.exclude) &&
+ options.imports.exclude.length === 0
+ ) {
+ // Exclude .git and buildDir by default
+ options.imports.exclude.push(/[/\\]\.git[/\\]/);
options.imports.exclude.push(options.buildDir);
- }
- // Normalise absolute auto-import paths for windows machines
- if (options.imports && options.dev) {
- options.imports.imports = options.imports.imports || [];
- for (const entry of options.imports.imports) {
- if (isAbsolute(entry.from)) {
- entry.from = pathToFileURL(entry.from).href;
- }
- }
+ // Exclude all node modules that are not a scanDir
+ const scanDirsInNodeModules = options.scanDirs
+ .map((dir) => dir.match(/(?<=\/)node_modules\/(.+)$/)?.[1])
+ .filter(Boolean);
+ options.imports.exclude.push(
+ scanDirsInNodeModules.length > 0
+ ? new RegExp(
+ `node_modules\\/(?!${scanDirsInNodeModules
+ .map((dir) => escapeRE(dir))
+ .join("|")})`
+ )
+ : /[/\\]node_modules[/\\]/
+ );
}
// Add h3 auto imports preset
@@ -306,13 +318,7 @@ export async function loadOptions(
}
// Resolve plugin paths
- options.plugins = options.plugins.map((p) => {
- const path = resolvePath(p, options);
- if (options.dev && isAbsolute(path)) {
- return pathToFileURL(path).href;
- }
- return path;
- });
+ options.plugins = options.plugins.map((p) => resolvePath(p, options));
return options;
}
diff --git a/src/prerender.ts b/src/prerender.ts
index d237bd9ad2..781e0942b7 100644
--- a/src/prerender.ts
+++ b/src/prerender.ts
@@ -281,6 +281,7 @@ function extractLinks(
const EXT_REGEX = /\.[\da-z]+$/;
-function getExtension(path: string): string {
- return (path.match(EXT_REGEX) || [])[0] || "";
+function getExtension(link: string): string {
+ const pathname = parseURL(link).pathname;
+ return (pathname.match(EXT_REGEX) || [])[0] || "";
}
diff --git a/src/presets/netlify.ts b/src/presets/netlify.ts
index 875e0439a2..db760aa982 100644
--- a/src/presets/netlify.ts
+++ b/src/presets/netlify.ts
@@ -21,20 +21,15 @@ export const netlify = defineNitroPreset({
await writeHeaders(nitro);
await writeRedirects(nitro);
- const serverCJSPath = join(nitro.options.output.serverDir, "server.js");
- const serverJSCode = `
-let _handler
-exports.handler = function handler (event, context) {
- if (_handler) {
- return _handler(event, context)
- }
- return import('./server.mjs').then(m => {
- _handler = m.handler
- return _handler(event, context)
- })
-}
-`.trim();
- await fsp.writeFile(serverCJSPath, serverJSCode);
+ const functionConfig = {
+ config: { nodeModuleFormat: "esm" },
+ version: 1,
+ };
+ const functionConfigPath = join(
+ nitro.options.output.serverDir,
+ "server.json"
+ );
+ await fsp.writeFile(functionConfigPath, JSON.stringify(functionConfig));
},
},
});
@@ -55,7 +50,7 @@ export const netlifyEdge = defineNitroPreset({
},
rollupConfig: {
output: {
- entryFileNames: "server.js",
+ entryFileNames: "server.mjs",
format: "esm",
},
},
@@ -89,12 +84,18 @@ async function writeRedirects(nitro: Nitro) {
);
// Rewrite static cached paths to builder functions
- for (const [key] of rules.filter(
- ([_, routeRules]) =>
- routeRules.cache && (routeRules.cache?.static || routeRules.cache?.swr)
+ for (const [key, value] of rules.filter(
+ ([_, value]) =>
+ value.cache === false ||
+ (value.cache && value.cache.swr === false) ||
+ (value.cache && (value.cache?.static || value.cache?.swr))
)) {
contents =
- `${key.replace("/**", "/*")}\t/.netlify/builders/server 200\n` + contents;
+ value.cache === false || value.cache.swr === false
+ ? `${key.replace("/**", "/*")}\t/.netlify/functions/server 200\n` +
+ contents
+ : `${key.replace("/**", "/*")}\t/.netlify/builders/server 200\n` +
+ contents;
}
for (const [key, routeRules] of rules.filter(
diff --git a/src/presets/vercel.ts b/src/presets/vercel.ts
index da3e5a5ff9..d2be2fea22 100644
--- a/src/presets/vercel.ts
+++ b/src/presets/vercel.ts
@@ -167,14 +167,29 @@ function generateBuildConfig(nitro: Nitro) {
...rules
.filter(
([key, value]) =>
- value.cache &&
- (value.cache.swr || value.cache.static) &&
- key.includes("/**")
+ value.cache === false ||
+ (value.cache && value.cache.swr === false) ||
+ (value.cache &&
+ (value.cache.swr || value.cache.static) &&
+ key.includes("/**"))
)
- .map(([key]) => ({
- src: key.replace(/^(.*)\/\*\*/, "(?$1/.*)"),
- dest: generateEndpoint(key) + "?url=$url",
- })),
+ .map(([key, value]) => {
+ const src = key.replace(/^(.*)\/\*\*/, "(?$1/.*)");
+ if (
+ value.cache === false ||
+ (value.cache && value.cache.swr === false)
+ ) {
+ // we need to write a rule to avoid route being shadowed by another cache rule elsewhere
+ return {
+ src,
+ dest: "/__nitro",
+ };
+ }
+ return {
+ src,
+ dest: generateEndpoint(key) + "?url=$url",
+ };
+ }),
// If we are using a prerender function as a fallback, then we do not need to output
// the below fallback route as well
...(!nitro.options.routeRules["/**"]?.cache ||
diff --git a/src/rollup/config.ts b/src/rollup/config.ts
index 3782f94d4b..61170d715a 100644
--- a/src/rollup/config.ts
+++ b/src/rollup/config.ts
@@ -25,6 +25,7 @@ import { replace } from "./plugins/replace";
import { virtual } from "./plugins/virtual";
import { dynamicRequire } from "./plugins/dynamic-require";
import { externals } from "./plugins/externals";
+import { externals as legacyExternals } from "./plugins/externals-legacy";
import { timing } from "./plugins/timing";
import { publicAssets } from "./plugins/public-assets";
import { serverAssets } from "./plugins/server-assets";
@@ -305,8 +306,11 @@ export const plugins = [
// Externals Plugin
if (!nitro.options.noExternals) {
+ const externalsPlugin = nitro.options.experimental.legacyExternals
+ ? legacyExternals
+ : externals;
rollupConfig.plugins.push(
- externals(
+ externalsPlugin(
defu(nitro.options.externals, {
outDir: nitro.options.output.serverDir,
moduleDirectories: nitro.options.nodeModulesDirs,
diff --git a/src/rollup/plugins/externals-legacy.ts b/src/rollup/plugins/externals-legacy.ts
new file mode 100644
index 0000000000..6b1c57e8dd
--- /dev/null
+++ b/src/rollup/plugins/externals-legacy.ts
@@ -0,0 +1,338 @@
+import { existsSync, promises as fsp } from "node:fs";
+import { resolve, dirname, normalize, join, isAbsolute } from "pathe";
+import consola from "consola";
+import { nodeFileTrace, NodeFileTraceOptions } from "@vercel/nft";
+import type { Plugin } from "rollup";
+import { resolvePath, isValidNodeImport, normalizeid } from "mlly";
+import semver from "semver";
+import { isDirectory, retry } from "../../utils";
+
+export interface NodeExternalsOptions {
+ inline?: string[];
+ external?: string[];
+ outDir?: string;
+ trace?: boolean;
+ traceOptions?: NodeFileTraceOptions;
+ moduleDirectories?: string[];
+ exportConditions?: string[];
+ traceInclude?: string[];
+}
+
+export function externals(opts: NodeExternalsOptions): Plugin {
+ const trackedExternals = new Set();
+
+ const _resolveCache = new Map();
+ const _resolve = async (id: string) => {
+ let resolved = _resolveCache.get(id);
+ if (resolved) {
+ return resolved;
+ }
+ resolved = await resolvePath(id, {
+ conditions: opts.exportConditions,
+ url: opts.moduleDirectories,
+ });
+ _resolveCache.set(id, resolved);
+ return resolved;
+ };
+
+ // Normalize options
+ opts.inline = (opts.inline || []).map((p) => normalize(p));
+ opts.external = (opts.external || []).map((p) => normalize(p));
+
+ return {
+ name: "node-externals",
+ async resolveId(originalId, importer, options) {
+ // Skip internals
+ if (
+ !originalId ||
+ originalId.startsWith("\u0000") ||
+ originalId.includes("?") ||
+ originalId.startsWith("#")
+ ) {
+ return null;
+ }
+
+ // Skip relative paths
+ if (originalId.startsWith(".")) {
+ return null;
+ }
+
+ // Normalize path (windows)
+ const id = normalize(originalId);
+
+ // Id without .../node_modules/
+ const idWithoutNodeModules = id.split("node_modules/").pop();
+
+ // Check for explicit inlines
+ if (
+ opts.inline.some(
+ (i) => id.startsWith(i) || idWithoutNodeModules.startsWith(i)
+ )
+ ) {
+ return null;
+ }
+
+ // Check for explicit externals
+ if (
+ opts.external.some(
+ (i) => id.startsWith(i) || idWithoutNodeModules.startsWith(i)
+ )
+ ) {
+ return { id, external: true };
+ }
+
+ // Resolve id using rollup resolver
+ const resolved = (await this.resolve(originalId, importer, {
+ ...options,
+ skipSelf: true,
+ })) || { id };
+
+ // Try resolving with mlly as fallback
+ if (
+ !isAbsolute(resolved.id) ||
+ !existsSync(resolved.id) ||
+ (await isDirectory(resolved.id))
+ ) {
+ resolved.id = await _resolve(resolved.id).catch(() => resolved.id);
+ }
+
+ // Inline invalid node imports
+ if (!(await isValidNodeImport(resolved.id).catch(() => false))) {
+ return null;
+ }
+
+ // Externalize with full path if trace is disabled
+ if (opts.trace === false) {
+ return {
+ ...resolved,
+ id: isAbsolute(resolved.id) ? normalizeid(resolved.id) : resolved.id,
+ external: true,
+ };
+ }
+
+ // -- Trace externals --
+
+ // Try to extract package name from path
+ const { pkgName, subpath } = parseNodeModulePath(resolved.id);
+
+ // Inline if cannot detect package name
+ if (!pkgName) {
+ return null;
+ }
+
+ // Normally package name should be same as originalId
+ // Edge cases: Subpath export and full paths
+ if (pkgName !== originalId) {
+ // Subpath export
+ if (!isAbsolute(originalId)) {
+ const fullPath = await _resolve(originalId);
+ trackedExternals.add(fullPath);
+ return {
+ id: originalId,
+ external: true,
+ };
+ }
+
+ // Absolute path, we are not sure about subpath to generate import statement
+ // Guess as main subpath export
+ const packageEntry = await _resolve(pkgName).catch(() => null);
+ if (packageEntry !== originalId) {
+ // Guess subpathexport
+ const guessedSubpath = pkgName + subpath.replace(/\.[a-z]+$/, "");
+ const resolvedGuess = await _resolve(guessedSubpath).catch(
+ () => null
+ );
+ if (resolvedGuess === originalId) {
+ trackedExternals.add(resolvedGuess);
+ return {
+ id: guessedSubpath,
+ external: true,
+ };
+ }
+ // Inline since we cannot guess subpath
+ return null;
+ }
+ }
+
+ trackedExternals.add(resolved.id);
+ return {
+ id: pkgName,
+ external: true,
+ };
+ },
+ async buildEnd() {
+ if (opts.trace === false) {
+ return;
+ }
+
+ // Force trace paths
+ for (const pkgName of opts.traceInclude || []) {
+ const path = await this.resolve(pkgName);
+ if (path?.id) {
+ trackedExternals.add(path.id.replace(/\?.+/, ""));
+ }
+ }
+
+ // Trace files
+ let tracedFiles = await nodeFileTrace(
+ [...trackedExternals],
+ opts.traceOptions
+ )
+ .then((r) =>
+ [...r.fileList].map((f) => resolve(opts.traceOptions.base, f))
+ )
+ .then((r) => r.filter((file) => file.includes("node_modules")));
+
+ // Resolve symlinks
+ tracedFiles = await Promise.all(
+ tracedFiles.map((file) => fsp.realpath(file))
+ );
+
+ // Read package.json with cache
+ const packageJSONCache = new Map(); // pkgDir => contents
+ const getPackageJson = async (pkgDir: string) => {
+ if (packageJSONCache.has(pkgDir)) {
+ return packageJSONCache.get(pkgDir);
+ }
+ const pkgJSON = JSON.parse(
+ await fsp.readFile(resolve(pkgDir, "package.json"), "utf8")
+ );
+ packageJSONCache.set(pkgDir, pkgJSON);
+ return pkgJSON;
+ };
+
+ // Keep track of npm packages
+ const tracedPackages = new Map(); // name => pkgDir
+ const ignoreDirs = [];
+ const ignoreWarns = new Set();
+ for (const file of tracedFiles) {
+ const { baseDir, pkgName } = parseNodeModulePath(file);
+ if (!pkgName) {
+ continue;
+ }
+ let pkgDir = resolve(baseDir, pkgName);
+
+ // Check for duplicate versions
+ const existingPkgDir = tracedPackages.get(pkgName);
+ if (existingPkgDir && existingPkgDir !== pkgDir) {
+ const v1 = await getPackageJson(existingPkgDir).then(
+ (r) => r.version
+ );
+ const v2 = await getPackageJson(pkgDir).then((r) => r.version);
+ const isNewer = semver.gt(v2, v1);
+
+ // Warn about major version differences
+ const getMajor = (v: string) => v.split(".").find((s) => s !== "0");
+ if (getMajor(v1) !== getMajor(v2)) {
+ const warn =
+ `Multiple major versions of package \`${pkgName}\` are being externalized. Picking latest version:\n\n` +
+ [
+ ` ${isNewer ? "-" : "+"} ` + existingPkgDir + "@" + v1,
+ ` ${isNewer ? "+" : "-"} ` + pkgDir + "@" + v2,
+ ].join("\n");
+ if (!ignoreWarns.has(warn)) {
+ consola.warn(warn);
+ ignoreWarns.add(warn);
+ }
+ }
+
+ const [newerDir, olderDir] = isNewer
+ ? [pkgDir, existingPkgDir]
+ : [existingPkgDir, pkgDir];
+ // Try to map traced files from one package to another for minor/patch versions
+ if (getMajor(v1) === getMajor(v2)) {
+ tracedFiles = tracedFiles.map((f) =>
+ f.startsWith(olderDir + "/") ? f.replace(olderDir, newerDir) : f
+ );
+ }
+ // Exclude older version files
+ ignoreDirs.push(olderDir + "/");
+ pkgDir = newerDir; // Update for tracedPackages
+ }
+
+ // Add to traced packages
+ tracedPackages.set(pkgName, pkgDir);
+ }
+
+ // Filter out files from ignored packages and dedup
+ tracedFiles = tracedFiles.filter(
+ (f) => !ignoreDirs.some((d) => f.startsWith(d))
+ );
+ tracedFiles = [...new Set(tracedFiles)];
+
+ // Ensure all package.json files are traced
+ for (const pkgDir of tracedPackages.values()) {
+ const pkgJSON = join(pkgDir, "package.json");
+ if (!tracedFiles.includes(pkgJSON)) {
+ tracedFiles.push(pkgJSON);
+ }
+ }
+
+ const writeFile = async (file: string) => {
+ if (!(await isFile(file))) {
+ return;
+ }
+ const src = resolve(opts.traceOptions.base, file);
+ const { pkgName, subpath } = parseNodeModulePath(file);
+ const dst = resolve(opts.outDir, `node_modules/${pkgName + subpath}`);
+ await fsp.mkdir(dirname(dst), { recursive: true });
+ try {
+ await fsp.copyFile(src, dst);
+ } catch {
+ consola.warn(`Could not resolve \`${src}\`. Skipping.`);
+ }
+ };
+
+ // Write traced files
+ await Promise.all(
+ tracedFiles.map((file) => retry(() => writeFile(file), 3))
+ );
+
+ // Write an informative package.json
+ await fsp.writeFile(
+ resolve(opts.outDir, "package.json"),
+ JSON.stringify(
+ {
+ name: "nitro-output",
+ version: "0.0.0",
+ private: true,
+ bundledDependencies: [...tracedPackages.keys()],
+ },
+ null,
+ 2
+ ),
+ "utf8"
+ );
+ },
+ };
+}
+
+function parseNodeModulePath(path: string) {
+ if (!path) {
+ return {};
+ }
+ const match = /^(.+\/node_modules\/)([^/@]+|@[^/]+\/[^/]+)(\/?.*?)?$/.exec(
+ normalize(path)
+ );
+ if (!match) {
+ return {};
+ }
+ const [, baseDir, pkgName, subpath] = match;
+ return {
+ baseDir,
+ pkgName,
+ subpath,
+ };
+}
+
+async function isFile(file: string) {
+ try {
+ const stat = await fsp.stat(file);
+ return stat.isFile();
+ } catch (err) {
+ if (err.code === "ENOENT") {
+ return false;
+ }
+ throw err;
+ }
+}
diff --git a/src/rollup/plugins/externals.ts b/src/rollup/plugins/externals.ts
index 1b018a1411..38f60de5a3 100644
--- a/src/rollup/plugins/externals.ts
+++ b/src/rollup/plugins/externals.ts
@@ -1,10 +1,8 @@
import { existsSync, promises as fsp } from "node:fs";
import { resolve, dirname, normalize, join, isAbsolute } from "pathe";
-import consola from "consola";
import { nodeFileTrace, NodeFileTraceOptions } from "@vercel/nft";
import type { Plugin } from "rollup";
import { resolvePath, isValidNodeImport, normalizeid } from "mlly";
-import semver from "semver";
import { isDirectory, retry } from "../../utils";
export interface NodeExternalsOptions {
@@ -35,6 +33,10 @@ export function externals(opts: NodeExternalsOptions): Plugin {
return resolved;
};
+ // Normalize options
+ opts.inline = (opts.inline || []).map((p) => normalize(p));
+ opts.external = (opts.external || []).map((p) => normalize(p));
+
return {
name: "node-externals",
async resolveId(originalId, importer, options) {
@@ -161,7 +163,7 @@ export function externals(opts: NodeExternalsOptions): Plugin {
return;
}
- // Force trace paths
+ // Manually traced paths
for (const pkgName of opts.traceInclude || []) {
const path = await this.resolve(pkgName);
if (path?.id) {
@@ -169,19 +171,10 @@ export function externals(opts: NodeExternalsOptions): Plugin {
}
}
- // Trace files
- let tracedFiles = await nodeFileTrace(
+ // Trace used files using nft
+ const _fileTrace = await nodeFileTrace(
[...trackedExternals],
opts.traceOptions
- )
- .then((r) =>
- [...r.fileList].map((f) => resolve(opts.traceOptions.base, f))
- )
- .then((r) => r.filter((file) => file.includes("node_modules")));
-
- // Resolve symlinks
- tracedFiles = await Promise.all(
- tracedFiles.map((file) => fsp.realpath(file))
);
// Read package.json with cache
@@ -197,94 +190,199 @@ export function externals(opts: NodeExternalsOptions): Plugin {
return pkgJSON;
};
- // Keep track of npm packages
- const tracedPackages = new Map(); // name => pkgDir
- const ignoreDirs = [];
- const ignoreWarns = new Set();
- for (const file of tracedFiles) {
- const { baseDir, pkgName } = parseNodeModulePath(file);
- if (!pkgName) {
- continue;
- }
- let pkgDir = resolve(baseDir, pkgName);
+ // Resolve traced files
+ type TracedFile = {
+ path: string;
+ subpath: string;
+ parents: string[];
- // Check for duplicate versions
- const existingPkgDir = tracedPackages.get(pkgName);
- if (existingPkgDir && existingPkgDir !== pkgDir) {
- const v1 = await getPackageJson(existingPkgDir).then(
- (r) => r.version
- );
- const v2 = await getPackageJson(pkgDir).then((r) => r.version);
- const isNewer = semver.gt(v2, v1);
-
- // Warn about major version differences
- const getMajor = (v: string) => v.split(".").find((s) => s !== "0");
- if (getMajor(v1) !== getMajor(v2)) {
- const warn =
- `Multiple major versions of package \`${pkgName}\` are being externalized. Picking latest version:\n\n` +
- [
- ` ${isNewer ? "-" : "+"} ` + existingPkgDir + "@" + v1,
- ` ${isNewer ? "+" : "-"} ` + pkgDir + "@" + v2,
- ].join("\n");
- if (!ignoreWarns.has(warn)) {
- consola.warn(warn);
- ignoreWarns.add(warn);
+ pkgPath: string;
+ pkgName: string;
+ pkgVersion: string;
+ };
+ const _resolveTracedPath = (p) =>
+ fsp.realpath(resolve(opts.traceOptions.base, p));
+ const tracedFiles: Record = Object.fromEntries(
+ await Promise.all(
+ [..._fileTrace.reasons.entries()].map(async ([_path, reasons]) => {
+ if (reasons.ignored) {
+ return;
}
- }
-
- const [newerDir, olderDir] = isNewer
- ? [pkgDir, existingPkgDir]
- : [existingPkgDir, pkgDir];
- // Try to map traced files from one package to another for minor/patch versions
- if (getMajor(v1) === getMajor(v2)) {
- tracedFiles = tracedFiles.map((f) =>
- f.startsWith(olderDir + "/") ? f.replace(olderDir, newerDir) : f
+ const path = await _resolveTracedPath(_path);
+ if (!path.includes("node_modules")) {
+ return;
+ }
+ if (!(await isFile(path))) {
+ return;
+ }
+ const { baseDir, pkgName, subpath } = parseNodeModulePath(path);
+ const pkgPath = join(baseDir, pkgName);
+ const parents = await Promise.all(
+ [...reasons.parents].map((p) => _resolveTracedPath(p))
);
- }
- // Exclude older version files
- ignoreDirs.push(olderDir + "/");
- pkgDir = newerDir; // Update for tracedPackages
- }
+ const tracedFile = {
+ path,
+ parents,
- // Add to traced packages
- tracedPackages.set(pkgName, pkgDir);
- }
+ subpath,
+ pkgName,
+ pkgPath,
+ };
+ return [path, tracedFile];
+ })
+ ).then((r) => r.filter(Boolean))
+ );
- // Filter out files from ignored packages and dedup
- tracedFiles = tracedFiles.filter(
- (f) => !ignoreDirs.some((d) => f.startsWith(d))
+ // Resolve traced packages
+ type TracedPackage = {
+ name: string;
+ versions: Record<
+ string,
+ {
+ path: string;
+ files: string[];
+ }
+ >;
+ };
+ const tracedPackages: Record = {};
+ await Promise.all(
+ Object.values(tracedFiles).map(async (tracedFile) => {
+ const pkgJSON = await getPackageJson(tracedFile.pkgPath);
+ const pkgName = tracedFile.pkgName; // Use file path as name to support aliases
+ let tracedPackage = tracedPackages[pkgName];
+ if (!tracedPackage) {
+ tracedPackage = {
+ name: pkgName,
+ versions: {},
+ };
+ tracedPackages[pkgName] = tracedPackage;
+ }
+ let tracedPackageVersion = tracedPackage.versions[pkgJSON.version];
+ if (!tracedPackageVersion) {
+ tracedPackageVersion = { path: tracedFile.pkgPath, files: [] };
+ tracedPackage.versions[pkgJSON.version] = tracedPackageVersion;
+ }
+ tracedPackageVersion.files.push(tracedFile.path);
+ tracedFile.pkgName = pkgName;
+ tracedFile.pkgVersion = pkgJSON.version;
+ })
);
- tracedFiles = [...new Set(tracedFiles)];
- // Ensure all package.json files are traced
- for (const pkgDir of tracedPackages.values()) {
- const pkgJSON = join(pkgDir, "package.json");
- if (!tracedFiles.includes(pkgJSON)) {
- tracedFiles.push(pkgJSON);
+ const writePackage = async (
+ name: string,
+ version: string,
+ outputName?: string
+ ) => {
+ // Find pkg
+ const pkg = tracedPackages[name];
+
+ // Copy files
+ for (const src of pkg.versions[version].files) {
+ const { subpath } = parseNodeModulePath(src);
+ const dst = join(
+ opts.outDir,
+ "node_modules",
+ outputName || pkg.name,
+ subpath
+ );
+ await fsp.mkdir(dirname(dst), { recursive: true });
+ await fsp.copyFile(src, dst);
}
- }
- const writeFile = async (file: string) => {
- if (!(await isFile(file))) {
- return;
+ // Copy package.json
+ const pkgJSONPath = join(
+ opts.outDir,
+ "node_modules",
+ outputName || pkg.name,
+ "package.json"
+ );
+ await fsp.mkdir(dirname(pkgJSONPath), { recursive: true });
+ await fsp.copyFile(
+ join(pkg.versions[version].path, "package.json"),
+ pkgJSONPath
+ );
+ };
+
+ const linkPackage = async (from: string, to: string) => {
+ const src = join(opts.outDir, "node_modules", from);
+ const dst = join(opts.outDir, "node_modules", to);
+ if (existsSync(dst)) {
+ return; // TODO: Warn?
}
- const src = resolve(opts.traceOptions.base, file);
- const { pkgName, subpath } = parseNodeModulePath(file);
- const dst = resolve(opts.outDir, `node_modules/${pkgName + subpath}`);
await fsp.mkdir(dirname(dst), { recursive: true });
- try {
- await fsp.copyFile(src, dst);
- } catch {
- consola.warn(`Could not resolve \`${src}\`. Skipping.`);
- }
+ // TODO: Use copy for windows for portable output?
+ await fsp.symlink(src, dst, "junction").catch((err) => {
+ console.error("Cannot link", src, "to", dst, ":", err.message);
+ });
};
- // Write traced files
+ // Utility to find package parents
+ const findPackageParents = (pkg: TracedPackage, version: string) => {
+ // Try to find parent packages
+ const versionFiles: TracedFile[] = pkg.versions[version].files.map(
+ (path) => tracedFiles[path]
+ );
+ const parentPkgs = [
+ ...new Set(
+ versionFiles.flatMap((file) =>
+ file.parents.flatMap(
+ (parentPath) =>
+ tracedFiles[parentPath].pkgName +
+ "@" +
+ tracedFiles[parentPath].pkgVersion
+ )
+ )
+ ),
+ ];
+ return parentPkgs;
+ };
+
+ // Write traced packages
await Promise.all(
- tracedFiles.map((file) => retry(() => writeFile(file), 3))
+ Object.values(tracedPackages).map(async (tracedPackage) => {
+ const versions = Object.keys(tracedPackage.versions); // TODO: sort by semver
+ if (versions.length === 1) {
+ // Write the only version into node_modules/{name}
+ await writePackage(tracedPackage.name, versions[0]);
+ } else {
+ for (const version of versions) {
+ const parentPkgs = findPackageParents(tracedPackage, version);
+ if (parentPkgs.length === 0) {
+ // No parent packages, assume as the hoisted version
+ await writePackage(tracedPackage.name, version);
+ } else {
+ // Write alternative version into node_modules/{name}@{version}
+ await writePackage(
+ tracedPackage.name,
+ version,
+ `${tracedPackage.name}@${version}`
+ );
+ // For each parent, link into node_modules/{parent}/node_modules/{name}
+ for (const parentPath of parentPkgs) {
+ await linkPackage(
+ `${tracedPackage.name}@${version}`,
+ `${parentPath}/node_modules/${tracedPackage.name}`
+ );
+ await linkPackage(
+ `${tracedPackage.name}@${version}`,
+ `${parentPath.split("@")[0]}/node_modules/${
+ tracedPackage.name
+ }`
+ );
+ }
+ }
+ }
+ }
+ })
);
// Write an informative package.json
+ const bundledDependencies = Object.fromEntries(
+ Object.values(tracedPackages).map((pkg) => [
+ pkg.name,
+ Object.keys(pkg.versions).join(" | "),
+ ])
+ );
await fsp.writeFile(
resolve(opts.outDir, "package.json"),
JSON.stringify(
@@ -292,7 +390,7 @@ export function externals(opts: NodeExternalsOptions): Plugin {
name: "nitro-output",
version: "0.0.0",
private: true,
- bundledDependencies: [...tracedPackages.keys()],
+ bundledDependencies,
},
null,
2
diff --git a/src/runtime/app.ts b/src/runtime/app.ts
index cfda794306..08cd7d1fe1 100644
--- a/src/runtime/app.ts
+++ b/src/runtime/app.ts
@@ -14,7 +14,6 @@ import {
} from "unenv/runtime/fetch/index";
import { createHooks, Hookable } from "hookable";
import { useRuntimeConfig } from "./config";
-import { timingMiddleware } from "./timing";
import { cachedEventHandler } from "./cache";
import { createRouteRulesHandler, getRouteRulesForPath } from "./route-rules";
import { plugins } from "#internal/nitro/virtual/plugins";
@@ -40,8 +39,6 @@ function createNitroApp(): NitroApp {
onError: errorHandler,
});
- h3App.use(config.app.baseURL, timingMiddleware);
-
const router = createRouter();
h3App.use(createRouteRulesHandler());
diff --git a/src/runtime/cache.ts b/src/runtime/cache.ts
index 47653d8aaa..9afe909089 100644
--- a/src/runtime/cache.ts
+++ b/src/runtime/cache.ts
@@ -154,7 +154,7 @@ export function defineCachedEventHandler(
if (key) {
return escapeKey(key);
}
- const url = event.req.originalUrl || event.req.url;
+ const url = event.node.req.originalUrl || event.node.req.url;
const friendlyName = escapeKey(decodeURI(parseURL(url).pathname)).slice(
0,
16
@@ -178,10 +178,10 @@ export function defineCachedEventHandler(
const _cachedHandler = cachedFunction>(
async (incomingEvent: H3Event) => {
// Create proxies to avoid sharing state with user request
- const reqProxy = cloneWithProxy(incomingEvent.req, { headers: {} });
+ const reqProxy = cloneWithProxy(incomingEvent.node.req, { headers: {} });
const resHeaders: Record = {};
let _resSendBody;
- const resProxy = cloneWithProxy(incomingEvent.res, {
+ const resProxy = cloneWithProxy(incomingEvent.node.res, {
statusCode: 200,
getHeader(name) {
return resHeaders[name];
@@ -243,7 +243,7 @@ export function defineCachedEventHandler(
const body = (await handler(event)) || _resSendBody;
// Collect cachable headers
- const headers = event.res.getHeaders();
+ const headers = event.node.res.getHeaders();
headers.etag = headers.Etag || headers.etag || `W/"${hash(body)}"`;
headers["last-modified"] =
headers["Last-Modified"] ||
@@ -268,7 +268,7 @@ export function defineCachedEventHandler(
// Create cache entry for response
const cacheEntry: ResponseCacheEntry = {
- code: event.res.statusCode,
+ code: event.node.res.statusCode,
headers,
body,
};
@@ -292,7 +292,7 @@ export function defineCachedEventHandler(
const response = await _cachedHandler(event);
// Don't continue if response is already handled by user
- if (event.res.headersSent || event.res.writableEnded) {
+ if (event.node.res.headersSent || event.node.res.writableEnded) {
return response.body;
}
@@ -308,9 +308,9 @@ export function defineCachedEventHandler(
}
// Send status and headers
- event.res.statusCode = response.code;
+ event.node.res.statusCode = response.code;
for (const name in response.headers) {
- event.res.setHeader(name, response.headers[name]);
+ event.node.res.setHeader(name, response.headers[name]);
}
// Send body
diff --git a/src/runtime/entries/aws-lambda.ts b/src/runtime/entries/aws-lambda.ts
index 08b78c5fd7..8fb0fca6f4 100644
--- a/src/runtime/entries/aws-lambda.ts
+++ b/src/runtime/entries/aws-lambda.ts
@@ -10,25 +10,18 @@ import "#internal/nitro/virtual/polyfill";
import { withQuery } from "ufo";
import { nitroApp } from "../app";
-// Compatibility types that work with AWS v1, AWS v2 & Netlify
-type Event =
- | Omit<
- APIGatewayProxyEvent,
- "pathParameters" | "stageVariables" | "requestContext" | "resource"
- >
- | Omit<
- APIGatewayProxyEventV2,
- "pathParameters" | "stageVariables" | "requestContext" | "resource"
- >;
-type Result = Exclude<
- APIGatewayProxyResult | APIGatewayProxyResultV2,
- string
-> & { statusCode: number };
-
-export const handler = async function handler(
- event: Event,
+export async function handler(
+ event: APIGatewayProxyEvent,
+ context: Context
+): Promise;
+export async function handler(
+ event: APIGatewayProxyEventV2,
context: Context
-): Promise {
+): Promise;
+export async function handler(
+ event: APIGatewayProxyEvent | APIGatewayProxyEventV2,
+ context: Context
+): Promise {
const query = {
...event.queryStringParameters,
...(event as APIGatewayProxyEvent).multiValueQueryStringParameters,
@@ -57,18 +50,26 @@ export const handler = async function handler(
body: event.body, // TODO: handle event.isBase64Encoded
});
- const outgoingCookies = r.headers["set-cookie"];
- const cookies = Array.isArray(outgoingCookies)
- ? outgoingCookies
- : outgoingCookies?.split(",") || [];
+ if ("cookies" in event || "rawPath" in event) {
+ const outgoingCookies = r.headers["set-cookie"];
+ const cookies = Array.isArray(outgoingCookies)
+ ? outgoingCookies
+ : outgoingCookies?.split(",") || [];
+
+ return {
+ cookies,
+ statusCode: r.status,
+ headers: normalizeOutgoingHeaders(r.headers, true),
+ body: r.body.toString(),
+ };
+ }
return {
- cookies,
statusCode: r.status,
headers: normalizeOutgoingHeaders(r.headers),
body: r.body.toString(),
};
-};
+}
function normalizeIncomingHeaders(headers?: APIGatewayProxyEventHeaders) {
return Object.fromEntries(
@@ -80,11 +81,14 @@ function normalizeIncomingHeaders(headers?: APIGatewayProxyEventHeaders) {
}
function normalizeOutgoingHeaders(
- headers: Record
+ headers: Record,
+ stripCookies = false
) {
+ const entries = stripCookies
+ ? Object.entries(headers).filter(([key]) => !["set-cookie"].includes(key))
+ : Object.entries(headers);
+
return Object.fromEntries(
- Object.entries(headers)
- .filter(([key]) => !["set-cookie"].includes(key))
- .map(([k, v]) => [k, Array.isArray(v) ? v.join(",") : v!])
+ entries.map(([k, v]) => [k, Array.isArray(v) ? v.join(",") : v!])
);
}
diff --git a/src/runtime/entries/netlify-builder.ts b/src/runtime/entries/netlify-builder.ts
index 45562aa043..d7d5b49096 100644
--- a/src/runtime/entries/netlify-builder.ts
+++ b/src/runtime/entries/netlify-builder.ts
@@ -1,4 +1,4 @@
import { builder } from "@netlify/functions";
-import { handler as _handler } from "#internal/nitro/entries/aws-lambda";
+import { lambda } from "./netlify-lambda";
-export const handler = builder(_handler);
+export const handler = builder(lambda);
diff --git a/src/runtime/entries/netlify-lambda.ts b/src/runtime/entries/netlify-lambda.ts
new file mode 100644
index 0000000000..1fdc27700a
--- /dev/null
+++ b/src/runtime/entries/netlify-lambda.ts
@@ -0,0 +1,58 @@
+import "#internal/nitro/virtual/polyfill";
+import type {
+ Handler,
+ HandlerResponse,
+ HandlerContext,
+ HandlerEvent,
+} from "@netlify/functions/dist/main";
+import type { APIGatewayProxyEventHeaders } from "aws-lambda";
+import { withQuery } from "ufo";
+import { nitroApp } from "../app";
+
+export async function lambda(
+ event: HandlerEvent,
+ context: HandlerContext
+): Promise {
+ const query = {
+ ...event.queryStringParameters,
+ ...event.multiValueQueryStringParameters,
+ };
+ const url = withQuery(event.path, query);
+ const method = event.httpMethod || "get";
+
+ const r = await nitroApp.localCall({
+ event,
+ url,
+ context,
+ headers: normalizeIncomingHeaders(event.headers),
+ method,
+ query,
+ body: event.body, // TODO: handle event.isBase64Encoded
+ });
+
+ return {
+ statusCode: r.status,
+ headers: normalizeOutgoingHeaders(r.headers),
+ body: r.body.toString(),
+ };
+}
+
+function normalizeIncomingHeaders(headers?: APIGatewayProxyEventHeaders) {
+ return Object.fromEntries(
+ Object.entries(headers || {}).map(([key, value]) => [
+ key.toLowerCase(),
+ value!,
+ ])
+ );
+}
+
+function normalizeOutgoingHeaders(
+ headers: Record
+) {
+ return Object.fromEntries(
+ Object.entries(headers).map(([k, v]) => [
+ k,
+ Array.isArray(v) ? v.join(",") : v!,
+ ])
+ );
+}
diff --git a/src/runtime/entries/netlify.ts b/src/runtime/entries/netlify.ts
index e86cd3de58..c67633b0e1 100644
--- a/src/runtime/entries/netlify.ts
+++ b/src/runtime/entries/netlify.ts
@@ -1,14 +1,8 @@
import "#internal/nitro/virtual/polyfill";
-import type {
- Handler,
- HandlerResponse,
- HandlerContext,
- HandlerEvent,
-} from "@netlify/functions/dist/main";
-import type { APIGatewayProxyEventHeaders } from "aws-lambda";
+import type { Handler } from "@netlify/functions/dist/main";
import { withQuery } from "ufo";
-import { nitroApp } from "../app";
import { getRouteRulesForPath } from "../route-rules";
+import { lambda } from "./netlify-lambda";
export const handler: Handler = async function handler(event, context) {
const query = {
@@ -33,50 +27,3 @@ export const handler: Handler = async function handler(event, context) {
return lambda(event, context);
};
-
-async function lambda(
- event: HandlerEvent,
- context: HandlerContext
-): Promise {
- const query = {
- ...event.queryStringParameters,
- ...event.multiValueQueryStringParameters,
- };
- const url = withQuery(event.path, query);
- const method = event.httpMethod || "get";
-
- const r = await nitroApp.localCall({
- event,
- url,
- context,
- headers: normalizeIncomingHeaders(event.headers),
- method,
- query,
- body: event.body, // TODO: handle event.isBase64Encoded
- });
-
- return {
- statusCode: r.status,
- headers: normalizeOutgoingHeaders(r.headers),
- body: r.body.toString(),
- };
-}
-
-function normalizeIncomingHeaders(headers?: APIGatewayProxyEventHeaders) {
- return Object.fromEntries(
- Object.entries(headers || {}).map(([key, value]) => [
- key.toLowerCase(),
- value!,
- ])
- );
-}
-
-function normalizeOutgoingHeaders(
- headers: Record
-) {
- return Object.fromEntries(
- Object.entries(headers)
- .filter(([key]) => !["set-cookie"].includes(key))
- .map(([k, v]) => [k, Array.isArray(v) ? v.join(",") : v!])
- );
-}
diff --git a/src/runtime/error.ts b/src/runtime/error.ts
index 63cf236f0e..b90a57a510 100644
--- a/src/runtime/error.ts
+++ b/src/runtime/error.ts
@@ -18,7 +18,7 @@ export default function (error, event) {
const showDetails = isDev && statusCode !== 404;
const errorObject = {
- url: event.req.url || "",
+ url: event.node.req.url || "",
statusCode,
statusMessage,
message,
@@ -41,17 +41,17 @@ export default function (error, event) {
);
}
- event.res.statusCode = statusCode;
+ event.node.res.statusCode = statusCode;
if (statusMessage) {
- event.res.statusMessage = statusMessage;
+ event.node.res.statusMessage = statusMessage;
}
if (isJsonRequest(event)) {
- event.res.setHeader("Content-Type", "application/json");
- event.res.end(JSON.stringify(errorObject));
+ event.node.res.setHeader("Content-Type", "application/json");
+ event.node.res.end(JSON.stringify(errorObject));
} else {
- event.res.setHeader("Content-Type", "text/html");
- event.res.end(renderHTMLError(errorObject));
+ event.node.res.setHeader("Content-Type", "text/html");
+ event.node.res.end(renderHTMLError(errorObject));
}
};
diff --git a/src/runtime/renderer.ts b/src/runtime/renderer.ts
index c77853ac02..5d125d0db5 100644
--- a/src/runtime/renderer.ts
+++ b/src/runtime/renderer.ts
@@ -15,9 +15,9 @@ export type RenderHandler = (
export function defineRenderHandler(handler: RenderHandler) {
return eventHandler(async (event) => {
// TODO: Use serve-placeholder
- if (event.req.url.endsWith("/favicon.ico")) {
- event.res.setHeader("Content-Type", "image/x-icon");
- event.res.end(
+ if (event.node.req.url.endsWith("/favicon.ico")) {
+ event.node.res.setHeader("Content-Type", "image/x-icon");
+ event.node.res.end(
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
);
return;
@@ -25,11 +25,11 @@ export function defineRenderHandler(handler: RenderHandler) {
const response = await handler(event);
if (!response) {
- if (!event.res.writableEnded) {
- event.res.statusCode =
- event.res.statusCode === 200 ? 500 : event.res.statusCode;
- event.res.end(
- "No response returned from render handler: " + event.req.url
+ if (!event.node.res.writableEnded) {
+ event.node.res.statusCode =
+ event.node.res.statusCode === 200 ? 500 : event.node.res.statusCode;
+ event.node.res.end(
+ "No response returned from render handler: " + event.node.req.url
);
}
return;
@@ -44,15 +44,15 @@ export function defineRenderHandler(handler: RenderHandler) {
// TODO: Caching support
// Send headers
- if (!event.res.headersSent && response.headers) {
+ if (!event.node.res.headersSent && response.headers) {
for (const header in response.headers) {
- event.res.setHeader(header, response.headers[header]);
+ event.node.res.setHeader(header, response.headers[header]);
}
if (response.statusCode) {
- event.res.statusCode = response.statusCode;
+ event.node.res.statusCode = response.statusCode;
}
if (response.statusMessage) {
- event.res.statusMessage = response.statusMessage;
+ event.node.res.statusMessage = response.statusMessage;
}
}
diff --git a/src/runtime/route-rules.ts b/src/runtime/route-rules.ts
index 42613aaa30..522f04fc5c 100644
--- a/src/runtime/route-rules.ts
+++ b/src/runtime/route-rules.ts
@@ -32,7 +32,7 @@ export function createRouteRulesHandler() {
export function getRouteRules(event: H3Event): NitroRouteRules {
event.context._nitro = event.context._nitro || {};
if (!event.context._nitro.routeRules) {
- const path = new URL(event.req.url, "http://localhost").pathname;
+ const path = new URL(event.node.req.url, "http://localhost").pathname;
event.context._nitro.routeRules = getRouteRulesForPath(
withoutBase(path, useRuntimeConfig().app.baseURL)
);
diff --git a/src/runtime/static.ts b/src/runtime/static.ts
index 3e50a4ecfe..fca8f7922e 100644
--- a/src/runtime/static.ts
+++ b/src/runtime/static.ts
@@ -11,16 +11,20 @@ const METHODS = new Set(["HEAD", "GET"]);
const EncodingMap = { gzip: ".gz", br: ".br" };
export default eventHandler((event) => {
- if (event.req.method && !METHODS.has(event.req.method)) {
+ if (event.node.req.method && !METHODS.has(event.node.req.method)) {
return;
}
let id = decodeURIComponent(
- withLeadingSlash(withoutTrailingSlash(parseURL(event.req.url).pathname))
+ withLeadingSlash(
+ withoutTrailingSlash(parseURL(event.node.req.url).pathname)
+ )
);
let asset;
- const encodingHeader = String(event.req.headers["accept-encoding"] || "");
+ const encodingHeader = String(
+ event.node.req.headers["accept-encoding"] || ""
+ );
const encodings = [
...encodingHeader
.split(",")
@@ -30,7 +34,7 @@ export default eventHandler((event) => {
"",
];
if (encodings.length > 1) {
- event.res.setHeader("Vary", "Accept-Encoding");
+ event.node.res.setHeader("Vary", "Accept-Encoding");
}
for (const encoding of encodings) {
@@ -54,48 +58,48 @@ export default eventHandler((event) => {
return;
}
- const ifNotMatch = event.req.headers["if-none-match"] === asset.etag;
+ const ifNotMatch = event.node.req.headers["if-none-match"] === asset.etag;
if (ifNotMatch) {
- event.res.statusCode = 304;
- event.res.end();
+ event.node.res.statusCode = 304;
+ event.node.res.end();
return;
}
- const ifModifiedSinceH = event.req.headers["if-modified-since"];
+ const ifModifiedSinceH = event.node.req.headers["if-modified-since"];
if (
ifModifiedSinceH &&
asset.mtime &&
new Date(ifModifiedSinceH) >= new Date(asset.mtime)
) {
- event.res.statusCode = 304;
- event.res.end();
+ event.node.res.statusCode = 304;
+ event.node.res.end();
return;
}
- if (asset.type && !event.res.getHeader("Content-Type")) {
- event.res.setHeader("Content-Type", asset.type);
+ if (asset.type && !event.node.res.getHeader("Content-Type")) {
+ event.node.res.setHeader("Content-Type", asset.type);
}
- if (asset.etag && !event.res.getHeader("ETag")) {
- event.res.setHeader("ETag", asset.etag);
+ if (asset.etag && !event.node.res.getHeader("ETag")) {
+ event.node.res.setHeader("ETag", asset.etag);
}
- if (asset.mtime && !event.res.getHeader("Last-Modified")) {
- event.res.setHeader("Last-Modified", asset.mtime);
+ if (asset.mtime && !event.node.res.getHeader("Last-Modified")) {
+ event.node.res.setHeader("Last-Modified", asset.mtime);
}
- if (asset.encoding && !event.res.getHeader("Content-Encoding")) {
- event.res.setHeader("Content-Encoding", asset.encoding);
+ if (asset.encoding && !event.node.res.getHeader("Content-Encoding")) {
+ event.node.res.setHeader("Content-Encoding", asset.encoding);
}
- if (asset.size > 0 && !event.res.getHeader("Content-Length")) {
- event.res.setHeader("Content-Length", asset.size);
+ if (asset.size > 0 && !event.node.res.getHeader("Content-Length")) {
+ event.node.res.setHeader("Content-Length", asset.size);
}
// TODO: Asset dir cache control
// if (isBuildAsset) {
// const TWO_DAYS = 2 * 60 * 60 * 24
- // event.res.setHeader('Cache-Control', `max-age=${TWO_DAYS}, immutable`)
+ // event.node.res.setHeader('Cache-Control', `max-age=${TWO_DAYS}, immutable`)
// }
return readAsset(id);
diff --git a/src/runtime/timing.ts b/src/runtime/timing.ts
index db79705c0a..94467c4515 100644
--- a/src/runtime/timing.ts
+++ b/src/runtime/timing.ts
@@ -1,17 +1,19 @@
import { eventHandler } from "h3";
-export const globalTiming = globalThis.__timing__ || {
+import { defineNitroPlugin } from "./plugin";
+
+const globalTiming = globalThis.__timing__ || {
start: () => 0,
end: () => 0,
metrics: [],
};
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing
-export const timingMiddleware = eventHandler((event) => {
+const timingMiddleware = eventHandler((event) => {
const start = globalTiming.start();
- const _end = event.res.end;
- event.res.end = function (
+ const _end = event.node.res.end;
+ event.node.res.end = function (
chunk: any,
encoding: BufferEncoding,
cb?: () => void
@@ -23,10 +25,18 @@ export const timingMiddleware = eventHandler((event) => {
const serverTiming = metrics
.map((m) => `-;dur=${m[1]};desc="${encodeURIComponent(m[0])}"`)
.join(", ");
- if (!event.res.headersSent) {
- event.res.setHeader("Server-Timing", serverTiming);
+ if (!event.node.res.headersSent) {
+ event.node.res.setHeader("Server-Timing", serverTiming);
}
- _end.call(event.res, chunk, encoding, cb);
+ _end.call(event.node.res, chunk, encoding, cb);
return this;
- }.bind(event.res);
+ }.bind(event.node.res);
+});
+
+export default defineNitroPlugin((nitro) => {
+ // Always add timing middleware to the beginning of handler stack
+ nitro.h3App.stack.unshift({
+ route: "/",
+ handler: timingMiddleware,
+ });
});
diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts
index 8014c54c0f..2bbe586dcf 100644
--- a/src/runtime/utils.ts
+++ b/src/runtime/utils.ts
@@ -41,8 +41,8 @@ export function isJsonRequest(event: H3Event) {
hasReqHeader(event, "accept", "application/json") ||
hasReqHeader(event, "user-agent", "curl/") ||
hasReqHeader(event, "user-agent", "httpie/") ||
- event.req.url?.endsWith(".json") ||
- event.req.url?.includes("/api/")
+ event.node.req.url?.endsWith(".json") ||
+ event.node.req.url?.includes("/api/")
);
}
diff --git a/src/scan.ts b/src/scan.ts
index 7d08777ae9..f0d255c97e 100644
--- a/src/scan.ts
+++ b/src/scan.ts
@@ -17,7 +17,15 @@ export async function scanHandlers(nitro: Nitro) {
scanRoutes(nitro, "routes", "/"),
]).then((r) => r.flat());
- nitro.scannedHandlers = handlers.flatMap((h) => h.handlers);
+ nitro.scannedHandlers = handlers
+ .flatMap((h) => h.handlers)
+ .filter((h, index, array) => {
+ return (
+ array.findIndex(
+ (h2) => h.route === h2.route && h.method === h2.method
+ ) === index
+ );
+ });
return handlers;
}
diff --git a/src/types/nitro.ts b/src/types/nitro.ts
index 1fdfc1b80b..70f27e1861 100644
--- a/src/types/nitro.ts
+++ b/src/types/nitro.ts
@@ -174,6 +174,7 @@ export interface NitroOptions extends PresetOptions {
noPublicDir: boolean;
experimental?: {
wasm?: boolean | RollupWasmOptions;
+ legacyExternals?: boolean;
};
serverAssets: ServerAssetDir[];
publicAssets: PublicAssetDir[];
diff --git a/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/index.mjs b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/index.mjs
index 3d64381bce..0029f235ab 100644
--- a/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/index.mjs
+++ b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/index.mjs
@@ -1 +1,3 @@
-export default '1.0.0';
+import nestedLib from 'nested-lib'
+
+export default 'nitro-lib@1.0.0+' + nestedLib
diff --git a/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/node_modules/nested-lib/index.mjs b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/node_modules/nested-lib/index.mjs
new file mode 100644
index 0000000000..da8927e1e8
--- /dev/null
+++ b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/node_modules/nested-lib/index.mjs
@@ -0,0 +1 @@
+export default 'nested-lib@1.0.0'
diff --git a/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/node_modules/nested-lib/package.json b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/node_modules/nested-lib/package.json
new file mode 100644
index 0000000000..5993dae833
--- /dev/null
+++ b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/node_modules/nested-lib/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "nested-lib",
+ "version": "1.0.0",
+ "exports": "./index.mjs"
+}
diff --git a/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/package.json b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/package.json
index dd674789ba..b8bd61c01d 100644
--- a/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/package.json
+++ b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/package.json
@@ -1,5 +1,8 @@
{
"name": "nitro-lib",
"version": "1.0.0",
- "exports": "./index.mjs"
+ "exports": "./index.mjs",
+ "dependencies": {
+ "nested-lib": "1.0.0"
+ }
}
diff --git a/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/index.mjs b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/index.mjs
index c9e08004a5..53aaa0be76 100644
--- a/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/index.mjs
+++ b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/index.mjs
@@ -1 +1,3 @@
-export default '2.0.1';
+import nestedLib from 'nested-lib'
+
+export default 'nitro-lib@2.0.1+' + nestedLib
diff --git a/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/node_modules/nested-lib/index.mjs b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/node_modules/nested-lib/index.mjs
new file mode 100644
index 0000000000..caace8b77b
--- /dev/null
+++ b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/node_modules/nested-lib/index.mjs
@@ -0,0 +1 @@
+export default 'nested-lib@2.0.1'
diff --git a/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/node_modules/nested-lib/package.json b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/node_modules/nested-lib/package.json
new file mode 100644
index 0000000000..fbc00e77b8
--- /dev/null
+++ b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/node_modules/nested-lib/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "nested-lib",
+ "version": "2.0.1",
+ "exports": "./index.mjs"
+}
diff --git a/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/package.json b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/package.json
index bfb99596eb..3396d6ee1d 100644
--- a/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/package.json
+++ b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/package.json
@@ -4,5 +4,8 @@
"exports": {
".": "./index.mjs",
"./subpath": "./subpath.mjs"
+ },
+ "dependencies": {
+ "nested-lib": "2.0.0"
}
}
diff --git a/test/fixture/_/node_modules/nitro-dep-b/package.json b/test/fixture/_/node_modules/nitro-dep-b/package.json
index 7e30c8dd8f..d7235353b7 100644
--- a/test/fixture/_/node_modules/nitro-dep-b/package.json
+++ b/test/fixture/_/node_modules/nitro-dep-b/package.json
@@ -3,6 +3,6 @@
"version": "1.0.0",
"exports": "./index.mjs",
"dependencies": {
- "nitro-lib": "2.0.0"
+ "nitro-lib": "2.0.1"
}
}
diff --git a/test/fixture/_/node_modules/nitro-lib/index.mjs b/test/fixture/_/node_modules/nitro-lib/index.mjs
index ff603d7009..5e07a96ab0 100644
--- a/test/fixture/_/node_modules/nitro-lib/index.mjs
+++ b/test/fixture/_/node_modules/nitro-lib/index.mjs
@@ -1 +1,3 @@
-export default '2.0.0';
+import nestedLib from 'nested-lib'
+
+export default 'nitro-lib@2.0.0+' + nestedLib
diff --git a/test/fixture/_/node_modules/nitro-lib/node_modules/nested-lib/index.mjs b/test/fixture/_/node_modules/nitro-lib/node_modules/nested-lib/index.mjs
new file mode 100644
index 0000000000..69cb6433e5
--- /dev/null
+++ b/test/fixture/_/node_modules/nitro-lib/node_modules/nested-lib/index.mjs
@@ -0,0 +1 @@
+export default 'nested-lib@2.0.0'
diff --git a/test/fixture/_/node_modules/nitro-lib/node_modules/nested-lib/package.json b/test/fixture/_/node_modules/nitro-lib/node_modules/nested-lib/package.json
new file mode 100644
index 0000000000..e8d4a98a0a
--- /dev/null
+++ b/test/fixture/_/node_modules/nitro-lib/node_modules/nested-lib/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "nested-lib",
+ "version": "2.0.0",
+ "exports": "./index.mjs"
+}
diff --git a/test/fixture/_/node_modules/nitro-lib/package.json b/test/fixture/_/node_modules/nitro-lib/package.json
index 81618a9dc9..3709569af3 100644
--- a/test/fixture/_/node_modules/nitro-lib/package.json
+++ b/test/fixture/_/node_modules/nitro-lib/package.json
@@ -1,5 +1,5 @@
{
- "name": "nitro-lib",
+ "name": "nitro-lib-aliased-from-another-name",
"version": "2.0.0",
"exports": {
".": "./index.mjs",
diff --git a/test/fixture/_/node_modules/nitro-lib/subpath.mjs b/test/fixture/_/node_modules/nitro-lib/subpath.mjs
index ff603d7009..c4ac86cb68 100644
--- a/test/fixture/_/node_modules/nitro-lib/subpath.mjs
+++ b/test/fixture/_/node_modules/nitro-lib/subpath.mjs
@@ -1 +1 @@
-export default '2.0.0';
+export default 'nitro-lib@2.0.0';
diff --git a/test/presets/netlify.test.ts b/test/presets/netlify.test.ts
index c8844042a5..99571d6765 100644
--- a/test/presets/netlify.test.ts
+++ b/test/presets/netlify.test.ts
@@ -9,7 +9,7 @@ describe("nitro:preset:netlify", async () => {
const ctx = await setupTest("netlify");
testNitro(ctx, async () => {
const { handler } = (await import(
- resolve(ctx.outDir, "server/server.js")
+ resolve(ctx.outDir, "server/server.mjs")
)) as { handler: Handler };
return async ({ url: rawRelativeUrl, headers, method, body }) => {
// creating new URL object to parse query easier
@@ -47,6 +47,7 @@ describe("nitro:preset:netlify", async () => {
/rules/swr-ttl/* /.netlify/builders/server 200
/rules/swr/* /.netlify/builders/server 200
/rules/static /.netlify/builders/server 200
+ /rules/dynamic /.netlify/functions/server 200
/* /.netlify/functions/server 200"
`);
/* eslint-enable no-tabs */
diff --git a/test/presets/vercel.test.ts b/test/presets/vercel.test.ts
index 84a287a598..51079876ea 100644
--- a/test/presets/vercel.test.ts
+++ b/test/presets/vercel.test.ts
@@ -97,6 +97,10 @@ describe("nitro:preset:vercel", async () => {
{
"handle": "filesystem",
},
+ {
+ "dest": "/__nitro",
+ "src": "/rules/dynamic",
+ },
{
"dest": "/__nitro--rules-swr?url=$url",
"src": "(?/rules/swr/.*)",
diff --git a/test/tests.ts b/test/tests.ts
index 6cbc11cc63..aa3ffcf361 100644
--- a/test/tests.ts
+++ b/test/tests.ts
@@ -3,6 +3,7 @@ import { listen, Listener } from "listhen";
import destr from "destr";
import { fetch } from "ofetch";
import { expect, it, afterAll } from "vitest";
+import { isWindows } from "std-env";
import { fileURLToPath } from "mlly";
import { joinURL } from "ufo";
import * as _nitro from "../src";
@@ -46,6 +47,7 @@ export async function setupTest(preset: string) {
cors: true,
headers: { "access-control-allowed-methods": "GET" },
},
+ "/rules/dynamic": { cache: false },
"/rules/redirect": { redirect: "/base" },
"/rules/static": { static: true },
"/rules/swr/**": { swr: true },
@@ -56,6 +58,7 @@ export async function setupTest(preset: string) {
"/rules/nested/**": { redirect: "/base", headers: { "x-test": "test" } },
"/rules/nested/override": { redirect: { to: "/other" } },
},
+ timing: preset !== "cloudflare" && preset !== "vercel-edge",
}));
if (ctx.isDev) {
@@ -195,7 +198,7 @@ export function testNitro(
it("universal import.meta", async () => {
const { status, data } = await callHandler({ url: "/api/import-meta" });
expect(status).toBe(200);
- expect(data.testFile).toMatch(/\/test.txt$/);
+ expect(data.testFile).toMatch(/[/\\]test.txt$/);
expect(data.hasEnv).toBe(true);
});
@@ -228,10 +231,10 @@ export function testNitro(
it("resolve module version conflicts", async () => {
const { data } = await callHandler({ url: "/modules" });
expect(data).toMatchObject({
- depA: "2.0.1",
- depB: "2.0.1",
- depLib: "2.0.1",
- subpathLib: "2.0.1",
+ depA: "nitro-lib@1.0.0+nested-lib@1.0.0",
+ depB: "nitro-lib@2.0.1+nested-lib@2.0.1",
+ depLib: "nitro-lib@2.0.0+nested-lib@2.0.0",
+ subpathLib: "nitro-lib@2.0.0",
});
});
@@ -239,4 +242,14 @@ export function testNitro(
additionalTests(ctx, callHandler);
}
}
+
+ if (ctx.nitro!.options.timing) {
+ it("set server timing header", async () => {
+ const { data, status, headers } = await callHandler({
+ url: "/api/hello",
+ });
+ expect(status).toBe(200);
+ expect(headers["server-timing"]).toMatch(/-;dur=\d+;desc="Generate"/);
+ });
+ }
}