3000 lines
95 KiB
JavaScript
3000 lines
95 KiB
JavaScript
import { Worker } from 'worker_threads';
|
|
import { promises, existsSync, createWriteStream } from 'fs';
|
|
import { debounce } from 'perfect-debounce';
|
|
import { eventHandler, createError, createApp, fromNodeMiddleware, toNodeListener } from 'h3';
|
|
import httpProxy from 'http-proxy';
|
|
import { listen } from 'listhen';
|
|
import { servePlaceholder } from 'serve-placeholder';
|
|
import serveStatic from 'serve-static';
|
|
import { resolve, dirname, relative, normalize, isAbsolute, join, extname } from 'pathe';
|
|
import { withLeadingSlash, withoutTrailingSlash, withBase, joinURL, withoutLeadingSlash, withTrailingSlash, withoutBase, parseURL } from 'ufo';
|
|
import { watch } from 'chokidar';
|
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
import chalk from 'chalk';
|
|
import { toRouteMatcher, createRouter } from 'radix3';
|
|
import { defu } from 'defu';
|
|
import { createHooks, createDebugger } from 'hookable';
|
|
import { createUnimport } from 'unimport';
|
|
import consola from 'consola';
|
|
import { loadConfig } from 'c12';
|
|
import { klona } from 'klona/full';
|
|
import { camelCase } from 'scule';
|
|
import { isValidNodeImport, normalizeid, resolvePath as resolvePath$1, sanitizeFilePath, resolveModuleExportNames } from 'mlly';
|
|
import { isTest, provider, isWindows, isDebug } from 'std-env';
|
|
import { readPackageJSON, findWorkspaceDir } from 'pkg-types';
|
|
import { createRequire } from 'module';
|
|
import fse, { move } from 'fs-extra';
|
|
import 'jiti';
|
|
import { getProperty } from 'dot-prop';
|
|
import archiver from 'archiver';
|
|
import { globby } from 'globby';
|
|
import fsp$1 from 'fs/promises';
|
|
import { normalizeKey, builtinDrivers, createStorage as createStorage$1 } from 'unstorage';
|
|
import { resolveAlias } from 'pathe/utils';
|
|
import * as rollup from 'rollup';
|
|
import prettyBytes from 'pretty-bytes';
|
|
import { gzipSize } from 'gzip-size';
|
|
import { terser } from 'rollup-plugin-terser';
|
|
import commonjs from '@rollup/plugin-commonjs';
|
|
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
|
import alias from '@rollup/plugin-alias';
|
|
import json from '@rollup/plugin-json';
|
|
import wasmPlugin from '@rollup/plugin-wasm';
|
|
import inject from '@rollup/plugin-inject';
|
|
import { visualizer } from 'rollup-plugin-visualizer';
|
|
import * as unenv from 'unenv';
|
|
import unimportPlugin from 'unimport/unplugin';
|
|
import { hash } from 'ohash';
|
|
import _replace from '@rollup/plugin-replace';
|
|
import { genSafeVariableName, genImport } from 'knitwork';
|
|
import { nodeFileTrace } from '@vercel/nft';
|
|
import semver from 'semver';
|
|
import createEtag from 'etag';
|
|
import mime from 'mime';
|
|
import { transform } from 'esbuild';
|
|
import { createFilter } from '@rollup/pluginutils';
|
|
import zlib from 'node:zlib';
|
|
import fsp from 'node:fs/promises';
|
|
import { existsSync as existsSync$1 } from 'node:fs';
|
|
|
|
async function printFSTree(dir) {
|
|
if (isTest) {
|
|
return;
|
|
}
|
|
const files = await globby("**/*.*", { cwd: dir, ignore: ["*.map"] });
|
|
const items = (await Promise.all(files.map(async (file) => {
|
|
const path = resolve(dir, file);
|
|
const src = await promises.readFile(path);
|
|
const size = src.byteLength;
|
|
const gzip = await gzipSize(src);
|
|
return { file, path, size, gzip };
|
|
}))).sort((a, b) => b.path.localeCompare(a.path));
|
|
let totalSize = 0;
|
|
let totalGzip = 0;
|
|
let totalNodeModulesSize = 0;
|
|
let totalNodeModulesGzip = 0;
|
|
items.forEach((item, index) => {
|
|
dirname(item.file);
|
|
const rpath = relative(process.cwd(), item.path);
|
|
const treeChar = index === items.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
|
|
const isNodeModules = item.file.includes("node_modules");
|
|
if (isNodeModules) {
|
|
totalNodeModulesSize += item.size;
|
|
totalNodeModulesGzip += item.gzip;
|
|
return;
|
|
}
|
|
process.stdout.write(chalk.gray(` ${treeChar} ${rpath} (${prettyBytes(item.size)}) (${prettyBytes(item.gzip)} gzip)
|
|
`));
|
|
totalSize += item.size;
|
|
totalGzip += item.gzip;
|
|
});
|
|
process.stdout.write(`${chalk.cyan("\u03A3 Total size:")} ${prettyBytes(totalSize + totalNodeModulesSize)} (${prettyBytes(totalGzip + totalNodeModulesGzip)} gzip)
|
|
`);
|
|
}
|
|
|
|
function hl(str) {
|
|
return chalk.cyan(str);
|
|
}
|
|
function prettyPath(p, highlight = true) {
|
|
p = relative(process.cwd(), p);
|
|
return highlight ? hl(p) : p;
|
|
}
|
|
function compileTemplate(contents) {
|
|
return (params) => contents.replace(/{{ ?([\w.]+) ?}}/g, (_, match) => {
|
|
const val = getProperty(params, match);
|
|
if (!val) {
|
|
consola.warn(`cannot resolve template param '${match}' in ${contents.slice(0, 20)}`);
|
|
}
|
|
return val || `${match}`;
|
|
});
|
|
}
|
|
async function writeFile$1(file, contents, log = false) {
|
|
await fse.mkdirp(dirname(file));
|
|
await fse.writeFile(file, contents, typeof contents === "string" ? "utf-8" : void 0);
|
|
if (log) {
|
|
consola.info("Generated", prettyPath(file));
|
|
}
|
|
}
|
|
function resolvePath(path, nitroOptions, base) {
|
|
if (typeof path !== "string") {
|
|
throw new TypeError("Invalid path: " + path);
|
|
}
|
|
path = compileTemplate(path)(nitroOptions);
|
|
for (const base2 in nitroOptions.alias) {
|
|
if (path.startsWith(base2)) {
|
|
path = nitroOptions.alias[base2] + path.substring(base2.length);
|
|
}
|
|
}
|
|
return resolve(base || nitroOptions.srcDir, path);
|
|
}
|
|
const autodetectableProviders = {
|
|
azure_static: "azure",
|
|
cloudflare_pages: "cloudflare_pages",
|
|
netlify: "netlify",
|
|
stormkit: "stormkit",
|
|
vercel: "vercel",
|
|
cleavr: "cleavr"
|
|
};
|
|
function detectTarget() {
|
|
return autodetectableProviders[provider];
|
|
}
|
|
async function isDirectory(path) {
|
|
try {
|
|
return (await fse.stat(path)).isDirectory();
|
|
} catch (_err) {
|
|
return false;
|
|
}
|
|
}
|
|
createRequire(import.meta.url);
|
|
function resolveAliases(_aliases) {
|
|
const aliases = Object.fromEntries(Object.entries(_aliases).sort(
|
|
([a], [b]) => b.split("/").length - a.split("/").length || b.length - a.length
|
|
));
|
|
for (const key in aliases) {
|
|
for (const alias in aliases) {
|
|
if (!["~", "@", "#"].includes(alias[0])) {
|
|
continue;
|
|
}
|
|
if (alias === "@" && !aliases[key].startsWith("@/")) {
|
|
continue;
|
|
}
|
|
if (aliases[key].startsWith(alias)) {
|
|
aliases[key] = aliases[alias] + aliases[key].slice(alias.length);
|
|
}
|
|
}
|
|
}
|
|
return aliases;
|
|
}
|
|
async function retry(fn, retries) {
|
|
let retry2 = 0;
|
|
let error;
|
|
while (retry2++ < retries) {
|
|
try {
|
|
return await fn();
|
|
} catch (err) {
|
|
error = err;
|
|
await new Promise((resolve2) => setTimeout(resolve2, 2));
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
let distDir = dirname(fileURLToPath(import.meta.url));
|
|
if (/(chunks|shared)$/.test(distDir)) {
|
|
distDir = dirname(distDir);
|
|
}
|
|
const pkgDir = resolve(distDir, "..");
|
|
const runtimeDir = resolve(distDir, "runtime");
|
|
|
|
const NO_REPLACE_RE = /ROLLUP_NO_REPLACE/;
|
|
function replace(options) {
|
|
const _plugin = _replace(options);
|
|
return {
|
|
..._plugin,
|
|
renderChunk(code, chunk, options2) {
|
|
if (!NO_REPLACE_RE.test(code)) {
|
|
return _plugin.renderChunk.call(this, code, chunk, options2);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
const PREFIX = "\0virtual:";
|
|
function virtual(modules, cache = {}) {
|
|
const _modules = /* @__PURE__ */ new Map();
|
|
for (const [id, mod] of Object.entries(modules)) {
|
|
cache[id] = mod;
|
|
_modules.set(id, mod);
|
|
_modules.set(resolve(id), mod);
|
|
}
|
|
return {
|
|
name: "virtual",
|
|
resolveId(id, importer) {
|
|
if (id in modules) {
|
|
return PREFIX + id;
|
|
}
|
|
if (importer) {
|
|
const importerNoPrefix = importer.startsWith(PREFIX) ? importer.slice(PREFIX.length) : importer;
|
|
const resolved = resolve(dirname(importerNoPrefix), id);
|
|
if (_modules.has(resolved)) {
|
|
return PREFIX + resolved;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
async load(id) {
|
|
if (!id.startsWith(PREFIX)) {
|
|
return null;
|
|
}
|
|
const idNoPrefix = id.slice(PREFIX.length);
|
|
if (!_modules.has(idNoPrefix)) {
|
|
return null;
|
|
}
|
|
let m = _modules.get(idNoPrefix);
|
|
if (typeof m === "function") {
|
|
m = await m();
|
|
}
|
|
cache[id.replace(PREFIX, "")] = m;
|
|
return {
|
|
code: m,
|
|
map: null
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
const PLUGIN_NAME = "dynamic-require";
|
|
const HELPER_DYNAMIC = `\0${PLUGIN_NAME}.mjs`;
|
|
const DYNAMIC_REQUIRE_RE = /import\("\.\/" ?\+(.*)\).then/g;
|
|
function dynamicRequire({ dir, ignore, inline }) {
|
|
return {
|
|
name: PLUGIN_NAME,
|
|
transform(code, _id) {
|
|
return {
|
|
code: code.replace(DYNAMIC_REQUIRE_RE, `import('${HELPER_DYNAMIC}').then(r => r.default || r).then(dynamicRequire => dynamicRequire($1)).then`),
|
|
map: null
|
|
};
|
|
},
|
|
resolveId(id) {
|
|
return id === HELPER_DYNAMIC ? id : null;
|
|
},
|
|
async load(_id) {
|
|
if (_id !== HELPER_DYNAMIC) {
|
|
return null;
|
|
}
|
|
let files = [];
|
|
try {
|
|
const wpManifest = resolve(dir, "./server.manifest.json");
|
|
files = await import(pathToFileURL(wpManifest).href).then((r) => Object.keys(r.files).filter((file) => !ignore.includes(file)));
|
|
} catch {
|
|
files = await globby("**/*.{cjs,mjs,js}", { cwd: dir, absolute: false, ignore });
|
|
}
|
|
const chunks = (await Promise.all(files.map(async (id) => ({
|
|
id,
|
|
src: resolve(dir, id).replace(/\\/g, "/"),
|
|
name: genSafeVariableName(id),
|
|
meta: await getWebpackChunkMeta(resolve(dir, id))
|
|
})))).filter((chunk) => chunk.meta);
|
|
return inline ? TMPL_INLINE({ chunks }) : TMPL_LAZY({ chunks });
|
|
}
|
|
};
|
|
}
|
|
async function getWebpackChunkMeta(src) {
|
|
const chunk = await import(pathToFileURL(src).href).then((r) => r.default || r || {});
|
|
const { id, ids, modules } = chunk;
|
|
if (!id && !ids) {
|
|
return null;
|
|
}
|
|
return {
|
|
id,
|
|
ids,
|
|
moduleIds: Object.keys(modules || {})
|
|
};
|
|
}
|
|
function TMPL_INLINE({ chunks }) {
|
|
return `${chunks.map((i) => `import * as ${i.name} from '${i.src}'`).join("\n")}
|
|
const dynamicChunks = {
|
|
${chunks.map((i) => ` ['${i.id}']: ${i.name}`).join(",\n")}
|
|
};
|
|
|
|
export default function dynamicRequire(id) {
|
|
return Promise.resolve(dynamicChunks[id]);
|
|
};`;
|
|
}
|
|
function TMPL_LAZY({ chunks }) {
|
|
return `
|
|
const dynamicChunks = {
|
|
${chunks.map((i) => ` ['${i.id}']: () => import('${i.src}')`).join(",\n")}
|
|
};
|
|
|
|
export default function dynamicRequire(id) {
|
|
return dynamicChunks[id]();
|
|
};`;
|
|
}
|
|
|
|
function externals(opts) {
|
|
const trackedExternals = /* @__PURE__ */ new Set();
|
|
const _resolveCache = /* @__PURE__ */ new Map();
|
|
const _resolve = async (id) => {
|
|
let resolved = _resolveCache.get(id);
|
|
if (resolved) {
|
|
return resolved;
|
|
}
|
|
resolved = await resolvePath$1(id, {
|
|
conditions: opts.exportConditions,
|
|
url: opts.moduleDirectories
|
|
});
|
|
_resolveCache.set(id, resolved);
|
|
return resolved;
|
|
};
|
|
return {
|
|
name: "node-externals",
|
|
async resolveId(originalId, importer, options) {
|
|
if (!originalId || originalId.startsWith("\0") || originalId.includes("?") || originalId.startsWith("#")) {
|
|
return null;
|
|
}
|
|
if (originalId.startsWith(".")) {
|
|
return null;
|
|
}
|
|
const id = normalize(originalId);
|
|
const idWithoutNodeModules = id.split("node_modules/").pop();
|
|
if (opts.inline.find((i) => id.startsWith(i) || idWithoutNodeModules.startsWith(i))) {
|
|
return null;
|
|
}
|
|
if (opts.external.find((i) => id.startsWith(i) || idWithoutNodeModules.startsWith(i))) {
|
|
return { id, external: true };
|
|
}
|
|
const resolved = await this.resolve(originalId, importer, { ...options, skipSelf: true }) || { id };
|
|
if (!isAbsolute(resolved.id) || !existsSync(resolved.id) || await isDirectory(resolved.id)) {
|
|
resolved.id = await _resolve(resolved.id).catch(() => resolved.id);
|
|
}
|
|
if (!await isValidNodeImport(resolved.id).catch(() => false)) {
|
|
return null;
|
|
}
|
|
if (opts.trace === false) {
|
|
return {
|
|
...resolved,
|
|
id: isAbsolute(resolved.id) ? normalizeid(resolved.id) : resolved.id,
|
|
external: true
|
|
};
|
|
}
|
|
const { pkgName, subpath } = parseNodeModulePath(resolved.id);
|
|
if (!pkgName) {
|
|
return null;
|
|
}
|
|
if (pkgName !== originalId) {
|
|
if (!isAbsolute(originalId)) {
|
|
const fullPath = await _resolve(originalId);
|
|
trackedExternals.add(fullPath);
|
|
return {
|
|
id: originalId,
|
|
external: true
|
|
};
|
|
}
|
|
const packageEntry = await _resolve(pkgName).catch(() => null);
|
|
if (packageEntry !== originalId) {
|
|
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
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
trackedExternals.add(resolved.id);
|
|
return {
|
|
id: pkgName,
|
|
external: true
|
|
};
|
|
},
|
|
async buildEnd() {
|
|
if (opts.trace === false) {
|
|
return;
|
|
}
|
|
for (const pkgName of opts.traceInclude || []) {
|
|
const path = await this.resolve(pkgName);
|
|
if (path?.id) {
|
|
trackedExternals.add(path.id.replace(/\?.+/, ""));
|
|
}
|
|
}
|
|
let tracedFiles = await nodeFileTrace(Array.from(trackedExternals), opts.traceOptions).then((r) => Array.from(r.fileList).map((f) => resolve(opts.traceOptions.base, f))).then((r) => r.filter((file) => file.includes("node_modules")));
|
|
tracedFiles = await Promise.all(tracedFiles.map((file) => promises.realpath(file)));
|
|
const packageJSONCache = /* @__PURE__ */ new Map();
|
|
const getPackageJson = async (pkgDir) => {
|
|
if (packageJSONCache.has(pkgDir)) {
|
|
return packageJSONCache.get(pkgDir);
|
|
}
|
|
const pkgJSON = JSON.parse(await promises.readFile(resolve(pkgDir, "package.json"), "utf8"));
|
|
packageJSONCache.set(pkgDir, pkgJSON);
|
|
return pkgJSON;
|
|
};
|
|
const tracedPackages = /* @__PURE__ */ new Map();
|
|
const ignoreDirs = [];
|
|
const ignoreWarns = /* @__PURE__ */ new Set();
|
|
for (const file of tracedFiles) {
|
|
const { baseDir, pkgName } = parseNodeModulePath(file);
|
|
if (!pkgName) {
|
|
continue;
|
|
}
|
|
let pkgDir = resolve(baseDir, pkgName);
|
|
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);
|
|
const getMajor = (v) => v.split(".").filter((s) => s !== "0")[0];
|
|
if (getMajor(v1) !== getMajor(v2)) {
|
|
const warn = `Multiple major versions of package \`${pkgName}\` are being externalized. Picking latest version:
|
|
|
|
` + [
|
|
` ${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];
|
|
if (getMajor(v1) === getMajor(v2)) {
|
|
tracedFiles = tracedFiles.map((f) => f.startsWith(olderDir + "/") ? f.replace(olderDir, newerDir) : f);
|
|
}
|
|
ignoreDirs.push(olderDir + "/");
|
|
pkgDir = newerDir;
|
|
}
|
|
tracedPackages.set(pkgName, pkgDir);
|
|
}
|
|
tracedFiles = tracedFiles.filter((f) => !ignoreDirs.some((d) => f.startsWith(d)));
|
|
tracedFiles = Array.from(new Set(tracedFiles));
|
|
for (const pkgDir of tracedPackages.values()) {
|
|
const pkgJSON = join(pkgDir, "package.json");
|
|
if (!tracedFiles.includes(pkgJSON)) {
|
|
tracedFiles.push(pkgJSON);
|
|
}
|
|
}
|
|
const writeFile = async (file) => {
|
|
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 promises.mkdir(dirname(dst), { recursive: true });
|
|
try {
|
|
await promises.copyFile(src, dst);
|
|
} catch (err) {
|
|
consola.warn(`Could not resolve \`${src}\`. Skipping.`);
|
|
}
|
|
};
|
|
await Promise.all(tracedFiles.map((file) => retry(() => writeFile(file), 3)));
|
|
await promises.writeFile(resolve(opts.outDir, "package.json"), JSON.stringify({
|
|
name: "nitro-output",
|
|
version: "0.0.0",
|
|
private: true,
|
|
bundledDependencies: Array.from(tracedPackages.keys())
|
|
}, null, 2), "utf8");
|
|
}
|
|
};
|
|
}
|
|
function parseNodeModulePath(path) {
|
|
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) {
|
|
try {
|
|
const stat = await promises.stat(file);
|
|
return stat.isFile();
|
|
} catch (err) {
|
|
if (err.code === "ENOENT") {
|
|
return false;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
const TIMING = "globalThis.__timing__";
|
|
const iife = (code) => `(function() { ${code.trim()} })();`.replace(/\n/g, "");
|
|
const HELPER = iife(`
|
|
const start = () => Date.now();
|
|
const end = s => Date.now() - s;
|
|
const _s = {};
|
|
const metrics = [];
|
|
const logStart = id => { _s[id] = Date.now(); };
|
|
const logEnd = id => { const t = end(_s[id]); delete _s[id]; metrics.push([id, t]); console.debug('>', id + ' (' + t + 'ms)'); };
|
|
${TIMING} = { start, end, metrics, logStart, logEnd };
|
|
`);
|
|
const HELPERIMPORT = "import './timing.js';";
|
|
function timing(_opts = {}) {
|
|
return {
|
|
name: "timing",
|
|
generateBundle() {
|
|
this.emitFile({
|
|
type: "asset",
|
|
fileName: "timing.js",
|
|
source: HELPER
|
|
});
|
|
},
|
|
renderChunk(code, chunk) {
|
|
let name = chunk.fileName || "";
|
|
name = name.replace(extname(name), "");
|
|
const logName = name === "index" ? "Nitro Start" : "Load " + name;
|
|
return {
|
|
code: (chunk.isEntry ? HELPERIMPORT : "") + `${TIMING}.logStart('${logName}');` + code + `;${TIMING}.logEnd('${logName}');`,
|
|
map: null
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
function publicAssets(nitro) {
|
|
return virtual({
|
|
"#internal/nitro/virtual/public-assets-data": async () => {
|
|
const assets = {};
|
|
const files = await globby("**", {
|
|
cwd: nitro.options.output.publicDir,
|
|
absolute: false,
|
|
dot: true
|
|
});
|
|
for (const id of files) {
|
|
let mimeType = mime.getType(id.replace(/\.(gz|br)$/, "")) || "text/plain";
|
|
if (mimeType.startsWith("text")) {
|
|
mimeType += "; charset=utf-8";
|
|
}
|
|
const fullPath = resolve(nitro.options.output.publicDir, id);
|
|
const assetData = await promises.readFile(fullPath);
|
|
const etag = createEtag(assetData);
|
|
const stat = await promises.stat(fullPath);
|
|
const assetId = "/" + decodeURIComponent(id);
|
|
assets[assetId] = {
|
|
type: mimeType,
|
|
encoding: id.endsWith(".gz") ? "gzip" : id.endsWith(".br") ? "br" : void 0,
|
|
etag,
|
|
mtime: stat.mtime.toJSON(),
|
|
size: stat.size,
|
|
path: relative(nitro.options.output.serverDir, fullPath)
|
|
};
|
|
}
|
|
return `export default ${JSON.stringify(assets, null, 2)};`;
|
|
},
|
|
"#internal/nitro/virtual/public-assets-node": () => {
|
|
return `
|
|
import { promises as fsp } from 'node:fs'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { resolve, dirname } from 'pathe'
|
|
import assets from '#internal/nitro/virtual/public-assets-data'
|
|
export function readAsset (id) {
|
|
const serverDir = dirname(fileURLToPath(import.meta.url))
|
|
return fsp.readFile(resolve(serverDir, assets[id].path))
|
|
}`;
|
|
},
|
|
"#internal/nitro/virtual/public-assets-deno": () => {
|
|
return `
|
|
import assets from '#internal/nitro/virtual/public-assets-data'
|
|
export function readAsset (id) {
|
|
// https://deno.com/deploy/docs/serve-static-assets
|
|
const path = '.' + new URL('../public/test.txt', 'file://').pathname
|
|
return Deno.readFile(path);
|
|
}`;
|
|
},
|
|
"#internal/nitro/virtual/public-assets": () => {
|
|
const publicAssetBases = nitro.options.publicAssets.filter((dir) => !dir.fallthrough && dir.baseURL !== "/").map((dir) => dir.baseURL);
|
|
return `
|
|
import assets from '#internal/nitro/virtual/public-assets-data'
|
|
${nitro.options.serveStatic ? `export * from "#internal/nitro/virtual/public-assets-${nitro.options.serveStatic === "deno" ? "deno" : "node"}"` : "export const readAsset = () => Promise(null)"}
|
|
|
|
export const publicAssetBases = ${JSON.stringify(publicAssetBases)}
|
|
|
|
export function isPublicAssetURL(id = '') {
|
|
if (assets[id]) {
|
|
return true
|
|
}
|
|
for (const base of publicAssetBases) {
|
|
if (id.startsWith(base)) { return true }
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function getAsset (id) {
|
|
return assets[id]
|
|
}
|
|
`;
|
|
}
|
|
}, nitro.vfs);
|
|
}
|
|
|
|
function serverAssets(nitro) {
|
|
if (nitro.options.dev || nitro.options.preset === "nitro-prerender") {
|
|
return virtual({ "#internal/nitro/virtual/server-assets": getAssetsDev(nitro) }, nitro.vfs);
|
|
}
|
|
return virtual({
|
|
"#internal/nitro/virtual/server-assets": async () => {
|
|
const assets = {};
|
|
for (const asset of nitro.options.serverAssets) {
|
|
const files = await globby("**/*.*", { cwd: asset.dir, absolute: false });
|
|
for (const _id of files) {
|
|
const fsPath = resolve(asset.dir, _id);
|
|
const id = asset.baseName + "/" + _id;
|
|
assets[id] = { fsPath, meta: {} };
|
|
let type = mime.getType(id) || "text/plain";
|
|
if (type.startsWith("text")) {
|
|
type += "; charset=utf-8";
|
|
}
|
|
const etag = createEtag(await promises.readFile(fsPath));
|
|
const mtime = await promises.stat(fsPath).then((s) => s.mtime.toJSON());
|
|
assets[id].meta = { type, etag, mtime };
|
|
}
|
|
}
|
|
return getAssetProd(assets);
|
|
}
|
|
}, nitro.vfs);
|
|
}
|
|
function getAssetsDev(nitro) {
|
|
return `
|
|
import { createStorage } from 'unstorage'
|
|
import fsDriver from 'unstorage/drivers/fs'
|
|
|
|
const serverAssets = ${JSON.stringify(nitro.options.serverAssets)}
|
|
|
|
export const assets = createStorage()
|
|
|
|
for (const asset of serverAssets) {
|
|
assets.mount(asset.baseName, fsDriver({ base: asset.dir }))
|
|
}`;
|
|
}
|
|
function getAssetProd(assets) {
|
|
return `
|
|
const _assets = {
|
|
${Object.entries(assets).map(
|
|
([id, asset]) => ` [${JSON.stringify(normalizeKey(id))}]: {
|
|
import: () => import(${JSON.stringify("raw:" + asset.fsPath)}).then(r => r.default || r),
|
|
meta: ${JSON.stringify(asset.meta)}
|
|
}`
|
|
).join(",\n")}
|
|
}
|
|
|
|
${normalizeKey.toString()}
|
|
|
|
export const assets = {
|
|
getKeys() {
|
|
return Promise.resolve(Object.keys(_assets))
|
|
},
|
|
hasItem (id) {
|
|
id = normalizeKey(id)
|
|
return Promise.resolve(id in _assets)
|
|
},
|
|
getItem (id) {
|
|
id = normalizeKey(id)
|
|
return Promise.resolve(_assets[id] ? _assets[id].import() : null)
|
|
},
|
|
getMeta (id) {
|
|
id = normalizeKey(id)
|
|
return Promise.resolve(_assets[id] ? _assets[id].meta : {})
|
|
}
|
|
}
|
|
`;
|
|
}
|
|
|
|
const unique = (arr) => Array.from(new Set(arr));
|
|
function handlers(nitro) {
|
|
const getImportId = (p, lazy) => (lazy ? "_lazy_" : "_") + hash(p).slice(0, 6);
|
|
return virtual({
|
|
"#internal/nitro/virtual/server-handlers": () => {
|
|
const handlers2 = [
|
|
...nitro.scannedHandlers,
|
|
...nitro.options.handlers
|
|
];
|
|
if (nitro.options.serveStatic) {
|
|
handlers2.unshift({ middleware: true, handler: "#internal/nitro/static" });
|
|
}
|
|
if (nitro.options.renderer) {
|
|
handlers2.push({ route: "/**", lazy: true, handler: nitro.options.renderer });
|
|
}
|
|
const imports = unique(handlers2.filter((h) => !h.lazy).map((h) => h.handler));
|
|
const lazyImports = unique(handlers2.filter((h) => h.lazy).map((h) => h.handler));
|
|
const code = `
|
|
${imports.map((handler) => `import ${getImportId(handler)} from '${handler}';`).join("\n")}
|
|
|
|
${lazyImports.map((handler) => `const ${getImportId(handler, true)} = () => import('${handler}');`).join("\n")}
|
|
|
|
export const handlers = [
|
|
${handlers2.map((h) => ` { route: '${h.route || ""}', handler: ${getImportId(h.handler, h.lazy)}, lazy: ${!!h.lazy}, middleware: ${!!h.middleware}, method: ${JSON.stringify(h.method)} }`).join(",\n")}
|
|
];
|
|
`.trim();
|
|
return code;
|
|
}
|
|
}, nitro.vfs);
|
|
}
|
|
|
|
const defaultLoaders = {
|
|
".ts": "ts",
|
|
".js": "js"
|
|
};
|
|
function esbuild(options) {
|
|
const loaders = {
|
|
...defaultLoaders
|
|
};
|
|
if (options.loaders) {
|
|
for (const key of Object.keys(options.loaders)) {
|
|
const value = options.loaders[key];
|
|
if (typeof value === "string") {
|
|
loaders[key] = value;
|
|
} else if (value === false) {
|
|
delete loaders[key];
|
|
}
|
|
}
|
|
}
|
|
const extensions = Object.keys(loaders);
|
|
const INCLUDE_REGEXP = new RegExp(
|
|
`\\.(${extensions.map((ext) => ext.slice(1)).join("|")})$`
|
|
);
|
|
const EXCLUDE_REGEXP = /node_modules/;
|
|
const filter = createFilter(
|
|
options.include || INCLUDE_REGEXP,
|
|
options.exclude || EXCLUDE_REGEXP
|
|
);
|
|
return {
|
|
name: "esbuild",
|
|
async transform(code, id) {
|
|
if (!filter(id)) {
|
|
return null;
|
|
}
|
|
const ext = extname(id);
|
|
const loader = loaders[ext];
|
|
if (!loader) {
|
|
return null;
|
|
}
|
|
const result = await transform(code, {
|
|
loader,
|
|
target: options.target,
|
|
define: options.define,
|
|
sourcemap: options.sourceMap,
|
|
sourcefile: id
|
|
});
|
|
printWarnings(id, result, this);
|
|
return result.code && {
|
|
code: result.code,
|
|
map: result.map || null
|
|
};
|
|
},
|
|
async renderChunk(code) {
|
|
if (options.minify) {
|
|
const result = await transform(code, {
|
|
loader: "js",
|
|
minify: true,
|
|
target: options.target
|
|
});
|
|
if (result.code) {
|
|
return {
|
|
code: result.code,
|
|
map: result.map || null
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
}
|
|
function printWarnings(id, result, plugin) {
|
|
if (result.warnings) {
|
|
for (const warning of result.warnings) {
|
|
let message = "[esbuild]";
|
|
if (warning.location) {
|
|
message += ` (${relative(process.cwd(), id)}:${warning.location.line}:${warning.location.column})`;
|
|
}
|
|
message += ` ${warning.text}`;
|
|
plugin.warn(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
function raw(opts = {}) {
|
|
const extensions = new Set([".md", ".mdx", ".yml", ".txt", ".css", ".htm", ".html"].concat(opts.extensions || []));
|
|
return {
|
|
name: "raw",
|
|
resolveId(id) {
|
|
if (id[0] === "\0") {
|
|
return;
|
|
}
|
|
let isRawId = id.startsWith("raw:");
|
|
if (isRawId) {
|
|
id = id.substring(4);
|
|
} else if (extensions.has(extname(id))) {
|
|
isRawId = true;
|
|
}
|
|
if (isRawId) {
|
|
return { id: "\0raw:" + id };
|
|
}
|
|
},
|
|
load(id) {
|
|
if (id.startsWith("\0raw:")) {
|
|
return promises.readFile(id.substring(5), "utf8");
|
|
}
|
|
},
|
|
transform(code, id) {
|
|
if (id.startsWith("\0raw:")) {
|
|
return {
|
|
code: `// ROLLUP_NO_REPLACE
|
|
export default ${JSON.stringify(code)}`,
|
|
map: null
|
|
};
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function storage(nitro) {
|
|
const mounts = [];
|
|
const isDevOrPrerender = nitro.options.dev || nitro.options.preset === "nitro-prerender";
|
|
const storageMounts = isDevOrPrerender ? { ...nitro.options.storage, ...nitro.options.devStorage } : nitro.options.storage;
|
|
for (const path in storageMounts) {
|
|
const mount = storageMounts[path];
|
|
mounts.push({
|
|
path,
|
|
driver: builtinDrivers[mount.driver] || mount.driver,
|
|
opts: mount
|
|
});
|
|
}
|
|
const driverImports = Array.from(new Set(mounts.map((m) => m.driver)));
|
|
const bundledStorageCode = `
|
|
import { prefixStorage } from 'unstorage'
|
|
import overlay from 'unstorage/drivers/overlay'
|
|
import memory from 'unstorage/drivers/memory'
|
|
|
|
const bundledStorage = ${JSON.stringify(nitro.options.bundledStorage)}
|
|
for (const base of bundledStorage) {
|
|
storage.mount(base, overlay({
|
|
layers: [
|
|
memory(),
|
|
// TODO
|
|
// prefixStorage(storage, base),
|
|
prefixStorage(storage, 'assets:nitro:bundled:' + base)
|
|
]
|
|
}))
|
|
}`;
|
|
return virtual({
|
|
"#internal/nitro/virtual/storage": `
|
|
import { createStorage } from 'unstorage'
|
|
import { assets } from '#internal/nitro/virtual/server-assets'
|
|
|
|
${driverImports.map((i) => genImport(i, genSafeVariableName(i))).join("\n")}
|
|
|
|
const storage = createStorage({})
|
|
|
|
export const useStorage = () => storage
|
|
|
|
storage.mount('/assets', assets)
|
|
|
|
${mounts.map((m) => `storage.mount('${m.path}', ${genSafeVariableName(m.driver)}(${JSON.stringify(m.opts)}))`).join("\n")}
|
|
|
|
${!isDevOrPrerender && nitro.options.bundledStorage.length ? bundledStorageCode : ""}
|
|
`
|
|
}, nitro.vfs);
|
|
}
|
|
|
|
function importMeta(nitro) {
|
|
const ImportMetaRe = /import\.meta|globalThis._importMeta_/;
|
|
return {
|
|
name: "import-meta",
|
|
renderChunk(code, chunk) {
|
|
const isEntry = chunk.isEntry;
|
|
if (!isEntry && (!ImportMetaRe.test(code) || code.includes("ROLLUP_NO_REPLACE"))) {
|
|
return;
|
|
}
|
|
const url = nitro.options.node && isEntry ? "_import_meta_url_" : '"file:///_entry.js"';
|
|
const env = nitro.options.node ? "process.env" : "{}";
|
|
const ref = "globalThis._importMeta_";
|
|
const stub = `{url:${url},env:${env}}`;
|
|
const stubInit = isEntry ? `${ref}=${stub};` : `${ref}=${ref}||${stub};`;
|
|
return {
|
|
code: stubInit + code,
|
|
map: null
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
const getRollupConfig = (nitro) => {
|
|
const extensions = [".ts", ".mjs", ".js", ".json", ".node"];
|
|
const nodePreset = nitro.options.node === false ? unenv.nodeless : unenv.node;
|
|
const builtinPreset = {
|
|
alias: {
|
|
debug: "unenv/runtime/npm/debug",
|
|
consola: "unenv/runtime/npm/consola",
|
|
...nitro.options.alias
|
|
}
|
|
};
|
|
const env = unenv.env(nodePreset, builtinPreset, nitro.options.unenv);
|
|
if (nitro.options.sourceMap) {
|
|
env.polyfill.push("source-map-support/register.js");
|
|
}
|
|
const buildServerDir = join(nitro.options.buildDir, "dist/server");
|
|
const runtimeAppDir = join(runtimeDir, "app");
|
|
const rollupConfig = defu(nitro.options.rollupConfig, {
|
|
input: nitro.options.entry,
|
|
output: {
|
|
dir: nitro.options.output.serverDir,
|
|
entryFileNames: "index.mjs",
|
|
chunkFileNames(chunkInfo) {
|
|
let prefix = "";
|
|
const modules = Object.keys(chunkInfo.modules);
|
|
const lastModule = modules[modules.length - 1];
|
|
if (lastModule.startsWith(buildServerDir)) {
|
|
prefix = join("app", relative(buildServerDir, dirname(lastModule)));
|
|
} else if (lastModule.startsWith(runtimeAppDir)) {
|
|
prefix = "app";
|
|
} else if (lastModule.startsWith(nitro.options.buildDir)) {
|
|
prefix = "build";
|
|
} else if (lastModule.startsWith(runtimeDir)) {
|
|
prefix = "nitro";
|
|
} else if (nitro.options.handlers.find((m) => lastModule.startsWith(m.handler))) {
|
|
prefix = "handlers";
|
|
} else if (lastModule.includes("assets") || lastModule.startsWith("\0raw:")) {
|
|
prefix = "raw";
|
|
} else if (lastModule.startsWith("\0")) {
|
|
prefix = "rollup";
|
|
}
|
|
return join("chunks", prefix, "[name].mjs");
|
|
},
|
|
inlineDynamicImports: nitro.options.inlineDynamicImports,
|
|
format: "esm",
|
|
exports: "auto",
|
|
intro: "",
|
|
outro: "",
|
|
preferConst: true,
|
|
sanitizeFileName: sanitizeFilePath,
|
|
sourcemap: nitro.options.sourceMap,
|
|
sourcemapExcludeSources: true,
|
|
sourcemapPathTransform(relativePath, sourcemapPath) {
|
|
return resolve(dirname(sourcemapPath), relativePath);
|
|
}
|
|
},
|
|
external: env.external,
|
|
makeAbsoluteExternalsRelative: false,
|
|
plugins: [],
|
|
onwarn(warning, rollupWarn) {
|
|
if (!["CIRCULAR_DEPENDENCY", "EVAL"].includes(warning.code) && !warning.message.includes("Unsupported source map comment")) {
|
|
rollupWarn(warning);
|
|
}
|
|
},
|
|
treeshake: {
|
|
moduleSideEffects(id) {
|
|
const normalizedId = normalize(id);
|
|
const idWithoutNodeModules = normalizedId.split("node_modules/").pop();
|
|
return nitro.options.moduleSideEffects.some((m) => normalizedId.startsWith(m) || idWithoutNodeModules.startsWith(m));
|
|
}
|
|
}
|
|
});
|
|
if (nitro.options.timing) {
|
|
rollupConfig.plugins.push(timing());
|
|
}
|
|
if (nitro.options.imports) {
|
|
rollupConfig.plugins.push(unimportPlugin.rollup(nitro.options.imports));
|
|
}
|
|
rollupConfig.plugins.push(raw());
|
|
if (nitro.options.experimental.wasm) {
|
|
const options = { ...nitro.options.experimental.wasm };
|
|
rollupConfig.plugins.push(wasmPlugin(options));
|
|
}
|
|
const buildEnvVars = {
|
|
NODE_ENV: nitro.options.dev ? "development" : nitro.options.preset === "nitro-prerender" ? "prerender" : "production",
|
|
prerender: nitro.options.preset === "nitro-prerender",
|
|
server: true,
|
|
client: false,
|
|
dev: String(nitro.options.dev),
|
|
RUNTIME_CONFIG: nitro.options.runtimeConfig,
|
|
DEBUG: nitro.options.dev
|
|
};
|
|
rollupConfig.plugins.push(importMeta(nitro));
|
|
rollupConfig.plugins.push(replace({
|
|
preventAssignment: true,
|
|
values: {
|
|
"typeof window": '"undefined"',
|
|
_import_meta_url_: "import.meta.url",
|
|
...Object.fromEntries([".", ";", ")", "[", "]", "}", " "].map((d) => [`import.meta${d}`, `globalThis._importMeta_${d}`])),
|
|
...Object.fromEntries([";", "(", "{", "}", " ", " ", "\n"].map((d) => [`${d}global.`, `${d}globalThis.`])),
|
|
...Object.fromEntries(Object.entries(buildEnvVars).map(([key, val]) => [`process.env.${key}`, JSON.stringify(val)])),
|
|
...Object.fromEntries(Object.entries(buildEnvVars).map(([key, val]) => [`import.meta.env.${key}`, JSON.stringify(val)])),
|
|
...nitro.options.replace
|
|
}
|
|
}));
|
|
rollupConfig.plugins.push(esbuild({
|
|
target: "es2019",
|
|
sourceMap: nitro.options.sourceMap,
|
|
...nitro.options.esbuild?.options
|
|
}));
|
|
rollupConfig.plugins.push(dynamicRequire({
|
|
dir: resolve(nitro.options.buildDir, "dist/server"),
|
|
inline: nitro.options.node === false || nitro.options.inlineDynamicImports,
|
|
ignore: [
|
|
"client.manifest.mjs",
|
|
"server.js",
|
|
"server.cjs",
|
|
"server.mjs",
|
|
"server.manifest.mjs"
|
|
]
|
|
}));
|
|
rollupConfig.plugins.push(serverAssets(nitro));
|
|
rollupConfig.plugins.push(publicAssets(nitro));
|
|
rollupConfig.plugins.push(storage(nitro));
|
|
rollupConfig.plugins.push(handlers(nitro));
|
|
rollupConfig.plugins.push(virtual({
|
|
"#internal/nitro/virtual/polyfill": env.polyfill.map((p) => `import '${p}';`).join("\n")
|
|
}, nitro.vfs));
|
|
rollupConfig.plugins.push(virtual(nitro.options.virtual, nitro.vfs));
|
|
rollupConfig.plugins.push(virtual({
|
|
"#internal/nitro/virtual/plugins": `
|
|
${nitro.options.plugins.map((plugin) => `import _${hash(plugin)} from '${plugin}';`).join("\n")}
|
|
|
|
export const plugins = [
|
|
${nitro.options.plugins.map((plugin) => `_${hash(plugin)}`).join(",\n")}
|
|
]
|
|
`
|
|
}, nitro.vfs));
|
|
let buildDir = nitro.options.buildDir;
|
|
if (isWindows && nitro.options.externals?.trace === false && nitro.options.dev) {
|
|
buildDir = pathToFileURL(buildDir).href;
|
|
}
|
|
rollupConfig.plugins.push(alias({
|
|
entries: resolveAliases({
|
|
"#build": buildDir,
|
|
"#internal/nitro/virtual/error-handler": nitro.options.errorHandler,
|
|
"~": nitro.options.srcDir,
|
|
"@/": nitro.options.srcDir,
|
|
"~~": nitro.options.rootDir,
|
|
"@@/": nitro.options.rootDir,
|
|
...env.alias
|
|
})
|
|
}));
|
|
if (!nitro.options.noExternals) {
|
|
rollupConfig.plugins.push(externals(defu(nitro.options.externals, {
|
|
outDir: nitro.options.output.serverDir,
|
|
moduleDirectories: nitro.options.nodeModulesDirs,
|
|
external: [
|
|
...nitro.options.dev ? [nitro.options.buildDir] : []
|
|
],
|
|
inline: [
|
|
"#",
|
|
"~",
|
|
"@/",
|
|
"~~",
|
|
"@@/",
|
|
"virtual:",
|
|
runtimeDir,
|
|
nitro.options.srcDir,
|
|
...nitro.options.handlers.map((m) => m.handler).filter((i) => typeof i === "string")
|
|
],
|
|
traceOptions: {
|
|
base: "/",
|
|
processCwd: nitro.options.rootDir,
|
|
exportsOnly: true
|
|
},
|
|
exportConditions: [
|
|
"default",
|
|
nitro.options.dev ? "development" : "production",
|
|
"module",
|
|
"node",
|
|
"import"
|
|
]
|
|
})));
|
|
} else {
|
|
rollupConfig.plugins.push({
|
|
name: "no-externals",
|
|
async resolveId(id, from, options) {
|
|
const resolved = await this.resolve(id, from, { ...options, skipSelf: true });
|
|
if (!resolved || resolved.external) {
|
|
throw new Error(`Cannot resolve ${JSON.stringify(id)} from ${JSON.stringify(from)} and externals are not allowed!`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
rollupConfig.plugins.push(nodeResolve({
|
|
extensions,
|
|
preferBuiltins: !!nitro.options.node,
|
|
rootDir: nitro.options.rootDir,
|
|
modulePaths: nitro.options.nodeModulesDirs,
|
|
mainFields: ["main"],
|
|
exportConditions: [
|
|
"default",
|
|
nitro.options.dev ? "development" : "production",
|
|
"module",
|
|
"node",
|
|
"import"
|
|
]
|
|
}));
|
|
rollupConfig.plugins.push(commonjs({
|
|
esmExternals: (id) => !id.startsWith("unenv/"),
|
|
requireReturnsDefault: "auto",
|
|
...nitro.options.commonJS
|
|
}));
|
|
rollupConfig.plugins.push(json());
|
|
rollupConfig.plugins.push(inject(env.inject));
|
|
if (nitro.options.minify) {
|
|
rollupConfig.plugins.push(terser({
|
|
mangle: {
|
|
keep_fnames: true,
|
|
keep_classnames: true
|
|
},
|
|
format: {
|
|
comments: false
|
|
}
|
|
}));
|
|
}
|
|
if (nitro.options.analyze) {
|
|
rollupConfig.plugins.push(visualizer({
|
|
...nitro.options.analyze,
|
|
filename: nitro.options.analyze.filename.replace("{name}", "nitro"),
|
|
title: "Nitro Server bundle stats"
|
|
}));
|
|
}
|
|
return rollupConfig;
|
|
};
|
|
|
|
const GLOB_SCAN_PATTERN = "**/*.{ts,mjs,js,cjs}";
|
|
const httpMethodRegex = /\.(connect|delete|get|head|options|patch|post|put|trace)/;
|
|
async function scanHandlers(nitro) {
|
|
const handlers = await Promise.all([
|
|
scanMiddleware(nitro),
|
|
scanRoutes(nitro, "api", "/api"),
|
|
scanRoutes(nitro, "routes", "/")
|
|
]).then((r) => r.flat());
|
|
nitro.scannedHandlers = handlers.flatMap((h) => h.handlers);
|
|
return handlers;
|
|
}
|
|
function scanMiddleware(nitro) {
|
|
return scanServerDir(nitro, "middleware", (file) => ({
|
|
middleware: true,
|
|
handler: file.fullPath
|
|
}));
|
|
}
|
|
function scanRoutes(nitro, dir, prefix = "/") {
|
|
return scanServerDir(nitro, dir, (file) => {
|
|
let route = file.path.replace(/\.[a-zA-Z]+$/, "").replace(/\[\.\.\.\]/g, "**").replace(/\[\.\.\.(\w+)]/g, "**:$1").replace(/\[(\w+)\]/g, ":$1");
|
|
route = withLeadingSlash(withoutTrailingSlash(withBase(route, prefix)));
|
|
let method;
|
|
const methodMatch = route.match(httpMethodRegex);
|
|
if (methodMatch) {
|
|
route = route.substring(0, methodMatch.index);
|
|
method = methodMatch[1];
|
|
}
|
|
route = route.replace(/\/index$/, "") || "/";
|
|
return {
|
|
handler: file.fullPath,
|
|
lazy: true,
|
|
route,
|
|
method
|
|
};
|
|
});
|
|
}
|
|
async function scanServerDir(nitro, name, mapper) {
|
|
const dirs = nitro.options.scanDirs.map((dir) => join(dir, name));
|
|
const files = await scanDirs(dirs);
|
|
const handlers = files.map(mapper);
|
|
return { dirs, files, handlers };
|
|
}
|
|
async function scanPlugins(nitro) {
|
|
const plugins = [];
|
|
for (const dir of nitro.options.scanDirs) {
|
|
const pluginDir = join(dir, "plugins");
|
|
const pluginFiles = await globby(GLOB_SCAN_PATTERN, { cwd: pluginDir, absolute: true });
|
|
plugins.push(...pluginFiles.sort());
|
|
}
|
|
return plugins;
|
|
}
|
|
function scanDirs(dirs) {
|
|
return Promise.all(dirs.map(async (dir) => {
|
|
const fileNames = await globby(GLOB_SCAN_PATTERN, { cwd: dir, dot: true });
|
|
return fileNames.map((fileName) => {
|
|
return {
|
|
dir,
|
|
path: fileName,
|
|
fullPath: resolve(dir, fileName)
|
|
};
|
|
}).sort((a, b) => b.path.localeCompare(a.path));
|
|
})).then((r) => r.flat());
|
|
}
|
|
|
|
async function createStorage(nitro) {
|
|
const storage = createStorage$1();
|
|
const mounts = {
|
|
...nitro.options.storage,
|
|
...nitro.options.devStorage
|
|
};
|
|
for (const [path, opts] of Object.entries(mounts)) {
|
|
const driver = await import(builtinDrivers[opts.driver] || opts.driver).then((r) => r.default || r);
|
|
storage.mount(path, driver(opts));
|
|
}
|
|
return storage;
|
|
}
|
|
async function snapshotStorage(nitro) {
|
|
const data = {};
|
|
const allKeys = Array.from(new Set(await Promise.all(
|
|
nitro.options.bundledStorage.map((base) => nitro.storage.getKeys(base))
|
|
).then((r) => r.flat())));
|
|
await Promise.all(allKeys.map(async (key) => {
|
|
data[key] = await nitro.storage.getItem(key);
|
|
}));
|
|
return data;
|
|
}
|
|
|
|
async function compressPublicAssets(nitro) {
|
|
const publicFiles = await globby("**", {
|
|
cwd: nitro.options.output.publicDir,
|
|
absolute: false,
|
|
dot: true,
|
|
ignore: ["**/*.gz", "**/*.br"]
|
|
});
|
|
for (const fileName of publicFiles) {
|
|
const filePath = resolve(nitro.options.output.publicDir, fileName);
|
|
const fileContents = await fsp.readFile(filePath);
|
|
if (existsSync$1(filePath + ".gz") || existsSync$1(filePath + ".br")) {
|
|
continue;
|
|
}
|
|
const mimeType = mime.getType(fileName) || "text/plain";
|
|
if (fileContents.length < 1024 || fileName.endsWith(".map") || !isCompressableMime(mimeType)) {
|
|
continue;
|
|
}
|
|
const { gzip, brotli } = nitro.options.compressPublicAssets || {};
|
|
const encodings = [gzip !== false && "gzip", brotli !== false && "br"].filter(Boolean);
|
|
for (const encoding of encodings) {
|
|
const suffix = "." + (encoding === "gzip" ? "gz" : "br");
|
|
const compressedPath = filePath + suffix;
|
|
if (existsSync$1(compressedPath)) {
|
|
continue;
|
|
}
|
|
const gzipOptions = { level: zlib.constants.Z_BEST_COMPRESSION };
|
|
const brotliOptions = {
|
|
[zlib.constants.BROTLI_PARAM_MODE]: isTextMime(mimeType) ? zlib.constants.BROTLI_MODE_TEXT : zlib.constants.BROTLI_MODE_GENERIC,
|
|
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
|
|
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: fileContents.length
|
|
};
|
|
const compressedBuff = await new Promise((resolve2, reject) => {
|
|
const cb = (error, result) => error ? reject(error) : resolve2(result);
|
|
if (encoding === "gzip") {
|
|
zlib.gzip(fileContents, gzipOptions, cb);
|
|
} else {
|
|
zlib.brotliCompress(fileContents, brotliOptions, cb);
|
|
}
|
|
});
|
|
await fsp.writeFile(compressedPath, compressedBuff);
|
|
}
|
|
}
|
|
}
|
|
function isTextMime(mimeType) {
|
|
return /text|javascript|json|xml/.test(mimeType);
|
|
}
|
|
function isCompressableMime(mimeType) {
|
|
return /image|text|font|json|xml|javascript/.test(mimeType);
|
|
}
|
|
|
|
async function prepare(nitro) {
|
|
await prepareDir(nitro.options.output.dir);
|
|
if (!nitro.options.noPublicDir) {
|
|
await prepareDir(nitro.options.output.publicDir);
|
|
}
|
|
await prepareDir(nitro.options.output.serverDir);
|
|
}
|
|
async function prepareDir(dir) {
|
|
await promises.mkdir(dir, { recursive: true });
|
|
await fse.emptyDir(dir);
|
|
}
|
|
async function copyPublicAssets(nitro) {
|
|
if (nitro.options.noPublicDir) {
|
|
return;
|
|
}
|
|
for (const asset of nitro.options.publicAssets) {
|
|
if (await isDirectory(asset.dir)) {
|
|
await fse.copy(asset.dir, join(nitro.options.output.publicDir, asset.baseURL));
|
|
}
|
|
}
|
|
if (nitro.options.compressPublicAssets) {
|
|
await compressPublicAssets(nitro);
|
|
}
|
|
nitro.logger.success("Generated public " + prettyPath(nitro.options.output.publicDir));
|
|
}
|
|
async function build(nitro) {
|
|
const rollupConfig = getRollupConfig(nitro);
|
|
await nitro.hooks.callHook("rollup:before", nitro);
|
|
return nitro.options.dev ? _watch(nitro, rollupConfig) : _build(nitro, rollupConfig);
|
|
}
|
|
async function writeTypes(nitro) {
|
|
const routeTypes = {};
|
|
const middleware = [
|
|
...nitro.scannedHandlers,
|
|
...nitro.options.handlers
|
|
];
|
|
for (const mw of middleware) {
|
|
if (typeof mw.handler !== "string" || !mw.route) {
|
|
continue;
|
|
}
|
|
const relativePath = relative(join(nitro.options.buildDir, "types"), mw.handler).replace(/\.[a-z]+$/, "");
|
|
routeTypes[mw.route] = routeTypes[mw.route] || [];
|
|
routeTypes[mw.route].push(`Awaited<ReturnType<typeof import('${relativePath}').default>>`);
|
|
}
|
|
let autoImportedTypes = [];
|
|
if (nitro.unimport) {
|
|
autoImportedTypes = [
|
|
(await nitro.unimport.generateTypeDeclarations({
|
|
exportHelper: false,
|
|
resolvePath: (i) => {
|
|
if (i.from.startsWith("#internal/nitro")) {
|
|
return resolveAlias(i.from, nitro.options.alias);
|
|
}
|
|
return i.from;
|
|
}
|
|
})).trim()
|
|
];
|
|
}
|
|
const lines = [
|
|
"// Generated by nitro",
|
|
"declare module 'nitropack' {",
|
|
" type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T",
|
|
" interface InternalApi {",
|
|
...Object.entries(routeTypes).map(([path, types]) => ` '${path}': ${types.join(" | ")}`),
|
|
" }",
|
|
"}",
|
|
...autoImportedTypes,
|
|
"export {}"
|
|
];
|
|
await writeFile$1(join(nitro.options.buildDir, "types/nitro.d.ts"), lines.join("\n"));
|
|
if (nitro.options.typescript.generateTsConfig) {
|
|
const tsConfig = {
|
|
compilerOptions: {
|
|
target: "ESNext",
|
|
module: "ESNext",
|
|
moduleResolution: "Node",
|
|
allowJs: true,
|
|
resolveJsonModule: true,
|
|
paths: nitro.options.typescript.internalPaths ? {
|
|
"#internal/nitro": [
|
|
join(runtimeDir, "index")
|
|
],
|
|
"#internal/nitro/*": [
|
|
join(runtimeDir, "*")
|
|
]
|
|
} : {}
|
|
},
|
|
include: [
|
|
"./nitro.d.ts",
|
|
join(relative(join(nitro.options.buildDir, "types"), nitro.options.rootDir), "**/*"),
|
|
...nitro.options.srcDir !== nitro.options.rootDir ? [join(relative(join(nitro.options.buildDir, "types"), nitro.options.srcDir), "**/*")] : []
|
|
]
|
|
};
|
|
await writeFile$1(join(nitro.options.buildDir, "types/tsconfig.json"), JSON.stringify(tsConfig, null, 2));
|
|
}
|
|
}
|
|
async function _snapshot(nitro) {
|
|
if (!nitro.options.bundledStorage.length || nitro.options.preset === "nitro-prerender") {
|
|
return;
|
|
}
|
|
const storageDir = resolve(nitro.options.buildDir, "snapshot");
|
|
nitro.options.serverAssets.push({
|
|
baseName: "nitro:bundled",
|
|
dir: storageDir
|
|
});
|
|
const data = await snapshotStorage(nitro);
|
|
await Promise.all(Object.entries(data).map(async ([path, contents]) => {
|
|
if (typeof contents !== "string") {
|
|
contents = JSON.stringify(contents);
|
|
}
|
|
const fsPath = join(storageDir, path.replace(/:/g, "/"));
|
|
await promises.mkdir(dirname(fsPath), { recursive: true });
|
|
await promises.writeFile(fsPath, contents, "utf8");
|
|
}));
|
|
}
|
|
async function _build(nitro, rollupConfig) {
|
|
await scanHandlers(nitro);
|
|
await writeTypes(nitro);
|
|
await _snapshot(nitro);
|
|
nitro.logger.info(`Building Nitro Server (preset: \`${nitro.options.preset}\`)`);
|
|
const build2 = await rollup.rollup(rollupConfig).catch((error) => {
|
|
nitro.logger.error(formatRollupError(error));
|
|
throw error;
|
|
});
|
|
await build2.write(rollupConfig.output);
|
|
const nitroConfigPath = resolve(nitro.options.output.dir, "nitro.json");
|
|
const buildInfo = {
|
|
date: new Date(),
|
|
preset: nitro.options.preset,
|
|
commands: {
|
|
preview: nitro.options.commands.preview,
|
|
deploy: nitro.options.commands.deploy
|
|
}
|
|
};
|
|
await writeFile$1(nitroConfigPath, JSON.stringify(buildInfo, null, 2));
|
|
nitro.logger.success("Nitro server built");
|
|
if (nitro.options.logLevel > 1) {
|
|
await printFSTree(nitro.options.output.serverDir);
|
|
}
|
|
await nitro.hooks.callHook("compiled", nitro);
|
|
const rOutput = relative(process.cwd(), nitro.options.output.dir);
|
|
const rewriteRelativePaths = (input) => {
|
|
return input.replace(/\s\.\/([^\s]*)/g, ` ${rOutput}/$1`);
|
|
};
|
|
if (buildInfo.commands.preview) {
|
|
nitro.logger.success(`You can preview this build using \`${rewriteRelativePaths(buildInfo.commands.preview)}\``);
|
|
}
|
|
if (buildInfo.commands.deploy) {
|
|
nitro.logger.success(`You can deploy this build using \`${rewriteRelativePaths(buildInfo.commands.deploy)}\``);
|
|
}
|
|
}
|
|
function startRollupWatcher(nitro, rollupConfig) {
|
|
const watcher = rollup.watch(defu(rollupConfig, {
|
|
watch: {
|
|
chokidar: nitro.options.watchOptions
|
|
}
|
|
}));
|
|
let start;
|
|
watcher.on("event", (event) => {
|
|
switch (event.code) {
|
|
case "START":
|
|
return;
|
|
case "BUNDLE_START":
|
|
start = Date.now();
|
|
return;
|
|
case "END":
|
|
nitro.hooks.callHook("compiled", nitro);
|
|
nitro.logger.success("Nitro built", start ? `in ${Date.now() - start} ms` : "");
|
|
nitro.hooks.callHook("dev:reload");
|
|
return;
|
|
case "ERROR":
|
|
nitro.logger.error(formatRollupError(event.error));
|
|
}
|
|
});
|
|
return watcher;
|
|
}
|
|
async function _watch(nitro, rollupConfig) {
|
|
let rollupWatcher;
|
|
const reload = debounce(async () => {
|
|
if (rollupWatcher) {
|
|
await rollupWatcher.close();
|
|
}
|
|
await scanHandlers(nitro);
|
|
rollupWatcher = startRollupWatcher(nitro, rollupConfig);
|
|
await writeTypes(nitro);
|
|
});
|
|
const watchPatterns = nitro.options.scanDirs.flatMap((dir) => [
|
|
join(dir, "api"),
|
|
join(dir, "routes"),
|
|
join(dir, "middleware", GLOB_SCAN_PATTERN)
|
|
]);
|
|
const watchReloadEvents = /* @__PURE__ */ new Set(["add", "addDir", "unlink", "unlinkDir"]);
|
|
const reloadWacher = watch(watchPatterns, { ignoreInitial: true }).on("all", (event) => {
|
|
if (watchReloadEvents.has(event)) {
|
|
reload();
|
|
}
|
|
});
|
|
nitro.hooks.hook("close", () => {
|
|
rollupWatcher.close();
|
|
reloadWacher.close();
|
|
});
|
|
await reload();
|
|
}
|
|
function formatRollupError(_error) {
|
|
try {
|
|
const logs = [];
|
|
for (const error of "errors" in _error ? _error.errors : [_error]) {
|
|
const id = error.path || error.id || _error.id;
|
|
let path = isAbsolute(id) ? relative(process.cwd(), id) : id;
|
|
const location = error.loc || error.location;
|
|
if (location) {
|
|
path += `:${location.line}:${location.column}`;
|
|
}
|
|
const text = error.text || error.frame;
|
|
logs.push(`Rollup error while processing \`${path}\`` + text ? "\n\n" + text : "");
|
|
}
|
|
return logs.join("\n\n");
|
|
} catch {
|
|
return _error?.toString();
|
|
}
|
|
}
|
|
|
|
function defineNitroPreset(preset) {
|
|
return preset;
|
|
}
|
|
|
|
const awsLambda = defineNitroPreset({
|
|
entry: "#internal/nitro/entries/aws-lambda"
|
|
});
|
|
|
|
const azureFunctions = defineNitroPreset({
|
|
serveStatic: true,
|
|
entry: "#internal/nitro/entries/azure-functions",
|
|
commands: {
|
|
deploy: "az functionapp deployment source config-zip -g <resource-group> -n <app-name> --src {{ output.dir }}/deploy.zip"
|
|
},
|
|
hooks: {
|
|
async "compiled"(ctx) {
|
|
await writeRoutes$2(ctx);
|
|
}
|
|
}
|
|
});
|
|
function zipDirectory(dir, outfile) {
|
|
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
const stream = createWriteStream(outfile);
|
|
return new Promise((resolve2, reject) => {
|
|
archive.directory(dir, false).on("error", (err) => reject(err)).pipe(stream);
|
|
stream.on("close", () => resolve2(void 0));
|
|
archive.finalize();
|
|
});
|
|
}
|
|
async function writeRoutes$2(nitro) {
|
|
const host = {
|
|
version: "2.0",
|
|
extensions: { http: { routePrefix: "" } }
|
|
};
|
|
const functionDefinition = {
|
|
entryPoint: "handle",
|
|
bindings: [
|
|
{
|
|
authLevel: "anonymous",
|
|
type: "httpTrigger",
|
|
direction: "in",
|
|
name: "req",
|
|
route: "{*url}",
|
|
methods: [
|
|
"delete",
|
|
"get",
|
|
"head",
|
|
"options",
|
|
"patch",
|
|
"post",
|
|
"put"
|
|
]
|
|
},
|
|
{
|
|
type: "http",
|
|
direction: "out",
|
|
name: "res"
|
|
}
|
|
]
|
|
};
|
|
await writeFile$1(resolve(nitro.options.output.serverDir, "function.json"), JSON.stringify(functionDefinition));
|
|
await writeFile$1(resolve(nitro.options.output.dir, "host.json"), JSON.stringify(host));
|
|
await zipDirectory(nitro.options.output.dir, join(nitro.options.output.dir, "deploy.zip"));
|
|
}
|
|
|
|
const azure = defineNitroPreset({
|
|
entry: "#internal/nitro/entries/azure",
|
|
output: {
|
|
serverDir: "{{ output.dir }}/server/functions"
|
|
},
|
|
commands: {
|
|
preview: "npx @azure/static-web-apps-cli start ./public --api-location ./server"
|
|
},
|
|
hooks: {
|
|
async "compiled"(ctx) {
|
|
await writeRoutes$1(ctx);
|
|
}
|
|
}
|
|
});
|
|
async function writeRoutes$1(nitro) {
|
|
const host = {
|
|
version: "2.0"
|
|
};
|
|
let nodeVersion = "16";
|
|
try {
|
|
const currentNodeVersion = fse.readJSONSync(join(nitro.options.rootDir, "package.json")).engines.node;
|
|
if (["16", "14"].includes(currentNodeVersion)) {
|
|
nodeVersion = currentNodeVersion;
|
|
}
|
|
} catch {
|
|
const currentNodeVersion = process.versions.node.slice(0, 2);
|
|
if (["16", "14"].includes(currentNodeVersion)) {
|
|
nodeVersion = currentNodeVersion;
|
|
}
|
|
}
|
|
const config = {
|
|
platform: {
|
|
apiRuntime: `node:${nodeVersion}`
|
|
},
|
|
routes: [],
|
|
navigationFallback: {
|
|
rewrite: "/api/server"
|
|
}
|
|
};
|
|
const routeFiles = nitro._prerenderedRoutes || [];
|
|
const indexFileExists = routeFiles.some((route) => route.fileName === "/index.html");
|
|
if (!indexFileExists) {
|
|
config.routes.unshift(
|
|
{
|
|
route: "/index.html",
|
|
redirect: "/"
|
|
},
|
|
{
|
|
route: "/",
|
|
rewrite: "/api/server"
|
|
}
|
|
);
|
|
}
|
|
const suffix = "/index.html".length;
|
|
for (const { fileName } of routeFiles) {
|
|
if (!fileName.endsWith("/index.html")) {
|
|
continue;
|
|
}
|
|
config.routes.unshift({
|
|
route: fileName.slice(0, -suffix) || "/",
|
|
rewrite: fileName
|
|
});
|
|
}
|
|
for (const { fileName } of routeFiles) {
|
|
if (!fileName.endsWith(".html") || fileName.endsWith("index.html")) {
|
|
continue;
|
|
}
|
|
const route = fileName.slice(0, -".html".length);
|
|
const existingRouteIndex = config.routes.findIndex((_route) => _route.route === route);
|
|
if (existingRouteIndex > -1) {
|
|
config.routes.splice(existingRouteIndex, 1);
|
|
}
|
|
config.routes.unshift({
|
|
route,
|
|
rewrite: fileName
|
|
});
|
|
}
|
|
const functionDefinition = {
|
|
entryPoint: "handle",
|
|
bindings: [
|
|
{
|
|
authLevel: "anonymous",
|
|
type: "httpTrigger",
|
|
direction: "in",
|
|
name: "req",
|
|
route: "{*url}",
|
|
methods: ["delete", "get", "head", "options", "patch", "post", "put"]
|
|
},
|
|
{
|
|
type: "http",
|
|
direction: "out",
|
|
name: "res"
|
|
}
|
|
]
|
|
};
|
|
await writeFile$1(resolve(nitro.options.output.serverDir, "function.json"), JSON.stringify(functionDefinition, null, 2));
|
|
await writeFile$1(resolve(nitro.options.output.serverDir, "../host.json"), JSON.stringify(host, null, 2));
|
|
const stubPackageJson = resolve(nitro.options.output.serverDir, "../package.json");
|
|
await writeFile$1(stubPackageJson, JSON.stringify({ private: true }));
|
|
await writeFile$1(resolve(nitro.options.rootDir, "staticwebapp.config.json"), JSON.stringify(config, null, 2));
|
|
if (!indexFileExists) {
|
|
await writeFile$1(resolve(nitro.options.output.publicDir, "index.html"), "");
|
|
}
|
|
}
|
|
|
|
const baseWorker = defineNitroPreset({
|
|
entry: null,
|
|
node: false,
|
|
minify: true,
|
|
noExternals: true,
|
|
rollupConfig: {
|
|
output: {
|
|
format: "iife",
|
|
generatedCode: {
|
|
symbols: true
|
|
}
|
|
}
|
|
},
|
|
inlineDynamicImports: true
|
|
});
|
|
|
|
const cloudflare = defineNitroPreset({
|
|
extends: "base-worker",
|
|
entry: "#internal/nitro/entries/cloudflare",
|
|
commands: {
|
|
preview: "npx wrangler dev ./server/index.mjs --site ./public --local",
|
|
deploy: "npx wrangler publish"
|
|
},
|
|
hooks: {
|
|
async "compiled"(nitro) {
|
|
await writeFile$1(resolve(nitro.options.output.dir, "package.json"), JSON.stringify({ private: true, main: "./server/index.mjs" }, null, 2));
|
|
await writeFile$1(resolve(nitro.options.output.dir, "package-lock.json"), JSON.stringify({ lockfileVersion: 1 }, null, 2));
|
|
}
|
|
}
|
|
});
|
|
const cloudflarePages = defineNitroPreset({
|
|
extends: "cloudflare",
|
|
entry: "#internal/nitro/entries/cloudflare-pages",
|
|
commands: {
|
|
preview: "npx wrangler pages dev .output/public",
|
|
deploy: "npx wrangler pages publish .output/public"
|
|
},
|
|
output: {
|
|
serverDir: "{{ rootDir }}/functions"
|
|
},
|
|
rollupConfig: {
|
|
output: {
|
|
entryFileNames: "path.js",
|
|
format: "esm"
|
|
}
|
|
},
|
|
hooks: {
|
|
async "compiled"(nitro) {
|
|
await move(resolve(nitro.options.output.serverDir, "path.js"), resolve(nitro.options.output.serverDir, "[[path]].js"));
|
|
await move(resolve(nitro.options.output.serverDir, "path.js.map"), resolve(nitro.options.output.serverDir, "[[path]].js.map"));
|
|
}
|
|
}
|
|
});
|
|
|
|
const deno = defineNitroPreset({
|
|
entry: "#internal/nitro/entries/deno",
|
|
node: false,
|
|
noExternals: true,
|
|
serveStatic: "deno",
|
|
commands: {
|
|
preview: "",
|
|
deploy: "cd ./ && deployctl deploy --project=<project_name> server/index.ts"
|
|
},
|
|
rollupConfig: {
|
|
preserveEntrySignatures: false,
|
|
external: [
|
|
"https://deno.land/std/http/server.ts"
|
|
],
|
|
output: {
|
|
entryFileNames: "index.ts",
|
|
manualChunks: () => "index",
|
|
format: "esm"
|
|
}
|
|
}
|
|
});
|
|
|
|
const digitalOcean = defineNitroPreset({
|
|
extends: "node-server"
|
|
});
|
|
|
|
const firebase = defineNitroPreset({
|
|
entry: "#internal/nitro/entries/firebase",
|
|
commands: {
|
|
deploy: "npx firebase deploy"
|
|
},
|
|
hooks: {
|
|
async "compiled"(ctx) {
|
|
await writeRoutes(ctx);
|
|
}
|
|
}
|
|
});
|
|
async function writeRoutes(nitro) {
|
|
if (!fse.existsSync(join(nitro.options.rootDir, "firebase.json"))) {
|
|
const firebase2 = {
|
|
functions: {
|
|
source: relative(nitro.options.rootDir, nitro.options.output.serverDir)
|
|
},
|
|
hosting: [
|
|
{
|
|
site: "<your_project_id>",
|
|
public: relative(nitro.options.rootDir, nitro.options.output.publicDir),
|
|
cleanUrls: true,
|
|
rewrites: [
|
|
{
|
|
source: "**",
|
|
function: "server"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
};
|
|
await writeFile$1(resolve(nitro.options.rootDir, "firebase.json"), JSON.stringify(firebase2));
|
|
}
|
|
const _require = createRequire(import.meta.url);
|
|
const jsons = await globby(join(nitro.options.output.serverDir, "node_modules/**/package.json"));
|
|
const prefixLength = `${nitro.options.output.serverDir}/node_modules/`.length;
|
|
const suffixLength = "/package.json".length;
|
|
const dependencies = jsons.reduce((obj, packageJson) => {
|
|
const dirname = packageJson.slice(prefixLength, -suffixLength);
|
|
if (!dirname.includes("node_modules")) {
|
|
obj[dirname] = _require(packageJson).version;
|
|
}
|
|
return obj;
|
|
}, {});
|
|
let nodeVersion = "14";
|
|
try {
|
|
const currentNodeVersion = fse.readJSONSync(join(nitro.options.rootDir, "package.json")).engines.node;
|
|
if (["16", "14"].includes(currentNodeVersion)) {
|
|
nodeVersion = currentNodeVersion;
|
|
}
|
|
} catch {
|
|
const currentNodeVersion = process.versions.node.slice(0, 2);
|
|
if (["16", "14"].includes(currentNodeVersion)) {
|
|
nodeVersion = currentNodeVersion;
|
|
}
|
|
}
|
|
const getPackageVersion = async (id) => {
|
|
const pkg = await readPackageJSON(id, { url: nitro.options.nodeModulesDirs });
|
|
return pkg.version;
|
|
};
|
|
await writeFile$1(
|
|
resolve(nitro.options.output.serverDir, "package.json"),
|
|
JSON.stringify(
|
|
{
|
|
private: true,
|
|
type: "module",
|
|
main: "./index.mjs",
|
|
dependencies: {
|
|
"firebase-functions-test": "latest",
|
|
"firebase-admin": await getPackageVersion("firebase-admin"),
|
|
"firebase-functions": await getPackageVersion("firebase-functions"),
|
|
...dependencies
|
|
},
|
|
engines: { node: nodeVersion }
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
);
|
|
}
|
|
|
|
const heroku = defineNitroPreset({
|
|
extends: "node-server"
|
|
});
|
|
|
|
const layer0 = defineNitroPreset({
|
|
extends: "node",
|
|
commands: {
|
|
deploy: "cd ./ && npm run deploy",
|
|
preview: "cd ./ && npm run preview"
|
|
},
|
|
hooks: {
|
|
async "compiled"(nitro) {
|
|
const layer0Config = {
|
|
connector: "./layer0",
|
|
name: "nitro-app",
|
|
routes: "routes.js",
|
|
backends: {},
|
|
includeFiles: {
|
|
"public/**/*": true,
|
|
"server/**/*": true
|
|
}
|
|
};
|
|
const configPath = resolve(nitro.options.output.dir, "layer0.config.js");
|
|
await writeFile(configPath, `module.exports = ${JSON.stringify(layer0Config, null, 2)}`);
|
|
const routerPath = resolve(nitro.options.output.dir, "routes.js");
|
|
await writeFile(routerPath, routesTemplate());
|
|
const connectorPath = resolve(nitro.options.output.dir, "layer0/prod.js");
|
|
await writeFile(connectorPath, entryTemplate());
|
|
const pkgJSON = {
|
|
private: true,
|
|
scripts: {
|
|
deploy: "npm install && 0 deploy",
|
|
preview: "npm install && 0 build && 0 run -p"
|
|
},
|
|
devDependencies: {
|
|
"@layer0/cli": "^4.13.2",
|
|
"@layer0/core": "^4.13.2"
|
|
}
|
|
};
|
|
await writeFile(resolve(nitro.options.output.dir, "package.json"), JSON.stringify(pkgJSON, null, 2));
|
|
}
|
|
}
|
|
});
|
|
async function writeFile(path, contents) {
|
|
await promises.mkdir(dirname(path), { recursive: true });
|
|
await promises.writeFile(path, contents, "utf-8");
|
|
}
|
|
function entryTemplate() {
|
|
return `
|
|
const http = require('http')
|
|
|
|
module.exports = async function prod(port) {
|
|
const { handler } = await import('../server/index.mjs')
|
|
const server = http.createServer(handler)
|
|
server.listen(port)
|
|
}
|
|
`.trim();
|
|
}
|
|
function routesTemplate() {
|
|
return `
|
|
import { Router } from '@layer0/core'
|
|
|
|
const router = new Router()
|
|
export default router
|
|
|
|
router.fallback(({ renderWithApp }) => {
|
|
renderWithApp()
|
|
})
|
|
`.trim();
|
|
}
|
|
|
|
const netlify = defineNitroPreset({
|
|
extends: "aws-lambda",
|
|
entry: "#internal/nitro/entries/netlify",
|
|
output: {
|
|
dir: "{{ rootDir }}/.netlify/functions-internal",
|
|
publicDir: "{{ rootDir }}/dist"
|
|
},
|
|
rollupConfig: {
|
|
output: {
|
|
entryFileNames: "server.mjs"
|
|
}
|
|
},
|
|
hooks: {
|
|
async "compiled"(nitro) {
|
|
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 promises.writeFile(serverCJSPath, serverJSCode);
|
|
}
|
|
}
|
|
});
|
|
const netlifyBuilder = defineNitroPreset({
|
|
extends: "netlify",
|
|
entry: "#internal/nitro/entries/netlify-builder"
|
|
});
|
|
const netlifyEdge = defineNitroPreset({
|
|
extends: "base-worker",
|
|
entry: "#internal/nitro/entries/netlify-edge",
|
|
output: {
|
|
serverDir: "{{ rootDir }}/.netlify/edge-functions",
|
|
publicDir: "{{ rootDir }}/dist"
|
|
},
|
|
rollupConfig: {
|
|
output: {
|
|
entryFileNames: "server.js",
|
|
format: "esm"
|
|
}
|
|
},
|
|
hooks: {
|
|
async "compiled"(nitro) {
|
|
const manifest = {
|
|
version: 1,
|
|
functions: [
|
|
{
|
|
function: "server",
|
|
pattern: "^.*$"
|
|
}
|
|
]
|
|
};
|
|
const manifestPath = join(nitro.options.rootDir, ".netlify/edge-functions/manifest.json");
|
|
await promises.mkdir(dirname(manifestPath), { recursive: true });
|
|
await promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
}
|
|
}
|
|
});
|
|
async function writeRedirects(nitro) {
|
|
const redirectsPath = join(nitro.options.output.publicDir, "_redirects");
|
|
let contents = "/* /.netlify/functions/server 200";
|
|
for (const [key] of Object.entries(nitro.options.routeRules).filter(([_, routeRules]) => routeRules.cache && (routeRules.cache?.static || routeRules.cache?.swr))) {
|
|
contents = `${key.replace("/**", "/*")} /.netlify/builders/server 200
|
|
` + contents;
|
|
}
|
|
for (const [key, routeRules] of Object.entries(nitro.options.routeRules).filter(([_, routeRules2]) => routeRules2.redirect)) {
|
|
let code = routeRules.redirect.statusCode;
|
|
code = { 307: 302, 308: 301 }[code] || code;
|
|
contents = `${key.replace("/**", "/*")} ${routeRules.redirect.to} ${code}
|
|
` + contents;
|
|
}
|
|
if (existsSync(redirectsPath)) {
|
|
const currentRedirects = await promises.readFile(redirectsPath, "utf-8");
|
|
if (currentRedirects.match(/^\/\* /m)) {
|
|
nitro.logger.info("Not adding Nitro fallback to `_redirects` (as an existing fallback was found).");
|
|
return;
|
|
}
|
|
nitro.logger.info("Adding Nitro fallback to `_redirects` to handle all unmatched routes.");
|
|
contents = currentRedirects + "\n" + contents;
|
|
}
|
|
await promises.writeFile(redirectsPath, contents);
|
|
}
|
|
async function writeHeaders(nitro) {
|
|
const headersPath = join(nitro.options.output.publicDir, "_headers");
|
|
let contents = "";
|
|
for (const [path, routeRules] of Object.entries(nitro.options.routeRules).filter(([_, routeRules2]) => routeRules2.headers)) {
|
|
const headers = [
|
|
path.replace("/**", "/*"),
|
|
...Object.entries({ ...routeRules.headers }).map(([header, value]) => ` ${header}: ${value}`)
|
|
].join("\n");
|
|
contents += headers + "\n";
|
|
}
|
|
if (existsSync(headersPath)) {
|
|
const currentHeaders = await promises.readFile(headersPath, "utf-8");
|
|
if (currentHeaders.match(/^\/\* /m)) {
|
|
nitro.logger.info("Not adding Nitro fallback to `_headers` (as an existing fallback was found).");
|
|
return;
|
|
}
|
|
nitro.logger.info("Adding Nitro fallback to `_headers` to handle all unmatched routes.");
|
|
contents = currentHeaders + "\n" + contents;
|
|
}
|
|
await promises.writeFile(headersPath, contents);
|
|
}
|
|
|
|
const nitroDev = defineNitroPreset({
|
|
extends: "node",
|
|
entry: "#internal/nitro/entries/nitro-dev",
|
|
output: {
|
|
serverDir: "{{ buildDir }}/dev"
|
|
},
|
|
externals: { trace: false },
|
|
inlineDynamicImports: true,
|
|
sourceMap: true
|
|
});
|
|
|
|
const nitroPrerender = defineNitroPreset({
|
|
extends: "node",
|
|
entry: "#internal/nitro/entries/nitro-prerenderer",
|
|
output: {
|
|
serverDir: "{{ buildDir }}/prerender"
|
|
},
|
|
commands: {
|
|
preview: "npx serve -s ./public"
|
|
},
|
|
externals: { trace: false }
|
|
});
|
|
|
|
const cli = defineNitroPreset({
|
|
extends: "node",
|
|
entry: "#internal/nitro/entries/cli",
|
|
commands: {
|
|
preview: "Run with node ./server/index.mjs [route]"
|
|
}
|
|
});
|
|
|
|
const nodeServer = defineNitroPreset({
|
|
extends: "node",
|
|
entry: "#internal/nitro/entries/node-server",
|
|
serveStatic: true,
|
|
commands: {
|
|
preview: "node ./server/index.mjs"
|
|
}
|
|
});
|
|
const nodeCluster = defineNitroPreset({
|
|
extends: "node-server",
|
|
entry: "#internal/nitro/entries/node-cluster"
|
|
});
|
|
|
|
const node = defineNitroPreset({
|
|
entry: "#internal/nitro/entries/node"
|
|
});
|
|
|
|
const renderCom = defineNitroPreset({
|
|
extends: "node-server"
|
|
});
|
|
|
|
const scriptTemplate = (baseURL = "/") => `
|
|
<script>
|
|
async function register () {
|
|
const registration = await navigator.serviceWorker.register('${joinURL(baseURL, "sw.js")}')
|
|
await navigator.serviceWorker.ready
|
|
registration.active.addEventListener('statechange', (event) => {
|
|
if (event.target.state === 'activated') {
|
|
window.location.reload()
|
|
}
|
|
})
|
|
}
|
|
if ('serviceWorker' in navigator) {
|
|
if (location.hostname !== 'localhost' && location.protocol === 'http:') {
|
|
location.replace(location.href.replace('http://', 'https://'))
|
|
} else {
|
|
register()
|
|
}
|
|
}
|
|
<\/script>
|
|
`;
|
|
const htmlTemplate = (baseURL = "/") => `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<link rel="prefetch" href="${joinURL(baseURL, "sw.js")}">
|
|
<link rel="prefetch" href="${joinURL(baseURL, "server/index.mjs")}">
|
|
${scriptTemplate(baseURL)}
|
|
</head>
|
|
<body>
|
|
Initializing nitro service worker...
|
|
</body>
|
|
</html>`;
|
|
const serviceWorker = defineNitroPreset(() => {
|
|
return {
|
|
extends: "base-worker",
|
|
entry: "#internal/nitro/entries/service-worker",
|
|
output: {
|
|
serverDir: "{{ output.dir }}/public/server"
|
|
},
|
|
commands: {
|
|
preview: "npx serve ./public"
|
|
},
|
|
hooks: {
|
|
"prerender:generate"(route, nitro) {
|
|
const script = scriptTemplate(nitro.options.baseURL);
|
|
route.contents = route.contents.replace("</head>", `${script}
|
|
</head>`);
|
|
},
|
|
async "compiled"(nitro) {
|
|
await promises.writeFile(resolve(nitro.options.output.publicDir, "sw.js"), `self.importScripts('${joinURL(nitro.options.baseURL, "server/index.mjs")}');`, "utf8");
|
|
const html = htmlTemplate(nitro.options.baseURL);
|
|
if (!existsSync(resolve(nitro.options.output.publicDir, "index.html"))) {
|
|
await promises.writeFile(resolve(nitro.options.output.publicDir, "index.html"), html, "utf8");
|
|
}
|
|
if (!existsSync(resolve(nitro.options.output.publicDir, "200.html"))) {
|
|
await promises.writeFile(resolve(nitro.options.output.publicDir, "200.html"), html, "utf8");
|
|
}
|
|
if (!existsSync(resolve(nitro.options.output.publicDir, "404.html"))) {
|
|
await promises.writeFile(resolve(nitro.options.output.publicDir, "404.html"), html, "utf8");
|
|
}
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
const stormkit = defineNitroPreset({
|
|
entry: "#internal/nitro/entries/stormkit",
|
|
output: {
|
|
dir: "{{ rootDir }}/.stormkit"
|
|
}
|
|
});
|
|
|
|
const vercel = defineNitroPreset({
|
|
extends: "node",
|
|
entry: "#internal/nitro/entries/vercel",
|
|
output: {
|
|
dir: "{{ rootDir }}/.vercel/output",
|
|
serverDir: "{{ output.dir }}/functions/__nitro.func",
|
|
publicDir: "{{ output.dir }}/static"
|
|
},
|
|
commands: {
|
|
deploy: "",
|
|
preview: ""
|
|
},
|
|
hooks: {
|
|
async "compiled"(nitro) {
|
|
const buildConfigPath = resolve(nitro.options.output.dir, "config.json");
|
|
const buildConfig = generateBuildConfig(nitro);
|
|
await writeFile$1(buildConfigPath, JSON.stringify(buildConfig, null, 2));
|
|
const functionConfigPath = resolve(nitro.options.output.serverDir, ".vc-config.json");
|
|
const functionConfig = {
|
|
runtime: "nodejs16.x",
|
|
handler: "index.mjs",
|
|
launcherType: "Nodejs",
|
|
shouldAddHelpers: false
|
|
};
|
|
await writeFile$1(functionConfigPath, JSON.stringify(functionConfig, null, 2));
|
|
for (const [key, value] of Object.entries(nitro.options.routeRules).filter(([_, value2]) => value2.cache && (value2.cache.swr || value2.cache.static))) {
|
|
if (!value.cache) {
|
|
continue;
|
|
}
|
|
const funcPrefix = resolve(nitro.options.output.serverDir, ".." + generateEndpoint(key));
|
|
await fsp$1.mkdir(dirname(funcPrefix), { recursive: true });
|
|
await fsp$1.symlink("./" + relative(dirname(funcPrefix), nitro.options.output.serverDir), funcPrefix + ".func", "junction");
|
|
await writeFile$1(funcPrefix + ".prerender-config.json", JSON.stringify({
|
|
expiration: value.cache.static ? false : typeof value.cache.swr === "number" ? value.cache.swr : 60,
|
|
allowQuery: key.includes("/**") ? ["url"] : void 0
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
const vercelEdge = defineNitroPreset({
|
|
extends: "base-worker",
|
|
entry: "#internal/nitro/entries/vercel-edge",
|
|
output: {
|
|
dir: "{{ rootDir }}/.vercel/output",
|
|
serverDir: "{{ output.dir }}/functions/__nitro.func",
|
|
publicDir: "{{ output.dir }}/static"
|
|
},
|
|
commands: {
|
|
deploy: "",
|
|
preview: ""
|
|
},
|
|
rollupConfig: {
|
|
output: {
|
|
format: "module"
|
|
}
|
|
},
|
|
hooks: {
|
|
async "compiled"(nitro) {
|
|
const buildConfigPath = resolve(nitro.options.output.dir, "config.json");
|
|
const buildConfig = generateBuildConfig(nitro);
|
|
await writeFile$1(buildConfigPath, JSON.stringify(buildConfig, null, 2));
|
|
const functionConfigPath = resolve(nitro.options.output.serverDir, ".vc-config.json");
|
|
const functionConfig = {
|
|
runtime: "edge",
|
|
entrypoint: "index.mjs"
|
|
};
|
|
await writeFile$1(functionConfigPath, JSON.stringify(functionConfig, null, 2));
|
|
}
|
|
}
|
|
});
|
|
function generateBuildConfig(nitro) {
|
|
return defu(nitro.options.vercel?.config, {
|
|
version: 3,
|
|
overrides: Object.fromEntries(
|
|
(nitro._prerenderedRoutes?.filter((r) => r.fileName !== r.route) || []).map(
|
|
({ route, fileName }) => [withoutLeadingSlash(fileName), { path: route.replace(/^\//, "") }]
|
|
)
|
|
),
|
|
routes: [
|
|
...Object.entries(nitro.options.routeRules).filter(([_, routeRules]) => routeRules.redirect || routeRules.headers).map(([path, routeRules]) => {
|
|
let route = {
|
|
src: path.replace("/**", "/.*")
|
|
};
|
|
if (routeRules.redirect) {
|
|
route = defu(route, {
|
|
status: routeRules.redirect.statusCode,
|
|
headers: { Location: routeRules.redirect.to }
|
|
});
|
|
}
|
|
if (routeRules.headers) {
|
|
route = defu(route, { headers: routeRules.headers });
|
|
}
|
|
return route;
|
|
}),
|
|
...nitro.options.publicAssets.filter((asset) => !asset.fallthrough).map((asset) => asset.baseURL).map((baseURL) => ({
|
|
src: baseURL + "(.*)",
|
|
headers: {
|
|
"cache-control": "public,max-age=31536000,immutable"
|
|
},
|
|
continue: true
|
|
})),
|
|
{
|
|
handle: "filesystem"
|
|
},
|
|
...Object.entries(nitro.options.routeRules).filter(([key, value]) => value.cache && (value.cache.swr || value.cache.static) && key.includes("/**")).map(([key]) => ({
|
|
src: key.replace(/^(.*)\/\*\*/, "(?<url>$1/.*)"),
|
|
dest: generateEndpoint(key) + "?url=$url"
|
|
})),
|
|
{
|
|
src: "/(.*)",
|
|
dest: "/__nitro"
|
|
}
|
|
]
|
|
});
|
|
}
|
|
function generateEndpoint(url) {
|
|
return url.includes("/**") ? "/__nitro-" + withoutLeadingSlash(url.replace(/\/\*\*.*/, "").replace(/[^a-z]/g, "-")) : url;
|
|
}
|
|
|
|
const cleavr = defineNitroPreset({
|
|
extends: "node-server"
|
|
});
|
|
|
|
const _PRESETS = {
|
|
__proto__: null,
|
|
awsLambda: awsLambda,
|
|
azureFunctions: azureFunctions,
|
|
azure: azure,
|
|
baseWorker: baseWorker,
|
|
cloudflare: cloudflare,
|
|
cloudflarePages: cloudflarePages,
|
|
deno: deno,
|
|
digitalOcean: digitalOcean,
|
|
firebase: firebase,
|
|
heroku: heroku,
|
|
layer0: layer0,
|
|
netlify: netlify,
|
|
netlifyBuilder: netlifyBuilder,
|
|
netlifyEdge: netlifyEdge,
|
|
nitroDev: nitroDev,
|
|
nitroPrerender: nitroPrerender,
|
|
cli: cli,
|
|
nodeServer: nodeServer,
|
|
nodeCluster: nodeCluster,
|
|
node: node,
|
|
renderCom: renderCom,
|
|
serviceWorker: serviceWorker,
|
|
stormkit: stormkit,
|
|
vercel: vercel,
|
|
vercelEdge: vercelEdge,
|
|
cleavr: cleavr
|
|
};
|
|
|
|
const nitroImports = [
|
|
{
|
|
from: "#internal/nitro",
|
|
imports: [
|
|
"defineCachedFunction",
|
|
"defineCachedEventHandler",
|
|
"cachedFunction",
|
|
"cachedEventHandler",
|
|
"useRuntimeConfig",
|
|
"useStorage",
|
|
"useNitroApp",
|
|
"defineNitroPlugin",
|
|
"nitroPlugin",
|
|
"defineRenderHandler",
|
|
"getRouteRules"
|
|
]
|
|
}
|
|
];
|
|
|
|
const NitroDefaults = {
|
|
debug: isDebug,
|
|
logLevel: isTest ? 1 : 3,
|
|
runtimeConfig: { app: {}, nitro: {} },
|
|
scanDirs: [],
|
|
buildDir: ".nitro",
|
|
output: {
|
|
dir: "{{ rootDir }}/.output",
|
|
serverDir: "{{ output.dir }}/server",
|
|
publicDir: "{{ output.dir }}/public"
|
|
},
|
|
experimental: {},
|
|
storage: {},
|
|
devStorage: {},
|
|
bundledStorage: [],
|
|
publicAssets: [],
|
|
serverAssets: [],
|
|
plugins: [],
|
|
imports: {
|
|
exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/],
|
|
presets: nitroImports
|
|
},
|
|
virtual: {},
|
|
compressPublicAssets: false,
|
|
dev: false,
|
|
devServer: { watch: [] },
|
|
watchOptions: { ignoreInitial: true },
|
|
devProxy: {},
|
|
baseURL: process.env.NITRO_APP_BASE_URL || "/",
|
|
handlers: [],
|
|
devHandlers: [],
|
|
errorHandler: "#internal/nitro/error",
|
|
routeRules: {},
|
|
prerender: {
|
|
crawlLinks: false,
|
|
ignore: [],
|
|
routes: []
|
|
},
|
|
alias: {
|
|
"#internal/nitro": runtimeDir
|
|
},
|
|
unenv: {},
|
|
analyze: false,
|
|
moduleSideEffects: [
|
|
"unenv/runtime/polyfill/",
|
|
"node-fetch-native/polyfill",
|
|
"node-fetch-native/dist/polyfill"
|
|
],
|
|
replace: {},
|
|
node: true,
|
|
sourceMap: true,
|
|
typescript: {
|
|
generateTsConfig: true,
|
|
internalPaths: false
|
|
},
|
|
nodeModulesDirs: [],
|
|
hooks: {},
|
|
commands: {}
|
|
};
|
|
async function loadOptions(configOverrides = {}) {
|
|
let presetOverride = configOverrides.preset || process.env.NITRO_PRESET;
|
|
const defaultPreset = detectTarget() || "node-server";
|
|
if (configOverrides.dev) {
|
|
presetOverride = "nitro-dev";
|
|
}
|
|
configOverrides = klona(configOverrides);
|
|
const { config, layers } = await loadConfig({
|
|
name: "nitro",
|
|
cwd: configOverrides.rootDir,
|
|
dotenv: configOverrides.dev,
|
|
extend: { extendKey: ["extends", "preset"] },
|
|
overrides: {
|
|
...configOverrides,
|
|
preset: presetOverride
|
|
},
|
|
defaultConfig: {
|
|
preset: defaultPreset
|
|
},
|
|
defaults: NitroDefaults,
|
|
resolve(id) {
|
|
const presets = _PRESETS;
|
|
let matchedPreset = presets[camelCase(id)] || presets[id];
|
|
if (!matchedPreset) {
|
|
return null;
|
|
}
|
|
if (typeof matchedPreset === "function") {
|
|
matchedPreset = matchedPreset();
|
|
}
|
|
return {
|
|
config: matchedPreset
|
|
};
|
|
}
|
|
});
|
|
const options = klona(config);
|
|
options._config = configOverrides;
|
|
options.preset = presetOverride || layers.find((l) => l.config.preset)?.config.preset || defaultPreset;
|
|
options.rootDir = resolve(options.rootDir || ".");
|
|
options.workspaceDir = await findWorkspaceDir(options.rootDir).catch(() => options.rootDir);
|
|
options.srcDir = resolve(options.srcDir || options.rootDir);
|
|
for (const key of ["srcDir", "publicDir", "buildDir"]) {
|
|
options[key] = resolve(options.rootDir, options[key]);
|
|
}
|
|
options.alias = {
|
|
...options.alias,
|
|
"~/": join(options.srcDir, "/"),
|
|
"@/": join(options.srcDir, "/"),
|
|
"~~/": join(options.rootDir, "/"),
|
|
"@@/": join(options.rootDir, "/")
|
|
};
|
|
if (!options.entry) {
|
|
throw new Error(`Nitro entry is missing! Is "${options.preset}" preset correct?`);
|
|
}
|
|
options.entry = resolvePath(options.entry, options);
|
|
options.output.dir = resolvePath(options.output.dir || NitroDefaults.output.dir, options);
|
|
options.output.publicDir = resolvePath(options.output.publicDir || NitroDefaults.output.publicDir, options);
|
|
options.output.serverDir = resolvePath(options.output.serverDir || NitroDefaults.output.serverDir, options);
|
|
options.nodeModulesDirs.push(resolve(options.workspaceDir, "node_modules"));
|
|
options.nodeModulesDirs.push(resolve(options.rootDir, "node_modules"));
|
|
options.nodeModulesDirs.push(resolve(pkgDir, "node_modules"));
|
|
options.nodeModulesDirs = Array.from(new Set(options.nodeModulesDirs.map((dir) => resolve(options.rootDir, dir))));
|
|
if (!options.scanDirs.length) {
|
|
options.scanDirs = [options.srcDir];
|
|
}
|
|
if (options.imports && Array.isArray(options.imports.exclude)) {
|
|
options.imports.exclude.push(options.buildDir);
|
|
}
|
|
if (options.imports) {
|
|
const h3Exports = await resolveModuleExportNames("h3", { url: import.meta.url });
|
|
options.imports.presets.push({
|
|
from: "h3",
|
|
imports: h3Exports.filter((n) => !n.match(/^[A-Z]/) && n !== "use")
|
|
});
|
|
}
|
|
options.routeRules = defu(options.routeRules, options.routes || {});
|
|
const normalizedRules = {};
|
|
for (const path in options.routeRules) {
|
|
const routeConfig = options.routeRules[path];
|
|
const routeRules = {
|
|
...routeConfig,
|
|
redirect: void 0
|
|
};
|
|
if (routeConfig.redirect) {
|
|
routeRules.redirect = {
|
|
to: "/",
|
|
statusCode: 307,
|
|
...typeof routeConfig.redirect === "string" ? { to: routeConfig.redirect } : routeConfig.redirect
|
|
};
|
|
}
|
|
if (routeConfig.cors) {
|
|
routeRules.headers = {
|
|
"access-control-allow-origin": "*",
|
|
"access-control-allowed-methods": "*",
|
|
"access-control-allow-headers": "*",
|
|
"access-control-max-age": "0",
|
|
...routeRules.headers
|
|
};
|
|
}
|
|
if (routeConfig.swr) {
|
|
routeRules.cache = routeRules.cache || {};
|
|
routeRules.cache.swr = true;
|
|
if (typeof routeConfig.swr === "number") {
|
|
routeRules.cache.maxAge = routeConfig.swr;
|
|
}
|
|
}
|
|
if (routeConfig.static) {
|
|
routeRules.cache = routeRules.cache || {};
|
|
routeRules.cache.static = true;
|
|
}
|
|
if (routeConfig.cache === false) {
|
|
routeRules.cache = false;
|
|
}
|
|
normalizedRules[path] = routeRules;
|
|
}
|
|
options.routeRules = normalizedRules;
|
|
options.baseURL = withLeadingSlash(withTrailingSlash(options.baseURL));
|
|
options.runtimeConfig = defu(options.runtimeConfig, {
|
|
app: {
|
|
baseURL: options.baseURL
|
|
},
|
|
nitro: {
|
|
routeRules: options.routeRules
|
|
}
|
|
});
|
|
for (const asset of options.publicAssets) {
|
|
asset.dir = resolve(options.srcDir, asset.dir);
|
|
asset.baseURL = withLeadingSlash(withoutTrailingSlash(asset.baseURL || "/"));
|
|
}
|
|
for (const pkg of ["defu", "h3", "radix3"]) {
|
|
if (!options.alias[pkg]) {
|
|
options.alias[pkg] = await resolvePath$1(pkg, { url: import.meta.url });
|
|
}
|
|
}
|
|
const fsMounts = {
|
|
root: resolve(options.rootDir),
|
|
src: resolve(options.srcDir),
|
|
build: resolve(options.buildDir),
|
|
cache: resolve(options.buildDir, "cache")
|
|
};
|
|
for (const p in fsMounts) {
|
|
options.devStorage[p] = options.devStorage[p] || { driver: "fs", base: fsMounts[p] };
|
|
}
|
|
options.plugins = options.plugins.map((p) => resolvePath(p, options));
|
|
return options;
|
|
}
|
|
function defineNitroConfig(config) {
|
|
return config;
|
|
}
|
|
|
|
async function createNitro(config = {}) {
|
|
const options = await loadOptions(config);
|
|
const nitro = {
|
|
options,
|
|
hooks: createHooks(),
|
|
vfs: {},
|
|
logger: consola.withTag("nitro"),
|
|
scannedHandlers: [],
|
|
close: () => nitro.hooks.callHook("close"),
|
|
storage: void 0
|
|
};
|
|
nitro.storage = await createStorage(nitro);
|
|
nitro.hooks.hook("close", async () => {
|
|
await nitro.storage.dispose();
|
|
});
|
|
if (nitro.options.debug) {
|
|
createDebugger(nitro.hooks, { tag: "nitro" });
|
|
nitro.options.plugins.push("#internal/nitro/debug");
|
|
}
|
|
if (nitro.options.logLevel !== void 0) {
|
|
nitro.logger.level = nitro.options.logLevel;
|
|
}
|
|
nitro.hooks.addHooks(nitro.options.hooks);
|
|
for (const dir of options.scanDirs) {
|
|
const publicDir = resolve(dir, "public");
|
|
if (!existsSync(publicDir)) {
|
|
continue;
|
|
}
|
|
if (options.publicAssets.find((asset) => asset.dir === publicDir)) {
|
|
continue;
|
|
}
|
|
options.publicAssets.push({ dir: publicDir });
|
|
}
|
|
for (const asset of options.publicAssets) {
|
|
asset.baseURL = asset.baseURL || "/";
|
|
const isTopLevel = asset.baseURL === "/";
|
|
asset.fallthrough = asset.fallthrough ?? isTopLevel;
|
|
asset.maxAge = asset.maxAge ?? (isTopLevel ? 0 : 60);
|
|
}
|
|
nitro.options.serverAssets.push({
|
|
baseName: "server",
|
|
dir: resolve(nitro.options.srcDir, "assets")
|
|
});
|
|
const scannedPlugins = await scanPlugins(nitro);
|
|
for (const plugin of scannedPlugins) {
|
|
if (!nitro.options.plugins.find((p) => p === plugin)) {
|
|
nitro.options.plugins.push(plugin);
|
|
}
|
|
}
|
|
if (nitro.options.imports) {
|
|
nitro.unimport = createUnimport(nitro.options.imports);
|
|
nitro.options.virtual["#imports"] = () => nitro.unimport.toExports();
|
|
nitro.options.virtual["#nitro"] = 'export * from "#imports"';
|
|
}
|
|
return nitro;
|
|
}
|
|
|
|
function createVFSHandler(nitro) {
|
|
return eventHandler(async (event) => {
|
|
const vfsEntries = {
|
|
...nitro.vfs,
|
|
...nitro.options.virtual
|
|
};
|
|
const items = Object.keys(vfsEntries).map((key) => {
|
|
const linkClass = event.req.url === `/${encodeURIComponent(key)}` ? "bg-gray-700 text-white" : "hover:bg-gray-800 text-gray-200";
|
|
return `<li class="flex flex-nowrap"><a href="/_vfs/${encodeURIComponent(key)}" class="w-full text-sm px-2 py-1 border-b border-gray-500 ${linkClass}">${key.replace(nitro.options.rootDir, "")}</a></li>`;
|
|
}).join("\n");
|
|
const files = `
|
|
<div>
|
|
<p class="bg-gray-700 text-white text-bold border-b border-gray-500 text-center">virtual files</p>
|
|
<ul class="flex flex-col">${items}</ul>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<div class="m-2">
|
|
<h1 class="text-white">Select a virtual file to inspect</h1>
|
|
</div>
|
|
`;
|
|
}
|
|
return `
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@unocss/reset/tailwind.min.css" />
|
|
<link rel="stylesheet" data-name="vs/editor/editor.main" href="${vsUrl}/editor/editor.main.min.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/@unocss/runtime"><\/script>
|
|
</head>
|
|
<body class="bg-[#1E1E1E]">
|
|
<div class="flex">
|
|
${files}
|
|
${file}
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
});
|
|
}
|
|
const monacoVersion = "0.30.0";
|
|
const monacoUrl = `https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/${monacoVersion}/min`;
|
|
const vsUrl = `${monacoUrl}/vs`;
|
|
const editorTemplate = (options) => `
|
|
<div id="editor" class="min-h-screen flex-1"></div>
|
|
<script src="${vsUrl}/loader.min.js"><\/script>
|
|
<script>
|
|
require.config({ paths: { vs: '${vsUrl}' } })
|
|
|
|
const proxy = URL.createObjectURL(new Blob([\`
|
|
self.MonacoEnvironment = { baseUrl: '${monacoUrl}' }
|
|
importScripts('${vsUrl}/base/worker/workerMain.min.js')
|
|
\`], { type: 'text/javascript' }))
|
|
window.MonacoEnvironment = { getWorkerUrl: () => proxy }
|
|
|
|
require(['vs/editor/editor.main'], function () {
|
|
monaco.editor.create(document.getElementById('editor'), ${JSON.stringify(options)})
|
|
})
|
|
<\/script>
|
|
`;
|
|
|
|
function errorHandler(error, event) {
|
|
event.res.setHeader("Content-Type", "text/html; charset=UTF-8");
|
|
event.res.statusCode = 503;
|
|
event.res.statusMessage = "Server Unavailable";
|
|
let body;
|
|
let title;
|
|
if (error) {
|
|
title = `${event.res.statusCode} ${event.res.statusMessage}`;
|
|
body = `<code><pre>${error.stack}</pre></code>`;
|
|
} else {
|
|
title = "Reloading server...";
|
|
body = "<progress></progress><script>document.querySelector('progress').indeterminate=true<\/script>";
|
|
}
|
|
event.res.end(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
${error ? "" : '<meta http-equiv="refresh" content="2">'}
|
|
<title>${title}</title>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico/css/pico.min.css">
|
|
</head>
|
|
<body>
|
|
<main class="container">
|
|
<article>
|
|
<header>
|
|
<h2>${title}</h2>
|
|
</header>
|
|
${body}
|
|
<footer>
|
|
Check console logs for more information.
|
|
</footer>
|
|
</article>
|
|
</main>
|
|
</body>
|
|
</html>
|
|
`);
|
|
}
|
|
|
|
function initWorker(filename) {
|
|
if (!existsSync(filename)) {
|
|
return null;
|
|
}
|
|
return new Promise((resolve2, reject) => {
|
|
const worker = new Worker(filename);
|
|
worker.once("exit", (code) => {
|
|
reject(new Error(code ? "[worker] exited with code: " + code : "[worker] exited"));
|
|
});
|
|
worker.once("error", (err) => {
|
|
err.message = "[worker init] " + err.message;
|
|
reject(err);
|
|
});
|
|
const addressListener = (event) => {
|
|
if (!event || !event.address) {
|
|
return;
|
|
}
|
|
worker.off("message", addressListener);
|
|
resolve2({
|
|
worker,
|
|
address: event.address
|
|
});
|
|
};
|
|
worker.on("message", addressListener);
|
|
});
|
|
}
|
|
async function killWorker(worker) {
|
|
if (!worker) {
|
|
return;
|
|
}
|
|
if (worker.worker) {
|
|
worker.worker.removeAllListeners();
|
|
await worker.worker.terminate();
|
|
worker.worker = null;
|
|
}
|
|
if (worker.address.socketPath && existsSync(worker.address.socketPath)) {
|
|
await promises.rm(worker.address.socketPath);
|
|
}
|
|
}
|
|
function createDevServer(nitro) {
|
|
const workerEntry = resolve(nitro.options.output.dir, nitro.options.output.serverDir, "index.mjs");
|
|
const errorHandler$1 = nitro.options.devErrorHandler || errorHandler;
|
|
let lastError = null;
|
|
let reloadPromise = null;
|
|
let currentWorker = null;
|
|
async function _reload() {
|
|
const oldWorker = currentWorker;
|
|
currentWorker = null;
|
|
await killWorker(oldWorker);
|
|
currentWorker = await initWorker(workerEntry);
|
|
}
|
|
const reload = debounce(() => {
|
|
reloadPromise = _reload().then(() => {
|
|
lastError = null;
|
|
}).catch((error) => {
|
|
console.error("[worker reload]", error);
|
|
lastError = error;
|
|
}).finally(() => {
|
|
reloadPromise = null;
|
|
});
|
|
return reloadPromise;
|
|
});
|
|
nitro.hooks.hook("dev:reload", reload);
|
|
const app = createApp();
|
|
for (const handler of nitro.options.devHandlers) {
|
|
app.use(handler.route || "/", handler.handler);
|
|
}
|
|
app.use("/_vfs", createVFSHandler(nitro));
|
|
for (const asset of nitro.options.publicAssets) {
|
|
const url = joinURL(nitro.options.runtimeConfig.app.baseURL, asset.baseURL);
|
|
app.use(url, fromNodeMiddleware(serveStatic(asset.dir)));
|
|
if (!asset.fallthrough) {
|
|
app.use(url, fromNodeMiddleware(servePlaceholder()));
|
|
}
|
|
}
|
|
for (const route of Object.keys(nitro.options.devProxy).sort().reverse()) {
|
|
let opts = nitro.options.devProxy[route];
|
|
if (typeof opts === "string") {
|
|
opts = { target: opts };
|
|
}
|
|
const proxy2 = createProxy(opts);
|
|
app.use(route, eventHandler(async (event) => {
|
|
await proxy2.handle(event);
|
|
}));
|
|
}
|
|
const proxy = createProxy();
|
|
proxy.proxy.on("proxyReq", (proxyReq, req) => {
|
|
if (req.socket.remoteAddress) {
|
|
proxyReq.setHeader("X-Forwarded-For", req.socket.remoteAddress);
|
|
}
|
|
if (req.socket.remotePort) {
|
|
proxyReq.setHeader("X-Forwarded-Port", req.socket.remotePort);
|
|
}
|
|
if (req.socket.remoteFamily) {
|
|
proxyReq.setHeader("X-Forwarded-Proto", req.socket.remoteFamily);
|
|
}
|
|
});
|
|
app.use(eventHandler(async (event) => {
|
|
await reloadPromise;
|
|
const address = currentWorker?.address;
|
|
if (!address || address.socketPath && !existsSync(address.socketPath)) {
|
|
return errorHandler$1(lastError, event);
|
|
}
|
|
await proxy.handle(event, { target: address }).catch((err) => {
|
|
lastError = err;
|
|
throw err;
|
|
});
|
|
}));
|
|
let listeners = [];
|
|
const _listen = async (port, opts) => {
|
|
const listener = await listen(toNodeListener(app), { port, ...opts });
|
|
listeners.push(listener);
|
|
return listener;
|
|
};
|
|
let watcher = null;
|
|
if (nitro.options.devServer.watch.length) {
|
|
watcher = watch(nitro.options.devServer.watch, nitro.options.watchOptions);
|
|
watcher.on("add", reload).on("change", reload);
|
|
}
|
|
async function close() {
|
|
if (watcher) {
|
|
await watcher.close();
|
|
}
|
|
await killWorker(currentWorker);
|
|
await Promise.all(listeners.map((l) => l.close()));
|
|
listeners = [];
|
|
}
|
|
nitro.hooks.hook("close", close);
|
|
return {
|
|
reload,
|
|
listen: _listen,
|
|
app,
|
|
close,
|
|
watcher
|
|
};
|
|
}
|
|
function createProxy(defaults = {}) {
|
|
const proxy = httpProxy.createProxy();
|
|
const handle = (event, opts = {}) => {
|
|
return new Promise((resolve2, reject) => {
|
|
proxy.web(event.req, event.res, { ...defaults, ...opts }, (error) => {
|
|
if (error.code !== "ECONNRESET") {
|
|
reject(error);
|
|
}
|
|
resolve2();
|
|
});
|
|
});
|
|
};
|
|
return {
|
|
proxy,
|
|
handle
|
|
};
|
|
}
|
|
|
|
const allowedExtensions = /* @__PURE__ */ new Set(["", ".json"]);
|
|
async function prerender(nitro) {
|
|
if (nitro.options.noPublicDir) {
|
|
console.warn("[nitro] Skipping prerender since `noPublicDir` option is enabled.");
|
|
return;
|
|
}
|
|
const routes = new Set(nitro.options.prerender.routes);
|
|
const prerenderRulePaths = Object.entries(nitro.options.routeRules).filter(([path, options]) => options.prerender && !path.includes("*")).map((e) => e[0]);
|
|
for (const route of prerenderRulePaths) {
|
|
routes.add(route);
|
|
}
|
|
if (nitro.options.prerender.crawlLinks && !routes.size) {
|
|
routes.add("/");
|
|
}
|
|
await nitro.hooks.callHook("prerender:routes", routes);
|
|
if (!routes.size) {
|
|
return;
|
|
}
|
|
nitro.logger.info("Initializing prerenderer");
|
|
nitro._prerenderedRoutes = [];
|
|
const nitroRenderer = await createNitro({
|
|
...nitro.options._config,
|
|
rootDir: nitro.options.rootDir,
|
|
logLevel: 0,
|
|
preset: "nitro-prerender"
|
|
});
|
|
await build(nitroRenderer);
|
|
const serverEntrypoint = resolve(nitroRenderer.options.output.serverDir, "index.mjs");
|
|
const { localFetch } = await import(pathToFileURL(serverEntrypoint).href);
|
|
const _routeRulesMatcher = toRouteMatcher(createRouter({ routes: nitro.options.routeRules }));
|
|
const _getRouteRules = (path) => defu({}, ..._routeRulesMatcher.matchAll(path).reverse());
|
|
const generatedRoutes = /* @__PURE__ */ new Set();
|
|
const canPrerender = (route = "/") => {
|
|
if (generatedRoutes.has(route)) {
|
|
return false;
|
|
}
|
|
if (route.length > 250) {
|
|
return false;
|
|
}
|
|
for (const ignore of nitro.options.prerender.ignore) {
|
|
if (route.startsWith(ignore)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (_getRouteRules(route).prerender === false) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
const generateRoute = async (route) => {
|
|
const start = Date.now();
|
|
if (!canPrerender(route)) {
|
|
return;
|
|
}
|
|
generatedRoutes.add(route);
|
|
routes.delete(route);
|
|
const _route = { route };
|
|
const res = await localFetch(withBase(route, nitro.options.baseURL), { headers: { "x-nitro-prerender": route } });
|
|
_route.data = await res.arrayBuffer();
|
|
Object.defineProperty(_route, "contents", {
|
|
get: () => {
|
|
if (!_route._contents) {
|
|
_route._contents = new TextDecoder("utf-8").decode(new Uint8Array(_route.data));
|
|
}
|
|
return _route._contents;
|
|
},
|
|
set(value) {
|
|
_route._contents = value;
|
|
_route.data = new TextEncoder().encode(value);
|
|
}
|
|
});
|
|
if (res.status !== 200) {
|
|
_route.error = new Error(`[${res.status}] ${res.statusText}`);
|
|
_route.error.statusCode = res.status;
|
|
_route.error.statusMessage = res.statusText;
|
|
}
|
|
const isImplicitHTML = !route.endsWith(".html") && (res.headers.get("content-type") || "").includes("html");
|
|
const routeWithIndex = route.endsWith("/") ? route + "index" : route;
|
|
_route.fileName = isImplicitHTML ? joinURL(route, "index.html") : routeWithIndex;
|
|
_route.fileName = withoutBase(_route.fileName, nitro.options.baseURL);
|
|
await nitro.hooks.callHook("prerender:generate", _route, nitro);
|
|
if (_route.skip || _route.error) {
|
|
return;
|
|
}
|
|
const filePath = join(nitro.options.output.publicDir, _route.fileName);
|
|
await writeFile$1(filePath, Buffer.from(_route.data));
|
|
nitro._prerenderedRoutes.push(_route);
|
|
if (!_route.error && isImplicitHTML) {
|
|
const extractedLinks = extractLinks(_route.contents, route, res, nitro.options.prerender.crawlLinks);
|
|
for (const _link of extractedLinks) {
|
|
if (canPrerender(_link)) {
|
|
routes.add(_link);
|
|
}
|
|
}
|
|
}
|
|
_route.generateTimeMS = Date.now() - start;
|
|
return _route;
|
|
};
|
|
nitro.logger.info(
|
|
nitro.options.prerender.crawlLinks ? `Prerendering ${routes.size} initial routes with crawler` : `Prerendering ${routes.size} routes`
|
|
);
|
|
for (let i = 0; i < 100 && routes.size; i++) {
|
|
for (const route of Array.from(routes)) {
|
|
const _route = await generateRoute(route).catch((error) => ({ route, error }));
|
|
if (!_route) {
|
|
continue;
|
|
}
|
|
await nitro.hooks.callHook("prerender:route", _route);
|
|
nitro.logger.log(chalk[_route.error ? "yellow" : "gray"](` \u251C\u2500 ${_route.route} (${_route.generateTimeMS}ms) ${_route.error ? `(${_route.error})` : ""}`));
|
|
}
|
|
}
|
|
if (nitro.options.compressPublicAssets) {
|
|
await compressPublicAssets(nitro);
|
|
}
|
|
}
|
|
const LINK_REGEX = /href=['"]?([^'" >]+)/g;
|
|
function extractLinks(html, from, res, crawlLinks) {
|
|
const links = [];
|
|
const _links = [];
|
|
if (crawlLinks) {
|
|
_links.push(
|
|
...Array.from(html.matchAll(LINK_REGEX)).map((m) => m[1]).filter((link) => allowedExtensions.has(getExtension(link)))
|
|
);
|
|
}
|
|
const header = res.headers.get("x-nitro-prerender") || "";
|
|
_links.push(...header.split(",").map((i) => i.trim()));
|
|
for (const link of _links.filter(Boolean)) {
|
|
const parsed = parseURL(link);
|
|
if (parsed.protocol) {
|
|
continue;
|
|
}
|
|
let { pathname } = parsed;
|
|
if (!pathname.startsWith("/")) {
|
|
const fromURL = new URL(from, "http://localhost");
|
|
pathname = new URL(pathname, fromURL).pathname;
|
|
}
|
|
links.push(pathname);
|
|
}
|
|
return links;
|
|
}
|
|
const EXT_REGEX = /\.[a-z0-9]+$/;
|
|
function getExtension(path) {
|
|
return (path.match(EXT_REGEX) || [])[0] || "";
|
|
}
|
|
|
|
export { GLOB_SCAN_PATTERN as G, createNitro as a, build as b, copyPublicAssets as c, scanMiddleware as d, scanRoutes as e, scanPlugins as f, createDevServer as g, defineNitroConfig as h, prerender as i, defineNitroPreset as j, loadOptions as l, prepare as p, scanHandlers as s, writeTypes as w };
|