astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
245 lines (244 loc) • 8.75 kB
JavaScript
import { fileURLToPath } from "node:url";
import { ASTRO_VERSION } from "../core/constants.js";
import { enhanceViteSSRError } from "../core/errors/dev/index.js";
import { AggregateError, CSSError, MarkdownError } from "../core/errors/index.js";
import { getLogger } from "../core/logger/manifest-logger.js";
import { req } from "../core/messages/runtime.js";
import { RedirectComponentInstance, RedirectSinglePageBuiltModule } from "../core/redirects/index.js";
import { loadRenderer } from "../core/render/index.js";
import { getDefaultRoutes } from "../core/routing/default.js";
import { routeIsRedirect } from "../core/routing/helpers.js";
import { createRewriteRouteValidator } from "../core/routing/rewrite-validate.js";
import { findRouteToRewrite } from "../core/routing/rewrite.js";
import { getRouteTable } from "../core/routing/route-table.js";
import { isPage } from "../core/util.js";
import { resolveIdToUrl } from "../core/viteUtils.js";
import { stringifyForScript } from "../runtime/server/escape.js";
import { getComponentMetadata } from "../vite-plugin-astro-server/metadata.js";
import { PAGE_SCRIPT_ID } from "../vite-plugin-scripts/index.js";
/**
* Per-manifest slot for the runnable dev server's renderers. Unlike every
* other environment, runnable dev reloads renderers on each request (see
* `getComponentByRoute` below), so the value is overwritten per request and
* concurrent requests can observe each other's writes.
*/
const devRenderers = new WeakMap();
export function getDevRenderers(manifest) {
return devRenderers.get(manifest) ?? [];
}
export function setDevRenderers(manifest, renderers) {
devRenderers.set(manifest, renderers);
}
// Identical to the production implementation: redirect + i18n-fallback
// handling over pageMap/pageModule.
async function getModuleForRoute(manifest, route) {
for (const defaultRoute of getDefaultRoutes(manifest)) {
if (route.component === defaultRoute.component) {
return { page: () => Promise.resolve(defaultRoute.instance) };
}
}
if (route.type === "redirect") {
return RedirectSinglePageBuiltModule;
} else {
if (manifest.pageMap) {
const importComponentInstance = manifest.pageMap.get(route.component);
if (!importComponentInstance) {
throw new Error(`Unexpectedly unable to find a component instance for route ${route.route}`);
}
return await importComponentInstance();
} else if (manifest.pageModule) {
return manifest.pageModule;
}
throw new Error("Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue.");
}
}
/**
* The runnable dev environment (the Vite SSR environment can load modules at
* runtime). The ModuleLoader and AstroSettings are captured in this closure at
* composition time (`createAstroServerApp`) and are unreachable from
* requests/states by design — only the environment functions close over them.
*/
export function createRunnableEnvironment({ loader, settings, getDebugInfo }) {
// Renderers are (re)loaded on every request before the route module is
// imported, into the per-manifest slot.
async function getComponentByRoute(manifest, routeData) {
if (routeIsRedirect(routeData)) {
return RedirectComponentInstance;
}
// Important: This needs to happen first, in case a renderer provides polyfills.
if (settings) {
const renderers__ = settings.renderers.map((r) => loadRenderer(r, loader));
const renderers_ = await Promise.all(renderers__);
setDevRenderers(manifest, renderers_.filter((r) => Boolean(r)));
}
const filePath = new URL(`${routeData.component}`, manifest.rootDir);
// Check built-in routes
for (const route of getDefaultRoutes(manifest)) {
if (route.matchesComponent(filePath)) {
return route.instance;
}
}
try {
// Load the module from the Vite SSR Runtime.
return await loader.import(filePath.toString());
} catch (error) {
// If the error came from Markdown or CSS, we already handled it and there's no need to enhance it
if (MarkdownError.is(error) || CSSError.is(error) || AggregateError.is(error)) {
throw error;
}
throw enhanceViteSSRError({
error,
filePath,
loader
});
}
}
return {
name: "dev-runnable",
runtimeMode: "development",
// Dev always streams.
defaultStreaming: () => true,
resolve(manifest, specifier) {
return resolveIdToUrl(loader, specifier, manifest.rootDir);
},
async headElements(manifest, routeData) {
const filePath = new URL(`${routeData.component}`, manifest.rootDir);
const scripts = new Set();
// Inject HMR scripts
if (settings) {
if (isPage(filePath, settings)) {
scripts.add({
props: {
type: "module",
src: "/@vite/client"
},
children: ""
});
if (manifest.devToolbar.enabled) {
scripts.add({
props: {
type: "module",
src: "/@id/astro/runtime/client/dev-toolbar/entrypoint.js"
},
children: ""
});
const additionalMetadata = {
root: fileURLToPath(settings.config.root),
version: ASTRO_VERSION,
latestAstroVersion: settings.latestAstroVersion,
// TODO: Currently the debug info is always fetched, which slows things down.
// We should look into not loading it if the dev toolbar is disabled. And when
// enabled, it would nice to request the debug info through import.meta.hot
// when the button is click to defer execution as much as possible
debugInfo: await getDebugInfo(),
placement: settings.config.devToolbar.placement
};
// Additional data for the dev overlay
const children = `window.__astro_dev_toolbar__ = ${stringifyForScript(additionalMetadata)}`;
scripts.add({
props: {},
children
});
}
}
// TODO: We should allow adding generic HTML elements to the head, not just scripts
for (const script of settings.scripts) {
if (script.stage === "head-inline") {
scripts.add({
props: {},
children: script.content
});
} else if (script.stage === "page" && isPage(filePath, settings)) {
scripts.add({
props: {
type: "module",
src: `/@id/${PAGE_SCRIPT_ID}`
},
children: ""
});
}
}
}
const { devCSSMap } = await import("virtual:astro:dev-css-all");
const importer = devCSSMap.get(routeData.component);
let css = new Set();
if (importer) {
const cssModule = await importer();
css = cssModule.css;
} else {
getLogger(manifest).warn("assets", `Unable to find CSS for ${routeData.component}. This is likely a bug in Astro.`);
}
// Pass framework CSS in as style tags to be appended to the page.
const links = new Set();
const styles = new Set();
for (const { id, url: src, content } of css) {
// Vite handles HMR for styles injected as scripts
scripts.add({
props: {
type: "module",
src
},
children: ""
});
// But we still want to inject the styles to avoid FOUC. The style tags
// should emulate what Vite injects so further HMR works as expected.
styles.add({
props: { "data-vite-dev-id": id },
children: content
});
}
return {
scripts,
styles,
links
};
},
componentMetadata(manifest, routeData) {
const filePath = new URL(`${routeData.component}`, manifest.rootDir);
return getComponentMetadata(filePath, loader);
},
getComponentByRoute,
getModuleForRoute,
async tryRewrite(manifest, payload, request) {
const { routeData, pathname, newUrl } = await findRouteToRewrite({
payload,
request,
// The single fresh route table: HMR route updates are visible
// to rewrites at the same instant as every other consumer.
routes: getRouteTable(manifest).routes,
trailingSlash: manifest.trailingSlash,
buildFormat: manifest.buildFormat,
base: manifest.base,
outDir: manifest.outDir,
validate: createRewriteRouteValidator(manifest, getComponentByRoute)
});
const componentInstance = await getComponentByRoute(manifest, routeData);
return {
newUrl,
pathname,
componentInstance,
routeData
};
},
getRenderers(manifest) {
return getDevRenderers(manifest);
},
errorStrategy: "dev",
rendersRedirectPage: () => false,
injectCspMetaTagsOnErrorPages: true,
logRequest(manifest, payload) {
const { pathname, method, statusCode, isRewrite, timeStart } = payload;
if (pathname === "/favicon.ico") {
return;
}
const reqTime = performance.now() - timeStart;
getLogger(manifest).info(null, req({
url: pathname,
method,
statusCode,
isRewrite,
reqTime
}));
}
};
}