astro-ghostcms/.pnpm-store/v3/files/e4/3877bf607516f1de730df20ea0a...

590 lines
22 KiB
Plaintext

import { bold } from "kleur/colors";
import { createRequire } from "module";
import nodeFs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { getPrerenderDefault } from "../../../prerender/utils.js";
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from "../../constants.js";
import { MissingIndexForInternationalization } from "../../errors/errors-data.js";
import { AstroError } from "../../errors/index.js";
import { removeLeadingForwardSlash, slash } from "../../path.js";
import { resolvePages } from "../../util.js";
import { getRouteGenerator } from "./generator.js";
const require2 = createRequire(import.meta.url);
function countOccurrences(needle, haystack) {
let count = 0;
for (const hay of haystack) {
if (hay === needle)
count += 1;
}
return count;
}
function getParts(part, file) {
const result = [];
part.split(/\[(.+?\(.+?\)|.+?)\]/).map((str, i) => {
if (!str)
return;
const dynamic = i % 2 === 1;
const [, content] = dynamic ? /([^(]+)$/.exec(str) || [null, null] : [null, str];
if (!content || dynamic && !/^(\.\.\.)?[a-zA-Z0-9_$]+$/.test(content)) {
throw new Error(`Invalid route ${file} \u2014 parameter name must match /^[a-zA-Z0-9_$]+$/`);
}
result.push({
content,
dynamic,
spread: dynamic && /^\.{3}.+$/.test(content)
});
});
return result;
}
function getPattern(segments, config, addTrailingSlash) {
const base = config.base;
const pathname = segments.map((segment) => {
if (segment.length === 1 && segment[0].spread) {
return "(?:\\/(.*?))?";
} else {
return "\\/" + segment.map((part) => {
if (part.spread) {
return "(.*?)";
} else if (part.dynamic) {
return "([^/]+?)";
} else {
return part.content.normalize().replace(/\?/g, "%3F").replace(/#/g, "%23").replace(/%5B/g, "[").replace(/%5D/g, "]").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
}).join("");
}
}).join("");
const trailing = addTrailingSlash && segments.length ? getTrailingSlashPattern(addTrailingSlash) : "$";
let initial = "\\/";
if (addTrailingSlash === "never" && base !== "/") {
initial = "";
}
return new RegExp(`^${pathname || initial}${trailing}`);
}
function getTrailingSlashPattern(addTrailingSlash) {
if (addTrailingSlash === "always") {
return "\\/$";
}
if (addTrailingSlash === "never") {
return "$";
}
return "\\/?$";
}
function validateSegment(segment, file = "") {
if (!file)
file = segment;
if (/\]\[/.test(segment)) {
throw new Error(`Invalid route ${file} \u2014 parameters must be separated`);
}
if (countOccurrences("[", segment) !== countOccurrences("]", segment)) {
throw new Error(`Invalid route ${file} \u2014 brackets are unbalanced`);
}
if ((/.+\[\.\.\.[^\]]+\]/.test(segment) || /\[\.\.\.[^\]]+\].+/.test(segment)) && file.endsWith(".astro")) {
throw new Error(`Invalid route ${file} \u2014 rest parameter must be a standalone segment`);
}
}
function isSemanticallyEqualSegment(segmentA, segmentB) {
if (segmentA.length !== segmentB.length) {
return false;
}
for (const [index, partA] of segmentA.entries()) {
const partB = segmentB[index];
if (partA.dynamic !== partB.dynamic || partA.spread !== partB.spread) {
return false;
}
if (!partA.dynamic && partA.content !== partB.content) {
return false;
}
}
return true;
}
function routeComparator(a, b) {
const commonLength = Math.min(a.segments.length, b.segments.length);
for (let index = 0; index < commonLength; index++) {
const aSegment = a.segments[index];
const bSegment = b.segments[index];
const aIsStatic = aSegment.every((part) => !part.dynamic && !part.spread);
const bIsStatic = bSegment.every((part) => !part.dynamic && !part.spread);
if (aIsStatic && bIsStatic) {
const aContent = aSegment.map((part) => part.content).join("");
const bContent = bSegment.map((part) => part.content).join("");
if (aContent !== bContent) {
return aContent.localeCompare(bContent);
}
}
if (aIsStatic !== bIsStatic) {
return aIsStatic ? -1 : 1;
}
const aHasSpread = aSegment.some((part) => part.spread);
const bHasSpread = bSegment.some((part) => part.spread);
if (aHasSpread !== bHasSpread) {
return aHasSpread ? 1 : -1;
}
}
if (Math.abs(a.segments.length - b.segments.length) === 1) {
const aEndsInRest = a.segments.at(-1)?.some((part) => part.spread);
const bEndsInRest = b.segments.at(-1)?.some((part) => part.spread);
if (a.segments.length > b.segments.length && !bEndsInRest) {
return 1;
}
if (b.segments.length > a.segments.length && !aEndsInRest) {
return -1;
}
}
if (a.isIndex !== b.isIndex) {
if (a.isIndex) {
const followingBSegment = b.segments.at(a.segments.length);
const followingBSegmentIsStatic = followingBSegment?.every(
(part) => !part.dynamic && !part.spread
);
return followingBSegmentIsStatic ? 1 : -1;
}
const followingASegment = a.segments.at(b.segments.length);
const followingASegmentIsStatic = followingASegment?.every(
(part) => !part.dynamic && !part.spread
);
return followingASegmentIsStatic ? -1 : 1;
}
const aLength = a.isIndex ? a.segments.length + 1 : a.segments.length;
const bLength = b.isIndex ? b.segments.length + 1 : b.segments.length;
if (aLength !== bLength) {
return aLength > bLength ? -1 : 1;
}
if (a.type === "endpoint" !== (b.type === "endpoint")) {
return a.type === "endpoint" ? -1 : 1;
}
return a.route.localeCompare(b.route);
}
function createFileBasedRoutes({ settings, cwd, fsMod }, logger) {
const components = [];
const routes = [];
const validPageExtensions = /* @__PURE__ */ new Set([
".astro",
...SUPPORTED_MARKDOWN_FILE_EXTENSIONS,
...settings.pageExtensions
]);
const validEndpointExtensions = /* @__PURE__ */ new Set([".js", ".ts"]);
const localFs = fsMod ?? nodeFs;
const prerender = getPrerenderDefault(settings.config);
function walk(fs, dir, parentSegments, parentParams) {
let items = [];
const files = fs.readdirSync(dir);
for (const basename of files) {
const resolved = path.join(dir, basename);
const file = slash(path.relative(cwd || fileURLToPath(settings.config.root), resolved));
const isDir = fs.statSync(resolved).isDirectory();
const ext = path.extname(basename);
const name = ext ? basename.slice(0, -ext.length) : basename;
if (name[0] === "_") {
continue;
}
if (basename[0] === "." && basename !== ".well-known") {
continue;
}
if (!isDir && !validPageExtensions.has(ext) && !validEndpointExtensions.has(ext)) {
logger.warn(
null,
`Unsupported file type ${bold(
resolved
)} found. Prefix filename with an underscore (\`_\`) to ignore.`
);
continue;
}
const segment = isDir ? basename : name;
validateSegment(segment, file);
const parts = getParts(segment, file);
const isIndex = isDir ? false : basename.startsWith("index.");
const routeSuffix = basename.slice(basename.indexOf("."), -ext.length);
const isPage = validPageExtensions.has(ext);
items.push({
basename,
ext,
parts,
file: file.replace(/\\/g, "/"),
isDir,
isIndex,
isPage,
routeSuffix
});
}
for (const item of items) {
const segments = parentSegments.slice();
if (item.isIndex) {
if (item.routeSuffix) {
if (segments.length > 0) {
const lastSegment = segments[segments.length - 1].slice();
const lastPart = lastSegment[lastSegment.length - 1];
if (lastPart.dynamic) {
lastSegment.push({
dynamic: false,
spread: false,
content: item.routeSuffix
});
} else {
lastSegment[lastSegment.length - 1] = {
dynamic: false,
spread: false,
content: `${lastPart.content}${item.routeSuffix}`
};
}
segments[segments.length - 1] = lastSegment;
} else {
segments.push(item.parts);
}
}
} else {
segments.push(item.parts);
}
const params = parentParams.slice();
params.push(...item.parts.filter((p) => p.dynamic).map((p) => p.content));
if (item.isDir) {
walk(fsMod ?? fs, path.join(dir, item.basename), segments, params);
} else {
components.push(item.file);
const component = item.file;
const { trailingSlash } = settings.config;
const pattern = getPattern(segments, settings.config, trailingSlash);
const generate = getRouteGenerator(segments, trailingSlash);
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join("/")}` : null;
const route = `/${segments.map(([{ dynamic, content }]) => dynamic ? `[${content}]` : content).join("/")}`.toLowerCase();
routes.push({
route,
isIndex: item.isIndex,
type: item.isPage ? "page" : "endpoint",
pattern,
segments,
params,
component,
generate,
pathname: pathname || void 0,
prerender,
fallbackRoutes: []
});
}
}
}
const { config } = settings;
const pages = resolvePages(config);
if (localFs.existsSync(pages)) {
walk(localFs, fileURLToPath(pages), [], []);
} else if (settings.injectedRoutes.length === 0) {
const pagesDirRootRelative = pages.href.slice(settings.config.root.href.length);
logger.warn(null, `Missing pages directory: ${pagesDirRootRelative}`);
}
return routes;
}
function createInjectedRoutes({ settings, cwd }) {
const { config } = settings;
const prerender = getPrerenderDefault(config);
const routes = {
normal: [],
legacy: []
};
const priority = computeRoutePriority(config);
for (const injectedRoute of settings.injectedRoutes) {
const { pattern: name, entrypoint, prerender: prerenderInjected } = injectedRoute;
let resolved;
try {
resolved = require2.resolve(entrypoint, { paths: [cwd || fileURLToPath(config.root)] });
} catch (e) {
resolved = fileURLToPath(new URL(entrypoint, config.root));
}
const component = slash(path.relative(cwd || fileURLToPath(config.root), resolved));
const segments = removeLeadingForwardSlash(name).split(path.posix.sep).filter(Boolean).map((s) => {
validateSegment(s);
return getParts(s, component);
});
const type = resolved.endsWith(".astro") ? "page" : "endpoint";
const isPage = type === "page";
const trailingSlash = isPage ? config.trailingSlash : "never";
const pattern = getPattern(segments, settings.config, trailingSlash);
const generate = getRouteGenerator(segments, trailingSlash);
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join("/")}` : null;
const params = segments.flat().filter((p) => p.dynamic).map((p) => p.content);
const route = `/${segments.map(([{ dynamic, content }]) => dynamic ? `[${content}]` : content).join("/")}`.toLowerCase();
routes[priority].push({
type,
// For backwards compatibility, an injected route is never considered an index route.
isIndex: false,
route,
pattern,
segments,
params,
component,
generate,
pathname: pathname || void 0,
prerender: prerenderInjected ?? prerender,
fallbackRoutes: []
});
}
return routes;
}
function createRedirectRoutes({ settings }, routeMap, logger) {
const { config } = settings;
const trailingSlash = config.trailingSlash;
const routes = {
normal: [],
legacy: []
};
const priority = computeRoutePriority(settings.config);
for (const [from, to] of Object.entries(settings.config.redirects)) {
const segments = removeLeadingForwardSlash(from).split(path.posix.sep).filter(Boolean).map((s) => {
validateSegment(s);
return getParts(s, from);
});
const pattern = getPattern(segments, settings.config, trailingSlash);
const generate = getRouteGenerator(segments, trailingSlash);
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join("/")}` : null;
const params = segments.flat().filter((p) => p.dynamic).map((p) => p.content);
const route = `/${segments.map(([{ dynamic, content }]) => dynamic ? `[${content}]` : content).join("/")}`.toLowerCase();
let destination;
if (typeof to === "string") {
destination = to;
} else {
destination = to.destination;
}
if (/^https?:\/\//.test(destination)) {
logger.warn(
"redirects",
`Redirecting to an external URL is not officially supported: ${from} -> ${destination}`
);
}
routes[priority].push({
type: "redirect",
// For backwards compatibility, a redirect is never considered an index route.
isIndex: false,
route,
pattern,
segments,
params,
component: from,
generate,
pathname: pathname || void 0,
prerender: false,
redirect: to,
redirectRoute: routeMap.get(destination),
fallbackRoutes: []
});
}
return routes;
}
function isStaticSegment(segment) {
return segment.every((part) => !part.dynamic && !part.spread);
}
function detectRouteCollision(a, b, config, logger) {
if (a.type === "fallback" || b.type === "fallback") {
return;
}
if (a.route === b.route && a.segments.every(isStaticSegment) && b.segments.every(isStaticSegment)) {
logger.warn(
"router",
`The route "${a.route}" is defined in both "${a.component}" and "${b.component}". A static route cannot be defined more than once.`
);
logger.warn(
"router",
"A collision will result in an hard error in following versions of Astro."
);
return;
}
if (a.prerender || b.prerender) {
return;
}
if (a.segments.length !== b.segments.length) {
return;
}
const segmentCount = a.segments.length;
for (let index = 0; index < segmentCount; index++) {
const segmentA = a.segments[index];
const segmentB = b.segments[index];
if (!isSemanticallyEqualSegment(segmentA, segmentB)) {
return;
}
}
logger.warn(
"router",
`The route "${a.route}" is defined in both "${a.component}" and "${b.component}" using SSR mode. A dynamic SSR route cannot be defined more than once.`
);
logger.warn("router", "A collision will result in an hard error in following versions of Astro.");
}
function createRouteManifest(params, logger) {
const { settings } = params;
const { config } = settings;
const routeMap = /* @__PURE__ */ new Map();
const fileBasedRoutes = createFileBasedRoutes(params, logger);
for (const route of fileBasedRoutes) {
routeMap.set(route.route, route);
}
const injectedRoutes = createInjectedRoutes(params);
for (const [, routes2] of Object.entries(injectedRoutes)) {
for (const route of routes2) {
routeMap.set(route.route, route);
}
}
const redirectRoutes = createRedirectRoutes(params, routeMap, logger);
const routes = [
...injectedRoutes["legacy"].sort(routeComparator),
...[...fileBasedRoutes, ...injectedRoutes["normal"], ...redirectRoutes["normal"]].sort(
routeComparator
),
...redirectRoutes["legacy"].sort(routeComparator)
];
if (config.experimental.globalRoutePriority) {
for (const [index, higherRoute] of routes.entries()) {
for (const lowerRoute of routes.slice(index + 1)) {
detectRouteCollision(higherRoute, lowerRoute, config, logger);
}
}
}
const i18n = settings.config.i18n;
if (i18n) {
if (i18n.routing === "pathname-prefix-always") {
let index = routes.find((route) => route.route === "/");
if (!index) {
let relativePath = path.relative(
fileURLToPath(settings.config.root),
fileURLToPath(new URL("pages", settings.config.srcDir))
);
throw new AstroError({
...MissingIndexForInternationalization,
message: MissingIndexForInternationalization.message(i18n.defaultLocale),
hint: MissingIndexForInternationalization.hint(relativePath)
});
}
}
const routesByLocale = /* @__PURE__ */ new Map();
const setRoutes = new Set(routes.filter((route) => route.type === "page"));
const filteredLocales = i18n.locales.filter((loc) => {
if (typeof loc === "string") {
return loc !== i18n.defaultLocale;
}
return loc.path !== i18n.defaultLocale;
}).map((locale) => {
if (typeof locale === "string") {
return locale;
}
return locale.path;
});
for (const locale of filteredLocales) {
for (const route of setRoutes) {
if (!route.route.includes(`/${locale}`)) {
continue;
}
const currentRoutes = routesByLocale.get(locale);
if (currentRoutes) {
currentRoutes.push(route);
routesByLocale.set(locale, currentRoutes);
} else {
routesByLocale.set(locale, [route]);
}
setRoutes.delete(route);
}
}
for (const route of setRoutes) {
const currentRoutes = routesByLocale.get(i18n.defaultLocale);
if (currentRoutes) {
currentRoutes.push(route);
routesByLocale.set(i18n.defaultLocale, currentRoutes);
} else {
routesByLocale.set(i18n.defaultLocale, [route]);
}
setRoutes.delete(route);
}
if (i18n.routing === "pathname-prefix-always") {
const defaultLocaleRoutes = routesByLocale.get(i18n.defaultLocale);
if (defaultLocaleRoutes) {
const indexDefaultRoute = defaultLocaleRoutes.find((routeData) => {
return routeData.component.includes("index");
});
if (indexDefaultRoute) {
const pathname = "/";
const route = "/";
const segments = removeLeadingForwardSlash(route).split(path.posix.sep).filter(Boolean).map((s) => {
validateSegment(s);
return getParts(s, route);
});
routes.push({
...indexDefaultRoute,
pathname,
route,
segments,
pattern: getPattern(segments, config, config.trailingSlash),
type: "fallback"
});
}
}
}
if (i18n.fallback) {
let fallback = Object.entries(i18n.fallback);
if (fallback.length > 0) {
for (const [fallbackFromLocale, fallbackToLocale] of fallback) {
let fallbackToRoutes;
if (fallbackToLocale === i18n.defaultLocale) {
fallbackToRoutes = routesByLocale.get(i18n.defaultLocale);
} else {
fallbackToRoutes = routesByLocale.get(fallbackToLocale);
}
const fallbackFromRoutes = routesByLocale.get(fallbackFromLocale);
if (!fallbackToRoutes) {
continue;
}
for (const fallbackToRoute of fallbackToRoutes) {
const hasRoute = fallbackFromRoutes && // we check if the fallback from locale (the origin) has already this route
fallbackFromRoutes.some((route) => {
if (fallbackToLocale === i18n.defaultLocale) {
return route.route.replace(`/${fallbackFromLocale}`, "") === fallbackToRoute.route;
} else {
return route.route.replace(`/${fallbackToLocale}`, `/${fallbackFromLocale}`) === fallbackToRoute.route;
}
});
if (!hasRoute) {
let pathname;
let route;
if (fallbackToLocale === i18n.defaultLocale && i18n.routing === "pathname-prefix-other-locales") {
if (fallbackToRoute.pathname) {
pathname = `/${fallbackFromLocale}${fallbackToRoute.pathname}`;
}
route = `/${fallbackFromLocale}${fallbackToRoute.route}`;
} else {
pathname = fallbackToRoute.pathname?.replace(`/${fallbackToLocale}/`, `/${fallbackFromLocale}/`).replace(`/${fallbackToLocale}`, `/${fallbackFromLocale}`);
route = fallbackToRoute.route.replace(`/${fallbackToLocale}`, `/${fallbackFromLocale}`).replace(`/${fallbackToLocale}/`, `/${fallbackFromLocale}/`);
}
const segments = removeLeadingForwardSlash(route).split(path.posix.sep).filter(Boolean).map((s) => {
validateSegment(s);
return getParts(s, route);
});
const generate = getRouteGenerator(segments, config.trailingSlash);
const index = routes.findIndex((r) => r === fallbackToRoute);
if (index >= 0) {
const fallbackRoute = {
...fallbackToRoute,
pathname,
route,
segments,
generate,
pattern: getPattern(segments, config, config.trailingSlash),
type: "fallback",
fallbackRoutes: []
};
const routeData = routes[index];
routeData.fallbackRoutes.push(fallbackRoute);
}
}
}
}
}
}
}
return {
routes
};
}
function computeRoutePriority(config) {
if (config.experimental.globalRoutePriority) {
return "normal";
}
return "legacy";
}
export {
createRouteManifest
};