UNPKG

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
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 };