import * as devalue from "devalue"; import { extname } from "node:path"; import { pathToFileURL } from "node:url"; import { AstroErrorData } from "../core/errors/errors-data.js"; import { AstroError } from "../core/errors/errors.js"; import { escapeViteEnvReferences } from "../vite-plugin-utils/index.js"; import { CONTENT_FLAG, DATA_FLAG } from "./consts.js"; import { getContentEntryExts, getContentEntryIdAndSlug, getContentPaths, getDataEntryExts, getDataEntryId, getEntryCollectionName, getEntryConfigByExtMap, getEntryData, getEntryType, globalContentConfigObserver, hasContentFlag, parseEntrySlug, reloadContentConfigObserver } from "./utils.js"; function getContentRendererByViteId(viteId, settings) { let ext = viteId.split(".").pop(); if (!ext) return void 0; for (const contentEntryType of settings.contentEntryTypes) { if (Boolean(contentEntryType.getRenderModule) && contentEntryType.extensions.includes("." + ext)) { return contentEntryType.getRenderModule; } } return void 0; } const CHOKIDAR_MODIFIED_EVENTS = ["add", "unlink", "change"]; const COLLECTION_TYPES_TO_INVALIDATE_ON = ["data", "content", "config"]; function astroContentImportPlugin({ fs, settings }) { const contentPaths = getContentPaths(settings.config, fs); const contentEntryExts = getContentEntryExts(settings); const dataEntryExts = getDataEntryExts(settings); const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes); const dataEntryConfigByExt = getEntryConfigByExtMap(settings.dataEntryTypes); const { contentDir } = contentPaths; const plugins = [ { name: "astro:content-imports", async transform(_, viteId) { if (hasContentFlag(viteId, DATA_FLAG)) { const fileId = viteId.split("?")[0] ?? viteId; const { id, data, collection, _internal } = await getDataEntryModule({ fileId, entryConfigByExt: dataEntryConfigByExt, contentDir, config: settings.config, fs, pluginContext: this }); const code = escapeViteEnvReferences(` export const id = ${JSON.stringify(id)}; export const collection = ${JSON.stringify(collection)}; export const data = ${stringifyEntryData(data)}; export const _internal = { type: 'data', filePath: ${JSON.stringify(_internal.filePath)}, rawData: ${JSON.stringify(_internal.rawData)}, }; `); return code; } else if (hasContentFlag(viteId, CONTENT_FLAG)) { const fileId = viteId.split("?")[0]; const { id, slug, collection, body, data, _internal } = await getContentEntryModule({ fileId, entryConfigByExt: contentEntryConfigByExt, contentDir, config: settings.config, fs, pluginContext: this }); const code = escapeViteEnvReferences(` export const id = ${JSON.stringify(id)}; export const collection = ${JSON.stringify(collection)}; export const slug = ${JSON.stringify(slug)}; export const body = ${JSON.stringify(body)}; export const data = ${stringifyEntryData(data)}; export const _internal = { type: 'content', filePath: ${JSON.stringify(_internal.filePath)}, rawData: ${JSON.stringify(_internal.rawData)}, };`); return { code, map: { mappings: "" } }; } }, configureServer(viteServer) { viteServer.watcher.on("all", async (event, entry) => { if (CHOKIDAR_MODIFIED_EVENTS.includes(event)) { const entryType = getEntryType( entry, contentPaths, contentEntryExts, dataEntryExts, settings.config.experimental.assets ); if (!COLLECTION_TYPES_TO_INVALIDATE_ON.includes(entryType)) return; if (entryType === "content" || entryType === "data") { await reloadContentConfigObserver({ fs, settings, viteServer }); } for (const modUrl of viteServer.moduleGraph.urlToModuleMap.keys()) { if (hasContentFlag(modUrl, CONTENT_FLAG) || hasContentFlag(modUrl, DATA_FLAG) || Boolean(getContentRendererByViteId(modUrl, settings))) { const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl); if (mod) { viteServer.moduleGraph.invalidateModule(mod); } } } } }); } } ]; if (settings.contentEntryTypes.some((t) => t.getRenderModule)) { plugins.push({ name: "astro:content-render-imports", async transform(contents, viteId) { const contentRenderer = getContentRendererByViteId(viteId, settings); if (!contentRenderer) return; const fileId = viteId.split("?")[0]; return contentRenderer.bind(this)({ viteId, contents, fileUrl: pathToFileURL(fileId) }); } }); } return plugins; } async function getContentEntryModule(params) { const { fileId, contentDir, pluginContext, config } = params; const { collectionConfig, entryConfig, entry, rawContents, collection } = await getEntryModuleBaseInfo(params); const { rawData, data: unvalidatedData, body, slug: frontmatterSlug } = await entryConfig.getEntryInfo({ fileUrl: pathToFileURL(fileId), contents: rawContents }); const _internal = { filePath: fileId, rawData }; const { id, slug: generatedSlug } = getContentEntryIdAndSlug({ entry, contentDir, collection }); const slug = parseEntrySlug({ id, collection, generatedSlug, frontmatterSlug }); const data = collectionConfig ? await getEntryData( { id, collection, _internal, unvalidatedData }, collectionConfig, pluginContext, config ) : unvalidatedData; const contentEntryModule = { id, slug, collection, data, body, _internal }; return contentEntryModule; } async function getDataEntryModule(params) { const { fileId, contentDir, pluginContext, config } = params; const { collectionConfig, entryConfig, entry, rawContents, collection } = await getEntryModuleBaseInfo(params); const { rawData = "", data: unvalidatedData } = await entryConfig.getEntryInfo({ fileUrl: pathToFileURL(fileId), contents: rawContents }); const _internal = { filePath: fileId, rawData }; const id = getDataEntryId({ entry, contentDir, collection }); const data = collectionConfig ? await getEntryData( { id, collection, _internal, unvalidatedData }, collectionConfig, pluginContext, config ) : unvalidatedData; const dataEntryModule = { id, collection, data, _internal }; return dataEntryModule; } async function getEntryModuleBaseInfo({ fileId, entryConfigByExt, contentDir, fs }) { const contentConfig = await getContentConfigFromGlobal(); let rawContents; try { rawContents = await fs.promises.readFile(fileId, "utf-8"); } catch (e) { throw new AstroError({ ...AstroErrorData.UnknownContentCollectionError, message: `Unexpected error reading entry ${JSON.stringify(fileId)}.`, stack: e instanceof Error ? e.stack : void 0 }); } const fileExt = extname(fileId); const entryConfig = entryConfigByExt.get(fileExt); if (!entryConfig) { throw new AstroError({ ...AstroErrorData.UnknownContentCollectionError, message: `No parser found for data entry ${JSON.stringify( fileId )}. Did you apply an integration for this file type?` }); } const entry = pathToFileURL(fileId); const collection = getEntryCollectionName({ entry, contentDir }); if (collection === void 0) throw new AstroError(AstroErrorData.UnknownContentCollectionError); const collectionConfig = contentConfig == null ? void 0 : contentConfig.collections[collection]; return { collectionConfig, entry, entryConfig, collection, rawContents }; } async function getContentConfigFromGlobal() { const observable = globalContentConfigObserver.get(); if (observable.status === "init") { throw new AstroError({ ...AstroErrorData.UnknownContentCollectionError, message: "Content config failed to load." }); } if (observable.status === "error") { throw observable.error; } let contentConfig = observable.status === "loaded" ? observable.config : void 0; if (observable.status === "loading") { contentConfig = await new Promise((resolve) => { const unsubscribe = globalContentConfigObserver.subscribe((ctx) => { if (ctx.status === "loaded") { resolve(ctx.config); unsubscribe(); } if (ctx.status === "error") { resolve(void 0); unsubscribe(); } }); }); } return contentConfig; } function stringifyEntryData(data) { try { return devalue.uneval(data, (value) => { if (value instanceof URL) { return `new URL(${JSON.stringify(value.href)})`; } }); } catch (e) { if (e instanceof Error) { throw new AstroError({ ...AstroErrorData.UnsupportedConfigTransformError, message: AstroErrorData.UnsupportedConfigTransformError.message(e.message), stack: e.stack }); } else { throw new AstroError({ message: "Unexpected error processing content collection data." }); } } } export { astroContentImportPlugin };