diff --git a/.changeset/big-jobs-make.md b/.changeset/big-jobs-make.md
new file mode 100644
index 000000000000..8e1201a4183e
--- /dev/null
+++ b/.changeset/big-jobs-make.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Fixes an issue where the use of the Astro internal logger couldn't work with Cloudflare Vite plugin.
diff --git a/.changeset/fix-large-static-routes-stack-overflow.md b/.changeset/fix-large-static-routes-stack-overflow.md
new file mode 100644
index 000000000000..a4c7a7ced4d5
--- /dev/null
+++ b/.changeset/fix-large-static-routes-stack-overflow.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Fixes a build error when generating projects with a large number of static routes
diff --git a/.changeset/floppy-cases-hug.md b/.changeset/floppy-cases-hug.md
new file mode 100644
index 000000000000..5e453a25456c
--- /dev/null
+++ b/.changeset/floppy-cases-hug.md
@@ -0,0 +1,6 @@
+---
+'@astrojs/cloudflare': patch
+'astro': patch
+---
+
+Fixes an issue where the use of the `Code` component would result in an unexpected error.
diff --git a/.changeset/green-zebras-lick.md b/.changeset/green-zebras-lick.md
new file mode 100644
index 000000000000..c433004b1e1d
--- /dev/null
+++ b/.changeset/green-zebras-lick.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Fixes an issue where the new Astro v6 development server didn't log anything when navigating the pages.
diff --git a/.changeset/orange-boats-refuse.md b/.changeset/orange-boats-refuse.md
new file mode 100644
index 000000000000..6feb31b812a2
--- /dev/null
+++ b/.changeset/orange-boats-refuse.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/cloudflare': patch
+---
+
+Fixes an issue where `esbuild` would throw a "Top-level return cannot be used inside an ECMAScript module" error during dependency scanning in certain environments.
diff --git a/benchmark/packages/adapter/src/server.ts b/benchmark/packages/adapter/src/server.ts
index 727444e06cd4..2a4d45f443d1 100644
--- a/benchmark/packages/adapter/src/server.ts
+++ b/benchmark/packages/adapter/src/server.ts
@@ -1,6 +1,6 @@
import * as fs from 'node:fs';
import type { SSRManifest } from 'astro';
-import { AppPipeline, BaseApp } from 'astro/app';
+import { AppPipeline, BaseApp, type LogRequestPayload } from 'astro/app';
class MyApp extends BaseApp {
#manifest: SSRManifest | undefined;
@@ -30,6 +30,8 @@ class MyApp extends BaseApp {
streaming,
});
}
+
+ logRequest(_options: LogRequestPayload) {}
}
export function createExports(manifest: SSRManifest) {
diff --git a/biome.jsonc b/biome.jsonc
index 2251f5e4db13..5d842c8e8096 100644
--- a/biome.jsonc
+++ b/biome.jsonc
@@ -137,7 +137,7 @@
// We don't want to have node modules in code that should be runtime agnostic
"includes": [
"**/packages/astro/src/**/runtime/**/*.ts",
- "**/packages/astro/src/**/runtime.ts"
+ "**/packages/astro/src/**/*runtime*.ts"
],
"linter": {
"rules": {
diff --git a/knip.js b/knip.js
index 0b6ebf7efc04..166660a63dc0 100644
--- a/knip.js
+++ b/knip.js
@@ -85,7 +85,7 @@ export default {
'packages/markdown/remark': {
entry: [testEntry],
// package.json#imports are not resolved at the moment
- ignore: ['src/import-plugin-browser.ts'],
+ ignore: ['src/import-plugin-browser.ts', 'src/shiki-engine-workerd.ts'],
},
'packages/upgrade': {
entry: ['src/index.ts', testEntry],
diff --git a/packages/astro/components/Code.astro b/packages/astro/components/Code.astro
index 860ea9ceea2e..30b2bb45d80a 100644
--- a/packages/astro/components/Code.astro
+++ b/packages/astro/components/Code.astro
@@ -1,10 +1,7 @@
---
-import {
- type ThemePresets,
- createShikiHighlighter,
- globalShikiStyleCollector,
- transformerStyleToClass,
-} from '@astrojs/markdown-remark';
+import { createShikiHighlighter, type ThemePresets } from '@astrojs/markdown-remark/shiki';
+import { globalShikiStyleCollector } from '@astrojs/markdown-remark/shiki-style-collector';
+import { transformerStyleToClass } from '@astrojs/markdown-remark/transformers/style-to-class';
import type { ShikiTransformer, ThemeRegistration, ThemeRegistrationRaw } from 'shiki';
import { bundledLanguages } from 'shiki/langs';
import type { CodeLanguage } from '../dist/types/public/common.js';
diff --git a/packages/astro/package.json b/packages/astro/package.json
index ba6826af980a..042a12e8c480 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -131,7 +131,6 @@
"common-ancestor-path": "^2.0.0",
"cookie": "^1.1.1",
"cssesc": "^3.0.0",
- "debug": "^4.4.3",
"deterministic-object-hash": "^2.0.2",
"devalue": "^5.6.2",
"diff": "^8.0.3",
@@ -149,6 +148,7 @@
"magicast": "^0.5.2",
"mrmime": "^2.0.1",
"neotraverse": "^0.6.18",
+ "obug": "^2.1.1",
"p-limit": "^7.3.0",
"p-queue": "^9.1.0",
"package-manager-detector": "^1.6.0",
@@ -181,7 +181,6 @@
"@playwright/test": "1.58.2",
"@types/aria-query": "^5.0.4",
"@types/cssesc": "^3.0.2",
- "@types/debug": "^4.1.12",
"@types/dlv": "^1.1.5",
"@types/hast": "^3.0.4",
"@types/html-escaper": "3.0.4",
diff --git a/packages/astro/src/assets/fonts/vite-plugin-fonts.ts b/packages/astro/src/assets/fonts/vite-plugin-fonts.ts
index 58a533a6a8a4..ed890f0ea443 100644
--- a/packages/astro/src/assets/fonts/vite-plugin-fonts.ts
+++ b/packages/astro/src/assets/fonts/vite-plugin-fonts.ts
@@ -8,7 +8,7 @@ import { generateCspDigest } from '../../core/encryption.js';
import { collectErrorMetadata } from '../../core/errors/dev/utils.js';
import { AstroError, AstroErrorData, isAstroError } from '../../core/errors/index.js';
import type { Logger } from '../../core/logger/core.js';
-import { formatErrorMessage } from '../../core/messages.js';
+import { formatErrorMessage } from '../../core/messages/runtime.js';
import { appendForwardSlash, joinPaths, prependForwardSlash } from '../../core/path.js';
import { getClientOutputDirectory } from '../../prerender/utils.js';
import type { AstroSettings } from '../../types/astro.js';
diff --git a/packages/astro/src/cli/add/index.ts b/packages/astro/src/cli/add/index.ts
index 8fa02854031d..ae1dcc62a83a 100644
--- a/packages/astro/src/cli/add/index.ts
+++ b/packages/astro/src/cli/add/index.ts
@@ -24,8 +24,8 @@ import {
updateTSConfigForFramework,
} from '../../core/config/tsconfig.js';
import type { Logger } from '../../core/logger/core.js';
-import * as msg from '../../core/messages.js';
-import { printHelp } from '../../core/messages.js';
+import * as msg from '../../core/messages/runtime.js';
+import { printHelp } from '../../core/messages/runtime.js';
import { appendForwardSlash } from '../../core/path.js';
import { ensureProcessNodeEnv, parseNpmName } from '../../core/util.js';
import { eventCliSession, telemetry } from '../../events/index.js';
diff --git a/packages/astro/src/cli/build/index.ts b/packages/astro/src/cli/build/index.ts
index 30f19bdccaab..c5e23ac5816c 100644
--- a/packages/astro/src/cli/build/index.ts
+++ b/packages/astro/src/cli/build/index.ts
@@ -1,5 +1,5 @@
import _build from '../../core/build/index.js';
-import { printHelp } from '../../core/messages.js';
+import { printHelp } from '../../core/messages/runtime.js';
import { type Flags, flagsToAstroInlineConfig } from '../flags.js';
interface BuildOptions {
diff --git a/packages/astro/src/cli/dev/index.ts b/packages/astro/src/cli/dev/index.ts
index f5ddba717c7b..60bc5c2b9264 100644
--- a/packages/astro/src/cli/dev/index.ts
+++ b/packages/astro/src/cli/dev/index.ts
@@ -1,6 +1,6 @@
import colors from 'piccolore';
import devServer from '../../core/dev/index.js';
-import { printHelp } from '../../core/messages.js';
+import { printHelp } from '../../core/messages/runtime.js';
import { type Flags, flagsToAstroInlineConfig } from '../flags.js';
interface DevOptions {
diff --git a/packages/astro/src/cli/flags.ts b/packages/astro/src/cli/flags.ts
index 5e1ea2f9258f..b73c18c669ee 100644
--- a/packages/astro/src/cli/flags.ts
+++ b/packages/astro/src/cli/flags.ts
@@ -1,6 +1,6 @@
import type { Arguments } from 'yargs-parser';
-import { Logger, type LogOptions } from '../core/logger/core.js';
-import { nodeLogDestination } from '../core/logger/node.js';
+import type { Logger, LogOptions } from '../core/logger/core.js';
+import { createNodeLogger, nodeLogDestination } from '../core/logger/node.js';
import type { AstroInlineConfig } from '../types/public/config.js';
// Alias for now, but allows easier migration to node's `parseArgs` in the future.
@@ -52,5 +52,5 @@ export function createLoggerFromFlags(flags: Flags): Logger {
logging.level = 'silent';
}
- return new Logger(logging);
+ return createNodeLogger({ logLevel: logging.level });
}
diff --git a/packages/astro/src/cli/preferences/index.ts b/packages/astro/src/cli/preferences/index.ts
index 0be869e02083..7b8f5953048a 100644
--- a/packages/astro/src/cli/preferences/index.ts
+++ b/packages/astro/src/cli/preferences/index.ts
@@ -6,7 +6,7 @@ import colors from 'piccolore';
import { resolveConfig } from '../../core/config/config.js';
import { createSettings } from '../../core/config/settings.js';
import { collectErrorMetadata } from '../../core/errors/dev/utils.js';
-import * as msg from '../../core/messages.js';
+import * as msg from '../../core/messages/runtime.js';
import { DEFAULT_PREFERENCES } from '../../preferences/defaults.js';
import { coerce, isValidKey, type PreferenceKey } from '../../preferences/index.js';
import type { AstroSettings } from '../../types/astro.js';
diff --git a/packages/astro/src/cli/preview/index.ts b/packages/astro/src/cli/preview/index.ts
index 9607d0bf1ef9..5c6a08f097e2 100644
--- a/packages/astro/src/cli/preview/index.ts
+++ b/packages/astro/src/cli/preview/index.ts
@@ -1,5 +1,5 @@
import colors from 'piccolore';
-import { printHelp } from '../../core/messages.js';
+import { printHelp } from '../../core/messages/runtime.js';
import previewServer from '../../core/preview/index.js';
import { type Flags, flagsToAstroInlineConfig } from '../flags.js';
diff --git a/packages/astro/src/cli/sync/index.ts b/packages/astro/src/cli/sync/index.ts
index 7f488836d6cf..c50742b4b8ad 100644
--- a/packages/astro/src/cli/sync/index.ts
+++ b/packages/astro/src/cli/sync/index.ts
@@ -1,4 +1,4 @@
-import { printHelp } from '../../core/messages.js';
+import { printHelp } from '../../core/messages/runtime.js';
import _sync from '../../core/sync/index.js';
import { type Flags, flagsToAstroInlineConfig } from '../flags.js';
diff --git a/packages/astro/src/cli/telemetry/index.ts b/packages/astro/src/cli/telemetry/index.ts
index 13d12af562cc..4fa0defcb026 100644
--- a/packages/astro/src/cli/telemetry/index.ts
+++ b/packages/astro/src/cli/telemetry/index.ts
@@ -1,4 +1,4 @@
-import * as msg from '../../core/messages.js';
+import * as msg from '../../core/messages/runtime.js';
import { telemetry } from '../../events/index.js';
import { createLoggerFromFlags, type Flags } from '../flags.js';
diff --git a/packages/astro/src/cli/throw-and-exit.ts b/packages/astro/src/cli/throw-and-exit.ts
index 239dab18091a..d66db31d8e11 100644
--- a/packages/astro/src/cli/throw-and-exit.ts
+++ b/packages/astro/src/cli/throw-and-exit.ts
@@ -2,7 +2,7 @@ import { collectErrorMetadata } from '../core/errors/dev/index.js';
import { isAstroConfigZodError } from '../core/errors/errors.js';
import { createSafeError } from '../core/errors/index.js';
import { debug } from '../core/logger/core.js';
-import { formatErrorMessage } from '../core/messages.js';
+import { formatErrorMessage } from '../core/messages/runtime.js';
import { eventError, telemetry } from '../events/index.js';
/** Display error and exit */
diff --git a/packages/astro/src/config/index.ts b/packages/astro/src/config/index.ts
index a7d3c324df0b..0f7eba7c3ac0 100644
--- a/packages/astro/src/config/index.ts
+++ b/packages/astro/src/config/index.ts
@@ -37,7 +37,7 @@ export function getViteConfig(
{ runHookConfigSetup, runHookConfigDone },
] = await Promise.all([
import('vite'),
- import('../core/config/logging.js'),
+ import('../core/logger/node.js'),
import('../core/config/index.js'),
import('../core/create-vite.js'),
import('../integrations/hooks.js'),
diff --git a/packages/astro/src/core/app/app.ts b/packages/astro/src/core/app/app.ts
index 0b6e6ac468f2..e672de736ffa 100644
--- a/packages/astro/src/core/app/app.ts
+++ b/packages/astro/src/core/app/app.ts
@@ -1,4 +1,4 @@
-import { BaseApp } from './base.js';
+import { BaseApp, type LogRequestPayload } from './base.js';
import { AppPipeline } from './pipeline.js';
export class App extends BaseApp {
@@ -12,4 +12,7 @@ export class App extends BaseApp {
isDev(): boolean {
return false;
}
+
+ // Should we log something for our users?
+ logRequest(_options: LogRequestPayload) {}
}
diff --git a/packages/astro/src/core/app/base.ts b/packages/astro/src/core/app/base.ts
index fae8bbf8a6bc..3169e359b7e5 100644
--- a/packages/astro/src/core/app/base.ts
+++ b/packages/astro/src/core/app/base.ts
@@ -18,6 +18,7 @@ import {
REROUTABLE_STATUS_CODES,
REROUTE_DIRECTIVE_HEADER,
responseSentSymbol,
+ REWRITE_DIRECTIVE_HEADER_KEY,
} from '../constants.js';
import { getSetCookiesFromResponse } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
@@ -342,6 +343,7 @@ export abstract class BaseApp
{
}
public async render(request: Request, renderOptions?: RenderOptions): Promise {
+ const timeStart = performance.now();
let routeData: RouteData | undefined = renderOptions?.routeData;
let locals: object | undefined;
let clientAddress: string | undefined;
@@ -379,7 +381,8 @@ export abstract class BaseApp {
'The adapter ' + this.manifest.adapterName + ' provided a custom RouteData for ',
request.url,
);
- this.logger.debug('router', 'RouteData:\n' + routeData);
+ this.logger.debug('router', 'RouteData');
+ this.logger.debug('router', routeData);
}
if (locals) {
if (typeof locals !== 'object') {
@@ -443,6 +446,16 @@ export abstract class BaseApp
{
});
session = renderContext.session;
response = await renderContext.render(componentInstance);
+
+ const isRewrite = response.headers.has(REWRITE_DIRECTIVE_HEADER_KEY);
+
+ this.logThisRequest({
+ pathname,
+ method: request.method,
+ statusCode: response.status,
+ isRewrite,
+ timeStart,
+ });
} catch (err: any) {
this.logger.error(null, err.stack || err.message || String(err));
return this.renderError(request, {
@@ -672,4 +685,52 @@ export abstract class BaseApp
{
public getManifest() {
return this.pipeline.manifest;
}
+
+ logThisRequest({
+ pathname,
+ method,
+ statusCode,
+ isRewrite,
+ timeStart,
+ }: {
+ pathname: string;
+ method: string;
+ statusCode: number;
+ isRewrite: boolean;
+ timeStart: number;
+ }) {
+ const timeEnd = performance.now();
+ this.logRequest({
+ pathname,
+ method,
+ statusCode,
+ isRewrite,
+ reqTime: timeEnd - timeStart,
+ });
+ }
+
+ public abstract logRequest(_options: LogRequestPayload): void;
}
+
+export type LogRequestPayload = {
+ /**
+ * The current path being rendered
+ */
+ pathname: string;
+ /**
+ * The method of the request
+ */
+ method: string;
+ /**
+ * The status code of the request
+ */
+ statusCode: number;
+ /**
+ * If the current request is a rewrite
+ */
+ isRewrite: boolean;
+ /**
+ * How long it took to render the request
+ */
+ reqTime: number;
+};
diff --git a/packages/astro/src/core/app/dev/app.ts b/packages/astro/src/core/app/dev/app.ts
index b36119646fac..ff64f5994eaa 100644
--- a/packages/astro/src/core/app/dev/app.ts
+++ b/packages/astro/src/core/app/dev/app.ts
@@ -3,7 +3,12 @@ import { MiddlewareNoDataOrNextCalled, MiddlewareNotAResponse } from '../../erro
import { type AstroError, isAstroError } from '../../errors/index.js';
import type { Logger } from '../../logger/core.js';
import type { CreateRenderContext, RenderContext } from '../../render-context.js';
-import { BaseApp, type DevMatch, type RenderErrorOptions } from '../base.js';
+import {
+ BaseApp,
+ type DevMatch,
+ type LogRequestPayload,
+ type RenderErrorOptions,
+} from '../base.js';
import type { SSRManifest } from '../types.js';
import { NonRunnablePipeline } from './pipeline.js';
import { getCustom404Route, getCustom500Route } from '../../routing/helpers.js';
@@ -11,6 +16,7 @@ import { ensure404Route } from '../../routing/astro-designed-error-pages.js';
import { matchRoute } from '../../routing/dev.js';
import type { RunnablePipeline } from '../../../vite-plugin-app/pipeline.js';
import type { RoutesList } from '../../../types/astro.js';
+import { req } from '../../messages/runtime.js';
export class DevApp extends BaseApp {
logger: Logger;
@@ -134,4 +140,20 @@ export class DevApp extends BaseApp {
return renderRoute(custom500);
}
}
+
+ logRequest({ pathname, method, statusCode, isRewrite, reqTime }: LogRequestPayload) {
+ if (pathname === '/favicon.ico') {
+ return;
+ }
+ this.logger.info(
+ null,
+ req({
+ url: pathname,
+ method,
+ statusCode,
+ isRewrite,
+ reqTime,
+ }),
+ );
+ }
}
diff --git a/packages/astro/src/core/app/entrypoints/index.ts b/packages/astro/src/core/app/entrypoints/index.ts
index 63fdafd87d91..2730ce8395f6 100644
--- a/packages/astro/src/core/app/entrypoints/index.ts
+++ b/packages/astro/src/core/app/entrypoints/index.ts
@@ -1,6 +1,11 @@
export type { RoutesList } from '../../../types/astro.js';
export { App } from '../app.js';
-export { BaseApp, type RenderErrorOptions, type RenderOptions } from '../base.js';
+export {
+ BaseApp,
+ type RenderErrorOptions,
+ type RenderOptions,
+ type LogRequestPayload,
+} from '../base.js';
export { fromRoutingStrategy, toRoutingStrategy } from '../common.js';
export { createConsoleLogger } from '../logging.js';
export {
diff --git a/packages/astro/src/core/build/app.ts b/packages/astro/src/core/build/app.ts
index 92900bbc80fd..aae4c30af6d9 100644
--- a/packages/astro/src/core/build/app.ts
+++ b/packages/astro/src/core/build/app.ts
@@ -4,6 +4,7 @@ import type { BuildInternals } from './internal.js';
import { BuildPipeline } from './pipeline.js';
import type { StaticBuildOptions } from './types.js';
import type { CreateRenderContext, RenderContext } from '../render-context.js';
+import type { LogRequestPayload } from '../app/base.js';
export class BuildApp extends BaseApp {
createPipeline(_streaming: boolean, manifest: SSRManifest, ..._args: any[]): BuildPipeline {
@@ -52,4 +53,6 @@ export class BuildApp extends BaseApp {
});
}
}
+
+ logRequest(_options: LogRequestPayload) {}
}
diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts
index 069f63651903..8842c4bbaee5 100644
--- a/packages/astro/src/core/build/index.ts
+++ b/packages/astro/src/core/build/index.ts
@@ -14,7 +14,7 @@ import {
import type { AstroSettings, RoutesList } from '../../types/astro.js';
import type { AstroInlineConfig, RuntimeMode } from '../../types/public/config.js';
import { resolveConfig } from '../config/config.js';
-import { createNodeLogger } from '../config/logging.js';
+import { createNodeLogger } from '../logger/node.js';
import { createSettings } from '../config/settings.js';
import { createVite } from '../create-vite.js';
import { createKey, getEnvironmentKey, hasEnvironmentKey } from '../encryption.js';
diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts
index dce4bccf3876..08d48e430c54 100644
--- a/packages/astro/src/core/config/config.ts
+++ b/packages/astro/src/core/config/config.ts
@@ -12,7 +12,7 @@ import type {
} from '../../types/public/config.js';
import { trackAstroConfigZodError } from '../errors/errors.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
-import { formatConfigErrorMessage } from '../messages.js';
+import { formatConfigErrorMessage } from '../messages/runtime.js';
import { mergeConfig } from './merge.js';
import { validateConfig } from './validate.js';
import { loadConfigWithVite } from './vite-load.js';
diff --git a/packages/astro/src/core/config/logging.ts b/packages/astro/src/core/config/logging.ts
deleted file mode 100644
index bd72f8b5e978..000000000000
--- a/packages/astro/src/core/config/logging.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { AstroInlineConfig } from '../../types/public/config.js';
-import { Logger } from '../logger/core.js';
-import { nodeLogDestination } from '../logger/node.js';
-
-export function createNodeLogger(inlineConfig: AstroInlineConfig): Logger {
- if (inlineConfig.logger) return inlineConfig.logger;
-
- return new Logger({
- dest: nodeLogDestination,
- level: inlineConfig.logLevel ?? 'info',
- });
-}
diff --git a/packages/astro/src/core/config/merge.ts b/packages/astro/src/core/config/merge.ts
index 6cfa2778d608..5f6487147998 100644
--- a/packages/astro/src/core/config/merge.ts
+++ b/packages/astro/src/core/config/merge.ts
@@ -1,7 +1,7 @@
import { mergeConfig as mergeViteConfig } from 'vite';
import type { DeepPartial } from '../../type-utils.js';
import type { AstroConfig, AstroInlineConfig } from '../../types/public/index.js';
-import { arraify, isObject, isURL } from '../util.js';
+import { arraify, isObject, isURL } from '../util-runtime.js';
function mergeConfigRecursively(
defaults: Record,
diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts
index 369027e35cdf..f58ecce9b9ba 100644
--- a/packages/astro/src/core/create-vite.ts
+++ b/packages/astro/src/core/create-vite.ts
@@ -46,7 +46,7 @@ import { vitePluginMiddleware } from './middleware/vite-plugin.js';
import { joinPaths } from './path.js';
import { vitePluginServerIslands } from './server-islands/vite-plugin-server-islands.js';
import { vitePluginSessionDriver } from './session/vite-plugin.js';
-import { isObject } from './util.js';
+import { isObject } from './util-runtime.js';
import { vitePluginEnvironment } from '../vite-plugin-environment/index.js';
import { ASTRO_VITE_ENVIRONMENT_NAMES } from './constants.js';
import { vitePluginChromedevtools } from '../vite-plugin-chromedevtools/index.js';
diff --git a/packages/astro/src/core/dev/dev.ts b/packages/astro/src/core/dev/dev.ts
index 1b6fb8abd930..fa3bbd8f6624 100644
--- a/packages/astro/src/core/dev/dev.ts
+++ b/packages/astro/src/core/dev/dev.ts
@@ -11,7 +11,8 @@ import { MutableDataStore } from '../../content/mutable-data-store.js';
import { globalContentConfigObserver } from '../../content/utils.js';
import { telemetry } from '../../events/index.js';
import type { AstroInlineConfig } from '../../types/public/config.js';
-import * as msg from '../messages.js';
+import * as msg from '../messages/runtime.js';
+import { newVersionAvailable } from '../messages/node.js';
import { ensureProcessNodeEnv } from '../util.js';
import { startContainer } from './container.js';
import { createContainerWithAutomaticRestart } from './restart.js';
@@ -71,7 +72,7 @@ export default async function dev(inlineConfig: AstroInlineConfig): Promise = {
},
};
-const debuggers: Record = {};
+const debuggers: Record> = {};
/**
* Emit a message only shown in debug mode.
@@ -32,18 +34,29 @@ const debuggers: Record = {};
*/
function debug(type: string, ...messages: Array) {
const namespace = `astro:${type}`;
- debuggers[namespace] = debuggers[namespace] || debugPackage(namespace);
- return debuggers[namespace](...messages);
+ debuggers[namespace] = debuggers[namespace] || createDebug(namespace);
+ return debuggers[namespace](...(messages as [any, ...any[]]));
}
// This is gross, but necessary since we are depending on globals.
(globalThis as any)._astroGlobalDebug = debug;
export function enableVerboseLogging() {
- debugPackage.enable('astro:*,vite:*');
+ // Enable debug logging via obug's enable function
+ // obug provides the same API as debug package
+ obugEnable('astro:*,vite:*');
debug('cli', '--verbose flag enabled! Enabling: DEBUG="astro:*,vite:*"');
debug(
'cli',
'Tip: Set the DEBUG env variable directly for more control. Example: "DEBUG=astro:*,vite:* astro build".',
);
}
+
+export function createNodeLogger(inlineConfig: AstroInlineConfig): Logger {
+ if (inlineConfig.logger) return inlineConfig.logger;
+
+ return new Logger({
+ dest: nodeLogDestination,
+ level: inlineConfig.logLevel ?? 'info',
+ });
+}
diff --git a/packages/astro/src/core/logger/vite.ts b/packages/astro/src/core/logger/vite.ts
index ae186d3b8697..c6acab200088 100644
--- a/packages/astro/src/core/logger/vite.ts
+++ b/packages/astro/src/core/logger/vite.ts
@@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url';
import { stripVTControlCharacters } from 'node:util';
import type { LogLevel, Rollup, Logger as ViteLogger } from 'vite';
import { isAstroError } from '../errors/errors.js';
-import { serverShortcuts as formatServerShortcuts } from '../messages.js';
+import { serverShortcuts as formatServerShortcuts } from '../messages/runtime.js';
import { type Logger as AstroLogger, isLogLevelEnabled } from './core.js';
const PKG_PREFIX = fileURLToPath(new URL('../../../', import.meta.url));
diff --git a/packages/astro/src/core/messages/node.ts b/packages/astro/src/core/messages/node.ts
new file mode 100644
index 000000000000..3616cc414794
--- /dev/null
+++ b/packages/astro/src/core/messages/node.ts
@@ -0,0 +1,21 @@
+/**
+ * Node.js-specific prestyled messages for the CLI.
+ * These functions use Node.js APIs and should not be imported in runtime-agnostic code.
+ */
+import { detect, resolveCommand } from 'package-manager-detector';
+import colors from 'piccolore';
+
+const { bgYellow, black, cyan, yellow } = colors;
+
+export async function newVersionAvailable({ latestVersion }: { latestVersion: string }) {
+ const badge = bgYellow(black(` update `));
+ const headline = yellow(`▶ New version of Astro available: ${latestVersion}`);
+ const packageManager = (await detect())?.agent ?? 'npm';
+ const execCommand = resolveCommand(packageManager, 'execute', ['@astrojs/upgrade']);
+ // NOTE: Usually it's impossible for `execCommand` to be null as `package-manager-detector` should
+ // already match a valid package manager
+ const details = !execCommand
+ ? ''
+ : ` Run ${cyan(`${execCommand.command} ${execCommand.args.join(' ')}`)} to update`;
+ return ['', `${badge} ${headline}`, details, ''].join('\n');
+}
diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages/runtime.ts
similarity index 92%
rename from packages/astro/src/core/messages.ts
rename to packages/astro/src/core/messages/runtime.ts
index c151b7f56477..256adb9f1aa4 100644
--- a/packages/astro/src/core/messages.ts
+++ b/packages/astro/src/core/messages/runtime.ts
@@ -1,15 +1,14 @@
-import { detect, resolveCommand } from 'package-manager-detector';
import colors from 'piccolore';
import type { ResolvedServerUrls } from 'vite';
import type { $ZodError } from 'zod/v4/core';
-import { getDocsForError, renderErrorMarkdown } from './errors/dev/utils.js';
+import { getDocsForError, renderErrorMarkdown } from '../errors/dev/runtime.js';
import {
AstroError,
AstroUserError,
CompilerError,
type ErrorWithMetadata,
-} from './errors/index.js';
-import { padMultilineString } from './util.js';
+} from '../errors/index.js';
+import { padMultilineString } from '../util-runtime.js';
const {
bgGreen,
@@ -31,6 +30,32 @@ const {
* Prestyled messages for the CLI. Used by astro CLI commands.
*/
+/** Display each request being served with the path and the status code. */
+export function req({
+ url,
+ method,
+ statusCode,
+ reqTime,
+ isRewrite,
+}: {
+ url: string;
+ statusCode: number;
+ method?: string;
+ reqTime?: number;
+ isRewrite?: boolean;
+}): string {
+ const color = statusCode >= 500 ? red : statusCode >= 300 ? yellow : blue;
+ return (
+ color(`[${statusCode}]`) +
+ ` ` +
+ `${isRewrite ? color('(rewrite) ') : ''}` +
+ (method && method !== 'GET' ? color(method) + ' ' : '') +
+ url +
+ ` ` +
+ (reqTime ? dim(Math.round(reqTime) + 'ms') : '')
+ );
+}
+
/** Display server host and startup time */
export function serverStart({
startupTime,
@@ -83,19 +108,6 @@ export function serverShortcuts({ key, label }: { key: string; label: string }):
return [dim(' Press'), key, dim('to'), label].join(' ');
}
-export async function newVersionAvailable({ latestVersion }: { latestVersion: string }) {
- const badge = bgYellow(black(` update `));
- const headline = yellow(`▶ New version of Astro available: ${latestVersion}`);
- const packageManager = (await detect())?.agent ?? 'npm';
- const execCommand = resolveCommand(packageManager, 'execute', ['@astrojs/upgrade']);
- // NOTE: Usually it's impossible for `execCommand` to be null as `package-manager-detector` should
- // already match a valid package manager
- const details = !execCommand
- ? ''
- : ` Run ${cyan(`${execCommand.command} ${execCommand.args.join(' ')}`)} to update`;
- return ['', `${badge} ${headline}`, details, ''].join('\n');
-}
-
export function telemetryNotice() {
const headline = blue(`▶ Astro collects anonymous usage data.`);
const why = ' This information helps us improve Astro.';
diff --git a/packages/astro/src/core/preview/index.ts b/packages/astro/src/core/preview/index.ts
index 03826ce31ded..c572510e80fe 100644
--- a/packages/astro/src/core/preview/index.ts
+++ b/packages/astro/src/core/preview/index.ts
@@ -8,7 +8,7 @@ import { runHookConfigDone, runHookConfigSetup } from '../../integrations/hooks.
import type { AstroInlineConfig } from '../../types/public/config.js';
import type { PreviewModule, PreviewServer } from '../../types/public/preview.js';
import { resolveConfig } from '../config/config.js';
-import { createNodeLogger } from '../config/logging.js';
+import { createNodeLogger } from '../logger/node.js';
import { createSettings } from '../config/settings.js';
import { createRoutesList } from '../routing/manifest/create.js';
import { ensureProcessNodeEnv } from '../util.js';
diff --git a/packages/astro/src/core/preview/static-preview-server.ts b/packages/astro/src/core/preview/static-preview-server.ts
index 15db0d3a62c7..04cd1a4a6eac 100644
--- a/packages/astro/src/core/preview/static-preview-server.ts
+++ b/packages/astro/src/core/preview/static-preview-server.ts
@@ -5,7 +5,7 @@ import type * as vite from 'vite';
import { preview, type PreviewServer as VitePreviewServer } from 'vite';
import type { AstroSettings } from '../../types/astro.js';
import type { Logger } from '../logger/core.js';
-import * as msg from '../messages.js';
+import * as msg from '../messages/runtime.js';
import { getResolvedHostForHttpServer } from './util.js';
import { vitePluginAstroPreview } from './vite-plugin-astro-preview.js';
diff --git a/packages/astro/src/core/sync/index.ts b/packages/astro/src/core/sync/index.ts
index c17a6a630d98..8ca64d9ce8a7 100644
--- a/packages/astro/src/core/sync/index.ts
+++ b/packages/astro/src/core/sync/index.ts
@@ -18,7 +18,7 @@ import type { AstroSettings } from '../../types/astro.js';
import type { AstroInlineConfig } from '../../types/public/config.js';
import { getTimeStat } from '../build/util.js';
import { resolveConfig } from '../config/config.js';
-import { createNodeLogger } from '../config/logging.js';
+import { createNodeLogger } from '../logger/node.js';
import { createSettings } from '../config/settings.js';
import { createVite } from '../create-vite.js';
import {
diff --git a/packages/astro/src/core/util-runtime.ts b/packages/astro/src/core/util-runtime.ts
new file mode 100644
index 000000000000..e73a1cbf5577
--- /dev/null
+++ b/packages/astro/src/core/util-runtime.ts
@@ -0,0 +1,24 @@
+/**
+ * Runtime-agnostic utility functions that can be used in any environment.
+ * These functions must not import Node.js modules.
+ */
+
+/** Returns true if argument is an object of any prototype/class (but not null). */
+export function isObject(value: unknown): value is Record {
+ return typeof value === 'object' && value != null;
+}
+
+/** Cross-realm compatible URL */
+export function isURL(value: unknown): value is URL {
+ return Object.prototype.toString.call(value) === '[object URL]';
+}
+
+/** Wraps an object in an array. If an array is passed, ignore it. */
+export function arraify(target: T | T[]): T[] {
+ return Array.isArray(target) ? target : [target];
+}
+
+export function padMultilineString(source: string, n = 2) {
+ const lines = source.split(/\r?\n/);
+ return lines.map((l) => ` `.repeat(n) + l).join(`\n`);
+}
diff --git a/packages/astro/src/core/util.ts b/packages/astro/src/core/util.ts
index 8b96af135e3a..9b7807480f71 100644
--- a/packages/astro/src/core/util.ts
+++ b/packages/astro/src/core/util.ts
@@ -8,16 +8,6 @@ import { hasSpecialQueries } from '../vite-plugin-utils/index.js';
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './constants.js';
import { removeQueryString, removeTrailingForwardSlash, slash } from './path.js';
-/** Returns true if argument is an object of any prototype/class (but not null). */
-export function isObject(value: unknown): value is Record {
- return typeof value === 'object' && value != null;
-}
-
-/** Cross-realm compatible URL */
-export function isURL(value: unknown): value is URL {
- return Object.prototype.toString.call(value) === '[object URL]';
-}
-
/** Check if a file is a markdown file based on its extension */
export function isMarkdownFile(fileId: string, option?: { suffix?: string }): boolean {
if (hasSpecialQueries(fileId)) {
@@ -31,16 +21,6 @@ export function isMarkdownFile(fileId: string, option?: { suffix?: string }): bo
return false;
}
-/** Wraps an object in an array. If an array is passed, ignore it. */
-export function arraify(target: T | T[]): T[] {
- return Array.isArray(target) ? target : [target];
-}
-
-export function padMultilineString(source: string, n = 2) {
- const lines = source.split(/\r?\n/);
- return lines.map((l) => ` `.repeat(n) + l).join(`\n`);
-}
-
const STATUS_CODE_PAGES = new Set(['/404', '/500']);
/**
diff --git a/packages/astro/src/runtime/prerender/static-paths.ts b/packages/astro/src/runtime/prerender/static-paths.ts
index d1aae0ff1638..5d520dc4d914 100644
--- a/packages/astro/src/runtime/prerender/static-paths.ts
+++ b/packages/astro/src/runtime/prerender/static-paths.ts
@@ -63,7 +63,13 @@ export class StaticPaths {
// Also process fallback routes
for (const currentRoute of eachRouteInRouteData(route)) {
const paths = await this.#getPathsForRoute(currentRoute);
- allPaths.push(...paths);
+ // Use a loop instead of spread operator (allPaths.push(...paths)) to avoid
+ // "Maximum call stack size exceeded" error with large arrays (issue #15578).
+ // The spread operator tries to pass all array elements as individual arguments,
+ // which hits the call stack limit when dealing with 100k+ routes.
+ for (const path of paths) {
+ allPaths.push(path);
+ }
}
}
diff --git a/packages/astro/src/vite-plugin-app/app.ts b/packages/astro/src/vite-plugin-app/app.ts
index a50c6966f23c..da1e743a8c6e 100644
--- a/packages/astro/src/vite-plugin-app/app.ts
+++ b/packages/astro/src/vite-plugin-app/app.ts
@@ -22,7 +22,8 @@ import { RunnablePipeline } from './pipeline.js';
import { getCustom404Route, getCustom500Route } from '../core/routing/helpers.js';
import { ensure404Route } from '../core/routing/astro-designed-error-pages.js';
import { matchRoute } from '../core/routing/dev.js';
-import type { DevMatch } from '../core/app/base.js';
+import type { DevMatch, LogRequestPayload } from '../core/app/base.js';
+import { req } from '../core/messages/runtime.js';
export class AstroServerApp extends BaseApp {
settings: AstroSettings;
@@ -297,6 +298,22 @@ export class AstroServerApp extends BaseApp {
return renderRoute(custom500);
}
}
+
+ logRequest({ pathname, method, statusCode, isRewrite, reqTime }: LogRequestPayload) {
+ if (pathname === '/favicon.ico') {
+ return;
+ }
+ this.logger.info(
+ null,
+ req({
+ url: pathname,
+ method,
+ statusCode,
+ isRewrite,
+ reqTime,
+ }),
+ );
+ }
}
type HandleRequest = {
diff --git a/packages/astro/src/vite-plugin-astro-server/error.ts b/packages/astro/src/vite-plugin-astro-server/error.ts
index 3cb04e914da4..ca726d852e98 100644
--- a/packages/astro/src/vite-plugin-astro-server/error.ts
+++ b/packages/astro/src/vite-plugin-astro-server/error.ts
@@ -1,7 +1,7 @@
import type { SSRManifest } from '../core/app/types.js';
import { collectErrorMetadata } from '../core/errors/dev/index.js';
import type { Logger } from '../core/logger/core.js';
-import { formatErrorMessage } from '../core/messages.js';
+import { formatErrorMessage } from '../core/messages/runtime.js';
import type { ModuleLoader } from '../core/module-loader/index.js';
export function recordServerError(
diff --git a/packages/astro/test/units/config/config-validate.test.js b/packages/astro/test/units/config/config-validate.test.js
index 04ab328eb77d..d3755dcf5f68 100644
--- a/packages/astro/test/units/config/config-validate.test.js
+++ b/packages/astro/test/units/config/config-validate.test.js
@@ -6,7 +6,7 @@ import * as z from 'zod/v4';
import { fontProviders } from '../../../dist/assets/fonts/providers/index.js';
import { LocalFontProvider } from '../../../dist/assets/fonts/providers/local.js';
import { validateConfig as _validateConfig } from '../../../dist/core/config/validate.js';
-import { formatConfigErrorMessage } from '../../../dist/core/messages.js';
+import { formatConfigErrorMessage } from '../../../dist/core/messages/runtime.js';
import { envField } from '../../../dist/env/config.js';
/**
diff --git a/packages/astro/test/units/runtime/static-paths.test.js b/packages/astro/test/units/runtime/static-paths.test.js
new file mode 100644
index 000000000000..9f94d3c18cbb
--- /dev/null
+++ b/packages/astro/test/units/runtime/static-paths.test.js
@@ -0,0 +1,238 @@
+import * as assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import { StaticPaths } from '../../../dist/runtime/prerender/static-paths.js';
+
+/**
+ * Creates a minimal mock app for testing StaticPaths.
+ * @param {object} options
+ * @param {Array} options.routes - Array of route objects with routeData
+ * @param {Map} [options.routeCache] - Optional route cache
+ * @param {object} [options.i18n] - Optional i18n config
+ */
+function createMockApp({ routes, routeCache = new Map(), i18n = undefined }) {
+ return {
+ manifest: {
+ routes,
+ i18n,
+ serverLike: false,
+ base: '/',
+ trailingSlash: 'ignore',
+ },
+ pipeline: {
+ routeCache,
+ async getComponentByRoute(route) {
+ // Return a mock component with getStaticPaths if route is dynamic
+ if (!route.pathname) {
+ return {
+ getStaticPaths: route.mockGetStaticPaths || (() => []),
+ };
+ }
+ return {};
+ },
+ },
+ };
+}
+
+/**
+ * Creates segments array from a route pattern.
+ * @param {string} route - Route pattern like '/blog/[slug]' or '/items/[id]'
+ * @returns {Array} Segments array
+ */
+function createSegments(route) {
+ const parts = route.split('/').filter(Boolean);
+ return parts.map((part) => {
+ if (part.startsWith('[') && part.endsWith(']')) {
+ const paramName = part.slice(1, -1);
+ return [{ content: paramName, dynamic: true, spread: false }];
+ }
+ return [{ content: part, dynamic: false, spread: false }];
+ });
+}
+
+/**
+ * Creates a mock route data object.
+ * @param {object} options
+ * @param {string} [options.pathname] - Static pathname (if undefined, route is dynamic)
+ * @param {boolean} [options.prerender=true] - Whether route should be prerendered
+ * @param {Function} [options.mockGetStaticPaths] - Mock getStaticPaths function for dynamic routes
+ * @param {string} [options.route='/[slug]'] - Route pattern for dynamic routes
+ */
+function createMockRoute({
+ pathname,
+ prerender = true,
+ mockGetStaticPaths,
+ route = '/[slug]',
+} = {}) {
+ // Extract param names from route pattern
+ const paramMatches = route.matchAll(/\[([^\]]+)\]/g);
+ const params = pathname ? [] : Array.from(paramMatches, (m) => m[1]);
+
+ return {
+ routeData: {
+ route,
+ pathname,
+ prerender,
+ type: 'page',
+ pattern: new RegExp('^' + route.replace(/\[[^\]]+\]/g, '([^/]+)') + '$'),
+ params,
+ component: 'src/pages' + route + '.astro',
+ generate: (data) => data.route,
+ segments: pathname ? [] : createSegments(route),
+ fallbackRoutes: [],
+ isIndex: false,
+ mockGetStaticPaths,
+ },
+ };
+}
+
+describe('StaticPaths', () => {
+ describe('getAll()', () => {
+ it('should return static paths for static routes', async () => {
+ const routes = [
+ createMockRoute({ pathname: '/about' }),
+ createMockRoute({ pathname: '/contact' }),
+ ];
+
+ const app = createMockApp({ routes });
+ const staticPaths = new StaticPaths(app);
+ const paths = await staticPaths.getAll();
+
+ assert.equal(paths.length, 2);
+ assert.equal(paths[0].pathname, '/about');
+ assert.equal(paths[1].pathname, '/contact');
+ });
+
+ it('should return static paths for dynamic routes', async () => {
+ const mockGetStaticPaths = () => [
+ { params: { slug: 'post-1' } },
+ { params: { slug: 'post-2' } },
+ ];
+
+ const routes = [
+ createMockRoute({
+ pathname: undefined,
+ route: '/blog/[slug]',
+ mockGetStaticPaths,
+ }),
+ ];
+
+ const app = createMockApp({ routes });
+ const staticPaths = new StaticPaths(app);
+ const paths = await staticPaths.getAll();
+
+ assert.equal(paths.length, 2);
+ assert.equal(paths[0].pathname, '/blog/post-1');
+ assert.equal(paths[1].pathname, '/blog/post-2');
+ });
+
+ it('should skip non-prerendered routes', async () => {
+ const routes = [
+ createMockRoute({ pathname: '/ssr-page', prerender: false }),
+ createMockRoute({ pathname: '/static-page', prerender: true }),
+ ];
+
+ const app = createMockApp({ routes });
+ const staticPaths = new StaticPaths(app);
+ const paths = await staticPaths.getAll();
+
+ assert.equal(paths.length, 1);
+ assert.equal(paths[0].pathname, '/static-page');
+ });
+
+ it('should handle a large number of static routes without stack overflow', async () => {
+ // This test specifically addresses issue #15578
+ // The old implementation used spread operator which caused stack overflow
+ // with large arrays: allPaths.push(...paths)
+ // The fix uses a loop instead: for (const path of paths) allPaths.push(path)
+
+ // Use 200,000 routes to reliably trigger stack overflow with spread operator.
+ // The spread operator fails because it tries to pass all array elements as
+ // individual arguments to push(), hitting the maximum call stack size limit.
+ const largeRouteCount = 200000;
+ const routes = [];
+
+ // Create a large number of static routes
+ for (let i = 0; i < largeRouteCount; i++) {
+ routes.push(createMockRoute({ pathname: `/page-${i}` }));
+ }
+
+ const app = createMockApp({ routes });
+ const staticPaths = new StaticPaths(app);
+
+ // This should not throw "Maximum call stack size exceeded"
+ const paths = await staticPaths.getAll();
+
+ assert.equal(paths.length, largeRouteCount);
+ assert.equal(paths[0].pathname, '/page-0');
+ assert.equal(paths[largeRouteCount - 1].pathname, `/page-${largeRouteCount - 1}`);
+ });
+
+ it('should handle a dynamic route with a large number of paths without stack overflow', async () => {
+ // This tests the same issue but for dynamic routes with many paths
+ // Use 200,000 paths to reliably trigger stack overflow with spread operator
+ const largePathCount = 200000;
+
+ const mockGetStaticPaths = () => {
+ const paths = [];
+ for (let i = 0; i < largePathCount; i++) {
+ paths.push({ params: { id: `item-${i}` } });
+ }
+ return paths;
+ };
+
+ const routes = [
+ createMockRoute({
+ pathname: undefined,
+ route: '/items/[id]',
+ mockGetStaticPaths,
+ }),
+ ];
+
+ const app = createMockApp({ routes });
+ const staticPaths = new StaticPaths(app);
+
+ // This should not throw "Maximum call stack size exceeded"
+ const paths = await staticPaths.getAll();
+
+ assert.equal(paths.length, largePathCount);
+ assert.equal(paths[0].pathname, '/items/item-0');
+ assert.equal(paths[largePathCount - 1].pathname, `/items/item-${largePathCount - 1}`);
+ });
+
+ it('should handle mixed static and dynamic routes with large counts', async () => {
+ const staticCount = 50000;
+ const dynamicCount = 50000;
+ const routes = [];
+
+ // Add static routes
+ for (let i = 0; i < staticCount; i++) {
+ routes.push(createMockRoute({ pathname: `/static-${i}` }));
+ }
+
+ // Add a dynamic route with many paths
+ const mockGetStaticPaths = () => {
+ const paths = [];
+ for (let i = 0; i < dynamicCount; i++) {
+ paths.push({ params: { slug: `dynamic-${i}` } });
+ }
+ return paths;
+ };
+
+ routes.push(
+ createMockRoute({
+ pathname: undefined,
+ route: '/blog/[slug]',
+ mockGetStaticPaths,
+ }),
+ );
+
+ const app = createMockApp({ routes });
+ const staticPaths = new StaticPaths(app);
+
+ // This should not throw "Maximum call stack size exceeded"
+ const paths = await staticPaths.getAll();
+
+ assert.equal(paths.length, staticCount + dynamicCount);
+ });
+ });
+});
diff --git a/packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts b/packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts
index 1256737c770d..44fbc3739d20 100644
--- a/packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts
+++ b/packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts
@@ -24,9 +24,16 @@ export function astroFrontmatterScanPlugin(): ESBuildPlugin {
// Extract frontmatter content between --- markers
const frontmatterMatch = FRONTMATTER_RE.exec(code);
if (frontmatterMatch) {
- // Return the frontmatter as TypeScript for import scanning
+ // Replace `return` with `throw` to avoid esbuild's "Top-level return" error during scanning.
+ // This aligns with Astro's core compiler logic for frontmatter error handling.
+ // See: packages/astro/src/vite-plugin-astro/compile.ts
+ //
+ // Known Limitation: Using regex /\breturn\b/ will incorrectly match
+ // identifiers like `$return` or aliases like `import { return as ret }`.
+ const contents = frontmatterMatch[1].replace(/\breturn\b/g, 'throw ');
+
return {
- contents: frontmatterMatch[1],
+ contents,
loader: 'ts',
};
}
diff --git a/packages/integrations/cloudflare/test/astro-dev-platform.test.js b/packages/integrations/cloudflare/test/astro-dev-platform.test.js
index d33edfdd54a8..779d2a1b2a25 100644
--- a/packages/integrations/cloudflare/test/astro-dev-platform.test.js
+++ b/packages/integrations/cloudflare/test/astro-dev-platform.test.js
@@ -59,4 +59,15 @@ describe('AstroDevPlatform', () => {
assert.equal($('#hasPRODKV').text(), 'true');
assert.equal($('#hasACCESS').text(), 'true');
});
+
+ it('Code component works in dev mode (no CommonJS module errors)', async () => {
+ const res = await fixture.fetch('/code-test');
+ assert.equal(res.status, 200);
+ const html = await res.text();
+ const $ = cheerio.load(html);
+ // Verify the page rendered successfully with Code component
+ assert.equal($('h1').text(), 'Testing Code Component');
+ // Verify the code block was rendered
+ assert.ok($('pre').length > 0, 'Code block should be rendered');
+ });
});
diff --git a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/code-test.astro b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/code-test.astro
new file mode 100644
index 000000000000..34993e471c5d
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/src/pages/code-test.astro
@@ -0,0 +1,12 @@
+---
+import { Code } from 'astro:components';
+---
+
+
+ Code Component Test
+
+
+ Testing Code Component
+
+
+
diff --git a/packages/integrations/cloudflare/test/fixtures/top-level-return/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/top-level-return/astro.config.mjs
new file mode 100644
index 000000000000..339f0e2a49c0
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/top-level-return/astro.config.mjs
@@ -0,0 +1,7 @@
+import cloudflare from '@astrojs/cloudflare';
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ adapter: cloudflare(),
+ output: 'server',
+});
diff --git a/packages/integrations/cloudflare/test/fixtures/top-level-return/package.json b/packages/integrations/cloudflare/test/fixtures/top-level-return/package.json
new file mode 100644
index 000000000000..a3bbe0091594
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/top-level-return/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@test/astro-cloudflare-top-level-return",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "build": "astro build"
+ },
+ "dependencies": {
+ "@astrojs/cloudflare": "workspace:*",
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/integrations/cloudflare/test/fixtures/top-level-return/src/lib/index.ts b/packages/integrations/cloudflare/test/fixtures/top-level-return/src/lib/index.ts
new file mode 100644
index 000000000000..bbb7aa5e95a7
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/top-level-return/src/lib/index.ts
@@ -0,0 +1,7 @@
+export function guard() {
+ return false;
+}
+
+const ret = 0;
+
+export { ret as return }
\ No newline at end of file
diff --git a/packages/integrations/cloudflare/test/fixtures/top-level-return/src/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/top-level-return/src/pages/index.astro
new file mode 100644
index 000000000000..171a79ab888c
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/top-level-return/src/pages/index.astro
@@ -0,0 +1,25 @@
+---
+// This import statement is necessary to indicate that this code is an ECMAScript module.
+import { guard } from "../lib/index.js"
+// Un-commenting the following lines will trigger the following error:
+// `X [ERROR] No matching export in "../lib/index.ts" for import "throw"`
+// This is because 'return' is replaced with 'throw', and it's highly unlikely
+// that the source library provides an export named 'throw', leading to a name mismatch.
+//
+// import { return as ret } from "../lib/index.js"
+// console.log(ret)
+
+if (guard()) {
+ return Astro.redirect("/404")
+}
+
+---
+
+
+
+ Top-level Return Test
+
+
+ Top-level Return Test
+
+
diff --git a/packages/integrations/cloudflare/test/top-level-return.test.js b/packages/integrations/cloudflare/test/top-level-return.test.js
new file mode 100644
index 000000000000..c801eef880bc
--- /dev/null
+++ b/packages/integrations/cloudflare/test/top-level-return.test.js
@@ -0,0 +1,62 @@
+import { rmSync } from 'node:fs';
+import { describe, before, it } from 'node:test';
+import { Writable } from 'node:stream';
+import { loadFixture } from './_test-utils.js';
+import assert from 'node:assert/strict';
+import { fileURLToPath } from 'node:url';
+import { Logger } from '../../../astro/dist/core/logger/core.js';
+
+describe('Top-level Return', () => {
+ /** @type {import('../../../astro/test/test-utils').Fixture} */
+ let fixture;
+ const logs = [];
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/top-level-return/',
+ });
+
+ // Clear the Vite cache before testing
+ const viteCacheDir = new URL('./node_modules/.vite/', fixture.config.root);
+
+ rmSync(fileURLToPath(viteCacheDir), { recursive: true, force: true });
+
+ await fixture.build({
+ vite: { logLevel: 'error' },
+ logger: new Logger({
+ level: 'error',
+ dest: new Writable({
+ objectMode: true,
+ write(event, _, callback) {
+ logs.push(event);
+ callback();
+ },
+ }),
+ }),
+ });
+ });
+
+ it('should avoid esbuild top-level return error by replacing with void', async () => {
+ const topLevelReturnErrorLog = logs.find(
+ (log) =>
+ log.message &&
+ log.message.includes('Top-level return cannot be used inside an ECMAScript module'),
+ );
+
+ assert.ok(
+ !topLevelReturnErrorLog,
+ `Should not see "Top-level return cannot be used inside an ECMAScript module" message, but got: ${topLevelReturnErrorLog?.message}`,
+ );
+ });
+
+ it('should not break JS syntax and should complete dependency scanning successfully', async () => {
+ const dependencyScanFailedLog = logs.find(
+ (log) => log.message && log.message.includes('Failed to run dependency scan'),
+ );
+
+ assert.ok(
+ !dependencyScanFailedLog,
+ `Should not see "Failed to run dependency scan" message, but got: ${dependencyScanFailedLog?.message}`,
+ );
+ });
+});
diff --git a/packages/markdown/remark/package.json b/packages/markdown/remark/package.json
index f77c4d6624ba..65fca0df5e17 100644
--- a/packages/markdown/remark/package.json
+++ b/packages/markdown/remark/package.json
@@ -13,12 +13,20 @@
"homepage": "https://astro.build",
"main": "./dist/index.js",
"exports": {
- ".": "./dist/index.js"
+ ".": "./dist/index.js",
+ "./shiki/engine": "./dist/engine.js",
+ "./shiki": "./dist/shiki.js",
+ "./shiki-style-collector": "./dist/shiki-style-collector.js",
+ "./transformers/style-to-class": "./dist/transformers/style-to-class.js"
},
"imports": {
"#import-plugin": {
"browser": "./dist/import-plugin-browser.js",
"default": "./dist/import-plugin-default.js"
+ },
+ "#shiki-engine": {
+ "workerd": "./dist/shiki-engine-workerd.js",
+ "default": "./dist/shiki-engine-default.js"
}
},
"files": [
diff --git a/packages/markdown/remark/src/shiki-engine-default.ts b/packages/markdown/remark/src/shiki-engine-default.ts
new file mode 100644
index 000000000000..6ac938d313ca
--- /dev/null
+++ b/packages/markdown/remark/src/shiki-engine-default.ts
@@ -0,0 +1,7 @@
+// shiki-engine-default.ts
+import type { RegexEngine } from 'shiki';
+import { createOnigurumaEngine } from 'shiki/engine/oniguruma';
+
+export function loadShikiEngine(): Promise {
+ return createOnigurumaEngine(import('shiki/wasm'));
+}
diff --git a/packages/markdown/remark/src/shiki-engine-workerd.ts b/packages/markdown/remark/src/shiki-engine-workerd.ts
new file mode 100644
index 000000000000..44c1da540458
--- /dev/null
+++ b/packages/markdown/remark/src/shiki-engine-workerd.ts
@@ -0,0 +1,8 @@
+// shiki-engine-worker.ts
+import type { RegexEngine } from 'shiki';
+import { createOnigurumaEngine } from 'shiki/engine/oniguruma';
+
+export function loadShikiEngine(): Promise {
+ // @ts-ignore wasm type
+ return createOnigurumaEngine(import('shiki/onig.wasm'));
+}
diff --git a/packages/markdown/remark/src/shiki.ts b/packages/markdown/remark/src/shiki.ts
index 1005254bbbbe..16876ab494e3 100644
--- a/packages/markdown/remark/src/shiki.ts
+++ b/packages/markdown/remark/src/shiki.ts
@@ -8,6 +8,7 @@ import {
type HighlighterGeneric,
isSpecialLang,
type LanguageRegistration,
+ type RegexEngine,
type ShikiTransformer,
type ThemeRegistration,
type ThemeRegistrationRaw,
@@ -15,6 +16,7 @@ import {
import { globalShikiStyleCollector } from './shiki-style-collector.js';
import { transformerStyleToClass } from './transformers/style-to-class.js';
import type { ThemePresets } from './types.js';
+import { loadShikiEngine } from '#shiki-engine';
export interface ShikiHighlighter {
codeToHast(
@@ -76,6 +78,8 @@ const cssVariablesTheme = () =>
// Caches Promise for reuse when the same theme and langs are provided
const cachedHighlighters = new Map();
+let shikiEngine: RegexEngine | undefined = undefined;
+
export async function createShikiHighlighter({
langs = [],
theme = 'github-dark',
@@ -84,10 +88,15 @@ export async function createShikiHighlighter({
}: CreateShikiHighlighterOptions = {}): Promise {
theme = theme === 'css-variables' ? cssVariablesTheme() : theme;
+ if (shikiEngine === undefined) {
+ shikiEngine = await loadShikiEngine();
+ }
+
const highlighterOptions = {
langs: ['plaintext', ...langs],
langAlias,
themes: Object.values(themes).length ? Object.values(themes) : [theme],
+ engine: shikiEngine,
};
const key = JSON.stringify(highlighterOptions, Object.keys(highlighterOptions).sort());
@@ -225,3 +234,6 @@ export async function createShikiHighlighter({
function normalizePropAsString(value: Properties[string]): string | null {
return Array.isArray(value) ? value.join(' ') : (value as string | null);
}
+
+// Re-export ThemePresets type for consumers
+export type { ThemePresets };
diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json
index daad1d3843d3..e68ce7e50783 100644
--- a/packages/telemetry/package.json
+++ b/packages/telemetry/package.json
@@ -30,7 +30,6 @@
],
"dependencies": {
"ci-info": "^4.4.0",
- "debug": "^4.4.3",
"dlv": "^1.1.3",
"dset": "^3.1.4",
"is-docker": "^4.0.0",
@@ -38,7 +37,6 @@
"which-pm-runs": "^1.1.0"
},
"devDependencies": {
- "@types/debug": "^4.1.12",
"@types/dlv": "^1.1.5",
"@types/node": "^18.17.8",
"@types/which-pm-runs": "^1.0.2",
diff --git a/packages/telemetry/src/index.ts b/packages/telemetry/src/index.ts
index 4662e598f5ac..c53bf4d73eaf 100644
--- a/packages/telemetry/src/index.ts
+++ b/packages/telemetry/src/index.ts
@@ -1,6 +1,5 @@
import { randomBytes } from 'node:crypto';
import { isCI } from 'ci-info';
-import debug from 'debug';
import { GlobalConfig } from './config.js';
import * as KEY from './config-keys.js';
import { post } from './post.js';
@@ -13,6 +12,13 @@ export type TelemetryEvent = { eventName: string; payload: Record }
// In the event of significant policy changes, update this!
const VALID_TELEMETRY_NOTICE_DATE = '2023-08-25';
+/**
+ * Get the debug function from global (set by astro's logger)
+ */
+function getDebug(): ((type: string, ...args: any[]) => void) | undefined {
+ return (globalThis as any)._astroGlobalDebug;
+}
+
type EventMeta = SystemInfo;
interface EventContext extends ProjectInfo {
anonymousId: string;
@@ -22,7 +28,6 @@ export class AstroTelemetry {
private _anonymousSessionId: string | undefined;
private _anonymousProjectInfo: ProjectInfo | undefined;
private config = new GlobalConfig({ name: 'astro' });
- private debug = debug('astro:telemetry');
private isCI = isCI;
private env = process.env;
@@ -108,20 +113,21 @@ export class AstroTelemetry {
}
async notify(callback: () => boolean | Promise) {
+ const debug = getDebug();
if (this.isDisabled || this.isCI) {
- this.debug(`[notify] telemetry has been disabled`);
+ debug?.('telemetry', `[notify] telemetry has been disabled`);
return;
}
// The end-user has already been notified about our telemetry integration!
// Don't bother them about it again.
if (this.isValidNotice()) {
- this.debug(`[notify] last notified on ${this.notifyDate}`);
+ debug?.('telemetry', `[notify] last notified on ${this.notifyDate}`);
return;
}
const enabled = await callback();
this.config.set(KEY.TELEMETRY_NOTIFY_DATE, new Date().valueOf().toString());
this.config.set(KEY.TELEMETRY_ENABLED, enabled);
- this.debug(`[notify] telemetry has been ${enabled ? 'enabled' : 'disabled'}`);
+ debug?.('telemetry', `[notify] telemetry has been ${enabled ? 'enabled' : 'disabled'}`);
}
async record(event: TelemetryEvent | TelemetryEvent[] = []) {
@@ -130,9 +136,11 @@ export class AstroTelemetry {
return Promise.resolve();
}
+ const debug = getDebug();
+
// Skip recording telemetry if the feature is disabled
if (this.isDisabled) {
- this.debug('[record] telemetry has been disabled');
+ debug?.('telemetry', '[record] telemetry has been disabled');
return Promise.resolve();
}
@@ -152,10 +160,15 @@ export class AstroTelemetry {
context.anonymousId = `CI.${meta.ciName || 'UNKNOWN'}`;
}
- if (this.debug.enabled) {
+ // Check if debug is enabled by trying to call it - if DEBUG is not set, nothing happens
+ const debugOutput =
+ process.env.DEBUG?.includes('astro:telemetry') ||
+ process.env.DEBUG?.includes('astro:*') ||
+ process.env.DEBUG === '*';
+ if (debugOutput && debug) {
// Print to standard error to simplify selecting the output
- this.debug({ context, meta });
- this.debug(JSON.stringify(events, null, 2));
+ debug('telemetry', { context, meta });
+ debug('telemetry', JSON.stringify(events, null, 2));
// Do not send the telemetry data if debugging. Users may use this feature
// to preview what data would be sent.
return Promise.resolve();
@@ -166,7 +179,7 @@ export class AstroTelemetry {
events,
}).catch((err) => {
// Log the error to the debugger, but otherwise do nothing.
- this.debug(`Error sending event: ${err.message}`);
+ debug?.('telemetry', `Error sending event: ${err.message}`);
});
}
}
diff --git a/packages/telemetry/test/index.test.js b/packages/telemetry/test/index.test.js
index 47d64198c282..2bfc353614de 100644
--- a/packages/telemetry/test/index.test.js
+++ b/packages/telemetry/test/index.test.js
@@ -4,7 +4,7 @@ import { AstroTelemetry } from '../dist/index.js';
function setup() {
const config = new Map();
- const telemetry = new AstroTelemetry({ version: '0.0.0-test.1' });
+ const telemetry = new AstroTelemetry({ astroVersion: '0.0.0-test.1', viteVersion: '0.0.0' });
const logs = [];
// Stub isCI to false so we can test user-facing behavior
telemetry.isCI = false;
@@ -12,11 +12,32 @@ function setup() {
telemetry.env = {};
// Override config so we can inspect it
telemetry.config = config;
- // Override debug so we can inspect it
- telemetry.debug.enabled = true;
- telemetry.debug.log = (...args) => logs.push(args);
- return { telemetry, config, logs };
+ // Mock the global debug function to capture logs
+ const originalDebug = globalThis._astroGlobalDebug;
+ globalThis._astroGlobalDebug = (type, ...args) => {
+ if (type === 'telemetry') {
+ logs.push(args);
+ }
+ // Call original if it exists (for other namespaces)
+ if (originalDebug) {
+ originalDebug(type, ...args);
+ }
+ };
+
+ // Enable debug for telemetry
+ const oldDebug = process.env.DEBUG;
+ process.env.DEBUG = 'astro:telemetry';
+
+ return {
+ telemetry,
+ config,
+ logs,
+ cleanup: () => {
+ globalThis._astroGlobalDebug = originalDebug;
+ process.env.DEBUG = oldDebug;
+ },
+ };
}
describe('AstroTelemetry', () => {
let oldCI;
@@ -29,11 +50,12 @@ describe('AstroTelemetry', () => {
process.env.CI = oldCI;
});
it('initializes when expected arguments are given', () => {
- const { telemetry } = setup();
+ const { telemetry, cleanup } = setup();
assert(telemetry instanceof AstroTelemetry);
+ cleanup();
});
it('does not record event if disabled', async () => {
- const { telemetry, config, logs } = setup();
+ const { telemetry, config, logs, cleanup } = setup();
telemetry.setEnabled(false);
const [key] = Array.from(config.keys());
assert.notEqual(key, undefined);
@@ -45,9 +67,10 @@ describe('AstroTelemetry', () => {
const [log] = logs;
assert.notEqual(log, undefined);
assert.match(logs.join(''), /disabled/);
+ cleanup();
});
it('records event if enabled', async () => {
- const { telemetry, config, logs } = setup();
+ const { telemetry, config, logs, cleanup } = setup();
telemetry.setEnabled(true);
const [key] = Array.from(config.keys());
assert.notEqual(key, undefined);
@@ -56,9 +79,10 @@ describe('AstroTelemetry', () => {
assert.equal(telemetry.isDisabled, false);
await telemetry.record(['TEST']);
assert.equal(logs.length, 2);
+ cleanup();
});
it('respects disable from notify', async () => {
- const { telemetry, config, logs } = setup();
+ const { telemetry, config, logs, cleanup } = setup();
await telemetry.notify(() => false);
const [key] = Array.from(config.keys());
assert.notEqual(key, undefined);
@@ -68,9 +92,10 @@ describe('AstroTelemetry', () => {
const [log] = logs;
assert.notEqual(log, undefined);
assert.match(logs.join(''), /disabled/);
+ cleanup();
});
it('respects enable from notify', async () => {
- const { telemetry, config, logs } = setup();
+ const { telemetry, config, logs, cleanup } = setup();
await telemetry.notify(() => true);
const [key] = Array.from(config.keys());
assert.notEqual(key, undefined);
@@ -80,5 +105,6 @@ describe('AstroTelemetry', () => {
const [log] = logs;
assert.notEqual(log, undefined);
assert.match(logs.join(''), /enabled/);
+ cleanup();
});
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 776ab04d61e9..4e8cbc6b69c2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -547,9 +547,6 @@ importers:
cssesc:
specifier: ^3.0.0
version: 3.0.0
- debug:
- specifier: ^4.4.3
- version: 4.4.3(supports-color@8.1.1)
deterministic-object-hash:
specifier: ^2.0.2
version: 2.0.2
@@ -601,6 +598,9 @@ importers:
neotraverse:
specifier: ^0.6.18
version: 0.6.18
+ obug:
+ specifier: ^2.1.1
+ version: 2.1.1
p-limit:
specifier: ^7.3.0
version: 7.3.0
@@ -683,9 +683,6 @@ importers:
'@types/cssesc':
specifier: ^3.0.2
version: 3.0.2
- '@types/debug':
- specifier: ^4.1.12
- version: 4.1.12
'@types/dlv':
specifier: ^1.1.5
version: 1.1.5
@@ -5101,6 +5098,15 @@ importers:
specifier: workspace:*
version: link:../../../../../astro
+ packages/integrations/cloudflare/test/fixtures/top-level-return:
+ dependencies:
+ '@astrojs/cloudflare':
+ specifier: workspace:*
+ version: link:../../..
+ astro:
+ specifier: workspace:*
+ version: link:../../../../../astro
+
packages/integrations/cloudflare/test/fixtures/vite-plugin:
dependencies:
'@astrojs/cloudflare':
@@ -6986,9 +6992,6 @@ importers:
ci-info:
specifier: ^4.4.0
version: 4.4.0
- debug:
- specifier: ^4.4.3
- version: 4.4.3(supports-color@8.1.1)
dlv:
specifier: ^1.1.3
version: 1.1.3
@@ -7005,9 +7008,6 @@ importers:
specifier: ^1.1.0
version: 1.1.0
devDependencies:
- '@types/debug':
- specifier: ^4.1.12
- version: 4.1.12
'@types/dlv':
specifier: ^1.1.5
version: 1.1.5