astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
85 lines (84 loc) • 3.83 kB
JavaScript
import { fileURLToPath } from "node:url";
import { normalizePath as viteNormalizePath } from "vite";
import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../constants.js";
/**
* Virtual module that exposes the app's top-level fetch handler.
*
* If the user has a `src/fetch.ts` (or `.js` / `.mjs` / `.mts`) file that
* default-exports a `{ fetch }` object, the virtual module re-exports it.
* Otherwise it re-exports `defaultFetchHandler` from the built-in pipeline.
*
* Consumed by the generated app entrypoints (`prod.ts` / `dev.ts`) which
* pass the resolved handler to `app.setFetchHandler(...)` after the `App`
* is instantiated.
*/
const FETCHABLE_MODULE_ID = "virtual:astro:fetchable";
const FETCHABLE_RESOLVED_MODULE_ID = "\0" + FETCHABLE_MODULE_ID;
// Default segment under `srcDir` to probe for the user's handler module.
// Matches how `vitePluginMiddleware` resolves `src/middleware`.
const DEFAULT_FETCH_FILE = "fetch";
export function vitePluginFetchable({ settings }) {
let resolvedUserAppId;
let userAppPresent = false;
const fetchFile = settings.config.fetchFile ?? DEFAULT_FETCH_FILE;
// When fetchFile is null the feature is explicitly disabled.
const fetchFileDisabled = settings.config.fetchFile === null;
const normalizedSrcDir = viteNormalizePath(fileURLToPath(settings.config.srcDir));
return {
name: "@astro/plugin-fetchable",
applyToEnvironment(environment) {
return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.astro || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
},
configureServer(server) {
server.watcher.on("change", (path) => {
const normalizedPath = viteNormalizePath(path);
if (!normalizedPath.startsWith(normalizedSrcDir)) return;
const relativePath = normalizedPath.slice(normalizedSrcDir.length);
// Dot-prefix guard: match `app.ts` but not e.g. `app-utils.ts`.
if (!relativePath.startsWith(`${fetchFile}.`)) return;
for (const name of [ASTRO_VITE_ENVIRONMENT_NAMES.ssr, ASTRO_VITE_ENVIRONMENT_NAMES.astro]) {
const environment = server.environments[name];
if (!environment) continue;
const virtualMod = environment.moduleGraph.getModuleById(FETCHABLE_RESOLVED_MODULE_ID);
if (virtualMod) {
environment.moduleGraph.invalidateModule(virtualMod);
}
}
});
},
resolveId: {
filter: { id: new RegExp(`^${FETCHABLE_MODULE_ID}$`) },
async handler() {
if (fetchFileDisabled) {
userAppPresent = false;
return FETCHABLE_RESOLVED_MODULE_ID;
}
const resolved = await this.resolve(`${normalizedSrcDir}${fetchFile}`);
userAppPresent = !!resolved;
resolvedUserAppId = resolved?.id;
return FETCHABLE_RESOLVED_MODULE_ID;
}
},
load: {
filter: { id: new RegExp(`^${FETCHABLE_RESOLVED_MODULE_ID}$`) },
handler() {
if (userAppPresent && resolvedUserAppId) {
// Re-export the user's default export (expected to be a
// `{ fetch }` object, per the web Fetch API convention).
return { code: `export { default } from ${JSON.stringify(resolvedUserAppId)};` };
}
// No user-authored app — fall back to the built-in pipeline.
// `isDefaultFetchHandler` lets the dev server's per-request
// re-import recognize this fallback WITHOUT relying on
// `instanceof`: in dev the module graph containing this virtual
// module can be invalidated (any src change invalidates the
// manifest module, whose importers include the default handler),
// so a re-evaluation would otherwise produce a different class
// identity than the facade's own DefaultFetchHandler.
return { code: `import { DefaultFetchHandler } from 'astro/app/fetch/default-handler';
export default new DefaultFetchHandler();
export const isDefaultFetchHandler = true;` };
}
}
};
}