astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
261 lines (260 loc) • 9.93 kB
JavaScript
import crypto from "node:crypto";
import { FONTS_SERVER_ADDRESS_PLACEHOLDER } from "../../../assets/fonts/constants.js";
import { PROPAGATED_ASSET_FLAG } from "../../../content/consts.js";
import { hasContentFlag } from "../../../content/utils.js";
import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../../constants.js";
import { removeQueryString } from "../../path.js";
import { rootRelativePath } from "../../viteUtils.js";
import { moduleIsTopLevelPage } from "../graph.js";
import { isContentDataIncrementalModule } from "../incremental-metadata.js";
import { getPageDataByViteID } from "../internal.js";
const ASSET_PLACEHOLDERS = [
{ token: "__ASTRO_ASSET_IMAGE__", pattern: /__ASTRO_ASSET_IMAGE__([\w$]+)__(?:_(.*?)__)?/g },
{ token: "__VITE_ASSET__", pattern: /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g }
];
const FONTS_ADDRESS_DECLARATION = new RegExp(
`(?:const|let|var)\\s+${FONTS_SERVER_ADDRESS_PLACEHOLDER}\\s*=[^;]+;`
);
function resolveAssetPlaceholders(graph, code) {
let resolved = code;
for (const { token, pattern } of ASSET_PLACEHOLDERS) {
if (!resolved.includes(token)) continue;
resolved = resolved.replace(pattern, (placeholder, handle, postfix = "") => {
try {
return graph.getFileName(handle) + postfix;
} catch {
return placeholder;
}
});
}
if (resolved.includes(FONTS_SERVER_ADDRESS_PLACEHOLDER)) {
resolved = resolved.replace(FONTS_ADDRESS_DECLARATION, "");
}
return resolved;
}
function hashModules(graph, sortedIds) {
const hasher = crypto.createHash("sha256");
for (const id of sortedIds) {
hasher.update(id);
hasher.update("\n");
const code = graph.getModuleInfo(id)?.code;
if (code != null) {
hasher.update(resolveAssetPlaceholders(graph, code));
}
hasher.update("\n");
}
return hasher.digest("hex");
}
function createTransitiveGraphCache(graph) {
const modules = /* @__PURE__ */ new Map();
const dependencies = /* @__PURE__ */ new Map();
const excludedModules = /* @__PURE__ */ new Set();
const pending = [...graph.getModuleIds()];
for (const id of pending) {
if (modules.has(id)) continue;
const info = graph.getModuleInfo(id);
modules.set(id, info);
if (isContentDataIncrementalModule(info)) {
excludedModules.add(id);
continue;
}
const importedIds = [...info?.importedIds ?? [], ...info?.dynamicallyImportedIds ?? []];
dependencies.set(id, importedIds);
pending.push(...importedIds);
}
for (const id of excludedModules) modules.delete(id);
for (const [id, importedIds] of dependencies) {
dependencies.set(
id,
importedIds.filter((importedId) => !excludedModules.has(importedId))
);
}
const reverseDependencies = /* @__PURE__ */ new Map();
for (const id of modules.keys()) reverseDependencies.set(id, []);
for (const [id, importedIds] of dependencies) {
for (const importedId of importedIds) reverseDependencies.get(importedId)?.push(id);
}
const visited = /* @__PURE__ */ new Set();
const finishOrder = [];
for (const rootId of modules.keys()) {
if (visited.has(rootId)) continue;
visited.add(rootId);
const stack = [[rootId, 0]];
while (stack.length > 0) {
const frame = stack[stack.length - 1];
const importedIds = dependencies.get(frame[0]) ?? [];
if (frame[1] < importedIds.length) {
const importedId = importedIds[frame[1]++];
if (!visited.has(importedId)) {
visited.add(importedId);
stack.push([importedId, 0]);
}
} else {
finishOrder.push(frame[0]);
stack.pop();
}
}
}
const componentByModule = /* @__PURE__ */ new Map();
const components = [];
for (const rootId of finishOrder.toReversed()) {
if (componentByModule.has(rootId)) continue;
const componentIndex = components.length;
const component = [];
const stack = [rootId];
componentByModule.set(rootId, componentIndex);
while (stack.length > 0) {
const id = stack.pop();
component.push(id);
for (const importerId of reverseDependencies.get(id) ?? []) {
if (!componentByModule.has(importerId)) {
componentByModule.set(importerId, componentIndex);
stack.push(importerId);
}
}
}
components.push(component.sort());
}
const componentDependencies = components.map(() => /* @__PURE__ */ new Set());
const componentImporters = components.map(() => /* @__PURE__ */ new Set());
for (const [id, importedIds] of dependencies) {
const componentIndex = componentByModule.get(id);
for (const importedId of importedIds) {
const dependencyIndex = componentByModule.get(importedId);
if (dependencyIndex === componentIndex) continue;
componentDependencies[componentIndex].add(dependencyIndex);
componentImporters[dependencyIndex].add(componentIndex);
}
}
const componentHashes = /* @__PURE__ */ new Map();
const componentHasServerIsland = /* @__PURE__ */ new Map();
const unresolvedDependencies = componentDependencies.map((items) => items.size);
const ready = unresolvedDependencies.flatMap((count, index) => count === 0 ? [index] : []);
for (const componentIndex of ready) {
const hasher = crypto.createHash("sha256");
hasher.update(hashModules(graph, components[componentIndex]));
const dependencyHashes = [...componentDependencies[componentIndex]].map((dependencyIndex) => componentHashes.get(dependencyIndex)).sort();
for (const dependencyHash of dependencyHashes) {
hasher.update("\n");
hasher.update(dependencyHash);
}
componentHashes.set(componentIndex, hasher.digest("hex"));
componentHasServerIsland.set(
componentIndex,
components[componentIndex].some(
(id) => (modules.get(id)?.meta?.astro?.serverComponents?.length ?? 0) > 0
) || [...componentDependencies[componentIndex]].some(
(dependencyIndex) => componentHasServerIsland.get(dependencyIndex)
)
);
for (const importerIndex of componentImporters[componentIndex]) {
unresolvedDependencies[importerIndex]--;
if (unresolvedDependencies[importerIndex] === 0) ready.push(importerIndex);
}
}
return {
hashes: new Map(
[...componentByModule].map(([id, componentIndex]) => [
id,
componentHashes.get(componentIndex)
])
),
serverIslandModules: new Set(
[...componentByModule].filter(([, componentIndex]) => componentHasServerIsland.get(componentIndex)).map(([id]) => id)
)
};
}
function collectClientEntrypointHashes(transitiveHashes, entrypointIds, pagesByEntrypoint, hashesByComponent) {
for (const entrypointId of entrypointIds) {
const pages = pagesByEntrypoint.get(entrypointId);
if (!pages?.size) continue;
const hash = transitiveHashes.get(entrypointId);
if (!hash) continue;
for (const pageData of pages) {
let list = hashesByComponent.get(pageData.component);
if (!list) {
list = [];
hashesByComponent.set(pageData.component, list);
}
list.push(hash);
}
}
}
function foldClientDependencies(graph, internals) {
const baseHashes = internals.pageDependencyHashes;
if (!baseHashes) return;
const { hashes: transitiveHashes } = createTransitiveGraphCache(graph);
const hashesByComponent = /* @__PURE__ */ new Map();
collectClientEntrypointHashes(
transitiveHashes,
internals.discoveredClientOnlyComponents.keys(),
internals.pagesByClientOnly,
hashesByComponent
);
collectClientEntrypointHashes(
transitiveHashes,
internals.discoveredScripts,
internals.pagesByScriptId,
hashesByComponent
);
for (const [component, clientHashes] of hashesByComponent) {
const hasher = crypto.createHash("sha256");
hasher.update(baseHashes.get(component) ?? "");
for (const hash of clientHashes.sort()) {
hasher.update("\n");
hasher.update(hash);
}
baseHashes.set(component, hasher.digest("hex"));
}
}
function collectContentEntryHashes(graph, root, transitiveHashes) {
const entryHashes = /* @__PURE__ */ new Map();
for (const id of graph.getModuleIds()) {
if (!hasContentFlag(id, PROPAGATED_ASSET_FLAG)) continue;
const renderModuleId = removeQueryString(id);
const key = rootRelativePath(root, renderModuleId, false);
const hash = transitiveHashes.get(renderModuleId);
if (hash) entryHashes.set(key, hash);
}
return entryHashes;
}
function pluginIncremental(internals, root) {
return {
name: "@astro/plugin-incremental",
applyToEnvironment(environment) {
return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client;
},
generateBundle() {
if (this.environment?.name === ASTRO_VITE_ENVIRONMENT_NAMES.client) {
foldClientDependencies(this, internals);
return;
}
const transitiveGraph = createTransitiveGraphCache(this);
const hashes = /* @__PURE__ */ new Map();
const serverIslandComponents = /* @__PURE__ */ new Set();
for (const id of this.getModuleIds()) {
const info = this.getModuleInfo(id);
if (!info) continue;
if (!moduleIsTopLevelPage(info)) continue;
const pageData = getPageDataByViteID(internals, info.id);
if (!pageData) continue;
const hash = transitiveGraph.hashes.get(info.id);
if (!hash) continue;
hashes.set(pageData.component, hash);
if (transitiveGraph.serverIslandModules.has(info.id)) {
serverIslandComponents.add(pageData.component);
}
}
internals.pageDependencyHashes = hashes;
internals.contentEntryRenderHashes = collectContentEntryHashes(
this,
root,
transitiveGraph.hashes
);
internals.serverIslandPageComponents = serverIslandComponents;
}
};
}
export {
pluginIncremental
};