diff --git a/.changeset/fix-netlify-external.md b/.changeset/fix-netlify-external.md new file mode 100644 index 000000000000..a7858a156824 --- /dev/null +++ b/.changeset/fix-netlify-external.md @@ -0,0 +1,7 @@ +--- +'@astrojs/netlify': patch +--- + +Fixes builds that were failing with "Entry module cannot be external" error when using the Netlify adapter + +This error was preventing sites from building after recent internal changes. Your builds should now work as expected without any changes to your code. \ No newline at end of file diff --git a/.changeset/happy-falcons-show.md b/.changeset/happy-falcons-show.md new file mode 100644 index 000000000000..01c2fb66fef1 --- /dev/null +++ b/.changeset/happy-falcons-show.md @@ -0,0 +1,8 @@ +--- +'@astrojs/cloudflare': patch +'astro': patch +--- + +Removes the `cssesc` dependency + +This CommonJS dependency could sometimes cause errors because Astro is ESM-only. It is now replaced with a built-in ESM-friendly implementation. diff --git a/.changeset/pre.json b/.changeset/pre.json index ea4ffeeee7a9..fdf9f855a195 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -109,14 +109,17 @@ "fix-create-astro-registry-hang", "fix-font-head-swap", "fix-forwarded-proto-allowed-domains", + "fix-large-static-build-promise-all", "fix-large-static-routes-stack-overflow", "fix-markdoc-table-attributes", "fix-mdx-slot-hydration", + "fix-netlify-external", "fix-preact-cloudflare-hooks", "fix-rewrite-non-ascii-paths", "fix-serve-files-outside-srcdir", "fix-ssr-prerendered-image-deletion", "fix-store-race-condition", + "fix-svg-content-collection-deadlock", "fix-vite-runner-closed", "flat-lions-care", "flat-symbols-arrive", @@ -140,6 +143,7 @@ "green-plants-act", "green-zebras-lick", "grumpy-tables-serve", + "happy-falcons-show", "heavy-beers-unite", "heavy-cats-own", "heavy-parts-throw", @@ -161,6 +165,7 @@ "large-lemons-relax", "late-carrots-cough", "late-spiders-change", + "lazy-insects-wave", "legacy-collections-backwards-compat-docs", "light-parrots-find", "little-goats-poke", @@ -215,6 +220,7 @@ "slimy-queens-punch", "slow-laws-marry", "small-ghosts-sort", + "smart-mammals-stop", "smooth-kids-tease", "social-kings-swim", "social-maps-shine", diff --git a/examples/basics/package.json b/examples/basics/package.json index 6dd43ce2167e..cddfa43731a2 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -13,6 +13,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^6.0.0-beta.15" + "astro": "^6.0.0-beta.16" } } diff --git a/examples/blog/package.json b/examples/blog/package.json index 1018763fa840..c95861fc5074 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -16,7 +16,7 @@ "@astrojs/mdx": "^5.0.0-beta.8", "@astrojs/rss": "^4.0.15-beta.4", "@astrojs/sitemap": "^3.6.1-beta.3", - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "sharp": "^0.34.3" } } diff --git a/examples/component/package.json b/examples/component/package.json index b989ae3cbeaf..6a9bb8a33d67 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -18,7 +18,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^6.0.0-beta.15" + "astro": "^6.0.0-beta.16" }, "peerDependencies": { "astro": "^5.0.0 || ^6.0.0" diff --git a/examples/container-with-vitest/package.json b/examples/container-with-vitest/package.json index 3e69f12869b9..bf0cf44e485c 100644 --- a/examples/container-with-vitest/package.json +++ b/examples/container-with-vitest/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@astrojs/react": "^5.0.0-beta.3", - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "react": "^18.3.1", "react-dom": "^18.3.1", "vitest": "^3.2.4" diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index 82f40e6989cc..91db50b27648 100644 --- a/examples/framework-alpine/package.json +++ b/examples/framework-alpine/package.json @@ -16,6 +16,6 @@ "@astrojs/alpinejs": "^0.5.0-beta.1", "@types/alpinejs": "^3.13.11", "alpinejs": "^3.15.8", - "astro": "^6.0.0-beta.15" + "astro": "^6.0.0-beta.16" } } diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index ae957fe092c6..316cf62842d0 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -20,7 +20,7 @@ "@astrojs/vue": "^6.0.0-beta.1", "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "preact": "^10.28.4", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index 3d9ff5762a49..4de3b8a55dac 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -15,7 +15,7 @@ "dependencies": { "@astrojs/preact": "^5.0.0-beta.4", "@preact/signals": "^2.8.1", - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "preact": "^10.28.4" } } diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index 8bee6084f962..09b4d0fc97fb 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -16,7 +16,7 @@ "@astrojs/react": "^5.0.0-beta.3", "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "react": "^18.3.1", "react-dom": "^18.3.1" } diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index 69c7a4543883..79ff40c8ea06 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@astrojs/solid-js": "^6.0.0-beta.2", - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "solid-js": "^1.9.11" } } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index 558cfc63350e..580ca29d0012 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@astrojs/svelte": "^8.0.0-beta.3", - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "svelte": "^5.53.0" } } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index b1e3ac39050a..6ce19149b9f2 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@astrojs/vue": "^6.0.0-beta.1", - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "vue": "^3.5.28" } } diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index b46a1ef883af..46fd63a3b39f 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -14,6 +14,6 @@ }, "dependencies": { "@astrojs/node": "^10.0.0-beta.5", - "astro": "^6.0.0-beta.15" + "astro": "^6.0.0-beta.16" } } diff --git a/examples/integration/package.json b/examples/integration/package.json index ea2ceceb7ebc..5a75c260e2e0 100644 --- a/examples/integration/package.json +++ b/examples/integration/package.json @@ -18,7 +18,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^6.0.0-beta.15" + "astro": "^6.0.0-beta.16" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/minimal/package.json b/examples/minimal/package.json index 604ba5ba1b46..6ee0ca0f7089 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -13,6 +13,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^6.0.0-beta.15" + "astro": "^6.0.0-beta.16" } } diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index 5c70faf0df07..093810392540 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -13,6 +13,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^6.0.0-beta.15" + "astro": "^6.0.0-beta.16" } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 6ea36c0471b0..4e03fdeca75e 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -16,7 +16,7 @@ "dependencies": { "@astrojs/node": "^10.0.0-beta.5", "@astrojs/svelte": "^8.0.0-beta.3", - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "svelte": "^5.53.0" } } diff --git a/examples/starlog/package.json b/examples/starlog/package.json index 8a7588d81029..e67655c67471 100644 --- a/examples/starlog/package.json +++ b/examples/starlog/package.json @@ -9,7 +9,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "sass": "^1.97.3", "sharp": "^0.34.3" }, diff --git a/examples/toolbar-app/package.json b/examples/toolbar-app/package.json index 30a7d079a762..d5a5731d9a2f 100644 --- a/examples/toolbar-app/package.json +++ b/examples/toolbar-app/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@types/node": "^18.17.8", - "astro": "^6.0.0-beta.15" + "astro": "^6.0.0-beta.16" }, "engines": { "node": ">=22.12.0" diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json index 681a8cd89ea6..83e101e68ea3 100644 --- a/examples/with-markdoc/package.json +++ b/examples/with-markdoc/package.json @@ -14,6 +14,6 @@ }, "dependencies": { "@astrojs/markdoc": "^1.0.0-beta.11", - "astro": "^6.0.0-beta.15" + "astro": "^6.0.0-beta.16" } } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index a2273ea68edb..aeb7832f70d9 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -15,7 +15,7 @@ "dependencies": { "@astrojs/mdx": "^5.0.0-beta.8", "@astrojs/preact": "^5.0.0-beta.4", - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "preact": "^10.28.4" } } diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index e2805bdbda69..f6861b7e5939 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -15,7 +15,7 @@ "dependencies": { "@astrojs/preact": "^5.0.0-beta.4", "@nanostores/preact": "^1.0.0", - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "nanostores": "^1.1.0", "preact": "^10.28.4" } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index 4ec0df45c66c..89270c446313 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -16,7 +16,7 @@ "@astrojs/mdx": "^5.0.0-beta.8", "@tailwindcss/vite": "^4.2.0", "@types/canvas-confetti": "^1.9.0", - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "canvas-confetti": "^1.9.4", "tailwindcss": "^4.2.0" } diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index 262ab26d7e08..4fa2cca1b3fe 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -14,7 +14,7 @@ "test": "vitest" }, "dependencies": { - "astro": "^6.0.0-beta.15", + "astro": "^6.0.0-beta.16", "vitest": "^3.2.4" } } diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 00ea43cc0b71..37ef4147d9b3 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,24 @@ # astro +## 6.0.0-beta.16 + +### Minor Changes + +- [#15646](https://github.com/withastro/astro/pull/15646) [`0dd9d00`](https://github.com/withastro/astro/commit/0dd9d00cf8be38c53217426f6b0e155a6f7c2a22) Thanks [@delucis](https://github.com/delucis)! - Removes redundant `fetchpriority` attributes from the output of Astro’s `` component + + Previously, Astro would always include `fetchpriority="auto"` on images not using the `priority` attribute. + However, this is the default value, so specifying it is redundant. This change omits the attribute by default. + +### Patch Changes + +- [#15661](https://github.com/withastro/astro/pull/15661) [`7150a2e`](https://github.com/withastro/astro/commit/7150a2e2aa022a9a957684ad8091f85aedb243f1) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a build error when generating projects with 100k+ static routes. + +- [#15603](https://github.com/withastro/astro/pull/15603) [`5bc2b2c`](https://github.com/withastro/astro/commit/5bc2b2c2f4a9928efa16452b64729586dc79a0c7) Thanks [@0xRozier](https://github.com/0xRozier)! - Fixes a deadlock that occurred when using SVG images in content collections + +- [#15669](https://github.com/withastro/astro/pull/15669) [`d5a888b`](https://github.com/withastro/astro/commit/d5a888ba645de356673605a0b70f9c721cf6cb3b) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Removes the `cssesc` dependency + + This CommonJS dependency could sometimes cause errors because Astro is ESM-only. It is now replaced with a built-in ESM-friendly implementation. + ## 6.0.0-beta.15 ### Minor Changes diff --git a/packages/astro/package.json b/packages/astro/package.json index 1a3f5ab126d7..8ae84a44cb34 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "astro", - "version": "6.0.0-beta.15", + "version": "6.0.0-beta.16", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.", "type": "module", "author": "withastro", @@ -130,7 +130,6 @@ "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", - "cssesc": "^3.0.0", "devalue": "^5.6.3", "diff": "^8.0.3", "dlv": "^1.1.3", @@ -180,7 +179,6 @@ "@astrojs/compiler-rs": "^0.1.1", "@playwright/test": "1.58.2", "@types/aria-query": "^5.0.4", - "@types/cssesc": "^3.0.2", "@types/dlv": "^1.1.5", "@types/hast": "^3.0.4", "@types/html-escaper": "3.0.4", diff --git a/packages/astro/src/core/routing/match.ts b/packages/astro/src/core/routing/match.ts index bc52c1ed8eaf..cccfdbdae438 100644 --- a/packages/astro/src/core/routing/match.ts +++ b/packages/astro/src/core/routing/match.ts @@ -1,7 +1,7 @@ import type { RoutesList } from '../../types/astro.js'; import type { RouteData } from '../../types/public/internal.js'; import { redirectIsExternal } from '../redirects/render.js'; -import { SERVER_ISLAND_BASE_PREFIX, SERVER_ISLAND_COMPONENT } from '../server-islands/endpoint.js'; +import { SERVER_ISLAND_COMPONENT } from '../server-islands/endpoint.js'; import { isRoute404, isRoute500 } from './internal/route-errors.js'; /** Find matching route from pathname */ @@ -39,35 +39,6 @@ export function isRouteServerIsland(route: RouteData): boolean { return route.component === SERVER_ISLAND_COMPONENT; } -/** - * Determines whether the given `Request` is targeted to a "server island" based on its URL. - * - * @param {Request} request - The request object to be evaluated. - * @param {string} [base=''] - The base path provided via configuration. - * @return {boolean} - Returns `true` if the request is for a server island, otherwise `false`. - */ -export function isRequestServerIsland(request: Request, base = ''): boolean { - const url = new URL(request.url); - const pathname = - base === '/' ? url.pathname.slice(base.length) : url.pathname.slice(base.length + 1); - - return pathname.startsWith(SERVER_ISLAND_BASE_PREFIX); -} - -/** - * Checks if the given request corresponds to a 404 or 500 route based on the specified base path. - * - * @param {Request} request - The HTTP request object to be checked. - * @param {string} [base=''] - The base path to trim from the request's URL before checking the route. Default is an empty string. - * @return {boolean} Returns true if the request matches a 404 or 500 route; otherwise, returns false. - */ -export function requestIs404Or500(request: Request, base = '') { - const url = new URL(request.url); - const pathname = url.pathname.slice(base.length); - - return isRoute404(pathname) || isRoute500(pathname); -} - /** * Determines whether a given route is an external redirect. * diff --git a/packages/astro/src/core/server-islands/endpoint.ts b/packages/astro/src/core/server-islands/endpoint.ts index 26508a92a559..230671fe66c0 100644 --- a/packages/astro/src/core/server-islands/endpoint.ts +++ b/packages/astro/src/core/server-islands/endpoint.ts @@ -13,7 +13,6 @@ import { getPattern } from '../routing/pattern.js'; export const SERVER_ISLAND_ROUTE = '/_server-islands/[name]'; export const SERVER_ISLAND_COMPONENT = '_server-islands.astro'; -export const SERVER_ISLAND_BASE_PREFIX = '_server-islands'; type ConfigFields = Pick; diff --git a/packages/astro/src/i18n/fallback.ts b/packages/astro/src/i18n/fallback.ts new file mode 100644 index 000000000000..8c2492275709 --- /dev/null +++ b/packages/astro/src/i18n/fallback.ts @@ -0,0 +1,118 @@ +import type { SSRManifest, SSRManifestI18n } from '../core/app/types.js'; +import type { APIContext } from '../types/public/context.js'; +import { getPathByLocale } from './index.js'; + +/** + * Fallback routing decision types + */ +export type FallbackRouteResult = + | { type: 'none' } // No fallback needed + | { type: 'redirect'; pathname: string } // Redirect to fallback locale + | { type: 'rewrite'; pathname: string }; // Rewrite to fallback locale + +/** + * Options for computing fallback routes. + * Uses types from APIContext and SSRManifest to ensure type safety. + */ +export interface ComputeFallbackRouteOptions { + /** Pathname from url.pathname */ + pathname: APIContext['url']['pathname']; + /** Response status code */ + responseStatus: number; + /** Current locale from APIContext */ + currentLocale: APIContext['currentLocale']; + /** Fallback configuration from i18n manifest */ + fallback: NonNullable; + /** Fallback type from i18n manifest */ + fallbackType: SSRManifestI18n['fallbackType']; + /** Locales from i18n manifest */ + locales: SSRManifestI18n['locales']; + /** Default locale from i18n manifest */ + defaultLocale: SSRManifestI18n['defaultLocale']; + /** Routing strategy from i18n manifest */ + strategy: SSRManifestI18n['strategy']; + /** Base path from manifest */ + base: SSRManifest['base']; +} + +/** + * Compute fallback route for failed responses. + * Pure function - no APIContext, no Response objects, no URL objects. + * + * This function determines whether a failed request should be redirected or rewritten + * to a fallback locale based on the i18n configuration. + */ +export function computeFallbackRoute(options: ComputeFallbackRouteOptions): FallbackRouteResult { + const { + pathname, + responseStatus, + fallback, + fallbackType, + locales, + defaultLocale, + strategy, + base, + } = options; + + // Only apply fallback for 3xx+ status codes + if (responseStatus < 300) { + return { type: 'none' }; + } + + // No fallback configured + if (!fallback || Object.keys(fallback).length === 0) { + return { type: 'none' }; + } + + // Extract locale from pathname + const segments = pathname.split('/'); + const urlLocale = segments.find((segment) => { + for (const locale of locales) { + if (typeof locale === 'string') { + if (locale === segment) { + return true; + } + } else if (locale.path === segment) { + return true; + } + } + return false; + }); + + // No locale found in pathname + if (!urlLocale) { + return { type: 'none' }; + } + + // Check if this locale has a fallback configured + const fallbackKeys = Object.keys(fallback); + if (!fallbackKeys.includes(urlLocale)) { + return { type: 'none' }; + } + + // Get the fallback locale + const fallbackLocale = fallback[urlLocale]; + + // Get the path for the fallback locale (handles granular locales) + const pathFallbackLocale = getPathByLocale(fallbackLocale, locales); + + let newPathname: string; + + // If fallback is to the default locale and strategy is prefix-other-locales, + // remove the locale prefix (default locale has no prefix) + if (pathFallbackLocale === defaultLocale && strategy === 'pathname-prefix-other-locales') { + if (pathname.includes(`${base}`)) { + newPathname = pathname.replace(`/${urlLocale}`, ``); + } else { + newPathname = pathname.replace(`/${urlLocale}`, `/`); + } + } else { + // Replace the current locale with the fallback locale + newPathname = pathname.replace(`/${urlLocale}`, `/${pathFallbackLocale}`); + } + + return { + type: fallbackType, + pathname: newPathname, + }; +} diff --git a/packages/astro/src/i18n/middleware.ts b/packages/astro/src/i18n/middleware.ts index 6b840749850f..1d87c16c6cf4 100644 --- a/packages/astro/src/i18n/middleware.ts +++ b/packages/astro/src/i18n/middleware.ts @@ -1,16 +1,10 @@ -import type { SSRManifest, SSRManifestI18n } from '../core/app/types.js'; +import { appendForwardSlash } from '@astrojs/internal-helpers/path'; +import type { SSRManifest } from '../core/app/types.js'; +import { shouldAppendForwardSlash } from '../core/build/util.js'; import { REROUTE_DIRECTIVE_HEADER, ROUTE_TYPE_HEADER } from '../core/constants.js'; -import { isRequestServerIsland, requestIs404Or500 } from '../core/routing/match.js'; import type { MiddlewareHandler } from '../types/public/common.js'; -import type { APIContext } from '../types/public/context.js'; -import { - type MiddlewarePayload, - normalizeTheLocale, - notFound, - redirectToDefaultLocale, - redirectToFallback, - requestHasLocale, -} from './index.js'; +import { computeFallbackRoute } from './fallback.js'; +import { I18nRouter, type I18nRouterContext } from './router.js'; export function createI18nMiddleware( i18n: SSRManifest['i18n'], @@ -19,148 +13,102 @@ export function createI18nMiddleware( format: SSRManifest['buildFormat'], ): MiddlewareHandler { if (!i18n) return (_, next) => next(); - const payload: MiddlewarePayload = { - ...i18n, - trailingSlash, - base, - format, - domains: {}, - }; - const _redirectToDefaultLocale = redirectToDefaultLocale(payload); - const _noFoundForNonLocaleRoute = notFound(payload); - const _requestHasLocale = requestHasLocale(payload.locales); - const _redirectToFallback = redirectToFallback(payload); - - const prefixAlways = (context: APIContext, response: Response): Response | undefined => { - const url = context.url; - if (url.pathname === base + '/' || url.pathname === base) { - return _redirectToDefaultLocale(context); - } - - // Astro can't know where the default locale is supposed to be, so it returns a 404. - else if (!_requestHasLocale(context)) { - return _noFoundForNonLocaleRoute(context, response); - } - - return undefined; - }; - - const prefixOtherLocales = (context: APIContext, response: Response): Response | undefined => { - let pathnameContainsDefaultLocale = false; - const url = context.url; - for (const segment of url.pathname.split('/')) { - if (normalizeTheLocale(segment) === normalizeTheLocale(i18n.defaultLocale)) { - pathnameContainsDefaultLocale = true; - break; - } - } - if (pathnameContainsDefaultLocale) { - const newLocation = url.pathname.replace(`/${i18n.defaultLocale}`, ''); - response.headers.set('Location', newLocation); - return _noFoundForNonLocaleRoute(context); - } - return undefined; - }; + // Create router once during middleware initialization + const i18nRouter = new I18nRouter({ + strategy: i18n.strategy, + defaultLocale: i18n.defaultLocale, + locales: i18n.locales, + base, + domains: i18n.domainLookupTable + ? Object.keys(i18n.domainLookupTable).reduce( + (acc, domain) => { + const locale = i18n.domainLookupTable[domain]; + if (!acc[domain]) { + acc[domain] = []; + } + acc[domain].push(locale); + return acc; + }, + {} as Record, + ) + : undefined, + }); return async (context, next) => { const response = await next(); - const type = response.headers.get(ROUTE_TYPE_HEADER); + const typeHeader = response.headers.get(ROUTE_TYPE_HEADER); // This is case where we are internally rendering a 404/500, so we need to bypass checks that were done already const isReroute = response.headers.get(REROUTE_DIRECTIVE_HEADER); if (isReroute === 'no' && typeof i18n.fallback === 'undefined') { return response; } - // If the route we're processing is not a page, then we ignore it - if (type !== 'page' && type !== 'fallback') { - return response; - } - // 404 and 500 are **known** routes (users can have their custom pages), so we need to let them be - if (requestIs404Or500(context.request, base)) { - return response; - } - - // This is a case where the rendering phase belongs to a server island. Server island are - // special routes, and should be exhempt from i18n routing - if (isRequestServerIsland(context.request, base)) { + // If the route we're processing is not a page, then we ignore it + if (typeHeader !== 'page' && typeHeader !== 'fallback') { return response; } - const { currentLocale } = context; - switch (i18n.strategy) { - // NOTE: theoretically, we should never hit this code path - case 'manual': { - return response; - } - case 'domains-prefix-other-locales': { - if (localeHasntDomain(i18n, currentLocale)) { - const result = prefixOtherLocales(context, response); - if (result) { - return result; - } + // Build context for router (typeHeader is guaranteed to be 'page' | 'fallback' here) + const routerContext: I18nRouterContext = { + currentLocale: context.currentLocale, + currentDomain: context.url.hostname, + routeType: typeHeader as 'page' | 'fallback', + isReroute: isReroute === 'yes', + }; + + // Step 1: Apply routing strategy + const routeDecision = i18nRouter.match(context.url.pathname, routerContext); + + switch (routeDecision.type) { + case 'redirect': { + // Apply trailing slash if needed + let location = routeDecision.location; + if (shouldAppendForwardSlash(trailingSlash, format)) { + location = appendForwardSlash(location); } - break; + return context.redirect(location, routeDecision.status); } - case 'pathname-prefix-other-locales': { - const result = prefixOtherLocales(context, response); - if (result) { - return result; + case 'notFound': { + const notFoundRes = new Response(response.body, { + status: 404, + headers: response.headers, + }); + notFoundRes.headers.set(REROUTE_DIRECTIVE_HEADER, 'no'); + if (routeDecision.location) { + notFoundRes.headers.set('Location', routeDecision.location); } - break; - } - - case 'domains-prefix-always-no-redirect': { - if (localeHasntDomain(i18n, currentLocale)) { - const result = _noFoundForNonLocaleRoute(context, response); - if (result) { - return result; - } - } - break; - } - - case 'pathname-prefix-always-no-redirect': { - const result = _noFoundForNonLocaleRoute(context, response); - if (result) { - return result; - } - break; + return notFoundRes; } + case 'continue': + break; // Continue to fallback check + } - case 'pathname-prefix-always': { - const result = prefixAlways(context, response); - if (result) { - return result; - } - break; - } - case 'domains-prefix-always': { - if (localeHasntDomain(i18n, currentLocale)) { - const result = prefixAlways(context, response); - if (result) { - return result; - } - } - break; + // Step 2: Apply fallback logic (if configured) + if (i18n.fallback && i18n.fallbackType) { + const fallbackDecision = computeFallbackRoute({ + pathname: context.url.pathname, + responseStatus: response.status, + currentLocale: context.currentLocale, + fallback: i18n.fallback, + fallbackType: i18n.fallbackType, + locales: i18n.locales, + defaultLocale: i18n.defaultLocale, + strategy: i18n.strategy, + base, + }); + + switch (fallbackDecision.type) { + case 'redirect': + return context.redirect(fallbackDecision.pathname + context.url.search); + case 'rewrite': + return await context.rewrite(fallbackDecision.pathname + context.url.search); + case 'none': + break; } } - return _redirectToFallback(context, response); + return response; }; } - -/** - * Checks if the current locale doesn't belong to a configured domain - * @param i18n - * @param currentLocale - */ -function localeHasntDomain(i18n: SSRManifestI18n, currentLocale: string | undefined) { - for (const domainLocale of Object.values(i18n.domainLookupTable)) { - if (domainLocale === currentLocale) { - return false; - } - } - return true; -} diff --git a/packages/astro/src/i18n/router.ts b/packages/astro/src/i18n/router.ts new file mode 100644 index 000000000000..612770cf1b3f --- /dev/null +++ b/packages/astro/src/i18n/router.ts @@ -0,0 +1,238 @@ +import { removeTrailingForwardSlash } from '@astrojs/internal-helpers/path'; +import type { SSRManifest, SSRManifestI18n } from '../core/app/types.js'; +import type { Locales, ValidRedirectStatus } from '../types/public/config.js'; +import type { APIContext } from '../types/public/context.js'; +import { normalizeTheLocale, pathHasLocale } from './index.js'; +import type { RoutingStrategies } from '../core/app/common.js'; + +/** + * I18n routing decision types + */ +export type I18nRouterMatch = + | { type: 'continue' } + | { type: 'redirect'; location: string; status?: ValidRedirectStatus } + | { type: 'notFound'; location?: string }; + +/** + * Options for i18n router. + * Uses types from SSRManifest to ensure type safety. + */ +export interface I18nRouterOptions { + /** Routing strategy from i18n manifest */ + strategy: SSRManifestI18n['strategy']; + /** Default locale from i18n manifest */ + defaultLocale: SSRManifestI18n['defaultLocale']; + /** Locales from i18n manifest */ + locales: SSRManifestI18n['locales']; + /** Base path from manifest */ + base: SSRManifest['base']; + /** Domain mapping (domain -> locale[]) */ + domains?: Record; +} + +/** + * Context for i18n router decisions. + * Uses types from APIContext to ensure type safety. + */ +export interface I18nRouterContext { + /** Current locale from APIContext */ + currentLocale: APIContext['currentLocale']; + /** Current domain from url.hostname */ + currentDomain: APIContext['url']['hostname']; + /** Route type from response headers */ + routeType?: 'page' | 'fallback'; + /** Whether this is a reroute from response headers */ + isReroute: boolean; +} + +/** + * Router for i18n routing strategy decisions. + * Determines whether to continue, redirect, or return 404 based on pathname and locale. + * + * This is a pure class that returns decision objects (not HTTP Responses). + * The middleware layer is responsible for converting decisions to HTTP responses. + */ +export class I18nRouter { + #strategy: RoutingStrategies; + #defaultLocale: string; + #locales: Locales; + #base: string; + #domains?: Record; + + constructor(options: I18nRouterOptions) { + this.#strategy = options.strategy; + this.#defaultLocale = options.defaultLocale; + this.#locales = options.locales; + // Normalize base to not have trailing slash (except for root '/') + this.#base = options.base === '/' ? '/' : removeTrailingForwardSlash(options.base || ''); + this.#domains = options.domains; + } + + /** + * Evaluate routing strategy for a pathname. + * Returns decision object (not HTTP Response). + */ + public match(pathname: string, context: I18nRouterContext): I18nRouterMatch { + // Skip i18n processing for certain route types + if (this.shouldSkipProcessing(pathname, context)) { + return { type: 'continue' }; + } + + // Apply strategy-specific logic + switch (this.#strategy) { + case 'manual': + return { type: 'continue' }; + + case 'pathname-prefix-always': + return this.matchPrefixAlways(pathname, context); + + case 'domains-prefix-always': + if (this.localeHasntDomain(context.currentLocale, context.currentDomain)) { + return { type: 'continue' }; + } + return this.matchPrefixAlways(pathname, context); + + case 'pathname-prefix-other-locales': + return this.matchPrefixOtherLocales(pathname, context); + + case 'domains-prefix-other-locales': + if (this.localeHasntDomain(context.currentLocale, context.currentDomain)) { + return { type: 'continue' }; + } + return this.matchPrefixOtherLocales(pathname, context); + + case 'pathname-prefix-always-no-redirect': + return this.matchPrefixAlwaysNoRedirect(pathname, context); + + case 'domains-prefix-always-no-redirect': + if (this.localeHasntDomain(context.currentLocale, context.currentDomain)) { + return { type: 'continue' }; + } + return this.matchPrefixAlwaysNoRedirect(pathname, context); + + default: + return { type: 'continue' }; + } + } + + /** + * Check if i18n processing should be skipped for this request + */ + private shouldSkipProcessing(pathname: string, context: I18nRouterContext): boolean { + // Skip 404/500 pages + if (pathname.includes('/404') || pathname.includes('/500')) { + return true; + } + + // Skip server islands + if (pathname.includes('/_server-islands/')) { + return true; + } + + // Skip reroutes + if (context.isReroute) { + return true; + } + + // Skip non-page routes (unless it's a fallback) + if (context.routeType && context.routeType !== 'page' && context.routeType !== 'fallback') { + return true; + } + + return false; + } + + /** + * Strategy: pathname-prefix-always + * All locales must have a prefix, including the default locale. + */ + private matchPrefixAlways(pathname: string, _context: I18nRouterContext): I18nRouterMatch { + const isRoot = pathname === this.#base + '/' || pathname === this.#base; + + if (isRoot) { + // Redirect root to default locale + return { + type: 'redirect', + location: `${this.#base}/${this.#defaultLocale}`, + }; + } + + // Check if pathname has a locale + if (!pathHasLocale(pathname, this.#locales)) { + return { type: 'notFound' }; + } + + return { type: 'continue' }; + } + + /** + * Strategy: pathname-prefix-other-locales + * Default locale has no prefix, other locales must have a prefix. + */ + private matchPrefixOtherLocales(pathname: string, _context: I18nRouterContext): I18nRouterMatch { + // Check if pathname contains the default locale as a segment + let pathnameContainsDefaultLocale = false; + for (const segment of pathname.split('/')) { + if (normalizeTheLocale(segment) === normalizeTheLocale(this.#defaultLocale)) { + pathnameContainsDefaultLocale = true; + break; + } + } + + if (pathnameContainsDefaultLocale) { + // Default locale should not have a prefix - return 404 with Location header + const newLocation = pathname.replace(`/${this.#defaultLocale}`, ''); + return { + type: 'notFound', + location: newLocation, + }; + } + + return { type: 'continue' }; + } + + /** + * Strategy: pathname-prefix-always-no-redirect + * Like prefix-always but allows root to serve instead of redirecting + */ + private matchPrefixAlwaysNoRedirect( + pathname: string, + _context: I18nRouterContext, + ): I18nRouterMatch { + const isRoot = pathname === this.#base + '/' || pathname === this.#base; + + // Root path is allowed (will be served by the default locale) + if (isRoot) { + return { type: 'continue' }; + } + + // Non-root paths must have a locale + if (!pathHasLocale(pathname, this.#locales)) { + return { type: 'notFound' }; + } + + return { type: 'continue' }; + } + + /** + * Check if the current locale doesn't belong to the configured domain. + * Used for domain-based routing strategies. + */ + private localeHasntDomain(currentLocale: string | undefined, currentDomain?: string): boolean { + if (!this.#domains || !currentDomain) { + return false; + } + + if (!currentLocale) { + return false; + } + + // Check if current locale is in the list of locales for this domain + const localesForDomain = this.#domains[currentDomain]; + if (!localesForDomain) { + return true; + } + + return !localesForDomain.includes(currentLocale); + } +} diff --git a/packages/astro/src/runtime/server/transition.ts b/packages/astro/src/runtime/server/transition.ts index 21cf31faa9fd..b494910447fa 100644 --- a/packages/astro/src/runtime/server/transition.ts +++ b/packages/astro/src/runtime/server/transition.ts @@ -1,4 +1,4 @@ -import cssesc from 'cssesc'; +import cssesc from '../../transitions/cssesc.js'; import { fade, slide } from '../../transitions/index.js'; import type { SSRResult } from '../../types/public/internal.js'; import type { diff --git a/packages/astro/src/transitions/cssesc.ts b/packages/astro/src/transitions/cssesc.ts new file mode 100644 index 000000000000..8e8b5f5baaa7 --- /dev/null +++ b/packages/astro/src/transitions/cssesc.ts @@ -0,0 +1,103 @@ +/* eslint-disable regexp/control-character-escape */ +/* eslint-disable no-control-regex */ +/* eslint-disable regexp/no-optional-assertion */ +/* eslint-disable regexp/no-useless-escape */ +/* eslint-disable regexp/no-obscure-range */ +// ESM vendored version of cssesc: https://github.com/mathiasbynens/cssesc/blob/cb894eb42f27c8d3cd793f16afe35b3ab38000a1/cssesc.js +// See https://github.com/withastro/astro/pull/15669 + +const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/; +const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/; +const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g; + +interface Options { + escapeEverything: boolean; + isIdentifier: boolean; + quotes: 'single' | 'double'; + wrap: boolean; +} + +const DEFAULT_OPTIONS: Options = { + escapeEverything: false, + isIdentifier: false, + quotes: 'single', + wrap: false, +}; + +export default function cssesc(string: string, options: Partial = {}) { + options = { ...DEFAULT_OPTIONS, ...options }; + const quote = options.quotes === 'double' ? '"' : "'"; + const { isIdentifier } = options; + + const firstChar = string.charAt(0); + let output = ''; + let counter = 0; + const length = string.length; + while (counter < length) { + const character = string.charAt(counter++); + let codePoint = character.charCodeAt(0); + let value: string; + // If it’s not a printable ASCII character… + if (codePoint < 0x20 || codePoint > 0x7e) { + if (codePoint >= 0xd800 && codePoint <= 0xdbff && counter < length) { + // It’s a high surrogate, and there is a next character. + const extra = string.charCodeAt(counter++); + if ((extra & 0xfc00) === 0xdc00) { + // next character is low surrogate + codePoint = ((codePoint & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; + } else { + // It’s an unmatched surrogate; only append this code unit, in case + // the next code unit is the high surrogate of a surrogate pair. + counter--; + } + } + value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + } else { + if (options.escapeEverything) { + if (regexAnySingleEscape.test(character)) { + value = '\\' + character; + } else { + value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + } + } else if (/[\t\n\f\r\x0B]/.test(character)) { + value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + } else if ( + character === '\\' || + (!isIdentifier && + ((character === '"' && quote === character) || + (character === "'" && quote === character))) || + (isIdentifier && regexSingleEscape.test(character)) + ) { + value = '\\' + character; + } else { + value = character; + } + } + output += value; + } + + if (isIdentifier) { + if (/^-[-\d]/.test(output)) { + output = '\\-' + output.slice(1); + } else if (/\d/.test(firstChar)) { + output = '\\3' + firstChar + ' ' + output.slice(1); + } + } + + // Remove spaces after `\HEX` escapes that are not followed by a hex digit, + // since they’re redundant. Note that this is only possible if the escape + // sequence isn’t preceded by an odd number of backslashes. + output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) { + if ($1 && $1.length % 2) { + // It’s not safe to remove the space, so don’t. + return $0; + } + // Strip the space. + return ($1 || '') + $2; + }); + + if (!isIdentifier && options.wrap) { + return quote + output + quote; + } + return output; +} diff --git a/packages/astro/src/transitions/vite-plugin-transitions.ts b/packages/astro/src/transitions/vite-plugin-transitions.ts index d8a270cbb04e..9d0cb5e95fac 100644 --- a/packages/astro/src/transitions/vite-plugin-transitions.ts +++ b/packages/astro/src/transitions/vite-plugin-transitions.ts @@ -10,13 +10,6 @@ const RESOLVED_VIRTUAL_CLIENT_MODULE_ID = '\0' + VIRTUAL_CLIENT_MODULE_ID; export default function astroTransitions({ settings }: { settings: AstroSettings }): vite.Plugin { return { name: 'astro:transitions', - config() { - return { - optimizeDeps: { - include: ['astro > cssesc'], - }, - }; - }, resolveId: { filter: { id: new RegExp(`^(${VIRTUAL_MODULE_ID}|${VIRTUAL_CLIENT_MODULE_ID})$`), diff --git a/packages/astro/src/vite-plugin-environment/index.ts b/packages/astro/src/vite-plugin-environment/index.ts index c07fb536e26e..32e8ece90946 100644 --- a/packages/astro/src/vite-plugin-environment/index.ts +++ b/packages/astro/src/vite-plugin-environment/index.ts @@ -14,8 +14,6 @@ const ONLY_DEV_EXTERNAL = [ 'prismjs/components/index.js', // Imported by `astro/assets` -> `packages/astro/src/core/logger/core.ts` 'string-width', - // Imported by `astro:transitions` -> packages/astro/src/runtime/server/transition.ts - 'cssesc', ]; const ALWAYS_NOEXTERNAL = [ diff --git a/packages/astro/test/fixtures/type-imports/package.json b/packages/astro/test/fixtures/type-imports/package.json deleted file mode 100644 index 6efd1e208d5a..000000000000 --- a/packages/astro/test/fixtures/type-imports/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/type-imports", - "version": "0.0.0", - "private": true, - "dependencies": { - "astro": "workspace:*" - } -} diff --git a/packages/astro/test/fixtures/type-imports/src/pages/index.astro b/packages/astro/test/fixtures/type-imports/src/pages/index.astro deleted file mode 100644 index 3577834fbb5f..000000000000 --- a/packages/astro/test/fixtures/type-imports/src/pages/index.astro +++ /dev/null @@ -1,19 +0,0 @@ ---- -import type { MarkdownInstance } from 'astro' ---- - - - - - Astro - - - -

