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