diff options
47 files changed, 483 insertions, 235 deletions
diff --git a/.changeset/beige-points-search.md b/.changeset/beige-points-search.md deleted file mode 100644 index ed7909972..000000000 --- a/.changeset/beige-points-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Fixes an issue where component styles were not correctly included in rendered MDX diff --git a/biome.json b/biome.json index 33f9070b9..4714f3706 100644 --- a/biome.json +++ b/biome.json @@ -61,6 +61,16 @@ "lineWidth": 1 } } + }, + { + "include": ["*.test.js"], + "linter": { + "rules": { + "suspicious": { + "noFocusedTests": "error" + } + } + } } ] } diff --git a/eslint.config.js b/eslint.config.js index cd70882c7..dbd379d27 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,7 +5,6 @@ import { builtinModules } from 'node:module'; import tseslint from 'typescript-eslint'; // plugins -import noOnlyTestsEslint from 'eslint-plugin-no-only-tests'; import regexpEslint from 'eslint-plugin-regexp'; const typescriptEslint = tseslint.plugin; @@ -47,7 +46,6 @@ export default [ }, plugins: { '@typescript-eslint': typescriptEslint, - 'no-only-tests': noOnlyTestsEslint, regexp: regexpEslint, }, rules: { @@ -62,7 +60,6 @@ export default [ ignoreRestSiblings: true, }, ], - 'no-only-tests/no-only-tests': 'error', '@typescript-eslint/no-shadow': 'error', 'no-console': 'warn', diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index a681ee8b9..c69f67781 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -11,7 +11,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/solid-js": "^4.4.1", + "@astrojs/solid-js": "^4.4.2", "astro": "^5.0.0-beta.1", "solid-js": "^1.8.22" } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index b68dd8178..5ac9a2a90 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -11,7 +11,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/svelte": "^5.7.0", + "@astrojs/svelte": "^5.7.1", "astro": "^5.0.0-beta.1", "svelte": "^4.2.19" } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index 3231f9364..8214c6316 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -11,8 +11,8 @@ "astro": "astro" }, "dependencies": { - "@astrojs/vue": "^4.5.0", + "@astrojs/vue": "^4.5.1", "astro": "^5.0.0-beta.1", - "vue": "^3.4.38" + "vue": "^3.5.3" } } diff --git a/examples/view-transitions/package.json b/examples/view-transitions/package.json index ef766714a..aff845df1 100644 --- a/examples/view-transitions/package.json +++ b/examples/view-transitions/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "devDependencies": { - "@astrojs/tailwind": "^5.1.0", + "@astrojs/tailwind": "^5.1.1", "@astrojs/node": "^9.0.0-alpha.1", "astro": "^5.0.0-beta.1" } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index a6e895f48..356a9c93b 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/mdx": "^4.0.0-beta.1", - "@astrojs/tailwind": "^5.1.0", + "@astrojs/tailwind": "^5.1.1", "@types/canvas-confetti": "^1.6.4", "astro": "^5.0.0-beta.1", "autoprefixer": "^10.4.20", diff --git a/package.json b/package.json index 456956981..bccaaccf0 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "@types/node": "^18.17.8", "esbuild": "^0.21.5", "eslint": "^9.10.0", - "eslint-plugin-no-only-tests": "^3.3.0", "eslint-plugin-regexp": "^2.6.0", "globby": "^14.0.2", "only-allow": "^1.2.1", diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 02ef40186..543473131 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -215,6 +215,34 @@ - [#11974](https://github.com/withastro/astro/pull/11974) [`60211de`](https://github.com/withastro/astro/commit/60211defbfb2992ba17d1369e71c146d8928b09a) Thanks [@ascorbic](https://github.com/ascorbic)! - Exports the `RenderResult` type +## 4.15.9 + +### Patch Changes + +- [#12034](https://github.com/withastro/astro/pull/12034) [`5b3ddfa`](https://github.com/withastro/astro/commit/5b3ddfadcb2d09b6cbd9cd42641f30ca565d0f58) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue where the middleware wasn't called when a project uses `404.astro`. + +- [#12042](https://github.com/withastro/astro/pull/12042) [`243ecb6`](https://github.com/withastro/astro/commit/243ecb6d6146dc483b4726d0e76142fb25e56243) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a problem in the Container API, where a polyfill wasn't correctly applied. This caused an issue in some environments where `crypto` isn't supported. + +- [#12038](https://github.com/withastro/astro/pull/12038) [`26ea5e8`](https://github.com/withastro/astro/commit/26ea5e814ab8c973e683fff62389fda28c180940) Thanks [@ascorbic](https://github.com/ascorbic)! - Resolves image paths in content layer with initial slash as project-relative + + When using the `image()` schema helper, previously paths with an initial slash were treated as public URLs. This was to match the behavior of markdown images. However this is a change from before, where paths with an initial slash were treated as project-relative. This change restores the previous behavior, so that paths with an initial slash are treated as project-relative. + +## 4.15.8 + +### Patch Changes + +- [#12014](https://github.com/withastro/astro/pull/12014) [`53cb41e`](https://github.com/withastro/astro/commit/53cb41e30ea5768bf33d9f6be608fb57d31b7b9e) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes an issue where component styles were not correctly included in rendered MDX + +- [#12031](https://github.com/withastro/astro/pull/12031) [`8c0cae6`](https://github.com/withastro/astro/commit/8c0cae6d1bd70b332286d83d0f01cfce5272fbbe) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a bug where the rewrite via `next(/*..*/)` inside a middleware didn't compute the new `APIContext.params` + +- [#12026](https://github.com/withastro/astro/pull/12026) [`40e7a1b`](https://github.com/withastro/astro/commit/40e7a1b05d9e5ea3fcda176c9663bbcff86edb63) Thanks [@bluwy](https://github.com/bluwy)! - Initializes the Markdown processor only when there's `.md` files + +- [#12028](https://github.com/withastro/astro/pull/12028) [`d3bd673`](https://github.com/withastro/astro/commit/d3bd673392e63720e241d6a002a131a3564c169c) Thanks [@bluwy](https://github.com/bluwy)! - Handles route collision detection only if it matches `getStaticPaths` + +- [#12027](https://github.com/withastro/astro/pull/12027) [`dd3b753`](https://github.com/withastro/astro/commit/dd3b753aba6400558671d85214e27b8e4fb1654b) Thanks [@fviolette](https://github.com/fviolette)! - Add `selected` to the list of boolean attributes + +- [#12001](https://github.com/withastro/astro/pull/12001) [`9be3e1b`](https://github.com/withastro/astro/commit/9be3e1bba789af96d8b21d9c8eca8542cfb4ff77) Thanks [@uwej711](https://github.com/uwej711)! - Remove dependency on path-to-regexp + ## 4.15.7 ### Patch Changes diff --git a/packages/astro/package.json b/packages/astro/package.json index 0278c1d0d..4ef5a0d79 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -126,8 +126,8 @@ "@astrojs/internal-helpers": "workspace:*", "@astrojs/markdown-remark": "workspace:*", "@astrojs/telemetry": "workspace:*", - "@babel/types": "^7.25.4", - "@oslojs/encoding": "^0.4.1", + "@babel/types": "^7.25.6", + "@oslojs/encoding": "^1.0.0", "@rollup/pluginutils": "^5.1.0", "@types/cookie": "^0.6.0", "acorn": "^8.12.1", @@ -165,7 +165,6 @@ "ora": "^8.1.0", "p-limit": "^6.1.0", "p-queue": "^8.0.1", - "path-to-regexp": "6.2.2", "preferred-pm": "^4.0.0", "prompts": "^2.4.2", "rehype": "^13.0.1", diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts index e4365ecf5..43b1c119b 100644 --- a/packages/astro/src/container/index.ts +++ b/packages/astro/src/container/index.ts @@ -1,3 +1,4 @@ +import './polyfill.js'; import { posix } from 'node:path'; import { getDefaultClientDirectives } from '../core/client-directive/index.js'; import { ASTRO_CONFIG_DEFAULTS } from '../core/config/schema.js'; @@ -5,13 +6,14 @@ import { validateConfig } from '../core/config/validate.js'; import { createKey } from '../core/encryption.js'; import { Logger } from '../core/logger/core.js'; import { nodeLogDestination } from '../core/logger/node.js'; +import { NOOP_MIDDLEWARE_FN } from '../core/middleware/noop-middleware.js'; import { removeLeadingForwardSlash } from '../core/path.js'; import { RenderContext } from '../core/render-context.js'; import { getParts, validateSegment } from '../core/routing/manifest/create.js'; import { getPattern } from '../core/routing/manifest/pattern.js'; import type { AstroComponentFactory } from '../runtime/server/index.js'; import type { ComponentInstance } from '../types/astro.js'; -import type { MiddlewareHandler, Props } from '../types/public/common.js'; +import type { AstroMiddlewareInstance, MiddlewareHandler, Props } from '../types/public/common.js'; import type { AstroConfig, AstroUserConfig } from '../types/public/config.js'; import type { NamedSSRLoadedRendererValue, @@ -121,9 +123,11 @@ function createManifest( renderers?: SSRLoadedRenderer[], middleware?: MiddlewareHandler, ): SSRManifest { - const defaultMiddleware: MiddlewareHandler = (_, next) => { - return next(); - }; + function middlewareInstance(): AstroMiddlewareInstance { + return { + onRequest: middleware ?? NOOP_MIDDLEWARE_FN, + }; + } return { hrefRoot: import.meta.url, @@ -142,7 +146,7 @@ function createManifest( inlinedScripts: manifest?.inlinedScripts ?? new Map(), i18n: manifest?.i18n, checkOrigin: false, - middleware: manifest?.middleware ?? middleware ?? defaultMiddleware, + middleware: manifest?.middleware ?? middlewareInstance, envGetSecretEnabled: false, key: createKey(), }; @@ -488,11 +492,10 @@ export class experimental_AstroContainer { params: options.params, type: routeType, }); - const renderContext = RenderContext.create({ + const renderContext = await RenderContext.create({ pipeline: this.#pipeline, routeData, status: 200, - middleware: this.#pipeline.middleware, request, pathname: url.pathname, locals: options?.locals ?? {}, diff --git a/packages/astro/src/container/pipeline.ts b/packages/astro/src/container/pipeline.ts index 167285158..154713028 100644 --- a/packages/astro/src/container/pipeline.ts +++ b/packages/astro/src/container/pipeline.ts @@ -84,7 +84,7 @@ export class ContainerPipeline extends Pipeline { return Promise.resolve(componentInstance); }, renderers: this.manifest.renderers, - onRequest: this.manifest.middleware, + onRequest: this.resolvedMiddleware, }); } diff --git a/packages/astro/src/container/polyfill.ts b/packages/astro/src/container/polyfill.ts new file mode 100644 index 000000000..baf533596 --- /dev/null +++ b/packages/astro/src/container/polyfill.ts @@ -0,0 +1,3 @@ +import { applyPolyfills } from '../core/app/node.js'; + +applyPolyfills(); diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts index 7cfe1c5dd..2a5922bca 100644 --- a/packages/astro/src/core/app/common.ts +++ b/packages/astro/src/core/app/common.ts @@ -1,4 +1,5 @@ import { decodeKey } from '../encryption.js'; +import { NOOP_MIDDLEWARE_FN } from '../middleware/noop-middleware.js'; import { deserializeRouteData } from '../routing/manifest/serialization.js'; import type { RouteInfo, SSRManifest, SerializedSSRManifest } from './types.js'; @@ -23,8 +24,8 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest): return { // in case user middleware exists, this no-op middleware will be reassigned (see plugin-ssr.ts) - middleware(_, next) { - return next(); + middleware() { + return { onRequest: NOOP_MIDDLEWARE_FN }; }, ...serializedManifest, assets, diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index e3539798e..9e124879e 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -12,7 +12,7 @@ import { getSetCookiesFromResponse } from '../cookies/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { consoleLogDestination } from '../logger/console.js'; import { AstroIntegrationLogger, Logger } from '../logger/core.js'; -import { sequence } from '../middleware/index.js'; +import { NOOP_MIDDLEWARE_FN } from '../middleware/noop-middleware.js'; import { appendForwardSlash, joinPaths, @@ -23,7 +23,6 @@ import { RenderContext } from '../render-context.js'; import { createAssetLink } from '../render/ssr-element.js'; import { createDefaultRoutes, injectDefaultRoutes } from '../routing/default.js'; import { matchRoute } from '../routing/match.js'; -import { createOriginCheckMiddleware } from './middlewares.js'; import { AppPipeline } from './pipeline.js'; export { deserializeManifest } from './common.js'; @@ -112,13 +111,6 @@ export class App { * @private */ #createPipeline(manifestData: ManifestData, streaming = false) { - if (this.#manifest.checkOrigin) { - this.#manifest.middleware = sequence( - createOriginCheckMiddleware(), - this.#manifest.middleware, - ); - } - return AppPipeline.create(manifestData, { logger: this.#logger, manifest: this.#manifest, @@ -293,7 +285,7 @@ export class App { // Load route module. We also catch its error here if it fails on initialization const mod = await this.#pipeline.getModuleForRoute(routeData); - const renderContext = RenderContext.create({ + const renderContext = await RenderContext.create({ pipeline: this.#pipeline, locals, pathname, @@ -389,10 +381,10 @@ export class App { } const mod = await this.#pipeline.getModuleForRoute(errorRouteData); try { - const renderContext = RenderContext.create({ + const renderContext = await RenderContext.create({ locals, pipeline: this.#pipeline, - middleware: skipMiddleware ? (_, next) => next() : undefined, + middleware: skipMiddleware ? NOOP_MIDDLEWARE_FN : undefined, pathname: this.#getPathnameFromRequest(request), request, routeData: errorRouteData, diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index 43ff91fdd..9317f9de1 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -85,11 +85,7 @@ export class AppPipeline extends Pipeline { return module.page(); } - async tryRewrite( - payload: RewritePayload, - request: Request, - _sourceRoute: RouteData, - ): Promise<TryRewriteResult> { + async tryRewrite(payload: RewritePayload, request: Request): Promise<TryRewriteResult> { const { newUrl, pathname, routeData } = findRouteToRewrite({ payload, request, diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 4bf2a350e..50ef7901c 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -1,13 +1,8 @@ import type { RoutingStrategies } from '../../i18n/utils.js'; import type { ComponentInstance, SerializedRouteData } from '../../types/astro.js'; -import type { MiddlewareHandler } from '../../types/public/common.js'; +import type { AstroMiddlewareInstance } from '../../types/public/common.js'; import type { Locales } from '../../types/public/config.js'; -import type { - RouteData, - SSRComponentMetadata, - SSRLoadedRenderer, - SSRResult, -} from '../../types/public/internal.js'; +import type { RouteData, SSRComponentMetadata, SSRLoadedRenderer, SSRResult } from '../../types/public/internal.js'; import type { SinglePageBuiltModule } from '../build/types.js'; export type ComponentPath = string; @@ -67,7 +62,7 @@ export type SSRManifest = { serverIslandNameMap?: Map<string, string>; key: Promise<CryptoKey>; i18n: SSRManifestI18n | undefined; - middleware: MiddlewareHandler; + middleware: () => Promise<AstroMiddlewareInstance> | AstroMiddlewareInstance; checkOrigin: boolean; envGetSecretEnabled: boolean; }; diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts index 2c8199446..614748cd8 100644 --- a/packages/astro/src/core/base-pipeline.ts +++ b/packages/astro/src/core/base-pipeline.ts @@ -88,13 +88,8 @@ export abstract class Pipeline { * * @param {RewritePayload} rewritePayload The payload provided by the user * @param {Request} request The original request - * @param {RouteData} sourceRoute The original `RouteData` */ - abstract tryRewrite( - rewritePayload: RewritePayload, - request: Request, - sourceRoute: RouteData, - ): Promise<TryRewriteResult>; + abstract tryRewrite(rewritePayload: RewritePayload, request: Request): Promise<TryRewriteResult>; /** * Tells the pipeline how to retrieve a component give a `RouteData` diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 7c8776f1d..c5f01a707 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -417,7 +417,12 @@ async function generatePath( logger, isPrerendered: true, }); - const renderContext = RenderContext.create({ pipeline, pathname, request, routeData: route }); + const renderContext = await RenderContext.create({ + pipeline, + pathname, + request, + routeData: route, + }); let body: string | Uint8Array; let response: Response; @@ -530,7 +535,11 @@ function createBuildManifest( componentMetadata: internals.componentMetadata, i18n: i18nManifest, buildFormat: settings.config.build.format, - middleware, + middleware() { + return { + onRequest: middleware, + }; + }, checkOrigin: settings.config.security?.checkOrigin ?? false, key, envGetSecretEnabled: false, diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index f6aa47e11..523962078 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -86,6 +86,10 @@ export class BuildPipeline extends Pipeline { ); } + getRoutes(): RouteData[] { + return this.options.manifest.routes; + } + static create({ internals, manifest, @@ -127,7 +131,11 @@ export class BuildPipeline extends Pipeline { const renderers = await import(renderersEntryUrl.toString()); const middleware = await import(new URL('middleware.mjs', baseDirectory).toString()) - .then((mod) => mod.onRequest) + .then((mod) => { + return function () { + return { onRequest: mod.onRequest }; + }; + }) // middleware.mjs is not emitted if there is no user middleware // in which case the import fails with ERR_MODULE_NOT_FOUND, and we fall back to a no-op middleware .catch(() => manifest.middleware); @@ -259,11 +267,7 @@ export class BuildPipeline extends Pipeline { return module.page(); } - async tryRewrite( - payload: RewritePayload, - request: Request, - _sourceRoute: RouteData, - ): Promise<TryRewriteResult> { + async tryRewrite(payload: RewritePayload, request: Request): Promise<TryRewriteResult> { const { routeData, pathname, newUrl } = findRouteToRewrite({ payload, request, diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index dab617d82..68745817b 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -180,7 +180,7 @@ function generateSSRCode(settings: AstroSettings, adapter: AstroAdapter, middlew ` pageMap,`, ` serverIslandMap,`, ` renderers,`, - ` middleware`, + ` middleware: ${edgeMiddleware ? 'undefined' : `() => import("${middlewareId}")`}`, `});`, `const _args = ${adapter.args ? JSON.stringify(adapter.args, null, 4) : 'undefined'};`, adapter.exports diff --git a/packages/astro/src/core/middleware/noop-middleware.ts b/packages/astro/src/core/middleware/noop-middleware.ts new file mode 100644 index 000000000..bf5f10d89 --- /dev/null +++ b/packages/astro/src/core/middleware/noop-middleware.ts @@ -0,0 +1,3 @@ +import type { MiddlewareHandler } from '../../@types/astro.js'; + +export const NOOP_MIDDLEWARE_FN: MiddlewareHandler = (_, next) => next(); diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index aefa66a92..1fbba7c66 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,6 +1,8 @@ import type { MiddlewareHandler, RewritePayload } from '../../types/public/common.js'; import type { APIContext } from '../../types/public/context.js'; import { AstroCookies } from '../cookies/cookies.js'; +import { apiContextRoutesSymbol } from '../render-context.js'; +import { type Pipeline, getParams } from '../render/index.js'; import { defineMiddleware } from './index.js'; // From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js @@ -16,7 +18,6 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { return next(); }); } - return defineMiddleware((context, next) => { /** * This variable is used to carry the rerouting payload across middleware functions. @@ -29,7 +30,7 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { // @ts-expect-error // SAFETY: Usually `next` always returns something in user land, but in `sequence` we are actually // doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`. - const result = handle(handleContext, async (payload: RewritePayload) => { + const result = handle(handleContext, async (payload?: RewritePayload) => { if (i < length - 1) { if (payload) { let newRequest; @@ -43,10 +44,16 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { handleContext.request, ); } + const pipeline: Pipeline = Reflect.get(handleContext, apiContextRoutesSymbol); + const { routeData, pathname } = await pipeline.tryRewrite( + payload, + handleContext.request, + ); carriedPayload = payload; handleContext.request = newRequest; handleContext.url = new URL(newRequest.url); handleContext.cookies = new AstroCookies(newRequest); + handleContext.params = getParams(routeData, pathname); } return applyHandle(i + 1, handleContext); } else { diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 13f7c6d77..0d458511c 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -29,14 +29,13 @@ import { sequence } from './middleware/index.js'; import { renderRedirect } from './redirects/render.js'; import { type Pipeline, Slots, getParams, getProps } from './render/index.js'; +export const apiContextRoutesSymbol = Symbol.for('context.routes'); + /** * Each request is rendered using a `RenderContext`. * It contains data unique to each request. It is responsible for executing middleware, calling endpoints, and rendering the page by gathering necessary data from a `Pipeline`. */ export class RenderContext { - // The first route that this instance of the context attempts to render - originalRoute: RouteData; - private constructor( readonly pipeline: Pipeline, public locals: App.Locals, @@ -49,9 +48,7 @@ export class RenderContext { public params = getParams(routeData, pathname), protected url = new URL(request.url), public props: Props = {}, - ) { - this.originalRoute = routeData; - } + ) {} /** * A flag that tells the render content if the rewriting was triggered @@ -62,7 +59,7 @@ export class RenderContext { */ counter = 0; - static create({ + static async create({ locals = {}, middleware, pathname, @@ -72,11 +69,14 @@ export class RenderContext { status = 200, props, }: Pick<RenderContext, 'pathname' | 'pipeline' | 'request' | 'routeData'> & - Partial<Pick<RenderContext, 'locals' | 'middleware' | 'status' | 'props'>>): RenderContext { + Partial< + Pick<RenderContext, 'locals' | 'middleware' | 'status' | 'props'> + >): Promise<RenderContext> { + const pipelineMiddleware = await pipeline.getMiddleware(); return new RenderContext( pipeline, locals, - sequence(...pipeline.internalMiddleware, middleware ?? pipeline.middleware), + sequence(...pipeline.internalMiddleware, middleware ?? pipelineMiddleware), pathname, request, routeData, @@ -133,14 +133,24 @@ export class RenderContext { if (payload) { pipeline.logger.debug('router', 'Called rewriting to:', payload); // we intentionally let the error bubble up - const { routeData, componentInstance: newComponent } = await pipeline.tryRewrite( - payload, - this.request, - this.originalRoute, - ); + const { + routeData, + componentInstance: newComponent, + pathname, + newUrl, + } = await pipeline.tryRewrite(payload, this.request); this.routeData = routeData; componentInstance = newComponent; + if (payload instanceof Request) { + this.request = payload; + } else { + this.request = this.#copyRequest(newUrl, this.request); + } this.isRewriting = true; + this.url = new URL(this.request.url); + this.cookies = new AstroCookies(this.request); + this.params = getParams(routeData, pathname); + this.pathname = pathname; this.status = 200; } let response: Response; @@ -214,6 +224,7 @@ export class RenderContext { const context = this.createActionAPIContext(); const redirect = (path: string, status = 302) => new Response(null, { status, headers: { Location: path } }); + Reflect.set(context, apiContextRoutesSymbol, this.pipeline); return Object.assign(context, { props, @@ -228,7 +239,6 @@ export class RenderContext { const { routeData, componentInstance, newUrl, pathname } = await this.pipeline.tryRewrite( reroutePayload, this.request, - this.originalRoute, ); this.routeData = routeData; if (reroutePayload instanceof Request) { diff --git a/packages/astro/src/core/render/params-and-props.ts b/packages/astro/src/core/render/params-and-props.ts index 0beebda79..43d36218e 100644 --- a/packages/astro/src/core/render/params-and-props.ts +++ b/packages/astro/src/core/render/params-and-props.ts @@ -36,12 +36,6 @@ export async function getProps(opts: GetParamsAndPropsOptions): Promise<Props> { return {}; } - // This is a dynamic route, start getting the params - const params = getParams(route, pathname); - if (mod) { - validatePrerenderEndpointCollision(route, mod, params); - } - // During build, the route cache should already be populated. // During development, the route cache is filled on-demand and may be empty. const staticPaths = await callGetStaticPaths({ @@ -53,6 +47,7 @@ export async function getProps(opts: GetParamsAndPropsOptions): Promise<Props> { base, }); + const params = getParams(route, pathname); const matchedStaticPath = findPathItemByKey(staticPaths, params, route, logger); if (!matchedStaticPath && (serverLike ? route.prerender : true)) { throw new AstroError({ @@ -62,6 +57,10 @@ export async function getProps(opts: GetParamsAndPropsOptions): Promise<Props> { }); } + if (mod) { + validatePrerenderEndpointCollision(route, mod, params); + } + const props: Props = matchedStaticPath?.props ? { ...matchedStaticPath.props } : {}; return props; diff --git a/packages/astro/src/core/routing/manifest/generator.ts b/packages/astro/src/core/routing/manifest/generator.ts index e3565864d..d6ed25b44 100644 --- a/packages/astro/src/core/routing/manifest/generator.ts +++ b/packages/astro/src/core/routing/manifest/generator.ts @@ -1,15 +1,12 @@ -import { compile } from 'path-to-regexp'; -import type { AstroConfig } from '../../../types/public/config.js'; -import type { RoutePart } from '../../../types/public/internal.js'; +import type { AstroConfig } from "../../../types/public/config.js"; +import type { RoutePart } from "../../../types/public/internal.js"; /** * Sanitizes the parameters object by normalizing string values and replacing certain characters with their URL-encoded equivalents. - * @param {Record<string, string | number | undefined>} params - The parameters object to be sanitized. - * @returns {Record<string, string | number | undefined>} The sanitized parameters object. + * @param {Record<string, string | number>} params - The parameters object to be sanitized. + * @returns {Record<string, string | number>} The sanitized parameters object. */ -function sanitizeParams( - params: Record<string, string | number | undefined>, -): Record<string, string | number | undefined> { +function sanitizeParams(params: Record<string, string | number>): Record<string, string | number> { return Object.fromEntries( Object.entries(params).map(([key, value]) => { if (typeof value === 'string') { @@ -20,49 +17,49 @@ function sanitizeParams( ); } +function getParameter(part: RoutePart, params: Record<string, string | number>): string | number { + if (part.spread) { + return params[part.content.slice(3)] || ''; + } + + if (part.dynamic) { + if (!params[part.content]) { + throw new TypeError(`Missing parameter: ${part.content}`); + } + + return params[part.content]; + } + + return part.content + .normalize() + .replace(/\?/g, '%3F') + .replace(/#/g, '%23') + .replace(/%5B/g, '[') + .replace(/%5D/g, ']'); +} + +function getSegment(segment: RoutePart[], params: Record<string, string | number>): string { + const segmentPath = segment.map((part) => getParameter(part, params)).join(''); + + return segmentPath ? '/' + segmentPath : ''; +} + export function getRouteGenerator( segments: RoutePart[][], addTrailingSlash: AstroConfig['trailingSlash'], ) { - const template = segments - .map((segment) => { - return ( - '/' + - segment - .map((part) => { - if (part.spread) { - return `:${part.content.slice(3)}(.*)?`; - } else if (part.dynamic) { - return `:${part.content}`; - } else { - return part.content - .normalize() - .replace(/\?/g, '%3F') - .replace(/#/g, '%23') - .replace(/%5B/g, '[') - .replace(/%5D/g, ']') - .replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - } - }) - .join('') - ); - }) - .join(''); - - // Unless trailingSlash config is set to 'always', don't automatically append it. - let trailing: '/' | '' = ''; - if (addTrailingSlash === 'always' && segments.length) { - trailing = '/'; - } - const toPath = compile(template + trailing); - return (params: Record<string, string | number | undefined>): string => { + return (params: Record<string, string | number>): string => { const sanitizedParams = sanitizeParams(params); - const path = toPath(sanitizedParams); - // When generating an index from a rest parameter route, `path-to-regexp` will return an - // empty string instead "/". This causes an inconsistency with static indexes that may result - // in the incorrect routes being rendered. - // To fix this, we return "/" when the path is empty. + // Unless trailingSlash config is set to 'always', don't automatically append it. + let trailing: '/' | '' = ''; + if (addTrailingSlash === 'always' && segments.length) { + trailing = '/'; + } + + const path = + segments.map((segment) => getSegment(segment, sanitizedParams)).join('') + trailing; + return path || '/'; }; } diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts index 19ac2f2b1..953488a83 100644 --- a/packages/astro/src/runtime/server/render/util.ts +++ b/packages/astro/src/runtime/server/render/util.ts @@ -7,7 +7,7 @@ import { HTMLString, markHTMLString } from '../escape.js'; export const voidElementNames = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i; const htmlBooleanAttributes = - /^(?:allowfullscreen|async|autofocus|autoplay|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|itemscope)$/i; + /^(?:allowfullscreen|async|autofocus|autoplay|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|selected|itemscope)$/i; const AMPERSAND_REGEX = /&/g; const DOUBLE_QUOTE_REGEX = /"/g; diff --git a/packages/astro/src/types/public/common.ts b/packages/astro/src/types/public/common.ts index 4bb94f5ca..bba1ca32d 100644 --- a/packages/astro/src/types/public/common.ts +++ b/packages/astro/src/types/public/common.ts @@ -110,6 +110,12 @@ export type MiddlewareHandler = ( next: MiddlewareNext, ) => Promise<Response> | Response | Promise<void> | void; +// NOTE: when updating this file with other functions, +// remember to update `plugin-page.ts` too, to add that function as a no-op function. +export type AstroMiddlewareInstance = { + onRequest?: MiddlewareHandler; +}; + /** * Infers the shape of the `params` property returned by `getStaticPaths()`. * diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index bbeba1eaa..c4bd54111 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -193,11 +193,7 @@ export class DevPipeline extends Pipeline { } } - async tryRewrite( - payload: RewritePayload, - request: Request, - _sourceRoute: RouteData, - ): Promise<TryRewriteResult> { + async tryRewrite(payload: RewritePayload, request: Request): Promise<TryRewriteResult> { if (!this.manifestData) { throw new Error('Missing manifest data. This is an internal error, please file an issue.'); } diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index ff67f487a..c69b511e4 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -9,6 +9,7 @@ import { getViteErrorPayload } from '../core/errors/dev/index.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { patchOverlay } from '../core/errors/overlay.js'; import type { Logger } from '../core/logger/core.js'; +import { NOOP_MIDDLEWARE_FN } from '../core/middleware/noop-middleware.js'; import { createViteLoader } from '../core/module-loader/index.js'; import { injectDefaultDevRoutes } from '../core/routing/dev-default.js'; import { createRouteManifest } from '../core/routing/index.js'; @@ -162,8 +163,10 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest checkOrigin: settings.config.security?.checkOrigin ?? false, envGetSecretEnabled: false, key: createKey(), - middleware(_, next) { - return next(); + middleware() { + return { + onRequest: NOOP_MIDDLEWARE_FN, + }; }, }; } diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 8df09b898..faccf2294 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -1,5 +1,6 @@ import type http from 'node:http'; import { + DEFAULT_404_COMPONENT, REROUTE_DIRECTIVE_HEADER, REWRITE_DIRECTIVE_HEADER_KEY, clientLocalsSymbol, @@ -194,13 +195,16 @@ export async function handleRoute({ mod = preloadedComponent; - const isPrerendered404 = matchedRoute.route.route === '/404' && matchedRoute.route.prerender; + const isDefaultPrerendered404 = + matchedRoute.route.route === '/404' && + matchedRoute.route.prerender && + matchedRoute.route.component === DEFAULT_404_COMPONENT; - renderContext = RenderContext.create({ + renderContext = await RenderContext.create({ locals, pipeline, pathname, - middleware: isPrerendered404 ? undefined : middleware, + middleware: isDefaultPrerendered404 ? undefined : middleware, request, routeData: route, }); diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index 1a01f223a..ddedef973 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -31,14 +31,11 @@ const astroErrorModulePath = normalizePath( ); export default function markdown({ settings, logger }: AstroPluginOptions): Plugin { - let processor: MarkdownProcessor | undefined; + let processor: Promise<MarkdownProcessor> | undefined; return { enforce: 'pre', name: 'astro:markdown', - async buildStart() { - processor = await createMarkdownProcessor(settings.config.markdown); - }, buildEnd() { processor = undefined; }, @@ -61,15 +58,12 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug const fileURL = pathToFileURL(fileId); - // `processor` is initialized in `buildStart`, and removed in `buildEnd`. `load` - // should be called in between those two lifecycles, so this error should never happen + // Lazily initialize the Markdown processor if (!processor) { - return this.error( - 'MDX processor is not initialized. This is an internal error. Please file an issue.', - ); + processor = createMarkdownProcessor(settings.config.markdown); } - const renderResult = await processor.render(raw.content, { + const renderResult = await (await processor).render(raw.content, { // @ts-expect-error passing internal prop fileURL, frontmatter: raw.data, diff --git a/packages/astro/test/dynamic-endpoint-collision.test.js b/packages/astro/test/dynamic-endpoint-collision.test.js index b1aa42f9f..91bbf3b67 100644 --- a/packages/astro/test/dynamic-endpoint-collision.test.js +++ b/packages/astro/test/dynamic-endpoint-collision.test.js @@ -49,5 +49,10 @@ describe('Dynamic endpoint collision', () => { const res = await fixture.fetch('/api/catch/one').then((r) => r.text()); assert.equal(res, '{"slug":"one"}'); }); + + it('returns 404 when user visits dynamic endpoint that has collision but not specified in getStaticPaths', async () => { + const res = await fixture.fetch('/api/safe'); + assert.equal(res.status, 404); + }); }); }); diff --git a/packages/astro/test/fixtures/dynamic-endpoint-collision/src/pages/api/safe/[...slug].ts b/packages/astro/test/fixtures/dynamic-endpoint-collision/src/pages/api/safe/[...slug].ts new file mode 100644 index 000000000..98fecec1c --- /dev/null +++ b/packages/astro/test/fixtures/dynamic-endpoint-collision/src/pages/api/safe/[...slug].ts @@ -0,0 +1,14 @@ +import type { APIRoute } from 'astro'; + +// No undefined so should not error +const slugs = ['one']; + +export const GET: APIRoute = ({ params }) => { + return Response.json({ + slug: params.slug || 'index', + }); +}; + +export function getStaticPaths() { + return slugs.map((u) => ({ params: { slug: u } })); +} diff --git a/packages/astro/test/fixtures/reroute/src/middleware.js b/packages/astro/test/fixtures/reroute/src/middleware.js index 09f3ef73c..eae50f866 100644 --- a/packages/astro/test/fixtures/reroute/src/middleware.js +++ b/packages/astro/test/fixtures/reroute/src/middleware.js @@ -1,4 +1,5 @@ import { sequence } from 'astro:middleware'; +import {defineMiddleware} from "astro/middleware"; let contextReroute = false; @@ -22,16 +23,23 @@ export const second = async (context, next) => { if (context.url.pathname.includes('/auth/params')) { return next('/?foo=bar'); } + + if (context.url.pathname.includes('/auth/astro-params')) { + return next('/auth/1234'); + } } return next(); }; -export const third = async (context, next) => { +export const third = defineMiddleware(async (context, next) => { // just making sure that we are testing the change in context coming from `next()` if (context.url.pathname.startsWith('/') && contextReroute === false) { context.locals.auth = 'Third function called'; } + if (context.params?.id === '1234') { + context.locals.auth = 'Params changed' + } return next(); -}; +}); export const onRequest = sequence(first, second, third); diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/[id].astro b/packages/astro/test/fixtures/reroute/src/pages/auth/[id].astro new file mode 100644 index 000000000..f60d2b807 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/auth/[id].astro @@ -0,0 +1,23 @@ +--- + +export function getStaticPaths( ) { + return [{ + params: { + id: "1234" + } + }] +} + +const { id } = Astro.params; +const auth = Astro.locals.auth; +--- +<html> +<head> + <title>Index with params</title> +</head> +<body> +<h1>Index with params</h1> +<p id="params">Param: {id}</p> +<p id="locals">Locals: {auth}</p> +</body> +</html> diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/astro-params.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/astro-params.astro new file mode 100644 index 000000000..853d812bb --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/auth/astro-params.astro @@ -0,0 +1,3 @@ +--- + +--- diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/params.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/params.astro index 3f4dfad87..7530bf7a5 100644 --- a/packages/astro/test/fixtures/reroute/src/pages/auth/params.astro +++ b/packages/astro/test/fixtures/reroute/src/pages/auth/params.astro @@ -1,4 +1,6 @@ --- +const { id } = Astro.params; +const auth = Astro.locals.auth; --- <html> <head> @@ -6,5 +8,7 @@ </head> <body> <h1>Index with params</h1> +<p id="params">Param: {id}</p> +<p id="locals">Locals: {auth}</p> </body> </html> diff --git a/packages/astro/test/i18n-routing-manual.test.js b/packages/astro/test/i18n-routing-manual.test.js index d0de75fe8..3a9efe643 100644 --- a/packages/astro/test/i18n-routing-manual.test.js +++ b/packages/astro/test/i18n-routing-manual.test.js @@ -53,9 +53,9 @@ describe('Dev server manual routing', () => { assert.equal(text.includes('Hola.'), true); }); - it('should not redirect prerendered 404 routes in dev', async () => { + it('should call the middleware for 404.astro pages', async () => { const response = await fixture.fetch('/redirect-me'); - assert.equal(response.status, 404); + assert.equal(response.status, 200); }); }); diff --git a/packages/astro/test/rewrite.test.js b/packages/astro/test/rewrite.test.js index 10d7c70d2..26cdac5c0 100644 --- a/packages/astro/test/rewrite.test.js +++ b/packages/astro/test/rewrite.test.js @@ -388,6 +388,15 @@ describe('Middleware', () => { assert.match($('h1').text(), /Index/); }); + + it('should render correctly compute the new params next("/auth/1234")', async () => { + const html = await fixture.fetch('/auth/astro-params').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.match($('h1').text(), /Index with params/); + assert.match($('#params').text(), /Param: 1234/); + assert.match($('#locals').text(), /Locals: Params changed/); + }); }); describe('Middleware with custom 404.astro and 500.astro', () => { diff --git a/packages/astro/test/units/render/head.test.js b/packages/astro/test/units/render/head.test.js index 0eb4c7715..4c4a9eb77 100644 --- a/packages/astro/test/units/render/head.test.js +++ b/packages/astro/test/units/render/head.test.js @@ -103,7 +103,7 @@ describe('core/render', () => { component: 'src/pages/index.astro', params: {}, }; - const renderContext = RenderContext.create({ pipeline, request, routeData }); + const renderContext = await RenderContext.create({ pipeline, request, routeData }); const response = await renderContext.render(PageModule); const html = await response.text(); @@ -184,7 +184,7 @@ describe('core/render', () => { component: 'src/pages/index.astro', params: {}, }; - const renderContext = RenderContext.create({ pipeline, request, routeData }); + const renderContext = await RenderContext.create({ pipeline, request, routeData }); const response = await renderContext.render(PageModule); const html = await response.text(); @@ -232,7 +232,7 @@ describe('core/render', () => { component: 'src/pages/index.astro', params: {}, }; - const renderContext = RenderContext.create({ pipeline, request, routeData }); + const renderContext = await RenderContext.create({ pipeline, request, routeData }); const response = await renderContext.render(PageModule); const html = await response.text(); diff --git a/packages/astro/test/units/routing/generator.test.js b/packages/astro/test/units/routing/generator.test.js new file mode 100644 index 000000000..d176c73eb --- /dev/null +++ b/packages/astro/test/units/routing/generator.test.js @@ -0,0 +1,152 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { getRouteGenerator } from '../../../dist/core/routing/manifest/generator.js'; + +describe('routing - generator', () => { + [ + { + routeData: [], + trailingSlash: 'never', + params: {}, + path: '/', + }, + { + routeData: [], + trailingSlash: 'always', + params: {}, + path: '/', + }, + { + routeData: [[{ spread: false, content: 'test', dynamic: false }]], + trailingSlash: 'never', + params: {}, + path: '/test', + }, + { + routeData: [[{ spread: false, content: 'test', dynamic: false }]], + trailingSlash: 'always', + params: {}, + path: '/test/', + }, + { + routeData: [[{ spread: false, content: 'test', dynamic: false }]], + trailingSlash: 'always', + params: { foo: 'bar' }, + path: '/test/', + }, + { + routeData: [[{ spread: false, content: 'foo', dynamic: true }]], + trailingSlash: 'always', + params: { foo: 'bar' }, + path: '/bar/', + }, + { + routeData: [[{ spread: false, content: 'foo', dynamic: true }]], + trailingSlash: 'never', + params: { foo: 'bar' }, + path: '/bar', + }, + { + routeData: [[{ spread: true, content: '...foo', dynamic: true }]], + trailingSlash: 'never', + params: {}, + path: '/', + }, + { + routeData: [ + [ + { spread: true, content: '...foo', dynamic: true }, + { spread: false, content: '-', dynamic: false }, + { spread: true, content: '...bar', dynamic: true }, + ], + ], + trailingSlash: 'never', + params: { foo: 'one', bar: 'two' }, + path: '/one-two', + }, + { + routeData: [ + [ + { spread: true, content: '...foo', dynamic: true }, + { spread: false, content: '-', dynamic: false }, + { spread: true, content: '...bar', dynamic: true }, + ], + ], + trailingSlash: 'never', + params: {}, + path: '/-', + }, + { + routeData: [ + [{ spread: true, content: '...foo', dynamic: true }], + [{ spread: true, content: '...bar', dynamic: true }], + ], + trailingSlash: 'never', + params: { foo: 'one' }, + path: '/one', + }, + { + routeData: [ + [{ spread: false, content: 'fix', dynamic: false }], + [{ spread: true, content: '...foo', dynamic: true }], + [{ spread: true, content: '...bar', dynamic: true }], + ], + trailingSlash: 'never', + params: { foo: 'one' }, + path: '/fix/one', + }, + { + routeData: [ + [{ spread: false, content: 'fix', dynamic: false }], + [{ spread: true, content: '...foo', dynamic: true }], + [{ spread: true, content: '...bar', dynamic: true }], + ], + trailingSlash: 'always', + params: { foo: 'one' }, + path: '/fix/one/', + }, + { + routeData: [ + [{ spread: false, content: 'fix', dynamic: false }], + [{ spread: true, content: '...foo', dynamic: true }], + [{ spread: true, content: '...bar', dynamic: true }], + ], + trailingSlash: 'never', + params: { foo: 'one', bar: 'two' }, + path: '/fix/one/two', + }, + { + routeData: [ + [{ spread: false, content: 'fix', dynamic: false }], + [{ spread: true, content: '...foo', dynamic: true }], + [{ spread: true, content: '...bar', dynamic: true }], + ], + trailingSlash: 'never', + params: { foo: 'one&two' }, + path: '/fix/one&two', + }, + { + routeData: [ + [{ spread: false, content: 'fix', dynamic: false }], + [{ spread: false, content: 'page', dynamic: true }], + ], + trailingSlash: 'never', + params: { page: 1 }, + path: '/fix/1', + }, + ].forEach(({ routeData, trailingSlash, params, path }) => { + it(`generates ${path}`, () => { + const generator = getRouteGenerator(routeData, trailingSlash); + assert.equal(generator(params), path); + }); + }); + + it('should throw an error when a dynamic parameter is missing', () => { + const generator = getRouteGenerator( + [[{ spread: false, content: 'foo', dynamic: true }]], + 'never', + ); + assert.throws(() => generator({}), TypeError); + }); +}); diff --git a/packages/astro/test/units/test-utils.js b/packages/astro/test/units/test-utils.js index a104ef2a4..bb7a8c8e1 100644 --- a/packages/astro/test/units/test-utils.js +++ b/packages/astro/test/units/test-utils.js @@ -10,6 +10,7 @@ import { createBaseSettings } from '../../dist/core/config/settings.js'; import { createContainer } from '../../dist/core/dev/container.js'; import { Logger } from '../../dist/core/logger/core.js'; import { nodeLogDestination } from '../../dist/core/logger/node.js'; +import { NOOP_MIDDLEWARE_FN } from '../../dist/core/middleware/noop-middleware.js'; import { Pipeline } from '../../dist/core/render/index.js'; import { RouteCache } from '../../dist/core/render/route-cache.js'; import { unixify } from './correct-path.js'; @@ -207,6 +208,9 @@ export function createBasicPipeline(options = {}) { ); pipeline.headElements = () => ({ scripts: new Set(), styles: new Set(), links: new Set() }); pipeline.componentMetadata = () => new Map(); + pipeline.getMiddleware = () => { + return NOOP_MIDDLEWARE_FN; + }; return pipeline; } diff --git a/packages/integrations/mdx/CHANGELOG.md b/packages/integrations/mdx/CHANGELOG.md index e8c220e4c..7eacad5c3 100644 --- a/packages/integrations/mdx/CHANGELOG.md +++ b/packages/integrations/mdx/CHANGELOG.md @@ -38,6 +38,12 @@ - astro@5.0.0-alpha.0 - @astrojs/markdown-remark@6.0.0-alpha.0 +## 3.1.7 + +### Patch Changes + +- [#12026](https://github.com/withastro/astro/pull/12026) [`40e7a1b`](https://github.com/withastro/astro/commit/40e7a1b05d9e5ea3fcda176c9663bbcff86edb63) Thanks [@bluwy](https://github.com/bluwy)! - Initializes the MDX processor only when there's `.mdx` files + ## 3.1.6 ### Patch Changes diff --git a/packages/integrations/mdx/src/vite-plugin-mdx.ts b/packages/integrations/mdx/src/vite-plugin-mdx.ts index e4d4387c3..238286202 100644 --- a/packages/integrations/mdx/src/vite-plugin-mdx.ts +++ b/packages/integrations/mdx/src/vite-plugin-mdx.ts @@ -8,6 +8,7 @@ import { parseFrontmatter } from './utils.js'; export function vitePluginMdx(mdxOptions: MdxOptions): Plugin { let processor: ReturnType<typeof createMdxProcessor> | undefined; + let sourcemapEnabled: boolean; return { name: '@mdx-js/rollup', @@ -16,13 +17,7 @@ export function vitePluginMdx(mdxOptions: MdxOptions): Plugin { processor = undefined; }, configResolved(resolved) { - // `mdxOptions` should be populated at this point, but `astro sync` doesn't call `astro:config:done` :( - // Workaround this for now by skipping here. `astro sync` shouldn't call the `transform()` hook here anyways. - if (Object.keys(mdxOptions).length === 0) return; - - processor = createMdxProcessor(mdxOptions, { - sourcemap: !!resolved.build.sourcemap, - }); + sourcemapEnabled = !!resolved.build.sourcemap; // HACK: Remove the `astro:jsx` plugin if defined as we handle the JSX transformation ourselves const jsxPluginIndex = resolved.plugins.findIndex((p) => p.name === 'astro:jsx'); @@ -56,12 +51,9 @@ export function vitePluginMdx(mdxOptions: MdxOptions): Plugin { }, }); - // `processor` is initialized in `configResolved`, and removed in `buildEnd`. `transform` - // should be called in between those two lifecycle, so this error should never happen + // Lazily initialize the MDX processor if (!processor) { - return this.error( - 'MDX processor is not initialized. This is an internal error. Please file an issue.', - ); + processor = createMdxProcessor(mdxOptions, { sourcemap: sourcemapEnabled }); } try { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39a5ebb0b..7096c4ff5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,9 +33,6 @@ importers: eslint: specifier: ^9.10.0 version: 9.10.0(jiti@1.21.0) - eslint-plugin-no-only-tests: - specifier: ^3.3.0 - version: 3.3.0 eslint-plugin-regexp: specifier: ^2.6.0 version: 2.6.0(eslint@9.10.0(jiti@1.21.0)) @@ -264,7 +261,7 @@ importers: examples/framework-solid: dependencies: '@astrojs/solid-js': - specifier: ^4.4.1 + specifier: ^4.4.2 version: link:../../packages/integrations/solid astro: specifier: ^5.0.0-beta.1 @@ -276,7 +273,7 @@ importers: examples/framework-svelte: dependencies: '@astrojs/svelte': - specifier: ^5.7.0 + specifier: ^5.7.1 version: link:../../packages/integrations/svelte astro: specifier: ^5.0.0-beta.1 @@ -288,13 +285,13 @@ importers: examples/framework-vue: dependencies: '@astrojs/vue': - specifier: ^4.5.0 + specifier: ^4.5.1 version: link:../../packages/integrations/vue astro: specifier: ^5.0.0-beta.1 version: link:../../packages/astro vue: - specifier: ^3.4.38 + specifier: ^3.5.3 version: 3.5.3(typescript@5.5.4) examples/hackernews: @@ -424,7 +421,7 @@ importers: specifier: ^9.0.0-alpha.1 version: 9.0.0-alpha.1(astro@packages+astro) '@astrojs/tailwind': - specifier: ^5.1.0 + specifier: ^5.1.1 version: link:../../packages/integrations/tailwind astro: specifier: ^5.0.0-beta.1 @@ -508,7 +505,7 @@ importers: specifier: ^4.0.0-beta.1 version: link:../../packages/integrations/mdx '@astrojs/tailwind': - specifier: ^5.1.0 + specifier: ^5.1.1 version: link:../../packages/integrations/tailwind '@types/canvas-confetti': specifier: ^1.6.4 @@ -553,11 +550,11 @@ importers: specifier: workspace:* version: link:../telemetry '@babel/types': - specifier: ^7.25.4 - version: 7.25.4 + specifier: ^7.25.6 + version: 7.25.6 '@oslojs/encoding': - specifier: ^0.4.1 - version: 0.4.1 + specifier: ^1.0.0 + version: 1.0.0 '@rollup/pluginutils': specifier: ^5.1.0 version: 5.1.0(rollup@4.21.2) @@ -669,9 +666,6 @@ importers: p-queue: specifier: ^8.0.1 version: 8.0.1 - path-to-regexp: - specifier: 6.2.2 - version: 6.2.2 preferred-pm: specifier: ^4.0.0 version: 4.0.0 @@ -5858,8 +5852,8 @@ packages: resolution: {integrity: sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==} engines: {node: '>=6.9.0'} - '@babel/types@7.25.4': - resolution: {integrity: sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==} + '@babel/types@7.25.6': + resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} engines: {node: '>=6.9.0'} '@biomejs/biome@1.8.3': @@ -6734,8 +6728,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oslojs/encoding@0.4.1': - resolution: {integrity: sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==} + '@oslojs/encoding@1.0.0': + resolution: {integrity: sha512-dyIB0SdZgMm5BhGwdSp8rMxEFIopLKxDG1vxIBaiogyom6ZqH2aXPb6DEC2WzOOWKdPSq1cxdNeRx2wAn1Z+ZQ==} '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -8136,10 +8130,6 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-plugin-no-only-tests@3.3.0: - resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==} - engines: {node: '>=5.0.0'} - eslint-plugin-regexp@2.6.0: resolution: {integrity: sha512-FCL851+kislsTEQEMioAlpDuK5+E5vs0hi1bF8cFlPlHcEjeRhuAzEsGikXRreE+0j4WhW2uO54MqTjXtYOi3A==} engines: {node: ^18 || >=20} @@ -9493,9 +9483,6 @@ packages: resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} engines: {node: '>=16 || 14 >=14.17'} - path-to-regexp@6.2.2: - resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -11244,7 +11231,7 @@ snapshots: '@babel/parser': 7.25.4 '@babel/template': 7.25.0 '@babel/traverse': 7.25.4 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 convert-source-map: 2.0.0 debug: 4.3.7 gensync: 1.0.0-beta.2 @@ -11255,14 +11242,14 @@ snapshots: '@babel/generator@7.25.5': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 '@babel/helper-annotate-as-pure@7.24.7': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-compilation-targets@7.25.2': dependencies: @@ -11289,32 +11276,32 @@ snapshots: '@babel/helper-environment-visitor@7.24.7': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-function-name@7.24.7': dependencies: '@babel/template': 7.25.0 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-member-expression-to-functions@7.24.7': dependencies: '@babel/traverse': 7.25.4 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.18.6': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-module-imports@7.22.15': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-module-imports@7.24.7': dependencies: '@babel/traverse': 7.25.4 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color @@ -11330,7 +11317,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.24.7': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-plugin-utils@7.24.8': {} @@ -11346,20 +11333,20 @@ snapshots: '@babel/helper-simple-access@7.24.7': dependencies: '@babel/traverse': 7.25.4 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.24.7': dependencies: '@babel/traverse': 7.25.4 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color '@babel/helper-split-export-declaration@7.24.7': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-string-parser@7.24.8': {} @@ -11370,7 +11357,7 @@ snapshots: '@babel/helpers@7.25.0': dependencies: '@babel/template': 7.25.0 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/highlight@7.24.7': dependencies: @@ -11381,7 +11368,7 @@ snapshots: '@babel/parser@7.25.4': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/plugin-proposal-decorators@7.24.1(@babel/core@7.25.2)': dependencies: @@ -11441,7 +11428,7 @@ snapshots: '@babel/helper-module-imports': 7.24.7 '@babel/helper-plugin-utils': 7.24.8 '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color @@ -11463,7 +11450,7 @@ snapshots: dependencies: '@babel/code-frame': 7.24.7 '@babel/parser': 7.25.4 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/traverse@7.25.4': dependencies: @@ -11471,13 +11458,13 @@ snapshots: '@babel/generator': 7.25.5 '@babel/parser': 7.25.4 '@babel/template': 7.25.0 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/types@7.25.4': + '@babel/types@7.25.6': dependencies: '@babel/helper-string-parser': 7.24.8 '@babel/helper-validator-identifier': 7.24.7 @@ -12346,7 +12333,7 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 - '@oslojs/encoding@0.4.1': {} + '@oslojs/encoding@1.0.0': {} '@pkgjs/parseargs@0.11.0': optional: true @@ -12519,23 +12506,23 @@ snapshots: '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.25.4 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.6 '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@types/babel__template@7.4.4': dependencies: '@babel/parser': 7.25.4 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@types/babel__traverse@7.20.6': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@types/body-parser@1.19.5': dependencies: @@ -12986,7 +12973,7 @@ snapshots: '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) '@babel/template': 7.25.0 '@babel/traverse': 7.25.4 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@vue/babel-helper-vue-transform-on': 1.2.2 '@vue/babel-plugin-resolve-type': 1.2.2(@babel/core@7.25.2) camelcase: 6.3.0 @@ -13250,7 +13237,7 @@ snapshots: '@babel/core': 7.25.2 '@babel/helper-module-imports': 7.18.6 '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 html-entities: 2.3.3 validate-html-nesting: 1.2.2 @@ -13789,8 +13776,6 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-plugin-no-only-tests@3.3.0: {} - eslint-plugin-regexp@2.6.0(eslint@9.10.0(jiti@1.21.0)): dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0(jiti@1.21.0)) @@ -14740,7 +14725,7 @@ snapshots: magicast@0.3.5: dependencies: '@babel/parser': 7.25.4 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 source-map-js: 1.2.0 manage-path@2.0.0: {} @@ -15575,8 +15560,6 @@ snapshots: lru-cache: 10.2.0 minipass: 7.1.2 - path-to-regexp@6.2.2: {} - path-type@4.0.0: {} path-type@5.0.0: {} @@ -16403,7 +16386,7 @@ snapshots: dependencies: '@babel/generator': 7.25.5 '@babel/helper-module-imports': 7.24.7 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 solid-js: 1.8.22 transitivePeerDependencies: - supports-color |