astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
94 lines (93 loc) • 3.74 kB
JavaScript
import { fileURLToPath } from "node:url";
import { UnableToLoadLogger } from "../errors/errors-data.js";
import { AstroError } from "../errors/index.js";
import { normalizeLoggerConfig } from "./utils.js";
export const VIRTUAL_LOGGER_ID = "virtual:astro:logger";
const RESOLVED_VIRTUAL_LOGGER_ID = "\0" + VIRTUAL_LOGGER_ID;
/**
* Emits the source of a logger destination, so that the handler is part of the bundle
* rather than imported at runtime from a path that no longer exists once deployed.
*
* This is the build-time counterpart of `createDestination()` in `./load.ts`: both walk
* the same normalized config, but this one *generates code* that instantiates the
* destination, while the Node one instantiates it directly through `import()`.
*/
export async function emitDestination(config, resolveEntrypoint, nameOffset = 0) {
let resolved = null;
let cause;
try {
resolved = await resolveEntrypoint(config.entrypoint);
} catch (e) {
// Resolution can throw for invalid package specifiers, while an entrypoint that
// simply cannot be found resolves to `null`. Both mean the same thing here.
cause = e;
}
if (!resolved) {
const error = new AstroError({
...UnableToLoadLogger,
message: UnableToLoadLogger.message(config.entrypoint)
});
if (cause instanceof Error) {
error.cause = cause;
}
throw error;
}
const name = `_logger${nameOffset}`;
const imports = [`import ${name} from ${JSON.stringify(resolved)};`];
// `astro/logger/compose` takes the composed destinations rather than
// a serializable config, so its children are instantiated inline.
if (config.loggers) {
const expressions = [];
for (const nested of config.loggers) {
const emitted = await emitDestination(nested, resolveEntrypoint, nameOffset + imports.length);
imports.push(...emitted.imports);
expressions.push(emitted.expression);
}
return {
expression: `${name}([${expressions.join(", ")}])`,
imports
};
}
return {
expression: `${name}(${JSON.stringify(config.config) ?? "undefined"})`,
imports
};
}
/**
* Emits `virtual:astro:logger`: the configured destination as the default export
* (`null` when the user configured none) plus the resolved log level.
*
* The module is always registered, even without a `logger` config, so that code
* generated ahead of the user's config — `astro:content`, which is built from a
* static template — can import it unconditionally. It deliberately imports
* nothing but the user's destination entrypoint: unlike `virtual:astro:manifest`
* it is a leaf, so reaching a logger through it cannot pull renderers and pages
* into an importer's module graph.
*/
export function vitePluginLogger({ settings }) {
const loggerConfig = settings.config.logger;
return {
name: VIRTUAL_LOGGER_ID,
enforce: "pre",
resolveId: {
filter: { id: new RegExp(`^${VIRTUAL_LOGGER_ID}$`) },
handler() {
return RESOLVED_VIRTUAL_LOGGER_ID;
}
},
load: {
filter: { id: new RegExp(`^${RESOLVED_VIRTUAL_LOGGER_ID}$`) },
async handler() {
const level = `export const level = ${JSON.stringify(settings.logLevel)};\n`;
if (!loggerConfig) {
return { code: `${level}export default null;\n` };
}
// Use the project root as the importer so that user-provided handlers
// resolve from the project's node_modules, not from astro core's location.
const importerPath = fileURLToPath(new URL("package.json", settings.config.root));
const { expression, imports } = await emitDestination(normalizeLoggerConfig(loggerConfig, settings.config.root), async (entrypoint) => (await this.resolve(entrypoint, importerPath))?.id ?? null);
return { code: `${imports.join("\n")}\n${level}export default ${expression};\n` };
}
}
};
}