Skip to content

Commit f61ba9c

Browse files
florian-lefebvreArmandPhilippotematipico
authored
fix(logger): loading at runtime when entrypoint is a URL (#17480)
Co-authored-by: Armand Philippot <git@armand.philippot.eu> Co-authored-by: Emanuele Stoppa <estoppa@cloudflare.com>
1 parent e614b7b commit f61ba9c

17 files changed

Lines changed: 443 additions & 87 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fixes a case where a custom `logger.entrypoint` failed to load at runtime in a built server bundle.

packages/astro/src/container/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,6 @@ function createManifest(
185185
placement: undefined,
186186
},
187187
logLevel: 'silent',
188-
loggerConfig: manifest?.loggerConfig ?? undefined,
189188
};
190189
}
191190

@@ -277,7 +276,6 @@ type AstroContainerManifest = Pick<
277276
| 'middlewareMode'
278277
| 'assetsDir'
279278
| 'image'
280-
| 'loggerConfig'
281279
>;
282280

283281
type AstroContainerConstructor = {

packages/astro/src/core/app/types.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import type { BaseSessionConfig, SessionDriverFactory } from '../session/types.j
2222
import type { DevToolbarPlacement } from '../../types/public/toolbar.js';
2323
import type { MiddlewareMode } from '../../types/public/integrations.js';
2424
import type { BaseApp } from './base.js';
25-
import type { LoggerHandlerConfig } from '../logger/config.js';
2625

2726
type ComponentPath = string;
2827

@@ -146,8 +145,6 @@ export type SSRManifest = {
146145
};
147146
internalFetchHeaders?: Record<string, string>;
148147
logLevel: AstroLoggerLevel;
149-
// Configuration that tells us how to load the logger
150-
loggerConfig: LoggerHandlerConfig | undefined;
151148
};
152149