Astro

- - - diff --git a/packages/astro/test/fixtures/vue-jsx/astro.config.mjs b/packages/astro/test/fixtures/vue-jsx/astro.config.mjs deleted file mode 100644 index 88abf64cac7e..000000000000 --- a/packages/astro/test/fixtures/vue-jsx/astro.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import vue from '@astrojs/vue'; -import { defineConfig } from 'astro/config'; - -// https://astro.build/config -export default defineConfig({ - integrations: [vue({ jsx: true })], -}); \ No newline at end of file diff --git a/packages/astro/test/fixtures/vue-jsx/package.json b/packages/astro/test/fixtures/vue-jsx/package.json deleted file mode 100644 index 32334683dff6..000000000000 --- a/packages/astro/test/fixtures/vue-jsx/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "@test/vue-jsx", - "version": "0.0.0", - "private": true, - "dependencies": { - "@astrojs/vue": "workspace:*", - "astro": "workspace:*", - "vue": "^3.5.28" - } -} diff --git a/packages/astro/test/i18n-routing.test.js b/packages/astro/test/i18n-routing.test.js deleted file mode 100644 index 1004c24e8a3b..000000000000 --- a/packages/astro/test/i18n-routing.test.js +++ /dev/null @@ -1,2528 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { after, afterEach, before, describe, it } from 'node:test'; -import * as cheerio from 'cheerio'; -import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; - -describe('astro:i18n virtual module', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing/', - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('correctly imports the functions', async () => { - const response = await fixture.fetch('/virtual-module'); - assert.equal(response.status, 200); - const text = await response.text(); - assert.equal(text.includes("Virtual module doesn't break"), true); - assert.equal(text.includes('About: /pt/about'), true); - assert.equal(text.includes('About spanish: /spanish/about'), true); - assert.equal(text.includes('Spain path: spanish'), true); - assert.equal(text.includes('Preferred path: es'), true); - assert.equal(text.includes('About it: /it/about'), true); - }); - - describe('absolute URLs', () => { - let app; - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-subdomain/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('correctly renders the absolute URL', async () => { - let request = new Request('http://example.com/'); - let response = await app.render(request); - assert.equal(response.status, 200); - - let html = await response.text(); - let $ = cheerio.load(html); - - assert.equal($('body').text().includes("Virtual module doesn't break"), true); - assert.equal($('body').text().includes('Absolute URL pt: https://example.pt/about'), true); - assert.equal($('body').text().includes('Absolute URL it: http://it.example.com/'), true); - }); - }); -}); -describe('[DEV] i18n routing', () => { - describe('should render a page that stars with a locale but it is a page', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing/', - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('renders the page', async () => { - const response = await fixture.fetch('/endurance'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Endurance'), true); - }); - - it('should render the 404.astro file', async () => { - const response = await fixture.fetch('/do-not-exist'); - assert.equal(response.status, 404); - assert.match(await response.text(), /Custom 404/); - }); - - it('should return the correct locale on 404 page for non existing default locale page', async () => { - const response = await fixture.fetch('/es/nonexistent-page'); - assert.equal(response.status, 404); - assert.equal((await response.text()).includes('Current Locale: es'), true); - }); - - it('should return the correct locale on 404 page for non existing english locale page', async () => { - const response = await fixture.fetch('/en/nonexistent-page'); - assert.equal(response.status, 404); - assert.equal((await response.text()).includes('Current Locale: en'), true); - }); - }); - - describe('i18n routing', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('should render the en locale', async () => { - const response = await fixture.fetch('/en/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - - const response2 = await fixture.fetch('/en/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Hello world'), true); - }); - - it('should render localised page correctly', async () => { - const response = await fixture.fetch('/pt/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start'), true); - - const response2 = await fixture.fetch('/pt/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Hola mundo'), true); - }); - - it('should render localised page correctly when using path+codes', async () => { - const response = await fixture.fetch('/spanish/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Espanol'), true); - - const response2 = await fixture.fetch('/spanish/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Lo siento'), true); - }); - - it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => { - const response = await fixture.fetch('/it/start'); - assert.equal(response.status, 404); - const html = await response.text(); - assert.match(html, /Can't find the page you're looking for./); - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - const response = await fixture.fetch('/fr/start'); - assert.equal(response.status, 404); - const html = await response.text(); - assert.match(html, /Can't find the page you're looking for./); - }); - - it('should render the custom 404.astro when navigating non-existing routes ', async () => { - const response = await fixture.fetch('/does-not-exist'); - assert.equal(response.status, 404); - const html = await response.text(); - assert.match(html, /Can't find the page you're looking for./); - }); - }); - - describe('i18n routing, with base', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-base/', - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('should render the en locale', async () => { - const response = await fixture.fetch('/new-site/en/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Hello'), true); - - const response2 = await fixture.fetch('/new-site/en/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Hello world'), true); - }); - - it('should render localised page correctly', async () => { - const response = await fixture.fetch('/new-site/pt/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Hola'), true); - - const response2 = await fixture.fetch('/new-site/pt/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Hola mundo'), true); - }); - - it('should render localised page correctly when using path+codes', async () => { - const response = await fixture.fetch('/new-site/spanish/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Espanol'), true); - - const response2 = await fixture.fetch('/new-site/spanish/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Lo siento'), true); - }); - - it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => { - const response = await fixture.fetch('/new-site/it/start'); - assert.equal(response.status, 404); - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - const response = await fixture.fetch('/new-site/fr/start'); - assert.equal(response.status, 404); - }); - }); - - describe('i18n routing with routing strategy [prefix-other-locales]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-other-locales/', - i18n: { - defaultLocale: 'en', - locales: [ - 'en', - 'pt', - 'it', - { - path: 'spanish', - codes: ['es', 'es-AR'], - }, - ], - fallback: { - it: 'en', - spanish: 'en', - }, - }, - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('should render the default locale without prefix', async () => { - const response = await fixture.fetch('/new-site/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - - const response2 = await fixture.fetch('/new-site/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Hello world'), true); - }); - - it('should return 404 when route contains the default locale', async () => { - const response = await fixture.fetch('/new-site/en/start'); - assert.equal(response.status, 404); - - const response2 = await fixture.fetch('/new-site/en/blog/1'); - assert.equal(response2.status, 404); - }); - - it('should render localised page correctly', async () => { - const response = await fixture.fetch('/new-site/pt/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start'), true); - - const response2 = await fixture.fetch('/new-site/pt/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Hola mundo'), true); - }); - - it('should render localised page correctly when using path+codes', async () => { - const response = await fixture.fetch('/new-site/spanish/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Espanol'), true); - - const response2 = await fixture.fetch('/new-site/spanish/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Lo siento'), true); - }); - - it('should redirect to the english locale, which is the first fallback', async () => { - const response = await fixture.fetch('/new-site/it/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - const response = await fixture.fetch('/new-site/fr/start'); - assert.equal(response.status, 404); - }); - }); - - describe('i18n routing with routing strategy [prefix-other-locales], when `build.format` is `directory`', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-other-locales/', - i18n: { - defaultLocale: 'en', - locales: [ - 'en', - 'pt', - 'it', - { - path: 'spanish', - codes: ['es', 'es-AR'], - }, - ], - fallback: { - it: 'en', - spanish: 'en', - }, - }, - build: { - format: 'directory', - }, - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('should redirect to the english locale with trailing slash', async () => { - const response = await fixture.fetch('/new-site/it/start/'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - }); - }); - - describe('i18n routing with routing strategy [pathname-prefix-always-no-redirect]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - i18n: { - routing: { - prefixDefaultLocale: true, - redirectToDefaultLocale: false, - }, - }, - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('should NOT redirect to the index of the default locale', async () => { - const response = await fixture.fetch('/new-site'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('I am index'), true); - }); - - it('can render the 404.astro route on unmatched requests', async () => { - const response = await fixture.fetch('/xyz'); - assert.equal(response.status, 404); - const text = await response.text(); - assert.equal(text.includes("Can't find the page you're looking for."), true); - }); - }); - - describe('i18n routing with routing strategy [pathname-prefix-always]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('should redirect to the index of the default locale', async () => { - const response = await fixture.fetch('/new-site'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Hello'), true); - }); - - it('should not render the default locale without prefix', async () => { - const response = await fixture.fetch('/new-site/start'); - assert.equal(response.status, 404); - assert.equal((await response.text()).includes('Start'), false); - - const response2 = await fixture.fetch('/new-site/blog/1'); - assert.equal(response2.status, 404); - assert.equal((await response2.text()).includes('Hello world'), false); - }); - - it('should render the default locale with prefix', async () => { - const response = await fixture.fetch('/new-site/en/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - - const response2 = await fixture.fetch('/new-site/en/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Hello world'), true); - }); - - it('should render localised page correctly', async () => { - const response = await fixture.fetch('/new-site/pt/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start'), true); - - const response2 = await fixture.fetch('/new-site/pt/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Hola mundo'), true); - }); - - it('should render localised page correctly when using path+codes', async () => { - const response = await fixture.fetch('/new-site/spanish/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Espanol'), true); - - const response2 = await fixture.fetch('/new-site/spanish/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Lo siento'), true); - }); - - it('should not redirect to the english locale', async () => { - const response = await fixture.fetch('/new-site/it/start'); - assert.equal(response.status, 404); - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - const response = await fixture.fetch('/new-site/fr/start'); - assert.equal(response.status, 404); - }); - }); - - describe('[trailingSlash: always]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - trailingSlash: 'always', - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('should redirect to the index of the default locale', async () => { - const response = await fixture.fetch('/new-site/'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Hello'), true); - }); - }); - - describe('i18n routing fallback', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback/', - i18n: { - defaultLocale: 'en', - locales: [ - 'en', - 'pt', - 'it', - { - path: 'spanish', - codes: ['es', 'es-AR'], - }, - ], - fallback: { - it: 'en', - spanish: 'en', - }, - routing: { - prefixDefaultLocale: false, - }, - }, - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('should render the default locale without prefix', async () => { - const response = await fixture.fetch('/new-site/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - - const response2 = await fixture.fetch('/new-site/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Hello world'), true); - }); - - it('should render localised page correctly', async () => { - const response = await fixture.fetch('/new-site/pt/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start'), true); - - const response2 = await fixture.fetch('/new-site/pt/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Hola mundo'), true); - }); - - it('should render localised page correctly when using path+codes', async () => { - const response = await fixture.fetch('/new-site/spanish/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - - const response2 = await fixture.fetch('/new-site/spanish/blog/1'); - assert.equal(response2.status, 200); - assert.equal((await response2.text()).includes('Hello world'), true); - }); - - it('should redirect to the english locale, which is the first fallback', async () => { - const response = await fixture.fetch('/new-site/it/start'); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - const response = await fixture.fetch('/new-site/fr/start'); - assert.equal(response.status, 404); - }); - }); -}); -describe('[SSG] i18n routing', () => { - describe('i18n routing', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - }); - await fixture.build(); - }); - - it('should render the en locale', async () => { - let html = await fixture.readFile('/en/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Start'), true); - - html = await fixture.readFile('/en/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Hello world'), true); - }); - - it('should render localised page correctly', async () => { - let html = await fixture.readFile('/pt/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Oi essa e start'), true); - - html = await fixture.readFile('/pt/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Hola mundo'), true); - }); - - it('should render localised page correctly when it has codes+path', async () => { - let html = await fixture.readFile('/spanish/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Espanol'), true); - - html = await fixture.readFile('/spanish/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Lo siento'), true); - }); - - it('should create a custom 404.html and 505.html', async () => { - let html = await fixture.readFile('/404.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes("Can't find the page you're looking for."), true); - - html = await fixture.readFile('/500.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Unexpected error.'), true); - }); - - it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => { - try { - await fixture.readFile('/it/start/index.html'); - // It should throw before reaching this point - assert.fail('The file should not exist'); - } catch (e) { - assert.equal(e.message.includes('ENOENT'), true); - } - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - try { - await fixture.readFile('/fr/start/index.html'); - // It should throw before reaching this point - assert.fail('The file should not exist'); - } catch (e) { - assert.equal(e.message.includes('ENOENT'), true); - } - }); - }); - - describe('i18n routing, with base', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-base/', - }); - await fixture.build(); - }); - - it('should render the en locale', async () => { - let html = await fixture.readFile('/en/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Hello'), true); - - html = await fixture.readFile('/en/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Hello world'), true); - }); - - it('should render localised page correctly', async () => { - let html = await fixture.readFile('/pt/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Hola'), true); - - html = await fixture.readFile('/pt/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Hola mundo'), true); - }); - - it('should render localised page correctly when it has codes+path', async () => { - let html = await fixture.readFile('/spanish/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Espanol'), true); - - html = await fixture.readFile('/spanish/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Lo siento'), true); - }); - - it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => { - try { - await fixture.readFile('/it/start/index.html'); - // It should throw before reaching this point - assert.fail('The file should not exist'); - } catch (e) { - assert.equal(e.message.includes('ENOENT'), true); - } - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - try { - await fixture.readFile('/fr/start/index.html'); - // It should throw before reaching this point - assert.fail('The file should not exist'); - } catch (e) { - assert.equal(e.message.includes('ENOENT'), true); - } - }); - }); - - describe('i18n routing with routing strategy [prefix-other-locales]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-other-locales/', - }); - await fixture.build(); - }); - - it('should render the en locale', async () => { - let html = await fixture.readFile('/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Start'), true); - - html = await fixture.readFile('/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Hello world'), true); - }); - - it('should return 404 when route contains the default locale', async () => { - try { - await fixture.readFile('/start/en/index.html'); - // It should throw before reaching this point - assert.fail('The file should not exist'); - } catch (e) { - assert.equal(e.message.includes('ENOENT'), true); - } - }); - - it('should render localised page correctly', async () => { - let html = await fixture.readFile('/pt/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Oi essa e start'), true); - - html = await fixture.readFile('/pt/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Hola mundo'), true); - }); - - it('should render localised page correctly when it has codes+path', async () => { - let html = await fixture.readFile('/spanish/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Espanol'), true); - - html = await fixture.readFile('/spanish/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Lo siento'), true); - }); - - it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => { - try { - await fixture.readFile('/it/start/index.html'); - // It should throw before reaching this point - assert.fail('The file should not exist'); - } catch (e) { - assert.equal(e.message.includes('ENOENT'), true); - } - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - try { - await fixture.readFile('/fr/start/index.html'); - // It should throw before reaching this point - assert.fail('The file should not exist'); - } catch (e) { - assert.equal(e.message.includes('ENOENT'), true); - } - }); - }); - - describe('i18n routing with routing strategy [pathname-prefix-always-no-redirect]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - i18n: { - routing: { - prefixDefaultLocale: true, - redirectToDefaultLocale: false, - }, - }, - }); - await fixture.build(); - }); - - it('should NOT redirect to the index of the default locale', async () => { - const html = await fixture.readFile('/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('I am index'), true); - }); - }); - - describe('i18n routing with routing strategy [pathname-prefix-always]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - }); - await fixture.build(); - }); - - it('should redirect to the index of the default locale', async () => { - const html = await fixture.readFile('/index.html'); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('url=/new-site/en'), true); - }); - - it('should render the en locale', async () => { - let html = await fixture.readFile('/en/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Start'), true); - - html = await fixture.readFile('/en/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Hello world'), true); - }); - - it('should render localised page correctly', async () => { - let html = await fixture.readFile('/pt/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Oi essa e start'), true); - - html = await fixture.readFile('/pt/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Hola mundo'), true); - }); - - it('should render localised page correctly when it has codes+path', async () => { - let html = await fixture.readFile('/spanish/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Espanol'), true); - - html = await fixture.readFile('/spanish/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Lo siento'), true); - }); - - it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => { - try { - await fixture.readFile('/it/start/index.html'); - // It should throw before reaching this point - assert.fail('The file should not exist'); - } catch (e) { - assert.equal(e.message.includes('ENOENT'), true); - } - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - try { - await fixture.readFile('/fr/start/index.html'); - // It should throw before reaching this point - assert.fail('The file should not exist'); - } catch (e) { - assert.equal(e.message.includes('ENOENT'), true); - } - }); - - describe('[trailingSlash: always]', () => { - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - trailingSlash: 'always', - }); - }); - - it('should redirect to the index of the default locale', async () => { - const html = await fixture.readFile('/index.html'); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('url=/new-site/en'), true); - }); - }); - - describe('when `build.format` is `directory`', () => { - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - build: { - format: 'directory', - }, - }); - await fixture.build(); - }); - - it('should redirect to the index of the default locale', async () => { - const html = await fixture.readFile('/index.html'); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('url=/new-site/en/'), true); - }); - }); - }); - - describe('i18n routing with fallback', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback/', - i18n: { - defaultLocale: 'en', - locales: [ - 'en', - 'pt', - 'it', - { - path: 'spanish', - codes: ['es', 'es-AR'], - }, - ], - fallback: { - it: 'en', - spanish: 'en', - }, - }, - }); - await fixture.build(); - }); - - it('should render the en locale', async () => { - let html = await fixture.readFile('/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Start'), true); - - html = await fixture.readFile('/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Hello world'), true); - }); - - it('should render localised page correctly', async () => { - let html = await fixture.readFile('/pt/start/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Oi essa e start: pt'), true); - - html = await fixture.readFile('/pt/blog/1/index.html'); - $ = cheerio.load(html); - assert.equal($('body').text().includes('Hola mundo'), true); - }); - - it('should redirect to the english locale correctly when it has codes+path', async () => { - let html = await fixture.readFile('/spanish/start/index.html'); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('url=/new-site/start'), true); - html = await fixture.readFile('/spanish/index.html'); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('url=/new-site'), true); - }); - - it('should redirect to the english locale, which is the first fallback', async () => { - let html = await fixture.readFile('/it/start/index.html'); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('url=/new-site/start'), true); - html = await fixture.readFile('/it/index.html'); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('url=/new-site'), true); - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - try { - await fixture.readFile('/fr/start/index.html'); - // It should throw before reaching this point - assert.fail('The file should not exist'); - } catch (e) { - assert.equal(e.message.includes('ENOENT'), true); - } - }); - - it('should render the page with client scripts', async () => { - let html = await fixture.readFile('/index.html'); - let $ = cheerio.load(html); - assert.equal($('script').text().includes('console.log("this is a script")'), true); - }); - - describe('with localised index pages', () => { - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback-index/', - i18n: { - defaultLocale: 'en', - locales: [ - 'en', - 'pt', - 'it', - { - path: 'spanish', - codes: ['es', 'es-AR'], - }, - ], - fallback: { - it: 'en', - spanish: 'en', - }, - }, - }); - await fixture.build(); - }); - - it('should render correctly', async () => { - let html = await fixture.readFile('/pt/index.html'); - let $ = cheerio.load(html); - assert.equal($('body').text().includes('Oi essa e index'), true); - }); - }); - }); - - describe('i18n routing with fallback and [pathname-prefix-always]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - i18n: { - defaultLocale: 'en', - locales: ['en', 'pt', 'it'], - fallback: { - it: 'en', - }, - routing: { - prefixDefaultLocale: true, - }, - }, - }); - await fixture.build(); - }); - - it('should render the en locale', async () => { - let html = await fixture.readFile('/it/start/index.html'); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('url=/new-site/en/start'), true); - html = await fixture.readFile('/it/index.html'); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('url=/new-site/en'), true); - }); - }); - - describe('i18n routing with fallback and redirect', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback/', - redirects: { - '/': '/en', - }, - i18n: { - defaultLocale: 'en', - locales: ['en', 'pt', 'it'], - fallback: { - it: 'en', - }, - }, - }); - await fixture.build(); - }); - - it('should render the en locale', async () => { - let html = await fixture.readFile('/index.html'); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('Redirecting to: /en'), true); - }); - }); - - describe('i18n routing with fallback rewrite from dynamic route', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback-rewrite/', - }); - await fixture.build(); - }); - - it('should rewrite dynamic fallback route', async () => { - let html = await fixture.readFile('/es/slug-1/index.html'); - assert.equal(html.includes('slug-1'), true); - }); - it('should rewrite dynamic fallback route with rest parameter', async () => { - let html = await fixture.readFile('/es/page-1/index.html'); - assert.equal(html.includes('page-1'), true); - }); - it('should rewrite dynamic fallback route with rest parameter and different depths', async () => { - let html = await fixture.readFile('/es/page/page-1/index.html'); - assert.equal(html.includes('page/page-1'), true); - }); - it('should rewrite a fallback route when a dynamic spread route exists in the locale folder', async () => { - let html = await fixture.readFile('/es/test/index.html'); - assert.equal(html.includes('test'), true); - }); - }); - - describe('i18n routing with fallback rewrite from dynamic route and config.build.format: file', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback-rewrite/', - build: { - format: 'file', - }, - }); - await fixture.build(); - }); - - it('should rewrite dynamic fallback route', async () => { - let html = await fixture.readFile('/es/slug-1.html'); - assert.equal(html.includes('slug-1'), true); - }); - it('should rewrite dynamic fallback route with rest parameter', async () => { - let html = await fixture.readFile('/es/page-1.html'); - assert.equal(html.includes('page-1'), true); - }); - it('should rewrite dynamic fallback route with rest parameter and different depths', async () => { - let html = await fixture.readFile('/es/page/page-1.html'); - assert.equal(html.includes('page/page-1'), true); - }); - it('should rewrite a fallback route when a dynamic spread route exists in the locale folder', async () => { - let html = await fixture.readFile('/es/test.html'); - assert.equal(html.includes('test'), true); - }); - }); - - describe('i18n routing with fallback rewrite from dynamic route with base', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback-rewrite/', - base: '/base', - }); - await fixture.build(); - }); - - it('should rewrite dynamic fallback route with base', async () => { - let html = await fixture.readFile('/es/slug-1/index.html'); - assert.equal(html.includes('slug-1'), true); - }); - it('should rewrite dynamic fallback route with rest parameter and base', async () => { - let html = await fixture.readFile('/es/page-1/index.html'); - assert.equal(html.includes('page-1'), true); - }); - it('should rewrite dynamic fallback route with rest parameter and different depths and base', async () => { - let html = await fixture.readFile('/es/page/page-1/index.html'); - assert.equal(html.includes('page/page-1'), true); - }); - }); - - describe('i18n routing with fallback and trailing slash', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback/', - trailingSlash: 'always', - build: { - format: 'directory', - }, - i18n: { - defaultLocale: 'en', - locales: ['en', 'pt', 'it'], - fallback: { - it: 'en', - }, - routing: { - prefixDefaultLocale: false, - }, - }, - }); - await fixture.build(); - }); - - it('should render the en locale', async () => { - let html = await fixture.readFile('/it/index.html'); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('Redirecting to: /new-site/'), true); - }); - }); - - describe('should render a page that stars with a locale but it is a page', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing/', - }); - await fixture.build(); - }); - - it('renders the page', async () => { - const html = await fixture.readFile('/endurance/index.html'); - assert.equal(html.includes('Endurance'), true); - }); - }); - - describe('i18n routing with fallback rewrite with locale-like filenames', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback-rewrite-filename/', - }); - await fixture.build(); - }); - - it('should generate fallback files for pages with without locale-like filenames', async () => { - let html = await fixture.readFile('/norway/index.html'); - assert.equal(html.includes('Norway'), true); - - html = await fixture.readFile('/de/norway/index.html'); - assert.equal(html.includes('Norway'), true); - }); - - it('should generate fallback files for pages with locale-like filenames starting with locale', async () => { - let html = await fixture.readFile('/denmark/index.html'); - assert.equal(html.includes('Denmark'), true); - - html = await fixture.readFile('/de/denmark/index.html'); - assert.equal(html.includes('Denmark'), true); - }); - - it('should generate fallback files for pages with locale-like filenames containing locale', async () => { - let html = await fixture.readFile('/index.html'); - assert.equal(html.includes('Index'), true); - - html = await fixture.readFile('/de/index.html'); - assert.equal(html.includes('Index'), true); - }); - - it('should generate fallback files for pages in subdirectories with locale-like name starting with locale', async () => { - let html = await fixture.readFile('/destinations/denmark/index.html'); - assert.equal(html.includes('Destination: Denmark'), true); - - html = await fixture.readFile('/de/destinations/denmark/index.html'); - assert.equal(html.includes('Destination: Denmark'), true); - - html = await fixture.readFile('/destinations/index.html'); - assert.equal(html.includes('Destination: Index'), true); - - html = await fixture.readFile('/de/destinations/index.html'); - assert.equal(html.includes('Destination: Index'), true); - - html = await fixture.readFile('/destinations/norway/index.html'); - assert.equal(html.includes('Destination: Norway'), true); - - html = await fixture.readFile('/de/destinations/norway/index.html'); - assert.equal(html.includes('Destination: Norway'), true); - }); - - it('should generate fallback files for pages in subdirectories with locale-like name containing locale', async () => { - let html = await fixture.readFile('/trade/denmark/index.html'); - assert.equal(html.includes('Trade: Denmark'), true); - - html = await fixture.readFile('/de/trade/denmark/index.html'); - assert.equal(html.includes('Trade: Denmark'), true); - - html = await fixture.readFile('/trade/index.html'); - assert.equal(html.includes('Trade: Index'), true); - - html = await fixture.readFile('/de/trade/index.html'); - assert.equal(html.includes('Trade: Index'), true); - - html = await fixture.readFile('/trade/norway/index.html'); - assert.equal(html.includes('Trade: Norway'), true); - - html = await fixture.readFile('/de/trade/norway/index.html'); - assert.equal(html.includes('Trade: Norway'), true); - }); - }); - - describe('current locale', () => { - describe('with [prefix-other-locales]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing/', - }); - await fixture.build(); - }); - - it('should return the default locale', async () => { - let html = await fixture.readFile('/current-locale/index.html'); - assert.equal(html.includes('Current Locale: es'), true); - }); - - it('should return the default locale when rendering a route with spread operator', async () => { - const html = await fixture.readFile('/blog/es/index.html'); - assert.equal(html.includes('Current Locale: es'), true); - }); - - it('should return the default locale of the current URL', async () => { - const html = await fixture.readFile('/pt/start/index.html'); - assert.equal(html.includes('Current Locale: pt'), true); - }); - - it('should return the default locale when a route is dynamic', async () => { - const html = await fixture.readFile('/dynamic/lorem/index.html'); - assert.equal(html.includes('Current Locale: es'), true); - }); - - it('should returns the correct locale when requesting a locale via path', async () => { - const html = await fixture.readFile('/spanish/index.html'); - assert.equal(html.includes('Current Locale: es'), true); - }); - }); - - describe('with [pathname-prefix-always]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - }); - await fixture.build(); - }); - - it('should return the locale of the current URL (en)', async () => { - const html = await fixture.readFile('/en/start/index.html'); - assert.equal(html.includes('Current Locale: en'), true); - }); - - it('should return the locale of the current URL (pt)', async () => { - const html = await fixture.readFile('/pt/start/index.html'); - assert.equal(html.includes('Current Locale: pt'), true); - }); - }); - - describe('with dynamic paths', async () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing/', - }); - devServer = await fixture.startDevServer(); - }); - - afterEach(async () => { - devServer.stop(); - }); - - it('should return the correct current locale', async () => { - let html = await fixture.fetch('/en').then((r) => r.text()); - assert.match(html, /en/); - html = await fixture.fetch('/ru').then((r) => r.text()); - assert.match(html, /ru/); - }); - }); - }); - - describe('[SSG] i18n routing when `build.format` is `file`, locales array contains objects, and locale indexes use getStaticPaths', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-locale-index-format-file/', - outDir: './dist/i18n-objects', - i18n: { - defaultLocale: 'en-us', - locales: [ - { - path: 'en-us', - codes: ['en-US'], - }, - { - path: 'es-mx', - codes: ['es-MX'], - }, - { - path: 'fr-fr', - codes: ['fr-FR'], - }, - ], - routing: { - prefixDefaultLocale: true, - redirectToDefaultLocale: false, - }, - }, - }); - await fixture.build(); - }); - - it('should return the locale code of the current URL (en-US)', async () => { - const html = await fixture.readFile('/en-us.html'); - assert.equal(html.includes('currentLocale: en-US'), true); - }); - - it('should return the locale code of the current URL (es-MX)', async () => { - const html = await fixture.readFile('/es-mx.html'); - assert.equal(html.includes('currentLocale: es-MX'), true); - }); - - it('should return the locale code of the current URL (fr-FR)', async () => { - const html = await fixture.readFile('/fr-fr.html'); - assert.equal(html.includes('currentLocale: fr-FR'), true); - }); - }); - - describe('[SSG] i18n routing when `build.format` is `file`, locales array contains strings, and locale indexes use getStaticPaths', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-locale-index-format-file/', - outDir: './dist/i18n-strings', - i18n: { - defaultLocale: 'en-us', - locales: ['en-us', 'es-mx', 'fr-fr'], - routing: { - prefixDefaultLocale: true, - redirectToDefaultLocale: false, - }, - }, - }); - await fixture.build(); - }); - - it('should return the locale of the current URL (en-us)', async () => { - const html = await fixture.readFile('/en-us.html'); - assert.equal(html.includes('currentLocale: en-us'), true); - }); - - it('should return the locale of the current URL (es-mx)', async () => { - const html = await fixture.readFile('/es-mx.html'); - assert.equal(html.includes('currentLocale: es-mx'), true); - }); - - it('should return the locale of the current URL (fr-fr)', async () => { - const html = await fixture.readFile('/fr-fr.html'); - assert.equal(html.includes('currentLocale: fr-fr'), true); - }); - }); - - describe('[SSR] i18n routing', () => { - let app; - - describe('should render a page that stars with a locale but it is a page', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('renders the page', async () => { - let request = new Request('http://example.com/endurance'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Endurance'), true); - }); - - it('should return the correct locale on 404 page for non existing default locale page', async () => { - let request = new Request('http://example.com/es/nonexistent-page'); - let response = await app.render(request); - assert.equal(response.status, 404); - assert.equal((await response.text()).includes('Current Locale: es'), true); - }); - - it('should return the correct locale on 404 page for non existing english locale page', async () => { - let request = new Request('http://example.com/en/nonexistent-page'); - let response = await app.render(request); - assert.equal(response.status, 404); - assert.equal((await response.text()).includes('Current Locale: en'), true); - }); - }); - - describe('default', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should redirect to the index of the default locale', async () => { - let request = new Request('http://example.com/new-site'); - let response = await app.render(request); - assert.equal(response.status, 302); - assert.equal(response.headers.get('location'), '/new-site/en/'); - }); - - it('should render the en locale', async () => { - let request = new Request('http://example.com/en/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - }); - - it('should render localised page correctly', async () => { - let request = new Request('http://example.com/pt/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start'), true); - }); - - it('should render localised page correctly when locale has codes+path', async () => { - let request = new Request('http://example.com/spanish/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Espanol'), true); - }); - - it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => { - let request = new Request('http://example.com/it/start'); - let response = await app.render(request); - assert.equal(response.status, 404); - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - let request = new Request('http://example.com/fr/start'); - let response = await app.render(request); - assert.equal(response.status, 404); - }); - }); - - describe('with base', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should render the en locale', async () => { - let request = new Request('http://example.com/en/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - }); - - it('should render localised page correctly', async () => { - let request = new Request('http://example.com/new-site/pt/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start'), true); - }); - - it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => { - let request = new Request('http://example.com/new-site/it/start'); - let response = await app.render(request); - assert.equal(response.status, 404); - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - let request = new Request('http://example.com/new-site/fr/start'); - let response = await app.render(request); - assert.equal(response.status, 404); - }); - }); - - describe('i18n routing with routing strategy [prefix-other-locales]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-other-locales/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should render the en locale', async () => { - let request = new Request('http://example.com/new-site/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - }); - - it('should return 404 if route contains the default locale', async () => { - let request = new Request('http://example.com/new-site/en/start'); - let response = await app.render(request); - assert.equal(response.status, 404); - }); - - it('should render localised page correctly', async () => { - let request = new Request('http://example.com/new-site/pt/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start'), true); - }); - - it('should render localised page correctly when locale has codes+path', async () => { - let request = new Request('http://example.com/new-site/spanish/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Espanol'), true); - }); - - it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => { - let request = new Request('http://example.com/new-site/it/start'); - let response = await app.render(request); - assert.equal(response.status, 404); - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - let request = new Request('http://example.com/new-site/fr/start'); - let response = await app.render(request); - assert.equal(response.status, 404); - }); - }); - - describe('i18n routing with routing strategy [pathname-prefix-always-no-redirect]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - output: 'server', - outDir: './dist/pathname-prefix-always-no-redirect', - adapter: testAdapter(), - i18n: { - routing: { - prefixDefaultLocale: true, - redirectToDefaultLocale: false, - }, - }, - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should NOT redirect the index to the default locale', async () => { - let request = new Request('http://example.com/new-site'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('I am index'), true); - }); - - it('can render the 404.astro route on unmatched requests', async () => { - const request = new Request('http://example.com/xyz'); - const response = await app.render(request); - assert.equal(response.status, 404); - const text = await response.text(); - assert.equal(text.includes("Can't find the page you're looking for."), true); - }); - }); - - describe('i18n routing with routing strategy [pathname-prefix-always]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should redirect the index to the default locale', async () => { - let request = new Request('http://example.com/new-site'); - let response = await app.render(request); - assert.equal(response.status, 302); - assert.equal(response.headers.get('location'), '/new-site/en/'); - }); - - it('should render the en locale', async () => { - let request = new Request('http://example.com/new-site/en/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - }); - - it('should render localised page correctly', async () => { - let request = new Request('http://example.com/pt/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start'), true); - }); - - it('should render localised page correctly when locale has codes+path', async () => { - let request = new Request('http://example.com/spanish/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Espanol'), true); - }); - - it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => { - let request = new Request('http://example.com/it/start'); - let response = await app.render(request); - assert.equal(response.status, 404); - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - let request = new Request('http://example.com/fr/start'); - let response = await app.render(request); - assert.equal(response.status, 404); - }); - - describe('[trailingSlash: always]', () => { - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - output: 'server', - adapter: testAdapter(), - trailingSlash: 'always', - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should redirect to the index of the default locale', async () => { - let request = new Request('http://example.com/new-site/'); - let response = await app.render(request); - assert.equal(response.status, 302); - assert.equal(response.headers.get('location'), '/new-site/en/'); - }); - }); - - describe('when `build.format` is `directory`', () => { - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - output: 'server', - adapter: testAdapter(), - build: { - format: 'directory', - }, - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should redirect to the index of the default locale', async () => { - let request = new Request('http://example.com/new-site/'); - let response = await app.render(request); - assert.equal(response.status, 302); - assert.equal(response.headers.get('location'), '/new-site/en/'); - }); - }); - }); - - describe('with fallback', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback/', - output: 'server', - adapter: testAdapter(), - i18n: { - defaultLocale: 'en', - locales: [ - 'en', - 'pt', - 'it', - { - codes: ['es', 'es-AR'], - path: 'spanish', - }, - ], - fallback: { - it: 'en', - spanish: 'en', - }, - }, - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should render the en locale', async () => { - let request = new Request('http://example.com/new-site/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Start'), true); - }); - - it('should render localised page correctly', async () => { - let request = new Request('http://example.com/new-site/pt/start'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start'), true); - }); - - it('should redirect to the english locale, which is the first fallback', async () => { - let request = new Request('http://example.com/new-site/it/start'); - let response = await app.render(request); - assert.equal(response.status, 302); - assert.equal(response.headers.get('location'), '/new-site/start'); - }); - - it('should redirect to the english locale when locale has codes+path', async () => { - let request = new Request('http://example.com/new-site/spanish/start'); - let response = await app.render(request); - assert.equal(response.status, 302); - assert.equal(response.headers.get('location'), '/new-site/start'); - }); - - it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => { - let request = new Request('http://example.com/new-site/fr/start'); - let response = await app.render(request); - assert.equal(response.status, 404); - }); - - it('should pass search to render when using requested locale', async () => { - let request = new Request('http://example.com/new-site/pt/start?search=1'); - let response = await app.render(request); - assert.equal(response.status, 200); - const text = await response.text(); - assert.match(text, /Oi essa e start/); - assert.match(text, /search=1/); - }); - - it('should include search on the redirect when using fallback', async () => { - let request = new Request('http://example.com/new-site/it/start?search=1'); - let response = await app.render(request); - assert.equal(response.status, 302); - assert.equal(response.headers.get('location'), '/new-site/start?search=1'); - }); - - describe('with routing strategy [pathname-prefix-always]', () => { - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback/', - output: 'server', - adapter: testAdapter(), - i18n: { - defaultLocale: 'en', - locales: ['en', 'pt', 'it'], - fallback: { - it: 'en', - }, - routing: { - prefixDefaultLocale: false, - }, - }, - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should redirect to the english locale, which is the first fallback', async () => { - let request = new Request('http://example.com/new-site/it/start'); - let response = await app.render(request); - assert.equal(response.status, 302); - assert.equal(response.headers.get('location'), '/new-site/start'); - }); - }); - }); - - describe('preferred locale', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should not render the locale when the value is *', async () => { - let request = new Request('http://example.com/preferred-locale', { - headers: { - 'Accept-Language': '*', - }, - }); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Locale: none'), true); - }); - - it('should render the locale pt', async () => { - let request = new Request('http://example.com/preferred-locale', { - headers: { - 'Accept-Language': 'pt', - }, - }); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Locale: pt'), true); - }); - - it('should render empty locales', async () => { - let request = new Request('http://example.com/preferred-locale', { - headers: { - 'Accept-Language': 'fr;q=0.1,fr-AU;q=0.9', - }, - }); - let response = await app.render(request); - const text = await response.text(); - assert.equal(response.status, 200); - assert.equal(text.includes('Locale: none'), true); - assert.equal(text.includes('Locale list: empty'), true); - }); - - it('should render none as preferred locale, but have a list of locales that correspond to the initial locales', async () => { - let request = new Request('http://example.com/preferred-locale', { - headers: { - 'Accept-Language': '*', - }, - }); - let response = await app.render(request); - const text = await response.text(); - assert.equal(response.status, 200); - assert.equal(text.includes('Locale: none'), true); - assert.equal(text.includes('Locale list: en, pt, it'), true); - }); - - it('should render the preferred locale when a locale is configured with codes', async () => { - let request = new Request('http://example.com/preferred-locale', { - headers: { - 'Accept-Language': 'es-SP;q=0.9,es;q=0.8,en-US;q=0.7,en;q=0.6', - }, - }); - let response = await app.render(request); - const text = await response.text(); - assert.equal(response.status, 200); - assert.equal(text.includes('Locale: es-SP'), true); - assert.equal(text.includes('Locale list: es-SP, es, en'), true); - }); - - describe('in case the configured locales use underscores', () => { - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing/', - output: 'server', - outDir: './dist/locales-underscore', - adapter: testAdapter(), - i18n: { - defaultLocale: 'en', - locales: ['en_AU', 'pt_BR', 'es_US'], - }, - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('they should be still considered when parsing the Accept-Language header', async () => { - let request = new Request('http://example.com/preferred-locale', { - headers: { - 'Accept-Language': 'en-AU;q=0.1,pt-BR;q=0.9', - }, - }); - let response = await app.render(request); - const text = await response.text(); - assert.equal(response.status, 200); - assert.equal(text.includes('Locale: pt_BR'), true); - assert.equal(text.includes('Locale list: pt_BR, en_AU'), true); - }); - }); - - describe('in case the configured locales are granular', () => { - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('they should be still considered when parsing the Accept-Language header', async () => { - let request = new Request('http://example.com/preferred-locale', { - headers: { - 'Accept-Language': 'en-AU;q=0.1,es;q=0.9', - }, - }); - let response = await app.render(request); - const text = await response.text(); - assert.equal(response.status, 200); - assert.equal(text.includes('Locale: es'), true); - assert.equal(text.includes('Locale list: es'), true); - }); - }); - }); - - describe('current locale', () => { - describe('with [prefix-other-locales]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should return the default locale', async () => { - let request = new Request('http://example.com/current-locale', {}); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Current Locale: es'), true); - }); - - it('should return the default locale when rendering a route with spread operator', async () => { - let request = new Request('http://example.com/blog/es', {}); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Current Locale: es'), true); - }); - - it('should return the default locale of the current URL', async () => { - let request = new Request('http://example.com/pt/start', {}); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Current Locale: pt'), true); - }); - - it('should return the default locale when a route is dynamic', async () => { - let request = new Request('http://example.com/dynamic/lorem', {}); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Current Locale: es'), true); - }); - }); - - describe('with [pathname-prefix-always]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should return the locale of the current URL (en)', async () => { - let request = new Request('http://example.com/en/start', {}); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Current Locale: en'), true); - }); - - it('should return the locale of the current URL (pt)', async () => { - let request = new Request('http://example.com/pt/start', {}); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Current Locale: pt'), true); - }); - }); - }); - - describe('i18n routing should work with hybrid rendering', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - output: 'static', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('and render the index page, which is static', async () => { - const html = await fixture.readFile('/client/index.html'); - assert.equal(html.includes('http-equiv="refresh'), true); - assert.equal(html.includes('url=/new-site/en'), true); - }); - }); - }); - - describe('i18n routing does not break assets and endpoints', () => { - describe('assets', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/core-image-base/', - i18n: { - defaultLocale: 'en', - locales: ['en', 'es'], - }, - base: '/blog', - }); - await fixture.build(); - }); - - it('should render the image', async () => { - const html = await fixture.readFile('/index.html'); - const $ = cheerio.load(html); - const src = $('#local img').attr('src'); - assert.equal(src.length > 0, true); - assert.equal(src.startsWith('/blog'), true); - }); - }); - - describe('endpoint', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - let app; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-always/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should return the assert.equaled data', async () => { - let request = new Request('http://example.com/new-site/test.json'); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('lorem'), true); - }); - }); - - describe('i18n routing with routing strategy [subdomain]', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let app; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-subdomain/', - output: 'server', - adapter: testAdapter(), - security: { - allowedDomains: [ - { hostname: 'example.pt' }, - { hostname: 'it.example.com' }, - { hostname: 'example.com' }, - ], - }, - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should render the en locale when X-Forwarded-Host header is passed', async () => { - let request = new Request('http://example.pt/start', { - headers: { - 'X-Forwarded-Host': 'example.pt', - 'X-Forwarded-Proto': 'https', - }, - }); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start\n'), true); - }); - - it('should render the en locale when Host header is passed', async () => { - let request = new Request('http://example.pt/start', { - headers: { - Host: 'example.pt', - 'X-Forwarded-Proto': 'https', - }, - }); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start\n'), true); - }); - - it('should render the en locale when Host header is passed and it has the port', async () => { - let request = new Request('http://example.pt/start', { - headers: { - Host: 'example.pt:8080', - 'X-Forwarded-Proto': 'https', - }, - }); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start\n'), true); - }); - - it('should render when the protocol header we fallback to the one of the host', async () => { - let request = new Request('https://example.pt/start', { - headers: { - Host: 'example.pt', - }, - }); - let response = await app.render(request); - assert.equal(response.status, 200); - assert.equal((await response.text()).includes('Oi essa e start\n'), true); - }); - }); - }); - - describe('SSR fallback from missing locale index to default locale index', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let app; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-prefix-other-locales/', - output: 'server', - outDir: './dist/missing-locale-to-default', - adapter: testAdapter(), - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr'], - routing: { - prefixDefaultLocale: false, - }, - fallback: { - fr: 'en', - }, - }, - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should correctly redirect', async () => { - let request = new Request('http://example.com/fr'); - let response = await app.render(request); - assert.equal(response.status, 302); - assert.equal(response.headers.get('location'), '/'); - }); - }); - - describe('Fallback rewrite dev server', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback/', - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr', 'es', 'it', 'pt'], - routing: { - prefixDefaultLocale: false, - fallbackType: 'rewrite', - }, - fallback: { - fr: 'en', - it: 'en', - es: 'pt', - }, - }, - }); - devServer = await fixture.startDevServer(); - }); - after(async () => { - devServer.stop(); - }); - - it('should correctly rewrite to en', async () => { - const html = await fixture.fetch('/fr').then((res) => res.text()); - assert.match(html, /Hello/); - assert.match(html, /locale - fr/); - // assert.fail() - }); - - it('should render fallback locale paths with path parameters correctly (fr)', async () => { - let response = await fixture.fetch('/fr/blog/1'); - assert.equal(response.status, 200); - const text = await response.text(); - assert.match(text, /Hello world/); - }); - - it('should render fallback locale paths with path parameters correctly (es)', async () => { - let response = await fixture.fetch('/es/blog/1'); - assert.equal(response.status, 200); - const text = await response.text(); - assert.match(text, /Hola mundo/); - }); - - it('should render fallback locale paths with query parameters correctly (it)', async () => { - let response = await fixture.fetch('/it/blog/1'); - assert.equal(response.status, 200); - const text = await response.text(); - assert.match(text, /Hello world/); - }); - }); - - describe('Fallback rewrite SSG', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback/', - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr', 'es', 'it', 'pt'], - routing: { - prefixDefaultLocale: false, - fallbackType: 'rewrite', - }, - fallback: { - fr: 'en', - it: 'en', - es: 'pt', - }, - }, - }); - await fixture.build(); - // app = await fixture.loadTestAdapterApp(); - }); - - it('should correctly rewrite to en', async () => { - const html = await fixture.readFile('/fr/index.html'); - assert.match(html, /Hello/); - assert.match(html, /locale - fr/); - // assert.fail() - }); - - it('should render fallback locale paths with path parameters correctly (fr)', async () => { - const html = await fixture.readFile('/fr/blog/1/index.html'); - assert.match(html, /Hello world/); - }); - - it('should render fallback locale paths with path parameters correctly (es)', async () => { - const html = await fixture.readFile('/es/blog/1/index.html'); - assert.match(html, /Hola mundo/); - }); - - it('should render fallback locale paths with query parameters correctly (it)', async () => { - const html = await fixture.readFile('/it/blog/1/index.html'); - assert.match(html, /Hello world/); - }); - }); - - describe('Fallback rewrite SSR', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let app; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback/', - output: 'server', - outDir: './dist/i18n-routing-fallback', - adapter: testAdapter(), - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr', 'es', 'it', 'pt'], - routing: { - prefixDefaultLocale: false, - fallbackType: 'rewrite', - }, - fallback: { - fr: 'en', - it: 'en', - es: 'pt', - }, - }, - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should correctly rewrite to en', async () => { - const request = new Request('http://example.com/fr'); - const response = await app.render(request); - assert.equal(response.status, 200); - const html = await response.text(); - assert.match(html, /locale - fr/); - assert.match(html, /Hello/); - }); - - it('should render fallback locale paths with path parameters correctly (fr)', async () => { - let request = new Request('http://example.com/new-site/fr/blog/1'); - let response = await app.render(request); - assert.equal(response.status, 200); - const text = await response.text(); - assert.match(text, /Hello world/); - }); - - it('should render fallback locale paths with path parameters correctly (es)', async () => { - let request = new Request('http://example.com/new-site/es/blog/1'); - let response = await app.render(request); - assert.equal(response.status, 200); - const text = await response.text(); - assert.match(text, /Hola mundo/); - }); - - it('should render fallback locale paths with query parameters correctly (it)', async () => { - let request = new Request('http://example.com/new-site/it/blog/1'); - let response = await app.render(request); - assert.equal(response.status, 200); - const text = await response.text(); - assert.match(text, /Hello world/); - }); - }); - - describe('Fallback rewrite hybrid', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let app; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-routing-fallback-rewrite-hybrid/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - app = await fixture.loadTestAdapterApp(); - }); - - it('should correctly prerender es index', async () => { - const html = await fixture.readFile('/client/es/index.html'); - assert.match(html, /ES index/); - }); - - it('should correctly prerender fallback locale paths with path parameters', async () => { - const html = await fixture.readFile('/client/es/slug-1/index.html'); - assert.match(html, /slug-1 - es/); - }); - - it('should rewrite fallback locale paths for ssr pages', async () => { - let request = new Request('http://example.com/es/about'); - let response = await app.render(request); - assert.equal(response.status, 200); - const text = await response.text(); - assert.match(text, /about - es/); - }); - }); - - describe('i18n routing with server islands', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-server-island/', - adapter: testAdapter(), - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('should render the en locale with server island', async () => { - const res = await fixture.fetch('/en/island'); - const html = await res.text(); - const $ = cheerio.load(html); - const serverIslandScript = $('script[data-island-id]'); - assert.equal(serverIslandScript.length, 1, 'has the island script'); - }); - }); - - describe('i18n routing with server islands and base path', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/i18n-server-island/', - base: '/custom', - adapter: testAdapter(), - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('should render the en locale with server island', async () => { - const res = await fixture.fetch('/custom/en/island'); - const html = await res.text(); - const $ = cheerio.load(html); - const serverIslandScript = $('script[data-island-id]'); - assert.equal(serverIslandScript.length, 1, 'has the island script'); - }); - }); -}); diff --git a/packages/astro/test/type-imports.test.js b/packages/astro/test/type-imports.test.js deleted file mode 100644 index fcdfe8af0aad..000000000000 --- a/packages/astro/test/type-imports.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import assert from 'node:assert/strict'; -import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; - -describe('Type Imports', () => { - let fixture; - - before(async () => { - fixture = await loadFixture({ root: './fixtures/type-imports' }); - await fixture.build(); - }); - - it('Allows importing types from "astro"', () => { - // if the build passes then the test succeeds - assert.equal(true, true); - }); -}); diff --git a/packages/astro/test/units/i18n/fallback.test.js b/packages/astro/test/units/i18n/fallback.test.js new file mode 100644 index 000000000000..37ca981e1223 --- /dev/null +++ b/packages/astro/test/units/i18n/fallback.test.js @@ -0,0 +1,374 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { computeFallbackRoute } from '../../../dist/i18n/fallback.js'; +import { makeFallbackOptions } from './test-helpers.js'; + +describe('computeFallbackRoute', () => { + describe('when response status < 300', () => { + it('returns none (no fallback needed)', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/missing', + responseStatus: 200, + currentLocale: 'es', + fallback: { es: 'en' }, + }), + ); + + assert.equal(result.type, 'none'); + }); + + it('returns none for 299 status', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/missing', + responseStatus: 299, + currentLocale: 'es', + fallback: { es: 'en' }, + }), + ); + + assert.equal(result.type, 'none'); + }); + }); + + describe('when no fallback configured', () => { + it('returns none for empty fallback object', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/missing', + responseStatus: 404, + currentLocale: 'es', + fallback: {}, + }), + ); + + assert.equal(result.type, 'none'); + }); + }); + + describe('when locale not in fallback config', () => { + it('returns none if current locale has no fallback', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/pt/missing', + responseStatus: 404, + currentLocale: 'pt', + fallback: { es: 'en' }, // Only es has fallback + }), + ); + + assert.equal(result.type, 'none'); + }); + }); + + describe('with fallbackType: redirect', () => { + it('returns redirect decision for fallback locale', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/missing', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'redirect', + strategy: 'pathname-prefix-always', + }), + ); + + assert.equal(result.type, 'redirect'); + assert.equal(result.pathname, '/en/missing'); + }); + + it('removes default locale prefix for prefix-other-locales strategy', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/missing', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'redirect', + strategy: 'pathname-prefix-other-locales', + defaultLocale: 'en', + }), + ); + + assert.equal(result.type, 'redirect'); + assert.equal(result.pathname, '/missing'); // No /en/ prefix + }); + + it('handles base path correctly', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/new-site/es/missing', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'redirect', + strategy: 'pathname-prefix-always', + base: '/new-site', + }), + ); + + assert.equal(result.type, 'redirect'); + assert.equal(result.pathname, '/new-site/en/missing'); + }); + + it('handles base path with prefix-other-locales strategy', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/new-site/es/missing', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'redirect', + strategy: 'pathname-prefix-other-locales', + defaultLocale: 'en', + base: '/new-site', + }), + ); + + assert.equal(result.type, 'redirect'); + assert.equal(result.pathname, '/new-site/missing'); + }); + + it('handles fallback to non-default locale', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/pt/missing', + responseStatus: 404, + currentLocale: 'pt', + fallback: { pt: 'es' }, // Fallback to Spanish, not English + fallbackType: 'redirect', + strategy: 'pathname-prefix-other-locales', + defaultLocale: 'en', + }), + ); + + assert.equal(result.type, 'redirect'); + assert.equal(result.pathname, '/es/missing'); + }); + + it('handles 3xx redirect status', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/redirect', + responseStatus: 301, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'redirect', + }), + ); + + assert.equal(result.type, 'redirect'); + }); + + it('handles 4xx status', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/notfound', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'redirect', + }), + ); + + assert.equal(result.type, 'redirect'); + }); + + it('handles 5xx status', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/error', + responseStatus: 500, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'redirect', + }), + ); + + assert.equal(result.type, 'redirect'); + }); + }); + + describe('with fallbackType: rewrite', () => { + it('returns rewrite decision for fallback locale', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/missing', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'rewrite', + strategy: 'pathname-prefix-always', + }), + ); + + assert.equal(result.type, 'rewrite'); + assert.equal(result.pathname, '/en/missing'); + }); + + it('removes default locale prefix for prefix-other-locales strategy', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/missing', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'rewrite', + strategy: 'pathname-prefix-other-locales', + defaultLocale: 'en', + }), + ); + + assert.equal(result.type, 'rewrite'); + assert.equal(result.pathname, '/missing'); + }); + + it('handles base path correctly', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/new-site/es/missing', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'rewrite', + strategy: 'pathname-prefix-always', + base: '/new-site', + }), + ); + + assert.equal(result.type, 'rewrite'); + assert.equal(result.pathname, '/new-site/en/missing'); + }); + + it('works with dynamic routes', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/blog/my-post', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'rewrite', + strategy: 'pathname-prefix-always', + }), + ); + + assert.equal(result.type, 'rewrite'); + assert.equal(result.pathname, '/en/blog/my-post'); + }); + + it('handles deep nested paths', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/blog/2024/01/post', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'rewrite', + strategy: 'pathname-prefix-always', + }), + ); + + assert.equal(result.type, 'rewrite'); + assert.equal(result.pathname, '/en/blog/2024/01/post'); + }); + }); + + describe('locale extraction from pathname', () => { + it('finds locale in first segment', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/page', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'redirect', + }), + ); + + assert.equal(result.type, 'redirect'); + }); + + it('handles paths without locale gracefully', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/page', + responseStatus: 404, + currentLocale: undefined, + fallback: { es: 'en' }, + fallbackType: 'redirect', + }), + ); + + assert.equal(result.type, 'none'); + }); + + it('handles granular locale configurations (object format)', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/spanish/page', + responseStatus: 404, + currentLocale: 'es', + locales: ['en', { path: 'spanish', codes: ['es', 'es-ES'] }, 'pt'], + fallback: { spanish: 'en' }, + fallbackType: 'redirect', + strategy: 'pathname-prefix-always', + }), + ); + + assert.equal(result.type, 'redirect'); + assert.equal(result.pathname, '/en/page'); + }); + }); + + describe('edge cases', () => { + it('handles root path', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'redirect', + strategy: 'pathname-prefix-always', + }), + ); + + assert.equal(result.type, 'redirect'); + assert.equal(result.pathname, '/en/'); + }); + + it('handles pathname without trailing slash', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'redirect', + strategy: 'pathname-prefix-always', + }), + ); + + assert.equal(result.type, 'redirect'); + assert.equal(result.pathname, '/en'); + }); + + it('preserves trailing content after locale replacement', () => { + const result = computeFallbackRoute( + makeFallbackOptions({ + pathname: '/es/a/b/c/d', + responseStatus: 404, + currentLocale: 'es', + fallback: { es: 'en' }, + fallbackType: 'rewrite', + strategy: 'pathname-prefix-always', + }), + ); + + assert.equal(result.type, 'rewrite'); + assert.equal(result.pathname, '/en/a/b/c/d'); + }); + }); +}); diff --git a/packages/astro/test/units/i18n/router.test.js b/packages/astro/test/units/i18n/router.test.js new file mode 100644 index 000000000000..bdcafaa2ac35 --- /dev/null +++ b/packages/astro/test/units/i18n/router.test.js @@ -0,0 +1,458 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { I18nRouter } from '../../../dist/i18n/router.js'; +import { makeI18nRouterConfig, makeRouterContext } from './test-helpers.js'; + +describe('I18nRouter', () => { + describe('strategy: pathname-prefix-always', () => { + let router; + + before(() => { + const config = makeI18nRouterConfig({ + strategy: 'pathname-prefix-always', + defaultLocale: 'en', + locales: ['en', 'es', 'pt'], + }); + router = new I18nRouter(config); + }); + + it('redirects root path to default locale', () => { + const context = makeRouterContext({ currentLocale: undefined }); + + const result = router.match('/', context); + + assert.equal(result.type, 'redirect'); + assert.equal(result.location, '/en'); + }); + + it('returns 404 for paths without locale prefix', () => { + const context = makeRouterContext({ currentLocale: undefined }); + + const result = router.match('/about', context); + + assert.equal(result.type, 'notFound'); + }); + + it('continues for paths with valid locale prefix', () => { + const context = makeRouterContext({ currentLocale: 'es' }); + + const result = router.match('/es/about', context); + + assert.equal(result.type, 'continue'); + }); + + it('continues for default locale with prefix', () => { + const context = makeRouterContext({ currentLocale: 'en' }); + + const result = router.match('/en/about', context); + + assert.equal(result.type, 'continue'); + }); + + describe('with base path', () => { + let routerWithBase; + + before(() => { + const configWithBase = makeI18nRouterConfig({ + strategy: 'pathname-prefix-always', + defaultLocale: 'en', + locales: ['en', 'es'], + base: '/new-site', + }); + routerWithBase = new I18nRouter(configWithBase); + }); + + it('handles base path - redirects base root to base + default locale', () => { + const context = makeRouterContext({ currentLocale: undefined }); + + const result = routerWithBase.match('/new-site/', context); + + assert.equal(result.type, 'redirect'); + assert.equal(result.location, '/new-site/en'); + }); + + it('handles base path without trailing slash', () => { + const context = makeRouterContext({ currentLocale: undefined }); + + const result = routerWithBase.match('/new-site', context); + + assert.equal(result.type, 'redirect'); + assert.equal(result.location, '/new-site/en'); + }); + + it('returns 404 for path without locale under base', () => { + const context = makeRouterContext({ currentLocale: undefined }); + + const result = routerWithBase.match('/new-site/about', context); + + assert.equal(result.type, 'notFound'); + }); + }); + }); + + describe('strategy: pathname-prefix-other-locales', () => { + let router; + + before(() => { + const config = makeI18nRouterConfig({ + strategy: 'pathname-prefix-other-locales', + defaultLocale: 'en', + locales: ['en', 'es', 'pt'], + }); + router = new I18nRouter(config); + }); + + it('returns 404 with Location header for default locale with prefix', () => { + const context = makeRouterContext({ currentLocale: 'en' }); + + const result = router.match('/en/about', context); + + assert.equal(result.type, 'notFound'); + assert.equal(result.location, '/about'); + }); + + it('continues for non-default locale with prefix', () => { + const context = makeRouterContext({ currentLocale: 'es' }); + + const result = router.match('/es/about', context); + + assert.equal(result.type, 'continue'); + }); + + it('continues for default locale without prefix', () => { + const context = makeRouterContext({ currentLocale: 'en' }); + + const result = router.match('/about', context); + + assert.equal(result.type, 'continue'); + }); + + it('continues for root path (default locale)', () => { + const context = makeRouterContext({ currentLocale: 'en' }); + + const result = router.match('/', context); + + assert.equal(result.type, 'continue'); + }); + + it('handles default locale in middle of path', () => { + const context = makeRouterContext({ currentLocale: 'en' }); + + const result = router.match('/blog/en/post', context); + + assert.equal(result.type, 'notFound'); + assert.equal(result.location, '/blog/post'); + }); + + it('handles base path with default locale prefix', () => { + const configWithBase = makeI18nRouterConfig({ + strategy: 'pathname-prefix-other-locales', + defaultLocale: 'en', + locales: ['en', 'es'], + base: '/new-site', + }); + const routerWithBase = new I18nRouter(configWithBase); + const context = makeRouterContext({ currentLocale: 'en' }); + + const result = routerWithBase.match('/new-site/en/about', context); + + assert.equal(result.type, 'notFound'); + assert.equal(result.location, '/new-site/about'); + }); + }); + + describe('strategy: pathname-prefix-always-no-redirect', () => { + let router; + + before(() => { + const config = makeI18nRouterConfig({ + strategy: 'pathname-prefix-always-no-redirect', + defaultLocale: 'en', + locales: ['en', 'es', 'pt'], + }); + router = new I18nRouter(config); + }); + + it('continues for root path (allows serving, no redirect)', () => { + const context = makeRouterContext({ currentLocale: undefined }); + + const result = router.match('/', context); + + assert.equal(result.type, 'continue'); + }); + + it('returns 404 for non-root paths without locale prefix', () => { + const context = makeRouterContext({ currentLocale: undefined }); + + const result = router.match('/about', context); + + assert.equal(result.type, 'notFound'); + }); + + it('continues for paths with valid locale prefix', () => { + const context = makeRouterContext({ currentLocale: 'es' }); + + const result = router.match('/es/about', context); + + assert.equal(result.type, 'continue'); + }); + + it('continues for base root path', () => { + const configWithBase = makeI18nRouterConfig({ + strategy: 'pathname-prefix-always-no-redirect', + defaultLocale: 'en', + locales: ['en', 'es'], + base: '/new-site', + }); + const routerWithBase = new I18nRouter(configWithBase); + const context = makeRouterContext({ currentLocale: undefined }); + + const result = routerWithBase.match('/new-site', context); + + assert.equal(result.type, 'continue'); + }); + }); + + describe('strategy: domains-prefix-always', () => { + let router; + + before(() => { + const config = makeI18nRouterConfig({ + strategy: 'domains-prefix-always', + defaultLocale: 'en', + locales: ['en', 'es', 'fr'], + domains: { + 'en.example.com': ['en'], + 'es.example.com': ['es'], + 'fr.example.com': ['fr'], + }, + }); + router = new I18nRouter(config); + }); + + it('redirects root when locale matches domain', () => { + const context = makeRouterContext({ + currentLocale: 'en', + currentDomain: 'en.example.com', + }); + + const result = router.match('/', context); + + assert.equal(result.type, 'redirect'); + assert.equal(result.location, '/en'); + }); + + it('continues when locale does not match domain (fallback to pathname logic)', () => { + const context = makeRouterContext({ + currentLocale: 'es', + currentDomain: 'en.example.com', + }); + + const result = router.match('/es/about', context); + + assert.equal(result.type, 'continue'); + }); + + it('returns 404 for path without locale when locale matches domain', () => { + const context = makeRouterContext({ + currentLocale: undefined, + currentDomain: 'en.example.com', + }); + + const result = router.match('/about', context); + + assert.equal(result.type, 'notFound'); + }); + }); + + describe('strategy: domains-prefix-other-locales', () => { + let router; + + before(() => { + const config = makeI18nRouterConfig({ + strategy: 'domains-prefix-other-locales', + defaultLocale: 'en', + locales: ['en', 'es', 'fr'], + domains: { + 'en.example.com': ['en'], + 'es.example.com': ['es'], + 'fr.example.com': ['fr'], + }, + }); + router = new I18nRouter(config); + }); + + it('returns 404 with Location for default locale prefix when locale matches domain', () => { + const context = makeRouterContext({ + currentLocale: 'en', + currentDomain: 'en.example.com', + }); + + const result = router.match('/en/about', context); + + assert.equal(result.type, 'notFound'); + assert.equal(result.location, '/about'); + }); + + it('continues for non-default locale when locale matches domain', () => { + const context = makeRouterContext({ + currentLocale: 'es', + currentDomain: 'es.example.com', + }); + + const result = router.match('/es/about', context); + + assert.equal(result.type, 'continue'); + }); + + it('continues when locale does not match domain', () => { + const context = makeRouterContext({ + currentLocale: 'es', + currentDomain: 'en.example.com', + }); + + const result = router.match('/es/about', context); + + assert.equal(result.type, 'continue'); + }); + }); + + describe('strategy: domains-prefix-always-no-redirect', () => { + let router; + + before(() => { + const config = makeI18nRouterConfig({ + strategy: 'domains-prefix-always-no-redirect', + defaultLocale: 'en', + locales: ['en', 'es', 'fr'], + domains: { + 'en.example.com': ['en'], + 'es.example.com': ['es'], + 'fr.example.com': ['fr'], + }, + }); + router = new I18nRouter(config); + }); + + it('continues for root when locale matches domain (allows serving, no redirect)', () => { + const context = makeRouterContext({ + currentLocale: undefined, + currentDomain: 'en.example.com', + }); + + const result = router.match('/', context); + + assert.equal(result.type, 'continue'); + }); + + it('continues for path with locale when locale matches domain', () => { + const context = makeRouterContext({ + currentLocale: 'en', + currentDomain: 'en.example.com', + }); + + const result = router.match('/en/about', context); + + assert.equal(result.type, 'continue'); + }); + }); + + describe('route filtering - skips i18n processing', () => { + let router; + + before(() => { + const config = makeI18nRouterConfig({ + strategy: 'pathname-prefix-always', + defaultLocale: 'en', + locales: ['en', 'es'], + }); + router = new I18nRouter(config); + }); + + it('skips 404 pages', () => { + const context = makeRouterContext({ currentLocale: undefined }); + + const result = router.match('/404', context); + + assert.equal(result.type, 'continue'); + }); + + it('skips 500 pages', () => { + const context = makeRouterContext({ currentLocale: undefined }); + + const result = router.match('/500', context); + + assert.equal(result.type, 'continue'); + }); + + it('skips server islands', () => { + const context = makeRouterContext({ currentLocale: undefined }); + + const result = router.match('/_server-islands/Counter', context); + + assert.equal(result.type, 'continue'); + }); + + it('skips non-page routes (endpoint)', () => { + const context = makeRouterContext({ + currentLocale: undefined, + routeType: 'endpoint', + }); + + const result = router.match('/api/data', context); + + assert.equal(result.type, 'continue'); + }); + + it('skips reroutes', () => { + const context = makeRouterContext({ + currentLocale: undefined, + isReroute: true, + }); + + const result = router.match('/about', context); + + assert.equal(result.type, 'continue'); + }); + + it('processes fallback routes', () => { + const context = makeRouterContext({ + currentLocale: undefined, + routeType: 'fallback', + }); + + const result = router.match('/about', context); + + assert.equal(result.type, 'notFound'); + }); + }); + + describe('strategy: manual', () => { + let router; + + before(() => { + const config = makeI18nRouterConfig({ + strategy: 'manual', + defaultLocale: 'en', + locales: ['en', 'es'], + }); + router = new I18nRouter(config); + }); + + it('always continues (no automatic routing)', () => { + const context = makeRouterContext({ currentLocale: undefined }); + + const result = router.match('/', context); + + assert.equal(result.type, 'continue'); + }); + + it('continues for any path', () => { + const context = makeRouterContext({ currentLocale: undefined }); + + const result = router.match('/any/path', context); + + assert.equal(result.type, 'continue'); + }); + }); +}); diff --git a/packages/astro/test/units/i18n/test-helpers.js b/packages/astro/test/units/i18n/test-helpers.js new file mode 100644 index 000000000000..2664ff7fa868 --- /dev/null +++ b/packages/astro/test/units/i18n/test-helpers.js @@ -0,0 +1,74 @@ +// @ts-check + +/** + * Creates an i18n router config for testing + * @param {object} [options] + * @param {import('../../../dist/core/app/common.js').RoutingStrategies} [options.strategy] + * @param {string} [options.defaultLocale] + * @param {import('../../../src/types/public/config.js').Locales} [options.locales] + * @param {string} [options.base] + * @param {Record} [options.domains] + */ +export function makeI18nRouterConfig({ + strategy = 'pathname-prefix-other-locales', + defaultLocale = 'en', + locales = ['en', 'es', 'pt'], + base = '', + domains, +} = {}) { + return { strategy, defaultLocale, locales, base, domains }; +} + +/** + * Creates router context for testing + * @param {object} [options] + * @param {string | undefined} [options.currentLocale] + * @param {string} [options.currentDomain] + * @param {string} [options.routeType] + * @param {boolean} [options.isReroute] + */ +export function makeRouterContext({ + currentLocale, + currentDomain = 'example.com', + routeType = 'page', + isReroute = false, +} = {}) { + return { currentLocale, currentDomain, routeType, isReroute }; +} + +/** + * Creates fallback options for testing + * @param {object} options + * @param {string} options.pathname + * @param {number} [options.responseStatus] + * @param {string | undefined} [options.currentLocale] + * @param {Record} [options.fallback] + * @param {'redirect' | 'rewrite'} [options.fallbackType] + * @param {import('../../../src/types/public/config.js').Locales} [options.locales] + * @param {string} [options.defaultLocale] + * @param {import('../../../dist/core/app/common.js').RoutingStrategies} [options.strategy] + * @param {string} [options.base] + */ +export function makeFallbackOptions({ + pathname, + responseStatus = 404, + currentLocale, + fallback = {}, + fallbackType = 'redirect', + locales = ['en', 'es', 'pt'], + defaultLocale = 'en', + strategy = 'pathname-prefix-other-locales', + base = '', +}) { + return { + pathname, + responseStatus, + currentLocale, + fallback, + fallbackType, + locales, + defaultLocale, + strategy, + base, + }; +} diff --git a/packages/astro/test/vue-jsx.test.js b/packages/astro/test/vue-jsx.test.js deleted file mode 100644 index 5ab00da8cec3..000000000000 --- a/packages/astro/test/vue-jsx.test.js +++ /dev/null @@ -1,31 +0,0 @@ -import assert from 'node:assert/strict'; -import { before, describe, it } from 'node:test'; -import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; - -describe('Vue JSX', () => { - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/vue-jsx/', - }); - }); - - describe('build', () => { - before(async () => { - await fixture.build(); - }); - - it('Can load Vue JSX', async () => { - const html = await fixture.readFile('/index.html'); - const $ = cheerio.load(html); - - const allPreValues = $('pre') - .toArray() - .map((el) => $(el).text()); - - assert.deepEqual(allPreValues, ['2345', '0', '1', '1', '1', '10', '100', '1000']); - }); - }); -}); diff --git a/packages/integrations/cloudflare/CHANGELOG.md b/packages/integrations/cloudflare/CHANGELOG.md index 12e659a7c7cf..566c9c55b12a 100644 --- a/packages/integrations/cloudflare/CHANGELOG.md +++ b/packages/integrations/cloudflare/CHANGELOG.md @@ -1,5 +1,18 @@ # @astrojs/cloudflare +## 13.0.0-beta.10 + +### Patch Changes + +- [#15669](https://github.com/withastro/astro/pull/15669) [`d5a888b`](https://github.com/withastro/astro/commit/d5a888ba645de356673605a0b70f9c721cf6cb3b) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Removes the `cssesc` dependency + + This CommonJS dependency could sometimes cause errors because Astro is ESM-only. It is now replaced with a built-in ESM-friendly implementation. + +- [#15648](https://github.com/withastro/astro/pull/15648) [`802426b`](https://github.com/withastro/astro/commit/802426b83c33c477ed66f1a429b9fc83b37f4515) Thanks [@rururux](https://github.com/rururux)! - Restore and fix `` component functionality on Cloudflare Workers. + +- Updated dependencies []: + - @astrojs/underscore-redirects@1.0.0 + ## 13.0.0-beta.9 ### Major Changes diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json index 1c0267a420f0..36538c3d00fd 100644 --- a/packages/integrations/cloudflare/package.json +++ b/packages/integrations/cloudflare/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/cloudflare", "description": "Deploy your site to Cloudflare Workers", - "version": "13.0.0-beta.9", + "version": "13.0.0-beta.10", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index c5b9eaf9c300..9b68803231e1 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -181,7 +181,6 @@ export default function createIntegration({ 'astro > zod/v4', 'astro > zod/v4/core', 'astro > clsx', - 'astro > cssesc', 'astro > cookie', 'astro > devalue', 'astro > @oslojs/encoding', diff --git a/packages/integrations/netlify/CHANGELOG.md b/packages/integrations/netlify/CHANGELOG.md index 6920c5b9b826..a257c0c5bbb3 100644 --- a/packages/integrations/netlify/CHANGELOG.md +++ b/packages/integrations/netlify/CHANGELOG.md @@ -1,5 +1,16 @@ # @astrojs/netlify +## 7.0.0-beta.11 + +### Patch Changes + +- [#15665](https://github.com/withastro/astro/pull/15665) [`52a7efd`](https://github.com/withastro/astro/commit/52a7efdde4c46e2b40b10f78ebb4a812e5d5b82f) Thanks [@matthewp](https://github.com/matthewp)! - Fixes builds that were failing with "Entry module cannot be external" error when using the Netlify adapter + + This error was preventing sites from building after recent internal changes. Your builds should now work as expected without any changes to your code. + +- Updated dependencies []: + - @astrojs/underscore-redirects@1.0.0 + ## 7.0.0-beta.10 ### Patch Changes diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json index 30e5f1987f6b..13efbd35e7b1 100644 --- a/packages/integrations/netlify/package.json +++ b/packages/integrations/netlify/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/netlify", "description": "Deploy your site to Netlify", - "version": "7.0.0-beta.10", + "version": "7.0.0-beta.11", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/packages/integrations/netlify/src/index.ts b/packages/integrations/netlify/src/index.ts index e89be60126e0..b73a1e86fbcc 100644 --- a/packages/integrations/netlify/src/index.ts +++ b/packages/integrations/netlify/src/index.ts @@ -649,6 +649,9 @@ export default function netlifyIntegration( packageVersion, }), ], + ssr: { + noExternal: ['@astrojs/netlify'], + }, server: { watch: { ignored: [fileURLToPath(new URL('./.netlify/**', rootDir))], diff --git a/packages/integrations/vue/test/basics.test.js b/packages/integrations/vue/test/basics.test.js index 16987f43df2b..84b274b0d40c 100644 --- a/packages/integrations/vue/test/basics.test.js +++ b/packages/integrations/vue/test/basics.test.js @@ -40,4 +40,12 @@ describe('Basics', () => { assert.equal(els.length, 2); assert.notEqual(els[0].getAttribute('id'), els[1].getAttribute('id')); }); + + it('Can load Vue JSX', async () => { + const data = await fixture.readFile('/jsx/index.html'); + const { document } = parseHTML(data); + + const allPreValues = [...document.querySelectorAll('pre')].map((e) => e.textContent); + assert.deepEqual(allPreValues, ['2345', '0', '1', '1', '1', '10', '100', '1000']); + }); }); diff --git a/packages/integrations/vue/test/fixtures/basics/astro.config.mjs b/packages/integrations/vue/test/fixtures/basics/astro.config.mjs index 4cc1189af740..51dcc333227c 100644 --- a/packages/integrations/vue/test/fixtures/basics/astro.config.mjs +++ b/packages/integrations/vue/test/fixtures/basics/astro.config.mjs @@ -2,5 +2,5 @@ import vue from '@astrojs/vue'; import { defineConfig } from 'astro/config'; export default defineConfig({ - integrations: [vue()], + integrations: [vue({ jsx: true })], }) diff --git a/packages/astro/test/fixtures/vue-jsx/src/components/Counter.jsx b/packages/integrations/vue/test/fixtures/basics/src/components/Counter.jsx similarity index 100% rename from packages/astro/test/fixtures/vue-jsx/src/components/Counter.jsx rename to packages/integrations/vue/test/fixtures/basics/src/components/Counter.jsx diff --git a/packages/astro/test/fixtures/vue-jsx/src/components/Result.vue b/packages/integrations/vue/test/fixtures/basics/src/components/Result.vue similarity index 100% rename from packages/astro/test/fixtures/vue-jsx/src/components/Result.vue rename to packages/integrations/vue/test/fixtures/basics/src/components/Result.vue diff --git a/packages/astro/test/fixtures/vue-jsx/src/pages/index.astro b/packages/integrations/vue/test/fixtures/basics/src/pages/jsx.astro similarity index 100% rename from packages/astro/test/fixtures/vue-jsx/src/pages/index.astro rename to packages/integrations/vue/test/fixtures/basics/src/pages/jsx.astro diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be4f2fdebffe..cc5852f73fb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,7 +184,7 @@ importers: examples/basics: dependencies: astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro examples/blog: @@ -199,7 +199,7 @@ importers: specifier: ^3.6.1-beta.3 version: link:../../packages/integrations/sitemap astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro sharp: specifier: ^0.34.3 @@ -208,7 +208,7 @@ importers: examples/component: devDependencies: astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro examples/container-with-vitest: @@ -217,7 +217,7 @@ importers: specifier: ^5.0.0-beta.3 version: link:../../packages/integrations/react astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro react: specifier: ^18.3.1 @@ -248,7 +248,7 @@ importers: specifier: ^3.15.8 version: 3.15.8 astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro examples/framework-multiple: @@ -275,7 +275,7 @@ importers: specifier: ^18.3.7 version: 18.3.7(@types/react@18.3.28) astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro preact: specifier: ^10.28.4 @@ -305,7 +305,7 @@ importers: specifier: ^2.8.1 version: 2.8.1(preact@10.28.4) astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro preact: specifier: ^10.28.4 @@ -323,7 +323,7 @@ importers: specifier: ^18.3.7 version: 18.3.7(@types/react@18.3.28) astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro react: specifier: ^18.3.1 @@ -338,7 +338,7 @@ importers: specifier: ^6.0.0-beta.2 version: link:../../packages/integrations/solid astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro solid-js: specifier: ^1.9.11 @@ -350,7 +350,7 @@ importers: specifier: ^8.0.0-beta.3 version: link:../../packages/integrations/svelte astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro svelte: specifier: ^5.53.0 @@ -362,7 +362,7 @@ importers: specifier: ^6.0.0-beta.1 version: link:../../packages/integrations/vue astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro vue: specifier: ^3.5.28 @@ -374,25 +374,25 @@ importers: specifier: ^10.0.0-beta.5 version: link:../../packages/integrations/node astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro examples/integration: devDependencies: astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro examples/minimal: dependencies: astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro examples/portfolio: dependencies: astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro examples/ssr: @@ -404,7 +404,7 @@ importers: specifier: ^8.0.0-beta.3 version: link:../../packages/integrations/svelte astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro svelte: specifier: ^5.53.0 @@ -413,7 +413,7 @@ importers: examples/starlog: dependencies: astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro sass: specifier: ^1.97.3 @@ -428,7 +428,7 @@ importers: specifier: ^18.17.8 version: 18.19.130 astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro examples/with-markdoc: @@ -437,7 +437,7 @@ importers: specifier: ^1.0.0-beta.11 version: link:../../packages/integrations/markdoc astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro examples/with-mdx: @@ -449,7 +449,7 @@ importers: specifier: ^5.0.0-beta.4 version: link:../../packages/integrations/preact astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro preact: specifier: ^10.28.4 @@ -464,7 +464,7 @@ importers: specifier: ^1.0.0 version: 1.0.0(nanostores@1.1.0)(preact@10.28.4) astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro nanostores: specifier: ^1.1.0 @@ -485,7 +485,7 @@ importers: specifier: ^1.9.0 version: 1.9.0 astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro canvas-confetti: specifier: ^1.9.4 @@ -497,7 +497,7 @@ importers: examples/with-vitest: dependencies: astro: - specifier: ^6.0.0-beta.15 + specifier: ^6.0.0-beta.16 version: link:../../packages/astro vitest: specifier: ^3.2.4 @@ -547,9 +547,6 @@ importers: cookie: specifier: ^1.1.1 version: 1.1.1 - cssesc: - specifier: ^3.0.0 - version: 3.0.0 devalue: specifier: ^5.6.3 version: 5.6.3 @@ -683,9 +680,6 @@ importers: '@types/aria-query': specifier: ^5.0.4 version: 5.0.4 - '@types/cssesc': - specifier: ^3.0.2 - version: 3.0.2 '@types/dlv': specifier: ^1.1.5 version: 1.1.5 @@ -4517,12 +4511,6 @@ importers: specifier: ^0.8.0 version: 0.8.0(astro@packages+astro) - packages/astro/test/fixtures/type-imports: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/unused-slot: dependencies: astro: @@ -4577,18 +4565,6 @@ importers: specifier: ^3.5.28 version: 3.5.28(typescript@5.9.3) - packages/astro/test/fixtures/vue-jsx: - dependencies: - '@astrojs/vue': - specifier: workspace:* - version: link:../../../../integrations/vue - astro: - specifier: workspace:* - version: link:../../.. - vue: - specifier: ^3.5.28 - version: 3.5.28(typescript@5.9.3) - packages/astro/test/fixtures/vue-with-multi-renderer: dependencies: '@astrojs/svelte': @@ -10025,9 +10001,6 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/cssesc@3.0.2': - resolution: {integrity: sha512-Qii6nTRktvtI380EloxH/V7MwgrYxkPgBI+NklUjQuhzgAd1AqT3QDJd+eD+0doRADgfwvtagLRo7JFa7aMHXg==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -19129,8 +19102,6 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/cssesc@3.0.2': {} - '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0