import { createHash } from "node:crypto"; import fsMod from "node:fs"; import { fileURLToPath } from "node:url"; import pLimit from "p-limit"; import { normalizePath } from "vite"; import { CONTENT_RENDER_FLAG, PROPAGATED_ASSET_FLAG } from "../../../content/consts.js"; import { hasContentFlag } from "../../../content/utils.js"; import { generateContentEntryFile, generateLookupMap } from "../../../content/vite-plugin-content-virtual-mod.js"; import { isServerLikeOutput } from "../../../prerender/utils.js"; import { joinPaths, removeFileExtension, removeLeadingForwardSlash } from "../../path.js"; import { addRollupInput } from "../add-rollup-input.js"; import {} from "../internal.js"; import { copyFiles } from "../static-build.js"; import { encodeName } from "../util.js"; import { extendManualChunks } from "./util.js"; const CONTENT_CACHE_DIR = "./content/"; const CONTENT_MANIFEST_FILE = "./manifest.json"; const CONTENT_MANIFEST_VERSION = 0; const virtualEmptyModuleId = `virtual:empty-content`; const resolvedVirtualEmptyModuleId = `\0${virtualEmptyModuleId}`; function createContentManifest() { return { version: -1, entries: [], serverEntries: [], clientEntries: [] }; } function vitePluginContent(opts, lookupMap, internals) { const { config } = opts.settings; const { cacheDir } = config; const distRoot = config.outDir; const distContentRoot = new URL("./content/", distRoot); const cachedChunks = new URL("./chunks/", opts.settings.config.cacheDir); const distChunks = new URL("./chunks/", opts.settings.config.outDir); const contentCacheDir = new URL(CONTENT_CACHE_DIR, cacheDir); const contentManifestFile = new URL(CONTENT_MANIFEST_FILE, contentCacheDir); const cache = contentCacheDir; const cacheTmp = new URL("./.tmp/", cache); let oldManifest = createContentManifest(); let newManifest = createContentManifest(); let entries; let injectedEmptyFile = false; if (fsMod.existsSync(contentManifestFile)) { try { const data = fsMod.readFileSync(contentManifestFile, { encoding: "utf8" }); oldManifest = JSON.parse(data); internals.cachedClientEntries = oldManifest.clientEntries; } catch { } } return { name: "@astro/plugin-build-content", async options(options) { let newOptions = Object.assign({}, options); newManifest = await generateContentManifest(opts, lookupMap); entries = getEntriesFromManifests(oldManifest, newManifest); for (const { type, entry } of entries.buildFromSource) { const fileURL = encodeURI(joinPaths(opts.settings.config.root.toString(), entry)); const input = fileURLToPath(fileURL); const inputs = [`${input}?${collectionTypeToFlag(type)}`]; if (type === "content") { inputs.push(`${input}?${CONTENT_RENDER_FLAG}`); } newOptions = addRollupInput(newOptions, inputs); } if (fsMod.existsSync(cachedChunks)) { await copyFiles(cachedChunks, distChunks, true); } if (entries.buildFromSource.length === 0) { newOptions = addRollupInput(newOptions, [virtualEmptyModuleId]); injectedEmptyFile = true; } return newOptions; }, outputOptions(outputOptions) { const rootPath = normalizePath(fileURLToPath(opts.settings.config.root)); const srcPath = normalizePath(fileURLToPath(opts.settings.config.srcDir)); extendManualChunks(outputOptions, { before(id, meta) { if (id.startsWith(srcPath) && id.slice(srcPath.length).startsWith("content")) { const info = meta.getModuleInfo(id); if (info?.dynamicImporters.length === 1 && hasContentFlag(info.dynamicImporters[0], PROPAGATED_ASSET_FLAG)) { const [srcRelativePath2] = id.replace(rootPath, "/").split("?"); const resultId = encodeName( `${removeLeadingForwardSlash(removeFileExtension(srcRelativePath2))}.render.mjs` ); return resultId; } const [srcRelativePath, flag] = id.replace(rootPath, "/").split("?"); const collectionEntry = findEntryFromSrcRelativePath(lookupMap, srcRelativePath); if (collectionEntry) { let suffix = ".mjs"; if (flag === PROPAGATED_ASSET_FLAG) { suffix = ".entry.mjs"; } id = removeLeadingForwardSlash( removeFileExtension(encodeName(id.replace(srcPath, "/"))) ) + suffix; return id; } } } }); }, resolveId(id) { if (id === virtualEmptyModuleId) { return resolvedVirtualEmptyModuleId; } }, async load(id) { if (id === resolvedVirtualEmptyModuleId) { return { code: `// intentionally left empty! export default {}` }; } }, async generateBundle(_options, bundle) { const code = await generateContentEntryFile({ settings: opts.settings, fs: fsMod, lookupMap, IS_DEV: false, IS_SERVER: false }); this.emitFile({ type: "prebuilt-chunk", code, fileName: "content/entry.mjs" }); if (!injectedEmptyFile) return; Object.keys(bundle).forEach((key) => { const mod = bundle[key]; if (mod.type === "asset") return; if (mod.facadeModuleId === resolvedVirtualEmptyModuleId) { delete bundle[key]; } }); }, async writeBundle() { const clientComponents = /* @__PURE__ */ new Set([ ...oldManifest.clientEntries, ...internals.discoveredHydratedComponents.keys(), ...internals.discoveredClientOnlyComponents.keys(), ...internals.discoveredScripts ]); const serverComponents = /* @__PURE__ */ new Set([ ...oldManifest.serverEntries, ...internals.discoveredHydratedComponents.keys() ]); newManifest.serverEntries = Array.from(serverComponents); newManifest.clientEntries = Array.from(clientComponents); await fsMod.promises.mkdir(contentCacheDir, { recursive: true }); await fsMod.promises.writeFile(contentManifestFile, JSON.stringify(newManifest), { encoding: "utf8" }); const cacheExists = fsMod.existsSync(cache); fsMod.mkdirSync(cache, { recursive: true }); await fsMod.promises.mkdir(cacheTmp, { recursive: true }); await copyFiles(distContentRoot, cacheTmp, true); if (cacheExists) { await copyFiles(contentCacheDir, distContentRoot, false); } await copyFiles(cacheTmp, contentCacheDir); await fsMod.promises.rm(cacheTmp, { recursive: true, force: true }); } }; } const entryCache = /* @__PURE__ */ new Map(); function findEntryFromSrcRelativePath(lookupMap, srcRelativePath) { let value = entryCache.get(srcRelativePath); if (value) return value; for (const collection of Object.values(lookupMap)) { for (const entry of Object.values(collection)) { for (const entryFile of Object.values(entry)) { if (entryFile === srcRelativePath) { value = entryFile; entryCache.set(srcRelativePath, entryFile); return value; } } } } } function getEntriesFromManifests(oldManifest, newManifest) { const { version: oldVersion, entries: oldEntries } = oldManifest; const { version: newVersion, entries: newEntries } = newManifest; let entries = { restoreFromCache: [], buildFromSource: [] }; const newEntryMap = new Map(newEntries); if (oldVersion !== newVersion || oldEntries.length === 0) { entries.buildFromSource = Array.from(newEntryMap.keys()); return entries; } const oldEntryHashMap = new Map( oldEntries.map(([key, hash]) => [hash, key]) ); for (const [entry, hash] of newEntryMap) { if (oldEntryHashMap.has(hash)) { entries.restoreFromCache.push(entry); } else { entries.buildFromSource.push(entry); } } return entries; } async function generateContentManifest(opts, lookupMap) { let manifest = { version: CONTENT_MANIFEST_VERSION, entries: [], serverEntries: [], clientEntries: [] }; const limit = pLimit(10); const promises = []; for (const [collection, { type, entries }] of Object.entries(lookupMap)) { for (const entry of Object.values(entries)) { const key = { collection, type, entry }; const fileURL = new URL(encodeURI(joinPaths(opts.settings.config.root.toString(), entry))); promises.push( limit(async () => { const data = await fsMod.promises.readFile(fileURL, { encoding: "utf8" }); manifest.entries.push([key, checksum(data)]); }) ); } } await Promise.all(promises); return manifest; } function checksum(data) { return createHash("sha1").update(data).digest("base64"); } function collectionTypeToFlag(type) { const name = type[0].toUpperCase() + type.slice(1); return `astro${name}CollectionEntry`; } function pluginContent(opts, internals) { const cachedChunks = new URL("./chunks/", opts.settings.config.cacheDir); const distChunks = new URL("./chunks/", opts.settings.config.outDir); return { targets: ["server"], hooks: { async "build:before"() { if (!opts.settings.config.experimental.contentCollectionCache) { return { vitePlugin: void 0 }; } if (isServerLikeOutput(opts.settings.config)) { return { vitePlugin: void 0 }; } const lookupMap = await generateLookupMap({ settings: opts.settings, fs: fsMod }); return { vitePlugin: vitePluginContent(opts, lookupMap, internals) }; }, async "build:post"() { if (!opts.settings.config.experimental.contentCollectionCache) { return; } if (isServerLikeOutput(opts.settings.config)) { return; } if (fsMod.existsSync(distChunks)) { await copyFiles(distChunks, cachedChunks, true); } } } }; } export { pluginContent };