astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
101 lines (100 loc) • 4.86 kB
JavaScript
import npath from "node:path";
import { isCSSRequest } from "vite";
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from "../core/constants.js";
import { isPropagatedAssetBoundary } from "../core/head-propagation/boundary.js";
import { unwrapId } from "../core/util.js";
import { hasSpecialQueries } from "../vite-plugin-utils/index.js";
/**
* List of file extensions signalling we can (and should) SSR ahead-of-time
* See usage below
*/
const fileExtensionsToSSR = new Set([
".astro",
".mdoc",
...SUPPORTED_MARKDOWN_FILE_EXTENSIONS
]);
const STRIP_QUERY_PARAMS_REGEX = /\?.*$/;
/** recursively crawl the module graph to get all style files imported by parent id */
export async function* crawlGraph(environment, _id, isRootFile, scanned = new Set()) {
const id = unwrapId(_id);
const importedModules = new Set();
const moduleEntriesForId = isRootFile ? environment.moduleGraph.getModulesByFile(id) ?? new Set() : // For non-root files, we're safe to pull from "getModuleById" based on testing.
// TODO: Find better invalidation strategy to use "getModuleById" in all cases!
new Set([environment.moduleGraph.getModuleById(id)]);
// Collect all imported modules for the module(s).
for (const entry of moduleEntriesForId) {
// Handle this in case an module entries weren't found for ID
// This seems possible with some virtual IDs (ex: `astro:markdown/*.md`)
if (!entry) {
continue;
}
if (id === entry.id) {
scanned.add(id);
// NOTE: It may be worth revisiting if we can crawl direct imports of the module since
// `.importedModules` would also include modules that are dynamically watched, not imported.
// That way we no longer need the below `continue` skips.
// CSS requests `importedModules` are usually from `@import`, but we don't really need
// to crawl into those as the `@import` code are already inlined into this `id`.
// If CSS requests `importedModules` contain non-CSS files, e.g. Tailwind might add HMR
// dependencies as `importedModules`, we should also skip them as they aren't really
// imported. Without this, every hoisted script in the project is added to every page!
if (isCSSRequest(id)) {
continue;
}
// Some special Vite queries like `?url` or `?raw` are known to be a simple default export
// and doesn't have any imports to crawl. However, since they would `this.addWatchFile` the
// underlying module, our logic would crawl into them anyways which is incorrect as they
// don't take part in the final rendering, so we skip it here.
if (hasSpecialQueries(id)) {
continue;
}
for (const importedModule of entry.importedModules) {
if (!importedModule.id) continue;
// some dynamically imported modules are *not* server rendered in time
// to only SSR modules that we can safely transform, we check against
// a list of file extensions based on our built-in vite plugins
// Strip special query params like "?content".
// NOTE: Cannot use `new URL()` here because not all IDs will be valid paths.
// For example, `virtual:image-loader` if you don't have the plugin installed.
const importedModulePathname = importedModule.id.replace(STRIP_QUERY_PARAMS_REGEX, "");
const isFileTypeNeedingSSR = fileExtensionsToSSR.has(npath.extname(importedModulePathname));
// A propagation stopping point is a module with the ?astroPropagatedAssets flag.
// When we encounter one of these modules we don't want to continue traversing.
const isPropagationStoppingPoint = isPropagatedAssetBoundary(importedModule.id);
if (isFileTypeNeedingSSR && !isPropagationStoppingPoint) {
const mod = environment.moduleGraph.getModuleById(importedModule.id);
if (!mod?.ssrModule) {
try {
await environment.runner.import(importedModule.id);
} catch {}
}
}
// Make sure the `importedModule` traversed is explicitly imported by the user, and not by HMR
// TODO: This isn't very performant. Maybe look into using `ssrTransformResult` but make sure it
// doesn't regress UnoCSS. https://github.com/withastro/astro/issues/7529
if (isImportedBy(id, importedModule) && !isPropagationStoppingPoint) {
importedModules.add(importedModule);
}
}
}
}
// scan imported modules for CSS imports & add them to our collection.
// Then, crawl that file to follow and scan all deep imports as well.
for (const importedModule of importedModules) {
if (!importedModule.id || scanned.has(importedModule.id)) {
continue;
}
yield importedModule;
yield* crawlGraph(environment, importedModule.id, false, scanned);
}
}
// Verify true imports. If the child module has the parent as an importers, it's
// a real import.
function isImportedBy(parent, entry) {
for (const importer of entry.importers) {
if (importer.id === parent) {
return true;
}
}
return false;
}