UNPKG

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
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("&#x22;", '"')); 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 };