274 lines
9.9 KiB
Plaintext
274 lines
9.9 KiB
Plaintext
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
|
|
};
|