astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
530 lines (529 loc) • 17.7 kB
JavaScript
import { Traverse } from "neotraverse/modern";
import pLimit from "p-limit";
import { ZodIssueCode, z } from "zod";
import { imageSrcToImportId } from "../assets/utils/resolveImports.js";
import { AstroError, AstroErrorData, AstroUserError } from "../core/errors/index.js";
import { prependForwardSlash } from "../core/path.js";
import {
createComponent,
createHeadAndContent,
renderComponent,
renderScriptElement,
renderTemplate,
renderUniqueStylesheet,
render as serverRender,
unescapeHTML
} from "../runtime/server/index.js";
import { CONTENT_LAYER_TYPE, IMAGE_IMPORT_PREFIX } from "./consts.js";
import { globalDataStore } from "./data-store.js";
function getImporterFilename() {
const stackLine = new Error().stack?.split("\n")?.[3];
if (!stackLine) {
return null;
}
const match = /\/(src\/.*?):\d+:\d+/.exec(stackLine);
return match?.[1] ?? null;
}
function defineCollection(config) {
if ("loader" in config) {
if (config.type && config.type !== CONTENT_LAYER_TYPE) {
throw new AstroUserError(
`Collections that use the Content Layer API must have a \`loader\` defined and no \`type\` set. Check your collection definitions in ${getImporterFilename() ?? "your content config file"}.`
);
}
config.type = CONTENT_LAYER_TYPE;
}
if (!config.type) config.type = "content";
return config;
}
function createCollectionToGlobResultMap({
globResult,
contentDir
}) {
const collectionToGlobResultMap = {};
for (const key in globResult) {
const keyRelativeToContentDir = key.replace(new RegExp(`^${contentDir}`), "");
const segments = keyRelativeToContentDir.split("/");
if (segments.length <= 1) continue;
const collection = segments[0];
collectionToGlobResultMap[collection] ??= {};
collectionToGlobResultMap[collection][key] = globResult[key];
}
return collectionToGlobResultMap;
}
function createGetCollection({
contentCollectionToEntryMap,
dataCollectionToEntryMap,
getRenderEntryImport,
cacheEntriesByCollection
}) {
return async function getCollection(collection, filter) {
const hasFilter = typeof filter === "function";
const store = await globalDataStore.get();
let type;
if (collection in contentCollectionToEntryMap) {
type = "content";
} else if (collection in dataCollectionToEntryMap) {
type = "data";
} else if (store.hasCollection(collection)) {
const { default: imageAssetMap } = await import("astro:asset-imports");
const result = [];
for (const rawEntry of store.values(collection)) {
const data = updateImageReferencesInData(rawEntry.data, rawEntry.filePath, imageAssetMap);
let entry = {
...rawEntry,
data,
collection
};
if (entry.legacyId) {
entry = emulateLegacyEntry(entry);
}
if (hasFilter && !filter(entry)) {
continue;
}
result.push(entry);
}
return result;
} else {
console.warn(
`The collection ${JSON.stringify(
collection
)} does not exist or is empty. Please check your content config file for errors.`
);
return [];
}
const lazyImports = Object.values(
type === "content" ? contentCollectionToEntryMap[collection] : dataCollectionToEntryMap[collection]
);
let entries = [];
if (!import.meta.env?.DEV && cacheEntriesByCollection.has(collection)) {
entries = cacheEntriesByCollection.get(collection);
} else {
const limit = pLimit(10);
entries = await Promise.all(
lazyImports.map(
(lazyImport) => limit(async () => {
const entry = await lazyImport();
return type === "content" ? {
id: entry.id,
slug: entry.slug,
body: entry.body,
collection: entry.collection,
data: entry.data,
async render() {
return render({
collection: entry.collection,
id: entry.id,
renderEntryImport: await getRenderEntryImport(collection, entry.slug)
});
}
} : {
id: entry.id,
collection: entry.collection,
data: entry.data
};
})
)
);
cacheEntriesByCollection.set(collection, entries);
}
if (hasFilter) {
return entries.filter(filter);
} else {
return entries.slice();
}
};
}
function createGetEntryBySlug({
getEntryImport,
getRenderEntryImport,
collectionNames,
getEntry
}) {
return async function getEntryBySlug(collection, slug) {
const store = await globalDataStore.get();
if (!collectionNames.has(collection)) {
if (store.hasCollection(collection)) {
const entry2 = await getEntry(collection, slug);
if (entry2 && "slug" in entry2) {
return entry2;
}
throw new AstroError({
...AstroErrorData.GetEntryDeprecationError,
message: AstroErrorData.GetEntryDeprecationError.message(collection, "getEntryBySlug")
});
}
console.warn(
`The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`
);
return void 0;
}
const entryImport = await getEntryImport(collection, slug);
if (typeof entryImport !== "function") return void 0;
const entry = await entryImport();
return {
id: entry.id,
slug: entry.slug,
body: entry.body,
collection: entry.collection,
data: entry.data,
async render() {
return render({
collection: entry.collection,
id: entry.id,
renderEntryImport: await getRenderEntryImport(collection, slug)
});
}
};
};
}
function createGetDataEntryById({
getEntryImport,
collectionNames,
getEntry
}) {
return async function getDataEntryById(collection, id) {
const store = await globalDataStore.get();
if (!collectionNames.has(collection)) {
if (store.hasCollection(collection)) {
return getEntry(collection, id);
}
console.warn(
`The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`
);
return void 0;
}
const lazyImport = await getEntryImport(collection, id);
if (!lazyImport) throw new Error(`Entry ${collection} \u2192 ${id} was not found.`);
const entry = await lazyImport();
return {
id: entry.id,
collection: entry.collection,
data: entry.data
};
};
}
function emulateLegacyEntry({ legacyId, ...entry }) {
const legacyEntry = {
...entry,
id: legacyId,
slug: entry.id
};
return {
...legacyEntry,
// Define separately so the render function isn't included in the object passed to `renderEntry()`
render: () => renderEntry(legacyEntry)
};
}
function createGetEntry({
getEntryImport,
getRenderEntryImport,
collectionNames
}) {
return async function getEntry(collectionOrLookupObject, _lookupId) {
let collection, lookupId;
if (typeof collectionOrLookupObject === "string") {
collection = collectionOrLookupObject;
if (!_lookupId)
throw new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: "`getEntry()` requires an entry identifier as the second argument."
});
lookupId = _lookupId;
} else {
collection = collectionOrLookupObject.collection;
lookupId = "id" in collectionOrLookupObject ? collectionOrLookupObject.id : collectionOrLookupObject.slug;
}
const store = await globalDataStore.get();
if (store.hasCollection(collection)) {
const entry2 = store.get(collection, lookupId);
if (!entry2) {
console.warn(`Entry ${collection} \u2192 ${lookupId} was not found.`);
return;
}
const { default: imageAssetMap } = await import("astro:asset-imports");
entry2.data = updateImageReferencesInData(entry2.data, entry2.filePath, imageAssetMap);
if (entry2.legacyId) {
return emulateLegacyEntry({ ...entry2, collection });
}
return {
...entry2,
collection
};
}
if (!collectionNames.has(collection)) {
console.warn(
`The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`
);
return void 0;
}
const entryImport = await getEntryImport(collection, lookupId);
if (typeof entryImport !== "function") return void 0;
const entry = await entryImport();
if (entry._internal.type === "content") {
return {
id: entry.id,
slug: entry.slug,
body: entry.body,
collection: entry.collection,
data: entry.data,
async render() {
return render({
collection: entry.collection,
id: entry.id,
renderEntryImport: await getRenderEntryImport(collection, lookupId)
});
}
};
} else if (entry._internal.type === "data") {
return {
id: entry.id,
collection: entry.collection,
data: entry.data
};
}
return void 0;
};
}
function createGetEntries(getEntry) {
return async function getEntries(entries) {
return Promise.all(entries.map((e) => getEntry(e)));
};
}
const CONTENT_LAYER_IMAGE_REGEX = /__ASTRO_IMAGE_="([^"]+)"/g;
async function updateImageReferencesInBody(html, fileName) {
const { default: imageAssetMap } = await import("astro:asset-imports");
const imageObjects = /* @__PURE__ */ new Map();
const { getImage } = await import("astro:assets");
for (const [_full, imagePath] of html.matchAll(CONTENT_LAYER_IMAGE_REGEX)) {
try {
const decodedImagePath = JSON.parse(imagePath.replaceAll(""", '"'));
const id = imageSrcToImportId(decodedImagePath.src, fileName);
const imported = imageAssetMap.get(id);
if (!id || imageObjects.has(id) || !imported) {
continue;
}
const image = await getImage({ ...decodedImagePath, src: imported });
imageObjects.set(imagePath, image);
} catch {
throw new Error(`Failed to parse image reference: ${imagePath}`);
}
}
return html.replaceAll(CONTENT_LAYER_IMAGE_REGEX, (full, imagePath) => {
const image = imageObjects.get(imagePath);
if (!image) {
return full;
}
const { index, ...attributes } = image.attributes;
return Object.entries({
...attributes,
src: image.src,
srcset: image.srcSet.attribute
}).map(([key, value]) => value ? `${key}=${JSON.stringify(String(value))}` : "").join(" ");
});
}
function updateImageReferencesInData(data, fileName, imageAssetMap) {
return new Traverse(data).map(function(ctx, val) {
if (typeof val === "string" && val.startsWith(IMAGE_IMPORT_PREFIX)) {
const src = val.replace(IMAGE_IMPORT_PREFIX, "");
const id = imageSrcToImportId(src, fileName);
if (!id) {
ctx.update(src);
return;
}
const imported = imageAssetMap?.get(id);
if (imported) {
ctx.update(imported);
} else {
ctx.update(src);
}
}
});
}
async function renderEntry(entry) {
if (!entry) {
throw new AstroError(AstroErrorData.RenderUndefinedEntryError);
}
if ("render" in entry && !("legacyId" in entry)) {
return entry.render();
}
if (entry.deferredRender) {
try {
const { default: contentModules } = await import("astro:content-module-imports");
const renderEntryImport = contentModules.get(entry.filePath);
return render({
collection: "",
id: entry.id,
renderEntryImport
});
} catch (e) {
console.error(e);
}
}
const html = entry?.rendered?.metadata?.imagePaths?.length && entry.filePath ? await updateImageReferencesInBody(entry.rendered.html, entry.filePath) : entry?.rendered?.html;
const Content = createComponent(() => serverRender`${unescapeHTML(html)}`);
return {
Content,
headings: entry?.rendered?.metadata?.headings ?? [],
remarkPluginFrontmatter: entry?.rendered?.metadata?.frontmatter ?? {}
};
}
async function render({
collection,
id,
renderEntryImport
}) {
const UnexpectedRenderError = new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: `Unexpected error while rendering ${String(collection)} \u2192 ${String(id)}.`
});
if (typeof renderEntryImport !== "function") throw UnexpectedRenderError;
const baseMod = await renderEntryImport();
if (baseMod == null || typeof baseMod !== "object") throw UnexpectedRenderError;
const { default: defaultMod } = baseMod;
if (isPropagatedAssetsModule(defaultMod)) {
const { collectedStyles, collectedLinks, collectedScripts, getMod } = defaultMod;
if (typeof getMod !== "function") throw UnexpectedRenderError;
const propagationMod = await getMod();
if (propagationMod == null || typeof propagationMod !== "object") throw UnexpectedRenderError;
const Content = createComponent({
factory(result, baseProps, slots) {
let styles = "", links = "", scripts = "";
if (Array.isArray(collectedStyles)) {
styles = collectedStyles.map((style) => {
return renderUniqueStylesheet(result, {
type: "inline",
content: style
});
}).join("");
}
if (Array.isArray(collectedLinks)) {
links = collectedLinks.map((link) => {
return renderUniqueStylesheet(result, {
type: "external",
src: prependForwardSlash(link)
});
}).join("");
}
if (Array.isArray(collectedScripts)) {
scripts = collectedScripts.map((script) => renderScriptElement(script)).join("");
}
let props = baseProps;
if (id.endsWith("mdx")) {
props = {
components: propagationMod.components ?? {},
...baseProps
};
}
return createHeadAndContent(
unescapeHTML(styles + links + scripts),
renderTemplate`${renderComponent(
result,
"Content",
propagationMod.Content,
props,
slots
)}`
);
},
propagation: "self"
});
return {
Content,
headings: propagationMod.getHeadings?.() ?? [],
remarkPluginFrontmatter: propagationMod.frontmatter ?? {}
};
} else if (baseMod.Content && typeof baseMod.Content === "function") {
return {
Content: baseMod.Content,
headings: baseMod.getHeadings?.() ?? [],
remarkPluginFrontmatter: baseMod.frontmatter ?? {}
};
} else {
throw UnexpectedRenderError;
}
}
function createReference({ lookupMap }) {
let store = null;
globalDataStore.get().then((s) => store = s);
return function reference(collection) {
return z.union([
z.string(),
z.object({
id: z.string(),
collection: z.string()
}),
z.object({
slug: z.string(),
collection: z.string()
})
]).transform(
(lookup, ctx) => {
if (!store) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `**${ctx.path.join(".")}:** Reference to ${collection} could not be resolved: store not available.
This is an Astro bug, so please file an issue at https://github.com/withastro/astro/issues.`
});
return;
}
const flattenedErrorPath = ctx.path.join(".");
const collectionIsInStore = store.hasCollection(collection);
if (typeof lookup === "object") {
if (lookup.collection !== collection) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Expected ${collection}. Received ${lookup.collection}.`
});
return;
}
return lookup;
}
if (collectionIsInStore) {
const entry2 = store.get(collection, lookup);
if (!entry2) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Entry ${lookup} does not exist.`
});
return;
}
return { id: lookup, collection };
}
if (!lookupMap[collection] && store.collections().size <= 1) {
return { id: lookup, collection };
}
const { type, entries } = lookupMap[collection];
const entry = entries[lookup];
if (!entry) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Expected ${Object.keys(
entries
).map((c) => JSON.stringify(c)).join(" | ")}. Received ${JSON.stringify(lookup)}.`
});
return;
}
if (type === "content") {
return { slug: lookup, collection };
}
return { id: lookup, collection };
}
);
};
}
function isPropagatedAssetsModule(module) {
return typeof module === "object" && module != null && "__astroPropagation" in module;
}
export {
createCollectionToGlobResultMap,
createGetCollection,
createGetDataEntryById,
createGetEntries,
createGetEntry,
createGetEntryBySlug,
createReference,
defineCollection,
getImporterFilename,
renderEntry
};