153150
export type SSRActions = {

packages/astro/src/core/base-pipeline.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import type { CacheProvider, CacheProviderFactory } from './cache/types.js';
2828
import type { CompiledCacheRoute } from './cache/runtime/route-matching.js';
2929
import type { SessionDriverFactory } from './session/types.js';
3030
import { FORBIDDEN_PATH_KEYS } from '@astrojs/internal-helpers/object';
31-
import { loadLoggerDestination } from './logger/load.js';
3231

3332
/**
3433
* Bit flags for pipeline features that handler classes register as
@@ -296,9 +295,10 @@ export abstract class Pipeline {
296295
return this.logger;
297296
}
298297
this.resolvedLogger = true;
299-
if (this.manifest.loggerConfig) {
298+
const destination = (await this.manifest.logger?.())?.default;
299+
if (destination) {
300300
this.logger = new AstroLogger({
301-
destination: await loadLoggerDestination(this.manifest.loggerConfig),
301+
destination,
302302
level: this.manifest.logLevel,
303303
});
304304
}

packages/astro/src/core/build/plugins/plugin-manifest.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -365,11 +365,6 @@ async function buildManifest(
365365

366366
const middlewareMode = resolveMiddlewareMode(opts.settings.adapter?.adapterFeatures);
367367

368-
let loggerConfig = undefined;
369-
if (settings.config.logger) {
370-
loggerConfig = settings.config.logger;
371-
}
372-
373368
return {
374369
rootDir: opts.settings.config.root.toString(),
375370
cacheDir: opts.settings.config.cacheDir.toString(),
@@ -426,6 +421,5 @@ async function buildManifest(
426421
internalFetchHeaders,
427422
logLevel: settings.logLevel,
428423
shouldInjectCspMetaTags: shouldTrackCspHashes(settings.config.security.csp),
429-
loggerConfig,
430424
};
431425
}

packages/astro/src/core/create-vite.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { ServerIslandsState } from './server-islands/shared-state.js';
5151
import { vitePluginServerIslands } from './server-islands/vite-plugin-server-islands.js';
5252
import { vitePluginCacheProvider } from './cache/vite-plugin.js';
5353
import { vitePluginSessionDriver } from './session/vite-plugin.js';
54+
import { vitePluginLogger } from './logger/vite-plugin.js';
5455
import { isObject } from './util-runtime.js';
5556
import { vitePluginEnvironment } from '../vite-plugin-environment/index.js';
5657
import { ASTRO_VITE_ENVIRONMENT_NAMES } from './constants.js';
@@ -231,6 +232,7 @@ export async function createVite(
231232
vitePluginServerIslands({ settings, logger, serverIslandsState }),
232233
vitePluginSessionDriver({ settings }),
233234
vitePluginCacheProvider({ settings }),
235+
vitePluginLogger({ settings }),
234236
astroContainer(),
235237
astroHmrReloadPlugin(),
236238
vitePluginChromedevtools({ settings }),

packages/astro/src/core/logger/load.ts

Lines changed: 42 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,62 @@
1+
import { isAbsolute } from 'node:path';
2+
import { pathToFileURL } from 'node:url';
13
import { AstroLogger, type AstroLoggerDestination } from './core.js';
24
import { AstroError } from '../errors/index.js';
35
import { UnableToLoadLogger } from '../errors/errors-data.js';
46
import type { LoggerHandlerConfig } from './config.js';
57
import type { AstroConfig, AstroInlineConfig } from '../../types/public/index.js';
6-
import { default as nodeLoggerCreator, createNodeLoggerFromFlags } from './impls/node.js';
7-
import { default as consoleLoggerCreator } from './impls/console.js';
8-
import { default as jsonLoggerCreator } from './impls/json.js';
9-
import { default as composeLoggerCreator } from './impls/compose.js';
8+
import { createNodeLoggerFromFlags } from './impls/node.js';
9+
import {
10+
COMPOSE_LOGGER_ENTRYPOINT,
11+
normalizeLoggerConfig,
12+
type NormalizedLoggerConfig,
13+
} from './utils.js';
1014

11-
function normalizeEntrypoint(entrypoint: LoggerHandlerConfig['entrypoint']): string {
12-
return entrypoint instanceof URL ? entrypoint.href : entrypoint;
15+
/**
16+
* Instantiates a logger destination in a Node context.
17+
*
18+
* This is the runtime counterpart of `emitDestination()` in `./vite-plugin.ts`: both walk
19+
* the same normalized config, but this one imports and instantiates the handler directly,
20+
* while the Vite one *generates code* doing so, to bundle the handler into the build output.
21+
*/
22+
async function createDestination(config: NormalizedLoggerConfig): Promise<AstroLoggerDestination> {
23+
// `normalizeLoggerConfig()` turns `URL` entrypoints into absolute paths, which
24+
// `import()` only accepts as file URLs on Windows. Package entrypoints keep their
25+
// specifier and resolve through the regular module resolution.
26+
const specifier = isAbsolute(config.entrypoint)
27+
? pathToFileURL(config.entrypoint).href
28+
: config.entrypoint;
29+
const logger = await import(/* @vite-ignore */ specifier);
30+
31+
// `astro/logger/compose` takes the composed destinations rather than a serializable config.
32+
if (config.entrypoint === COMPOSE_LOGGER_ENTRYPOINT) {
33+
return logger.default(await Promise.all((config.loggers ?? []).map(createDestination)));
34+
}
35+
36+
return logger.default(config.config);
1337
}
1438

39+
/**
40+
* Loads a logger destination in a Node context, i.e. outside of a built server bundle.
41+
* Inside the bundle, the destination comes from the `virtual:astro:logger` module instead.
42+
*/
1543
export async function loadLoggerDestination(
1644
config: LoggerHandlerConfig,
1745
): Promise<AstroLoggerDestination> {
18-
let cause: Error | undefined = undefined;
19-
const entrypoint = normalizeEntrypoint(config.entrypoint);
46+
const normalized = normalizeLoggerConfig(config);
2047

2148
try {
22-
switch (config.entrypoint) {
23-
case 'astro/logger/node': {
24-
return nodeLoggerCreator(config.config);
25-
}
26-
case 'astro/logger/console': {
27-
return consoleLoggerCreator(config.config);
28-
}
29-
case 'astro/logger/json': {
30-
return jsonLoggerCreator(config.config);
31-
}
32-
case 'astro/logger/compose': {
33-
let destinations: AstroLoggerDestination[] = [];
34-
if (config.config?.loggers) {
35-
const loggers: LoggerHandlerConfig[] = config.config?.loggers;
36-
destinations = await Promise.all(
37-
loggers.map(async (loggerConfig) => {
38-
const logger = await import(
39-
/* @vite-ignore */ normalizeEntrypoint(loggerConfig.entrypoint)
40-
);
41-
return logger.default(loggerConfig.config) as AstroLoggerDestination;
42-
}),
43-
);
44-
}
45-
46-
return composeLoggerCreator(destinations);
47-
}
48-
default: {
49-
const logger = await import(/* @vite-ignore */ entrypoint);
50-
return logger.default(config.config);
51-
}
52-
}
49+
return await createDestination(normalized);
5350
} catch (e: unknown) {
51+
const error = new AstroError({
52+
...UnableToLoadLogger,
53+
message: UnableToLoadLogger.message(normalized.entrypoint),
54+
});
5455
if (e instanceof Error) {
55-
cause = e;
56+
error.cause = e;
5657
}
58+
throw error;
5759
}
58-
59-
const error = new AstroError({
60-
...UnableToLoadLogger,
61-
message: UnableToLoadLogger.message(entrypoint),
62-
});
63-
if (cause) {
64-
error.cause = cause;
65-
}
66-
throw error;
6760
}
6861

6962
/**
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { fileURLToPath } from 'node:url';
2+
import type { LoggerHandlerConfig } from './config.js';
3+
4+
export const COMPOSE_LOGGER_ENTRYPOINT = 'astro/logger/compose';
5+
6+
export interface NormalizedLoggerConfig {
7+
/** An absolute file path or a package specifier */
8+
entrypoint: string;
9+
/** Serializable options passed to the handler factory */
10+
config?: Record<string, any> | undefined;
11+
/** The composed handlers. Only set for `astro/logger/compose` */
12+
loggers?: NormalizedLoggerConfig[];
13+
}
14+
15+
/**
16+
* Normalizes a user-provided logger config by turning its `entrypoint` into a string that
17+
* can be resolved as-is, either by Vite (see `vitePluginLogger`) or by Node
18+
* (see `loadLoggerDestination`), and by applying the same treatment to the handlers
19+
* composed through `astro/logger/compose`.
20+
*
21+
* Concretely, a `URL` entrypoint becomes an absolute file path — mirroring what session
22+
* drivers do, since both Vite and `import()` can handle those — while a string entrypoint,
23+
* e.g. a package specifier, is left untouched.
24+
*/
25+
export function normalizeLoggerConfig(logger: LoggerHandlerConfig): NormalizedLoggerConfig {
26+
const entrypoint = normalizeEntrypoint(logger.entrypoint);
27+
28+
if (entrypoint === COMPOSE_LOGGER_ENTRYPOINT) {
29+
const loggers: LoggerHandlerConfig[] = logger.config?.loggers ?? [];
30+
return {
31+
entrypoint,
32+
loggers: loggers.map((nested) => normalizeLoggerConfig(nested)),
33+
};
34+
}
35+
36+
return { entrypoint, config: logger.config };
37+
}
38+
39+
function normalizeEntrypoint(entrypoint: LoggerHandlerConfig['entrypoint']): string {
40+
return entrypoint instanceof URL ? fileURLToPath(entrypoint) : entrypoint;
41+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { fileURLToPath } from 'node:url';
2+
import type { Plugin as VitePlugin } from 'vite';
3+
import type { AstroSettings } from '../../types/astro.js';
4+
import { UnableToLoadLogger } from '../errors/errors-data.js';
5+
import { AstroError } from '../errors/index.js';
6+
import { normalizeLoggerConfig, type NormalizedLoggerConfig } from './utils.js';
7+
8+
export const VIRTUAL_LOGGER_ID = 'virtual:astro:logger';
9+
const RESOLVED_VIRTUAL_LOGGER_ID = '\0' + VIRTUAL_LOGGER_ID;
10+
11+
/** Resolves an entrypoint to a module id, or `null` if it cannot be resolved. */
12+
type ResolveEntrypoint = (entrypoint: string) => Promise<string | null>;
13+
14+
interface EmittedDestination {
15+
/** The expression instantiating the destination, e.g. `_logger0({ level: 'info' })` */
16+
expression: string;
17+
/** The import statements `expression` depends on, in declaration order */
18+
imports: string[];
19+
}
20+
21+
/**
22+
* Emits the source of a logger destination, so that the handler is part of the bundle
23+
* rather than imported at runtime from a path that no longer exists once deployed.
24+
*
25+
* This is the build-time counterpart of `createDestination()` in `./load.ts`: both walk
26+
* the same normalized config, but this one *generates code* that instantiates the
27+
* destination, while the Node one instantiates it directly through `import()`.
28+
*/
29+
export async function emitDestination(
30+
config: NormalizedLoggerConfig,
31+
resolveEntrypoint: ResolveEntrypoint,
32+
/**
33+
* Import names must be unique across the whole virtual module, so nested
34+
* destinations continue numbering where their parent left off.
35+
*/
36+
nameOffset = 0,
37+
): Promise<EmittedDestination> {
38+
let resolved: string | null = null;
39+
let cause: unknown;
40+
try {
41+
resolved = await resolveEntrypoint(config.entrypoint);
42+
} catch (e) {
43+
// Resolution can throw for invalid package specifiers, while an entrypoint that
44+
// simply cannot be found resolves to `null`. Both mean the same thing here.
45+
cause = e;
46+
}
47+
if (!resolved) {
48+
const error = new AstroError({
49+
...UnableToLoadLogger,
50+
message: UnableToLoadLogger.message(config.entrypoint),
51+
});
52+
if (cause instanceof Error) {
53+
error.cause = cause;
54+
}
55+
throw error;
56+
}
57+
58+
const name = `_logger${nameOffset}`;
59+
const imports = [`import ${name} from ${JSON.stringify(resolved)};`];
60+
61+
// `astro/logger/compose` takes the composed destinations rather than
62+
// a serializable config, so its children are instantiated inline.
63+
if (config.loggers) {
64+
const expressions: string[] = [];
65+
for (const nested of config.loggers) {
66+
const emitted = await emitDestination(nested, resolveEntrypoint, nameOffset + imports.length);
67+
imports.push(...emitted.imports);
68+
expressions.push(emitted.expression);
69+
}
70+
return { expression: `${name}([${expressions.join(', ')}])`, imports };
71+
}
72+
73+
return { expression: `${name}(${JSON.stringify(config.config) ?? 'undefined'})`, imports };
74+
}
75+
76+
export function vitePluginLogger({
77+
settings,
78+
}: {
79+
settings: AstroSettings;
80+
}): VitePlugin | undefined {
81+
const loggerConfig = settings.config.logger;
82+
if (!loggerConfig) {
83+
return;
84+
}
85+
86+
return {
87+
name: VIRTUAL_LOGGER_ID,
88+
enforce: 'pre',
89+
90+
resolveId: {
91+
filter: {
92+
id: new RegExp(`^${VIRTUAL_LOGGER_ID}$`),
93+
},
94+
handler() {
95+
return RESOLVED_VIRTUAL_LOGGER_ID;
96+
},
97+
},
98+
99+
load: {
100+
filter: {
101+
id: new RegExp(`^${RESOLVED_VIRTUAL_LOGGER_ID}$`),
102+
},
103+
async handler() {
104+
// Use the project root as the importer so that user-provided handlers
105+
// resolve from the project's node_modules, not from astro core's location.
106+
const importerPath = fileURLToPath(new URL('package.json', settings.config.root));
107+
const { expression, imports } = await emitDestination(
108+
normalizeLoggerConfig(loggerConfig),
109+
async (entrypoint) => (await this.resolve(entrypoint, importerPath))?.id ?? null,
110+
);
111+
112+
return {
113+
code: `${imports.join('\n')}\nexport default ${expression};\n`,
114+
};
115+
},
116+
},
117+
};
118+
}

0 commit comments

Comments
 (0)