astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
315 lines (314 loc) • 12.1 kB
JavaScript
import { fileURLToPath } from "node:url";
import { resolve as importMetaResolve } from "import-meta-resolve";
import { glob } from "tinyglobby";
import { builtinDrivers } from "unstorage";
import { getAssetsPrefix } from "../../../assets/utils/getAssetsPrefix.js";
import { normalizeTheLocale } from "../../../i18n/index.js";
import { toFallbackType, toRoutingStrategy } from "../../../i18n/utils.js";
import { runHookBuildSsr } from "../../../integrations/hooks.js";
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from "../../../vite-plugin-scripts/index.js";
import {
getAlgorithm,
getDirectives,
getScriptHashes,
getScriptResources,
getStrictDynamic,
getStyleHashes,
getStyleResources,
shouldTrackCspHashes,
trackScriptHashes,
trackStyleHashes
} from "../../csp/common.js";
import { encodeKey } from "../../encryption.js";
import { fileExtension, joinPaths, prependForwardSlash } from "../../path.js";
import { DEFAULT_COMPONENTS } from "../../routing/default.js";
import { serializeRouteData } from "../../routing/index.js";
import { addRollupInput } from "../add-rollup-input.js";
import { getOutFile, getOutFolder } from "../common.js";
import { cssOrder, mergeInlineCss } from "../internal.js";
import { makePageDataKey } from "./util.js";
const manifestReplace = "@@ASTRO_MANIFEST_REPLACE@@";
const replaceExp = new RegExp(`['"]${manifestReplace}['"]`, "g");
const SSR_MANIFEST_VIRTUAL_MODULE_ID = "@astrojs-manifest";
const RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID = "\0" + SSR_MANIFEST_VIRTUAL_MODULE_ID;
function resolveSessionDriver(driver) {
if (!driver) {
return null;
}
try {
if (driver === "fs") {
return importMetaResolve(builtinDrivers.fsLite, import.meta.url);
}
if (driver in builtinDrivers) {
return importMetaResolve(builtinDrivers[driver], import.meta.url);
}
} catch {
return null;
}
return driver;
}
function vitePluginManifest(options, internals) {
return {
name: "@astro/plugin-build-manifest",
enforce: "post",
options(opts) {
return addRollupInput(opts, [SSR_MANIFEST_VIRTUAL_MODULE_ID]);
},
resolveId(id) {
if (id === SSR_MANIFEST_VIRTUAL_MODULE_ID) {
return RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID;
}
},
augmentChunkHash(chunkInfo) {
if (chunkInfo.facadeModuleId === RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID) {
return Date.now().toString();
}
},
load(id) {
if (id === RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID) {
const imports = [
`import { deserializeManifest as _deserializeManifest } from 'astro/app'`,
`import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest'`
];
const resolvedDriver = resolveSessionDriver(options.settings.config.session?.driver);
const contents = [
`const manifest = _deserializeManifest('${manifestReplace}');`,
`if (manifest.sessionConfig) manifest.sessionConfig.driverModule = ${resolvedDriver ? `() => import(${JSON.stringify(resolvedDriver)})` : "null"};`,
`_privateSetManifestDontUseThis(manifest);`
];
const exports = [`export { manifest }`];
return { code: [...imports, ...contents, ...exports].join("\n") };
}
},
async generateBundle(_opts, bundle) {
for (const [chunkName, chunk] of Object.entries(bundle)) {
if (chunk.type === "asset") {
continue;
}
if (chunk.modules[RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID]) {
internals.manifestEntryChunk = chunk;
delete bundle[chunkName];
}
if (chunkName.startsWith("manifest")) {
internals.manifestFileName = chunkName;
}
}
}
};
}
function pluginManifest(options, internals) {
return {
targets: ["server"],
hooks: {
"build:before": () => {
return {
vitePlugin: vitePluginManifest(options, internals)
};
},
"build:post": async ({ mutate }) => {
if (!internals.manifestEntryChunk) {
throw new Error(`Did not generate an entry chunk for SSR`);
}
const manifest = await createManifest(options, internals);
const shouldPassMiddlewareEntryPoint = options.settings.adapter?.adapterFeatures?.edgeMiddleware;
await runHookBuildSsr({
config: options.settings.config,
manifest,
logger: options.logger,
entryPoints: internals.entryPoints,
middlewareEntryPoint: shouldPassMiddlewareEntryPoint ? internals.middlewareEntryPoint : void 0
});
const code = injectManifest(manifest, internals.manifestEntryChunk);
mutate(internals.manifestEntryChunk, ["server"], code);
}
}
};
}
async function createManifest(buildOpts, internals) {
if (!internals.manifestEntryChunk) {
throw new Error(`Did not generate an entry chunk for SSR`);
}
const clientStatics = new Set(
await glob("**/*", {
cwd: fileURLToPath(buildOpts.settings.config.build.client)
})
);
for (const file of clientStatics) {
internals.staticFiles.add(file);
}
const staticFiles = internals.staticFiles;
const encodedKey = await encodeKey(await buildOpts.key);
return await buildManifest(buildOpts, internals, Array.from(staticFiles), encodedKey);
}
function injectManifest(manifest, chunk) {
const code = chunk.code;
return code.replace(replaceExp, () => {
return JSON.stringify(manifest);
});
}
async function buildManifest(opts, internals, staticFiles, encodedKey) {
const { settings } = opts;
const routes = [];
const domainLookupTable = {};
const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries());
if (settings.scripts.some((script) => script.stage === "page")) {
staticFiles.push(entryModules[PAGE_SCRIPT_ID]);
}
const assetQueryParams = settings.adapter?.client?.assetQueryParams;
const assetQueryString = assetQueryParams ? assetQueryParams.toString() : void 0;
const prefixAssetPath = (pth) => {
let result = "";
if (settings.config.build.assetsPrefix) {
const pf = getAssetsPrefix(fileExtension(pth), settings.config.build.assetsPrefix);
result = joinPaths(pf, pth);
} else {
result = prependForwardSlash(joinPaths(settings.config.base, pth));
}
if (assetQueryString) {
result += "?" + assetQueryString;
}
return result;
};
for (const route of opts.routesList.routes) {
if (!DEFAULT_COMPONENTS.find((component) => route.component === component)) {
continue;
}
routes.push({
file: "",
links: [],
scripts: [],
styles: [],
routeData: serializeRouteData(route, settings.config.trailingSlash)
});
}
for (const route of opts.routesList.routes) {
if (!route.prerender) continue;
if (!route.pathname) continue;
const outFolder = getOutFolder(opts.settings, route.pathname, route);
const outFile = getOutFile(opts.settings.config, outFolder, route.pathname, route);
const file = outFile.toString().replace(opts.settings.config.build.client.toString(), "");
routes.push({
file,
links: [],
scripts: [],
styles: [],
routeData: serializeRouteData(route, settings.config.trailingSlash)
});
staticFiles.push(file);
}
const needsStaticHeaders = settings.adapter?.adapterFeatures?.experimentalStaticHeaders ?? false;
for (const route of opts.routesList.routes) {
const pageData = internals.pagesByKeys.get(makePageDataKey(route.route, route.component));
if (!pageData) continue;
if (route.prerender && route.type !== "redirect" && !needsStaticHeaders) {
continue;
}
const scripts = [];
if (settings.scripts.some((script) => script.stage === "page")) {
const src = entryModules[PAGE_SCRIPT_ID];
scripts.push({
type: "external",
value: prefixAssetPath(src)
});
}
const links = [];
const styles = pageData.styles.sort(cssOrder).map(({ sheet }) => sheet).map((s) => s.type === "external" ? { ...s, src: prefixAssetPath(s.src) } : s).reduce(mergeInlineCss, []);
routes.push({
file: "",
links,
scripts: [
...scripts,
...settings.scripts.filter((script) => script.stage === "head-inline").map(({ stage, content }) => ({ stage, children: content }))
],
styles,
routeData: serializeRouteData(route, settings.config.trailingSlash)
});
}
const i18n = settings.config.i18n;
if (i18n && i18n.domains) {
for (const [locale, domainValue] of Object.entries(i18n.domains)) {
domainLookupTable[domainValue] = normalizeTheLocale(locale);
}
}
if (!(BEFORE_HYDRATION_SCRIPT_ID in entryModules)) {
entryModules[BEFORE_HYDRATION_SCRIPT_ID] = "";
}
let i18nManifest = void 0;
if (settings.config.i18n) {
i18nManifest = {
fallback: settings.config.i18n.fallback,
fallbackType: toFallbackType(settings.config.i18n.routing),
strategy: toRoutingStrategy(settings.config.i18n.routing, settings.config.i18n.domains),
locales: settings.config.i18n.locales,
defaultLocale: settings.config.i18n.defaultLocale,
domainLookupTable
};
}
let csp = void 0;
if (shouldTrackCspHashes(settings.config.experimental.csp)) {
const algorithm = getAlgorithm(settings.config.experimental.csp);
const scriptHashes = [
...getScriptHashes(settings.config.experimental.csp),
...await trackScriptHashes(internals, settings, algorithm)
];
const styleHashes = [
...getStyleHashes(settings.config.experimental.csp),
...settings.injectedCsp.styleHashes,
...await trackStyleHashes(internals, settings, algorithm)
];
csp = {
cspDestination: settings.adapter?.adapterFeatures?.experimentalStaticHeaders ? "adapter" : void 0,
scriptHashes,
scriptResources: getScriptResources(settings.config.experimental.csp),
styleHashes,
styleResources: getStyleResources(settings.config.experimental.csp),
algorithm,
directives: getDirectives(settings),
isStrictDynamic: getStrictDynamic(settings.config.experimental.csp)
};
}
let internalFetchHeaders = void 0;
if (settings.adapter?.client?.internalFetchHeaders) {
const headers = typeof settings.adapter.client.internalFetchHeaders === "function" ? settings.adapter.client.internalFetchHeaders() : settings.adapter.client.internalFetchHeaders;
if (Object.keys(headers).length > 0) {
internalFetchHeaders = headers;
}
}
return {
hrefRoot: opts.settings.config.root.toString(),
cacheDir: opts.settings.config.cacheDir.toString(),
outDir: opts.settings.config.outDir.toString(),
srcDir: opts.settings.config.srcDir.toString(),
publicDir: opts.settings.config.publicDir.toString(),
buildClientDir: opts.settings.config.build.client.toString(),
buildServerDir: opts.settings.config.build.server.toString(),
adapterName: opts.settings.adapter?.name ?? "",
routes,
site: settings.config.site,
base: settings.config.base,
userAssetsBase: settings.config?.vite?.base,
trailingSlash: settings.config.trailingSlash,
compressHTML: settings.config.compressHTML,
assetsPrefix: settings.config.build.assetsPrefix,
componentMetadata: Array.from(internals.componentMetadata),
renderers: [],
clientDirectives: Array.from(settings.clientDirectives),
entryModules,
inlinedScripts: Array.from(internals.inlinedScripts),
assets: staticFiles.map(prefixAssetPath),
i18n: i18nManifest,
buildFormat: settings.config.build.format,
checkOrigin: (settings.config.security?.checkOrigin && settings.buildOutput === "server") ?? false,
allowedDomains: settings.config.security?.allowedDomains,
serverIslandNameMap: Array.from(settings.serverIslandNameMap),
key: encodedKey,
sessionConfig: settings.config.session,
csp,
internalFetchHeaders
};
}
export {
RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID,
SSR_MANIFEST_VIRTUAL_MODULE_ID,
pluginManifest
};