diff --git a/.changeset/cool-colts-watch.md b/.changeset/cool-colts-watch.md deleted file mode 100644 index cf72fdcb8c21..000000000000 --- a/.changeset/cool-colts-watch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@astrojs/markdown-remark": patch ---- - -Fixes a bug where non-UTF-8 file names are not displayed when using relative paths in markdowns. diff --git a/.changeset/cuddly-moons-hang.md b/.changeset/cuddly-moons-hang.md new file mode 100644 index 000000000000..c726f5aa4398 --- /dev/null +++ b/.changeset/cuddly-moons-hang.md @@ -0,0 +1,14 @@ +--- +"astro": minor +--- + +Adds a new `ComponentProps` type export from `astro/types` to get the props type of an Astro component. + +```astro +--- +import type { ComponentProps } from 'astro/types'; +import { Button } from "./Button.astro"; + +type myButtonProps = ComponentProps; +--- +``` diff --git a/.changeset/famous-seas-press.md b/.changeset/famous-seas-press.md deleted file mode 100644 index 67d32b257797..000000000000 --- a/.changeset/famous-seas-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Expose `ContentConfig` type from `astro:content` diff --git a/.changeset/four-masks-smell.md b/.changeset/four-masks-smell.md new file mode 100644 index 000000000000..e0207fbe40d0 --- /dev/null +++ b/.changeset/four-masks-smell.md @@ -0,0 +1,6 @@ +--- +"@astrojs/vercel": minor +"@astrojs/node": minor +--- + +Adds experimental support for internationalization domains diff --git a/.changeset/large-kangaroos-camp.md b/.changeset/large-kangaroos-camp.md deleted file mode 100644 index bc42ff75661c..000000000000 --- a/.changeset/large-kangaroos-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"astro": patch ---- - -Fixes a bug in `Astro.currentLocale` where the value was incorrectly computed during the build. diff --git a/.changeset/little-panthers-relate.md b/.changeset/little-panthers-relate.md new file mode 100644 index 000000000000..5fb2c523f237 --- /dev/null +++ b/.changeset/little-panthers-relate.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Fixes an issue where the function `getLocaleRelativeUrlList` wasn't normalising the paths by default diff --git a/.changeset/old-cherries-beg.md b/.changeset/old-cherries-beg.md new file mode 100644 index 000000000000..cf074297e7ab --- /dev/null +++ b/.changeset/old-cherries-beg.md @@ -0,0 +1,9 @@ +--- +'astro': minor +--- + +Adds CLI shortcuts as an easter egg for the dev server: + +- `o + enter`: opens the site in your browser +- `q + enter`: quits the dev server +- `h + enter`: prints all available shortcuts diff --git a/.changeset/silent-buckets-retire.md b/.changeset/silent-buckets-retire.md deleted file mode 100644 index f682c97d786f..000000000000 --- a/.changeset/silent-buckets-retire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"astro": patch ---- - -Fixes an issue where `astro:i18n` could not be used in framework components. diff --git a/.changeset/tidy-carrots-jump.md b/.changeset/tidy-carrots-jump.md new file mode 100644 index 000000000000..e18f235e3aaf --- /dev/null +++ b/.changeset/tidy-carrots-jump.md @@ -0,0 +1,52 @@ +--- +'astro': minor +--- + +Adds experimental support for a new i18n domain routing option (`"domains"`) that allows you to configure different domains for individual locales in entirely server-rendered projects. + +To enable this in your project, first configure your `server`-rendered project's i18n routing with your preferences if you have not already done so. Then, set the `experimental.i18nDomains` flag to `true` and add `i18n.domains` to map any of your supported `locales` to custom URLs: + +```js +//astro.config.mjs" +import { defineConfig } from "astro/config" +export default defineConfig({ + site: "https://example.com", + output: "server", // required, with no prerendered pages + adapter: node({ + mode: 'standalone', + }), + i18n: { + defaultLocale: "en", + locales: ["es", "en", "fr", "ja"], + routing: { + prefixDefaultLocale: false + }, + domains: { + fr: "https://fr.example.com", + es: "https://example.es" + } + }, + experimental: { + i18nDomains: true + } +}) +``` +With `"domains"` configured, the URLs emitted by `getAbsoluteLocaleUrl()` and `getAbsoluteLocaleUrlList()` will use the options set in `i18n.domains`. + +```js +import { getAbsoluteLocaleUrl } from "astro:i18n"; + +getAbsoluteLocaleUrl("en", "about"); // will return "https://example.com/about" +getAbsoluteLocaleUrl("fr", "about"); // will return "https://fr.example.com/about" +getAbsoluteLocaleUrl("es", "about"); // will return "https://example.es/about" +getAbsoluteLocaleUrl("ja", "about"); // will return "https://example.com/ja/about" +``` + +Similarly, your localized files will create routes at corresponding URLs: + +- The file `/en/about.astro` will be reachable at the URL `https://example.com/about`. +- The file `/fr/about.astro` will be reachable at the URL `https://fr.example.com/about`. +- The file `/es/about.astro` will be reachable at the URL `https://example.es/about`. +- The file `/ja/about.astro` will be reachable at the URL `https://example.com/ja/about`. + +See our [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains-experimental) for more details and limitations on this experimental routing feature. diff --git a/.changeset/young-eyes-film.md b/.changeset/young-eyes-film.md new file mode 100644 index 000000000000..092da9882d52 --- /dev/null +++ b/.changeset/young-eyes-film.md @@ -0,0 +1,15 @@ +--- +"astro": minor +--- + +Fixes an issue where images in Markdown required a relative specifier (e.g. `./`) + +Now, you can use the standard `![](img.png)` syntax in Markdown files for images colocated in the same folder: no relative specifier required! + +There is no need to update your project; your existing images will still continue to work. However, you may wish to remove any relative specifiers from these Markdown images as they are no longer necessary: + +```diff +- ![A cute dog](./dog.jpg) ++ ![A cute dog](dog.jpg) + +``` diff --git a/examples/basics/package.json b/examples/basics/package.json index 16491881c2f6..33291e9103fb 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.2.6" + "astro": "^4.2.8" } } diff --git a/examples/blog/package.json b/examples/blog/package.json index 398956eb2f05..5335605cb17a 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -11,9 +11,9 @@ "astro": "astro" }, "dependencies": { - "@astrojs/mdx": "^2.1.0", + "@astrojs/mdx": "^2.1.1", "@astrojs/rss": "^4.0.4", "@astrojs/sitemap": "^3.0.5", - "astro": "^4.2.6" + "astro": "^4.2.8" } } diff --git a/examples/blog/src/components/HeaderLink.astro b/examples/blog/src/components/HeaderLink.astro index 3dd439be1af6..bb600fb65ac3 100644 --- a/examples/blog/src/components/HeaderLink.astro +++ b/examples/blog/src/components/HeaderLink.astro @@ -6,7 +6,8 @@ type Props = HTMLAttributes<'a'>; const { href, class: className, ...props } = Astro.props; const { pathname } = Astro.url; -const isActive = href === pathname || href === pathname.replace(/\/$/, ''); +const subpath = pathname.match(/[^\/]+/g); +const isActive = href === pathname || href === '/' + subpath?.[0]; --- diff --git a/examples/component/package.json b/examples/component/package.json index 22d357f43342..583fd5226de8 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^4.2.6" + "astro": "^4.2.8" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index 6938d628497f..9efd2abfada7 100644 --- a/examples/framework-alpine/package.json +++ b/examples/framework-alpine/package.json @@ -14,6 +14,6 @@ "@astrojs/alpinejs": "^0.4.0", "@types/alpinejs": "^3.13.5", "alpinejs": "^3.13.3", - "astro": "^4.2.6" + "astro": "^4.2.8" } } diff --git a/examples/framework-lit/package.json b/examples/framework-lit/package.json index c655def71bf0..0c34af72783c 100644 --- a/examples/framework-lit/package.json +++ b/examples/framework-lit/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/lit": "^4.0.1", "@webcomponents/template-shadowroot": "^0.2.1", - "astro": "^4.2.6", + "astro": "^4.2.8", "lit": "^2.8.0" } } diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index 22cc6eb450b1..5318dc347628 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -16,7 +16,7 @@ "@astrojs/solid-js": "^4.0.1", "@astrojs/svelte": "^5.0.3", "@astrojs/vue": "^4.0.8", - "astro": "^4.2.6", + "astro": "^4.2.8", "preact": "^10.19.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index b75f37cd13f2..e40889273324 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.1.0", "@preact/signals": "^1.2.1", - "astro": "^4.2.6", + "astro": "^4.2.8", "preact": "^10.19.2" } } diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index 05cbe425ace9..053b8878509d 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -14,7 +14,7 @@ "@astrojs/react": "^3.0.9", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", - "astro": "^4.2.6", + "astro": "^4.2.8", "react": "^18.2.0", "react-dom": "^18.2.0" } diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index 31c640df2413..57afbf165dc7 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/solid-js": "^4.0.1", - "astro": "^4.2.6", + "astro": "^4.2.8", "solid-js": "^1.8.5" } } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index 063a3fba05c6..07956cd8c5d6 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/svelte": "^5.0.3", - "astro": "^4.2.6", + "astro": "^4.2.8", "svelte": "^4.2.5" } } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index c33bead18c2b..ba25ebf16998 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/vue": "^4.0.8", - "astro": "^4.2.6", + "astro": "^4.2.8", "vue": "^3.3.8" } } diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index 3ac1450dc466..ac9bdb79c9d5 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -12,6 +12,6 @@ }, "dependencies": { "@astrojs/node": "^8.1.0", - "astro": "^4.2.6" + "astro": "^4.2.8" } } diff --git a/examples/integration/package.json b/examples/integration/package.json index 3d0b5ca6e888..8d6751b14c86 100644 --- a/examples/integration/package.json +++ b/examples/integration/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^4.2.6" + "astro": "^4.2.8" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/middleware/package.json b/examples/middleware/package.json index 02a93ca5d7a7..f8619d4a0afc 100644 --- a/examples/middleware/package.json +++ b/examples/middleware/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@astrojs/node": "^8.1.0", - "astro": "^4.2.6", + "astro": "^4.2.8", "html-minifier": "^4.0.0" }, "devDependencies": { diff --git a/examples/minimal/package.json b/examples/minimal/package.json index cb1e411aaa14..9ef644995f2c 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.2.6" + "astro": "^4.2.8" } } diff --git a/examples/non-html-pages/package.json b/examples/non-html-pages/package.json index ab5ef4169b49..80b6f39e289f 100644 --- a/examples/non-html-pages/package.json +++ b/examples/non-html-pages/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.2.6" + "astro": "^4.2.8" } } diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index 5eed33b63d31..af774639d710 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.2.6" + "astro": "^4.2.8" } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 4fe370fdffa9..75238260ba00 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -14,7 +14,7 @@ "dependencies": { "@astrojs/node": "^8.1.0", "@astrojs/svelte": "^5.0.3", - "astro": "^4.2.6", + "astro": "^4.2.8", "svelte": "^4.2.5" } } diff --git a/examples/starlog/package.json b/examples/starlog/package.json index 3c01f0b67530..c780c28fdac1 100644 --- a/examples/starlog/package.json +++ b/examples/starlog/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.2.6", + "astro": "^4.2.8", "sass": "^1.69.5", "sharp": "^0.32.6" } diff --git a/examples/view-transitions/package.json b/examples/view-transitions/package.json index b38a7e1bbeb4..1536bb767480 100644 --- a/examples/view-transitions/package.json +++ b/examples/view-transitions/package.json @@ -12,6 +12,6 @@ "devDependencies": { "@astrojs/tailwind": "^5.1.0", "@astrojs/node": "^8.1.0", - "astro": "^4.2.6" + "astro": "^4.2.8" } } diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json index 1fb7fe411ddd..1ee2f30722c4 100644 --- a/examples/with-markdoc/package.json +++ b/examples/with-markdoc/package.json @@ -12,6 +12,6 @@ }, "dependencies": { "@astrojs/markdoc": "^0.8.3", - "astro": "^4.2.6" + "astro": "^4.2.8" } } diff --git a/examples/with-markdown-plugins/package.json b/examples/with-markdown-plugins/package.json index 1de8e52e1289..db5057c19f9f 100644 --- a/examples/with-markdown-plugins/package.json +++ b/examples/with-markdown-plugins/package.json @@ -11,8 +11,8 @@ "astro": "astro" }, "dependencies": { - "@astrojs/markdown-remark": "^4.2.0", - "astro": "^4.2.6", + "@astrojs/markdown-remark": "^4.2.1", + "astro": "^4.2.8", "hast-util-select": "^6.0.2", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", diff --git a/examples/with-markdown-shiki/package.json b/examples/with-markdown-shiki/package.json index 99d8aa4eafdb..f46f4acf6636 100644 --- a/examples/with-markdown-shiki/package.json +++ b/examples/with-markdown-shiki/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.2.6" + "astro": "^4.2.8" } } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index 644d7ea532ab..e62b6636e24b 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -11,9 +11,9 @@ "astro": "astro" }, "dependencies": { - "@astrojs/mdx": "^2.1.0", + "@astrojs/mdx": "^2.1.1", "@astrojs/preact": "^3.1.0", - "astro": "^4.2.6", + "astro": "^4.2.8", "preact": "^10.19.2" } } diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index 498d76e689ef..4b4686943502 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.1.0", "@nanostores/preact": "^0.5.0", - "astro": "^4.2.6", + "astro": "^4.2.8", "nanostores": "^0.9.5", "preact": "^10.19.2" } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index 077dca85897a..f00b77307fb7 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -11,10 +11,10 @@ "astro": "astro" }, "dependencies": { - "@astrojs/mdx": "^2.1.0", + "@astrojs/mdx": "^2.1.1", "@astrojs/tailwind": "^5.1.0", "@types/canvas-confetti": "^1.6.3", - "astro": "^4.2.6", + "astro": "^4.2.8", "autoprefixer": "^10.4.15", "canvas-confetti": "^1.9.1", "postcss": "^8.4.28", diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index 3130d8ff1c34..1d3ec43f0555 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -12,7 +12,7 @@ "test": "vitest" }, "dependencies": { - "astro": "^4.2.6", + "astro": "^4.2.8", "vitest": "^1.2.1" } } diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 605e3d0d908e..321409cf459c 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,30 @@ # astro +## 4.2.8 + +### Patch Changes + +- [#9884](https://github.com/withastro/astro/pull/9884) [`37369550ab57ca529fd6c796e5b0e96e897ca6e5`](https://github.com/withastro/astro/commit/37369550ab57ca529fd6c796e5b0e96e897ca6e5) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixes an issue where multiple cookies were sent in a single Set-Cookie header in the dev mode. + +- [#9876](https://github.com/withastro/astro/pull/9876) [`e9027f194b939ac5a4d795ee1a2c24e4a6fbefc0`](https://github.com/withastro/astro/commit/e9027f194b939ac5a4d795ee1a2c24e4a6fbefc0) Thanks [@friedemannsommer](https://github.com/friedemannsommer)! - Fixes an issue where using `Response.redirect` in an endpoint led to an error. + +- [#9882](https://github.com/withastro/astro/pull/9882) [`13c3b712c7ba45d0081f459fc06f142216a4ec59`](https://github.com/withastro/astro/commit/13c3b712c7ba45d0081f459fc06f142216a4ec59) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Improves handling of YAML parsing errors + +- [#9878](https://github.com/withastro/astro/pull/9878) [`a40a0ff5883c7915dd55881dcebd052b9f94a0eb`](https://github.com/withastro/astro/commit/a40a0ff5883c7915dd55881dcebd052b9f94a0eb) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixes an issue where setting trailingSlash to "never" had no effect on `Astro.url`. + +## 4.2.7 + +### Patch Changes + +- [#9840](https://github.com/withastro/astro/pull/9840) [`70fdf1a5c660057152c1ca111dcc89ceda5c8840`](https://github.com/withastro/astro/commit/70fdf1a5c660057152c1ca111dcc89ceda5c8840) Thanks [@delucis](https://github.com/delucis)! - Expose `ContentConfig` type from `astro:content` + +- [#9865](https://github.com/withastro/astro/pull/9865) [`00ba9f1947ca9016cd0ee4d8f6048027fab2ab9a`](https://github.com/withastro/astro/commit/00ba9f1947ca9016cd0ee4d8f6048027fab2ab9a) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a bug in `Astro.currentLocale` where the value was incorrectly computed during the build. + +- [#9838](https://github.com/withastro/astro/pull/9838) [`0a06d87a1e2b94be00a954f350c184222fa0594d`](https://github.com/withastro/astro/commit/0a06d87a1e2b94be00a954f350c184222fa0594d) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixes an issue where `astro:i18n` could not be used in framework components. + +- Updated dependencies [[`44c957f893c6bf5f5b7c78301de7b21c5975584d`](https://github.com/withastro/astro/commit/44c957f893c6bf5f5b7c78301de7b21c5975584d)]: + - @astrojs/markdown-remark@4.2.1 + ## 4.2.6 ### Patch Changes diff --git a/packages/astro/package.json b/packages/astro/package.json index e8d64f9ff044..ba1509826cdb 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "astro", - "version": "4.2.6", + "version": "4.2.8", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.", "type": "module", "author": "withastro", diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 88220405b308..bc0ab1e3ea13 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1533,6 +1533,45 @@ export interface AstroUserConfig { * - `"pathanme": The strategy is applied to the pathname of the URLs */ strategy: 'pathname'; + + /** + * @name i18n.domains + * @type {Record } + * @default '{}' + * @version 4.3.0 + * @description + * + * Configures the URL pattern of one or more supported languages to use a custom domain (or sub-domain). + * + * When a locale is mapped to a domain, a `/[locale]/` path prefix will not be used. + * However, localized folders within `src/pages/` are still required, including for your configured `defaultLocale`. + * + * Any other locale not configured will default to a localized path-based URL according to your `prefixDefaultLocale` strategy (e.g. `https://example.com/[locale]/blog`). + * + * ```js + * //astro.config.mjs + * export default defineConfig({ + * site: "https://example.com", + * output: "server", // required, with no prerendered pages + * adapter: node({ + * mode: 'standalone', + * }), + * i18n: { + * defaultLocale: "en", + * locales: ["en", "fr", "pt-br", "es"], + * prefixDefaultLocale: false, + * domains: { + * fr: "https://fr.example.com", + * es: "https://example.es" + * }, + * }) + * ``` + * + * Both page routes built and URLs returned by the `astro:i18n` helper functions [`getAbsoluteLocaleUrl()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurl) and [`getAbsoluteLocaleUrlList()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurllist) will use the options set in `i18n.domains`. + * + * See the [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains) for more details, including the limitations of this feature. + */ + domains?: Record; }; }; @@ -1665,6 +1704,48 @@ export interface AstroUserConfig { * In the event of route collisions, where two routes of equal route priority attempt to build the same URL, Astro will log a warning identifying the conflicting routes. */ globalRoutePriority?: boolean; + + /** + * @docs + * @name experimental.i18nDomains + * @type {boolean} + * @default `false` + * @version 4.3.0 + * @description + * + * Enables domain support for the [experimental `domains` routing strategy](https://docs.astro.build/en/guides/internationalization/#domains-experimental) which allows you to configure the URL pattern of one or more supported languages to use a custom domain (or sub-domain). + * + * When a locale is mapped to a domain, a `/[locale]/` path prefix will not be used. However, localized folders within `src/pages/` are still required, including for your configured `defaultLocale`. + * + * Any other locale not configured will default to a localized path-based URL according to your `prefixDefaultLocale` strategy (e.g. `https://example.com/[locale]/blog`). + * + * ```js + * //astro.config.mjs + * export default defineConfig({ + * site: "https://example.com", + * output: "server", // required, with no prerendered pages + * adapter: node({ + * mode: 'standalone', + * }), + * i18n: { + * defaultLocale: "en", + * locales: ["en", "fr", "pt-br", "es"], + * prefixDefaultLocale: false, + * domains: { + * fr: "https://fr.example.com", + * es: "https://example.es" + * }, + * experimental: { + * i18nDomains: true + * } + * }) + * ``` + * + * Both page routes built and URLs returned by the `astro:i18n` helper functions [`getAbsoluteLocaleUrl()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurl) and [`getAbsoluteLocaleUrlList()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurllist) will use the options set in `i18n.domains`. + * + * See the [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains-experimental) for more details, including the limitations of this experimental feature. + */ + i18nDomains?: boolean; }; } @@ -2134,7 +2215,7 @@ export type AstroFeatureMap = { /** * List of features that orbit around the i18n routing */ - i18n?: AstroInternationalizationFeature; + i18nDomains?: SupportsKind; }; export interface AstroAssetsFeature { @@ -2151,9 +2232,9 @@ export interface AstroAssetsFeature { export interface AstroInternationalizationFeature { /** - * Whether the adapter is able to detect the language of the browser, usually using the `Accept-Language` header. + * The adapter should be able to create the proper redirects */ - detectBrowserLanguage?: SupportsKind; + domains?: SupportsKind; } export type Locales = (string | { codes: string[]; path: string })[]; diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 3515a43c7216..804925f0fcef 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -14,7 +14,8 @@ import type { } from '../@types/astro.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; -import { formatYAMLException, isYAMLException } from '../core/errors/utils.js'; +import { MarkdownError } from '../core/errors/index.js'; +import { isYAMLException } from '../core/errors/utils.js'; import { CONTENT_FLAGS, CONTENT_TYPES_FILE } from './consts.js'; import { errorMap } from './error-map.js'; import { createImage } from './runtime-assets.js'; @@ -287,18 +288,32 @@ function getYAMLErrorLine(rawData: string | undefined, objectKey: string) { return numNewlinesBeforeKey; } -export function parseFrontmatter(fileContents: string) { +export function safeParseFrontmatter(source: string, id?: string) { try { - // `matter` is empty string on cache results - // clear cache to prevent this - (matter as any).clearCache(); - return matter(fileContents); - } catch (e) { - if (isYAMLException(e)) { - throw formatYAMLException(e); - } else { - throw e; + return matter(source); + } catch (err: any) { + const markdownError = new MarkdownError({ + name: 'MarkdownError', + message: err.message, + stack: err.stack, + location: id + ? { + file: id, + } + : undefined, + }); + + if (isYAMLException(err)) { + markdownError.setLocation({ + file: id, + line: err.mark.line, + column: err.mark.column, + }); + + markdownError.setMessage(err.reason); } + + throw markdownError; } } diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index c7dca57ec23c..108b0f231a77 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -13,7 +13,9 @@ import { consoleLogDestination } from '../logger/console.js'; import { AstroIntegrationLogger, Logger } from '../logger/core.js'; import { sequence } from '../middleware/index.js'; import { + appendForwardSlash, collapseDuplicateSlashes, + joinPaths, prependForwardSlash, removeTrailingForwardSlash, } from '../path.js'; @@ -28,6 +30,7 @@ import { import { matchRoute } from '../routing/match.js'; import { SSRRoutePipeline } from './ssrPipeline.js'; import type { RouteInfo } from './types.js'; +import { normalizeTheLocale } from '../../i18n/index.js'; export { deserializeManifest } from './common.js'; const localsSymbol = Symbol.for('astro.locals'); @@ -172,13 +175,85 @@ export class App { const url = new URL(request.url); // ignore requests matching public assets if (this.#manifest.assets.has(url.pathname)) return undefined; - const pathname = prependForwardSlash(this.removeBase(url.pathname)); - const routeData = matchRoute(pathname, this.#manifestData); - // missing routes fall-through, prerendered are handled by static layer + let pathname = this.#computePathnameFromDomain(request); + if (!pathname) { + pathname = prependForwardSlash(this.removeBase(url.pathname)); + } + let routeData = matchRoute(pathname, this.#manifestData); + + // missing routes fall-through, pre rendered are handled by static layer if (!routeData || routeData.prerender) return undefined; return routeData; } + #computePathnameFromDomain(request: Request): string | undefined { + let pathname: string | undefined = undefined; + const url = new URL(request.url); + + if ( + this.#manifest.i18n && + (this.#manifest.i18n.routing === 'domains-prefix-always' || + this.#manifest.i18n.routing === 'domains-prefix-other-locales' || + this.#manifest.i18n.routing === 'domains-prefix-other-no-redirect') + ) { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host + let host = request.headers.get('X-Forwarded-Host'); + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto + let protocol = request.headers.get('X-Forwarded-Proto'); + if (protocol) { + // this header doesn't have the colum at the end, so we added to be in line with URL#protocol, which has it + protocol = protocol + ':'; + } else { + // we fall back to the protocol of the request + protocol = url.protocol; + } + if (!host) { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host + host = request.headers.get('Host'); + } + // If we don't have a host and a protocol, it's impossible to proceed + if (host && protocol) { + // The header might have a port in their name, so we remove it + host = host.split(':')[0]; + try { + let locale; + const hostAsUrl = new URL(`${protocol}//${host}`); + for (const [domainKey, localeValue] of Object.entries( + this.#manifest.i18n.domainLookupTable + )) { + // This operation should be safe because we force the protocol via zod inside the configuration + // If not, then it means that the manifest was tampered + const domainKeyAsUrl = new URL(domainKey); + + if ( + hostAsUrl.host === domainKeyAsUrl.host && + hostAsUrl.protocol === domainKeyAsUrl.protocol + ) { + locale = localeValue; + break; + } + } + + if (locale) { + pathname = prependForwardSlash( + joinPaths(normalizeTheLocale(locale), this.removeBase(url.pathname)) + ); + if (url.pathname.endsWith('/')) { + pathname = appendForwardSlash(pathname); + } + } + } catch (e: any) { + this.#logger.error( + 'router', + `Astro tried to parse ${protocol}//${host} as an URL, but it threw a parsing error. Check the X-Forwarded-Host and X-Forwarded-Proto headers.` + ); + this.#logger.error('router', `Error: ${e}`); + } + } + } + return pathname; + } + async render(request: Request, options?: RenderOptions): Promise; /** * @deprecated Instead of passing `RouteData` and locals individually, pass an object with `routeData` and `locals` properties. diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index f6148796bf85..17b7c0872b0f 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -63,6 +63,7 @@ export type SSRManifestI18n = { routing: RoutingStrategies; locales: Locales; defaultLocale: string; + domainLookupTable: Record; }; export type SerializedSSRManifest = Omit< diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 25df50ad5bc0..b108da5f56c4 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'; import PQueue from 'p-queue'; import type { OutputAsset, OutputChunk } from 'rollup'; import type { + AstroConfig, AstroSettings, ComponentInstance, GetStaticPathsItem, @@ -66,6 +67,7 @@ import type { StylesheetAsset, } from './types.js'; import { getTimeStat, shouldAppendForwardSlash } from './util.js'; +import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js'; function createEntryURL(filePath: string, outFolder: URL) { return new URL('./' + filePath + `?time=${Date.now()}`, outFolder); @@ -179,9 +181,18 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn logger.info('SKIP_FORMAT', `\n${bgGreen(black(` ${verb} static routes `))}`); const builtPaths = new Set(); const pagesToGenerate = pipeline.retrieveRoutesToGenerate(); + const config = pipeline.getConfig(); if (ssr) { for (const [pageData, filePath] of pagesToGenerate) { if (pageData.route.prerender) { + // i18n domains won't work with pre rendered routes at the moment, so we need to to throw an error + if (config.experimental.i18nDomains) { + throw new AstroError({ + ...NoPrerenderedRoutesWithDomains, + message: NoPrerenderedRoutesWithDomains.message(pageData.component), + }); + } + const ssrEntryURLPage = createEntryURL(filePath, outFolder); const ssrEntryPage = await import(ssrEntryURLPage.toString()); if (opts.settings.adapter?.adapterFeatures?.functionPerRoute) { @@ -455,7 +466,8 @@ function getUrlForPath( pathname: string, base: string, origin: string, - format: 'directory' | 'file' | 'preserve', + format: AstroConfig['build']['format'], + trailingSlash: AstroConfig['trailingSlash'], routeType: RouteType ): URL { /** @@ -463,7 +475,7 @@ function getUrlForPath( * pathname: /, /foo * base: / */ - const ending = format === 'directory' ? '/' : '.html'; + const ending = format === 'directory' ? (trailingSlash === 'never' ? '' : '/') : '.html'; let buildPathname: string; if (pathname === '/' || pathname === '') { buildPathname = base; @@ -538,6 +550,7 @@ async function generatePath( pipeline.getConfig().base, pipeline.getStaticBuildOptions().origin, pipeline.getConfig().build.format, + pipeline.getConfig().trailingSlash, route.type ); @@ -649,6 +662,7 @@ function createBuildManifest( routing: settings.config.i18n.routing, defaultLocale: settings.config.i18n.defaultLocale, locales: settings.config.i18n.locales, + domainLookupTable: {}, }; } return { diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 538d7705e392..e5a1c1b065d3 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -16,6 +16,7 @@ import { getOutFile, getOutFolder } from '../common.js'; import { cssOrder, mergeInlineCss, type BuildInternals } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; import type { StaticBuildOptions } from '../types.js'; +import { normalizeTheLocale } from '../../../i18n/index.js'; const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@'; const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g'); @@ -153,6 +154,7 @@ function buildManifest( const { settings } = opts; const routes: SerializedRouteInfo[] = []; + const domainLookupTable: Record = {}; const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries()); if (settings.scripts.some((script) => script.stage === 'page')) { staticFiles.push(entryModules[PAGE_SCRIPT_ID]); @@ -229,6 +231,23 @@ function buildManifest( }); } + /** + * logic meant for i18n domain support, where we fill the lookup table + */ + const i18n = settings.config.i18n; + if ( + settings.config.experimental.i18nDomains && + i18n && + i18n.domains && + (i18n.routing === 'domains-prefix-always' || + i18n.routing === 'domains-prefix-other-locales' || + i18n.routing === 'domains-prefix-other-no-redirect') + ) { + for (const [locale, domainValue] of Object.entries(i18n.domains)) { + domainLookupTable[domainValue] = normalizeTheLocale(locale); + } + } + // HACK! Patch this special one. if (!(BEFORE_HYDRATION_SCRIPT_ID in entryModules)) { // Set this to an empty string so that the runtime knows not to try and load this. @@ -241,6 +260,7 @@ function buildManifest( routing: settings.config.i18n.routing, locales: settings.config.i18n.locales, defaultLocale: settings.config.i18n.defaultLocale, + domainLookupTable, }; } diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index c75c436bd210..95f33eb419fe 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -62,13 +62,17 @@ const ASTRO_CONFIG_DEFAULTS = { contentCollectionCache: false, clientPrerender: false, globalRoutePriority: false, + i18nDomains: false, }, } satisfies AstroUserConfig & { server: { open: boolean } }; export type RoutingStrategies = | 'pathname-prefix-always' | 'pathname-prefix-other-locales' - | 'pathname-prefix-always-no-redirect'; + | 'pathname-prefix-always-no-redirect' + | 'domains-prefix-always' + | 'domains-prefix-other-locales' + | 'domains-prefix-other-no-redirect'; export const AstroConfigSchema = z.object({ root: z @@ -330,6 +334,16 @@ export const AstroConfigSchema = z.object({ }), ]) ), + domains: z + .record( + z.string(), + z + .string() + .url( + "The domain value must be a valid URL, and it has to start with 'https' or 'http'." + ) + ) + .optional(), fallback: z.record(z.string(), z.string()).optional(), routing: z .object({ @@ -346,29 +360,43 @@ export const AstroConfigSchema = z.object({ message: 'The option `i18n.redirectToDefaultLocale` is only useful when the `i18n.prefixDefaultLocale` is set to `true`. Remove the option `i18n.redirectToDefaultLocale`, or change its value to `true`.', } - ) - .transform((routing) => { - let strategy: RoutingStrategies; - switch (routing.strategy) { - case 'pathname': { - if (routing.prefixDefaultLocale === true) { - if (routing.redirectToDefaultLocale) { - strategy = 'pathname-prefix-always'; - } else { - strategy = 'pathname-prefix-always-no-redirect'; - } - } else { - strategy = 'pathname-prefix-other-locales'; - } + ), + }) + .optional() + .transform((i18n) => { + if (i18n) { + let { routing, domains } = i18n; + let strategy: RoutingStrategies; + const hasDomains = domains ? Object.keys(domains).length > 0 : false; + if (!hasDomains) { + if (routing.prefixDefaultLocale === true) { + if (routing.redirectToDefaultLocale) { + strategy = 'pathname-prefix-always'; + } else { + strategy = 'pathname-prefix-always-no-redirect'; + } + } else { + strategy = 'pathname-prefix-other-locales'; + } + } else { + if (routing.prefixDefaultLocale === true) { + if (routing.redirectToDefaultLocale) { + strategy = 'domains-prefix-always'; + } else { + strategy = 'domains-prefix-other-no-redirect'; } + } else { + strategy = 'domains-prefix-other-locales'; } - return strategy; - }), + } + + return { ...i18n, routing: strategy }; + } + return undefined; }) - .optional() .superRefine((i18n, ctx) => { if (i18n) { - const { defaultLocale, locales: _locales, fallback } = i18n; + const { defaultLocale, locales: _locales, fallback, domains, routing } = i18n; const locales = _locales.map((locale) => { if (typeof locale === 'string') { return locale; @@ -406,6 +434,51 @@ export const AstroConfigSchema = z.object({ } } } + if (domains) { + const entries = Object.entries(domains); + if (entries.length > 0) { + if ( + routing !== 'domains-prefix-other-locales' && + routing !== 'domains-prefix-other-no-redirect' && + routing !== 'domains-prefix-always' + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `When specifying some domains, the property \`i18n.routingStrategy\` must be set to \`"domains"\`.`, + }); + } + } + + for (const [domainKey, domainValue] of Object.entries(domains)) { + if (!locales.includes(domainKey)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The locale \`${domainKey}\` key in the \`i18n.domains\` record doesn't exist in the \`i18n.locales\` array.`, + }); + } + if (!domainValue.startsWith('https') && !domainValue.startsWith('http')) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "The domain value must be a valid URL, and it has to start with 'https' or 'http'.", + path: ['domains'], + }); + } else { + try { + const domainUrl = new URL(domainValue); + if (domainUrl.pathname !== '/') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The URL \`${domainValue}\` must contain only the origin. A subsequent pathname isn't allowed here. Remove \`${domainUrl.pathname}\`.`, + path: ['domains'], + }); + } + } catch { + // no need to catch the error + } + } + } + } } }) ), @@ -427,6 +500,7 @@ export const AstroConfigSchema = z.object({ .boolean() .optional() .default(ASTRO_CONFIG_DEFAULTS.experimental.globalRoutePriority), + i18nDomains: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.i18nDomains), }) .strict( `Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.` @@ -547,6 +621,30 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: string) { .refine((obj) => !obj.outDir.toString().startsWith(obj.publicDir.toString()), { message: 'The value of `outDir` must not point to a path within the folder set as `publicDir`, this will cause an infinite loop', + }) + .superRefine((configuration, ctx) => { + const { site, experimental, i18n, output } = configuration; + if (experimental.i18nDomains) { + if ( + i18n?.routing === 'domains-prefix-other-locales' || + i18n?.routing === 'domains-prefix-other-no-redirect' || + i18n?.routing === 'domains-prefix-always' + ) { + if (!site) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "The option `site` isn't set. When using the 'domains' strategy for `i18n`, `site` is required to create absolute URLs for locales that aren't mapped to a domain.", + }); + } + if (output !== 'server') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Domain support is only available when `output` is `"server"`.', + }); + } + } + } }); return AstroConfigRelativeSchema; diff --git a/packages/astro/src/core/dev/restart.ts b/packages/astro/src/core/dev/restart.ts index 3d77ef0c0cc5..23e6af369abc 100644 --- a/packages/astro/src/core/dev/restart.ts +++ b/packages/astro/src/core/dev/restart.ts @@ -137,7 +137,7 @@ export async function createContainerWithAutomaticRestart({ } else { // Restart success. Add new watches because this is a new container with a new Vite server restart.container = result; - addWatches(); + setupContainer(); resolveRestart(null); } restartComplete = new Promise((resolve) => { @@ -153,8 +153,8 @@ export async function createContainerWithAutomaticRestart({ }; } - // Set up watches - function addWatches() { + // Set up watchers, vite restart API, and shortcuts + function setupContainer() { const watcher = restart.container.viteServer.watcher; watcher.on('change', handleChangeRestart('Configuration file updated.')); watcher.on('unlink', handleChangeRestart('Configuration file removed.')); @@ -163,7 +163,17 @@ export async function createContainerWithAutomaticRestart({ // Restart the Astro dev server instead of Vite's when the API is called by plugins. // Ignore the `forceOptimize` parameter for now. restart.container.viteServer.restart = () => handleServerRestart(); + + // Set up shortcuts, overriding Vite's default shortcuts so it works for Astro + restart.container.viteServer.bindCLIShortcuts({ + customShortcuts: [ + // Disable Vite's builtin "r" (restart server), "u" (print server urls) and "c" (clear console) shortcuts + { key: 'r', description: '' }, + { key: 'u', description: '' }, + { key: 'c', description: '' }, + ], + }); } - addWatches(); + setupContainer(); return restart; } diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 4af4d2e6430a..3fc459ef8bfb 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1017,6 +1017,18 @@ export const MissingIndexForInternationalization = { hint: (src: string) => `Create an index page (\`index.astro, index.md, etc.\`) in \`${src}\`.`, } satisfies ErrorData; +/** + * @docs + * @description + * Static pages aren't yet supported with i18n domains. If you wish to enable this feature, you have to disable pre-rendering. + */ +export const NoPrerenderedRoutesWithDomains = { + name: 'NoPrerenderedRoutesWithDomains', + title: "Pre-rendered routes aren't supported when internationalization domains are enabled.", + message: (component: string) => + `Static pages aren't yet supported with multiple domains. If you wish to enable this feature, you have to disable pre-rendering for the page ${component}`, +} satisfies ErrorData; + /** * @docs * @description diff --git a/packages/astro/src/core/logger/vite.ts b/packages/astro/src/core/logger/vite.ts index 8a3079158b55..9604a68f05d0 100644 --- a/packages/astro/src/core/logger/vite.ts +++ b/packages/astro/src/core/logger/vite.ts @@ -3,6 +3,7 @@ import stripAnsi from 'strip-ansi'; import type { Logger as ViteLogger, Rollup, LogLevel } from 'vite'; import { isAstroError } from '../errors/errors.js'; import { isLogLevelEnabled, type Logger as AstroLogger } from './core.js'; +import { serverShortcuts as formatServerShortcuts } from '../messages.js'; const PKG_PREFIX = fileURLToPath(new URL('../../../', import.meta.url)); const E2E_PREFIX = fileURLToPath(new URL('../../../e2e', import.meta.url)); @@ -16,6 +17,10 @@ const vitePageReloadMsg = /page reload (.*)( \(.*\))?/; const viteHmrUpdateMsg = /hmr update (.*)/; // capture "vite v5.0.0 building SSR bundle for production..." and "vite v5.0.0 building for production..." messages const viteBuildMsg = /vite.*building.*for production/; +// capture "\n Shortcuts" messages +const viteShortcutTitleMsg = /^\s*Shortcuts\s*$/s; +// capture "press * + enter to ..." messages +const viteShortcutHelpMsg = /press\s+(.*?)\s+to\s+(.*)$/s; export function createViteLogger( astroLogger: AstroLogger, @@ -42,10 +47,15 @@ export function createViteLogger( if (isAstroSrcFile(m[1])) return; astroLogger.info('watch', m[1]); } - // Don't log Vite build messages - else if (viteBuildMsg.test(stripped)) { + // Don't log Vite build messages and shortcut titles + else if (viteBuildMsg.test(stripped) || viteShortcutTitleMsg.test(stripped)) { // noop } + // Log shortcuts help messages without indent + else if (viteShortcutHelpMsg.test(stripped)) { + const [, key, label] = viteShortcutHelpMsg.exec(stripped)! as string[]; + astroLogger.info('SKIP_FORMAT', formatServerShortcuts({ key, label })); + } // Fallback else { astroLogger.info('vite', msg); diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts index b105e985cb05..5b424944feda 100644 --- a/packages/astro/src/core/messages.ts +++ b/packages/astro/src/core/messages.ts @@ -95,6 +95,11 @@ export function serverStart({ return messages.filter((msg) => typeof msg === 'string').join('\n'); } +/** Display custom dev server shortcuts */ +export function serverShortcuts({ key, label }: { key: string; label: string }): string { + return [dim(' Press'), key, dim('to'), label].join(' '); +} + export function telemetryNotice() { const headline = blue(`▶ Astro collects anonymous usage data.`); const why = ' This information helps us improve Astro.'; diff --git a/packages/astro/src/core/render/context.ts b/packages/astro/src/core/render/context.ts index 898e74d8f396..faba9b86a266 100644 --- a/packages/astro/src/core/render/context.ts +++ b/packages/astro/src/core/render/context.ts @@ -263,7 +263,10 @@ export function computeCurrentLocale( } } } - if (routingStrategy === 'pathname-prefix-other-locales') { + if ( + routingStrategy === 'pathname-prefix-other-locales' || + routingStrategy === 'domains-prefix-other-locales' + ) { return defaultLocale; } return undefined; diff --git a/packages/astro/src/i18n/index.ts b/packages/astro/src/i18n/index.ts index 4b882b7829df..6a8834b541d5 100644 --- a/packages/astro/src/i18n/index.ts +++ b/packages/astro/src/i18n/index.ts @@ -13,6 +13,8 @@ type GetLocaleRelativeUrl = GetLocaleOptions & { format: AstroConfig['build']['format']; routing?: RoutingStrategies; defaultLocale: string; + domains: Record | undefined; + path?: string; }; export type GetLocaleOptions = { @@ -21,10 +23,6 @@ export type GetLocaleOptions = { * @default true */ normalizeLocale?: boolean; - /** - * An optional path to add after the `locale`. - */ - path?: string; /** * An optional path to prepend to `locale`. */ @@ -33,6 +31,7 @@ export type GetLocaleOptions = { type GetLocaleAbsoluteUrl = GetLocaleRelativeUrl & { site: AstroConfig['site']; + isBuild: boolean; }; /** * The base URL @@ -75,22 +74,37 @@ export function getLocaleRelativeUrl({ /** * The absolute URL */ -export function getLocaleAbsoluteUrl({ site, ...rest }: GetLocaleAbsoluteUrl) { - const locale = getLocaleRelativeUrl(rest); - if (site) { - return joinPaths(site, locale); +export function getLocaleAbsoluteUrl({ site, isBuild, ...rest }: GetLocaleAbsoluteUrl) { + const localeUrl = getLocaleRelativeUrl(rest); + const { domains, locale } = rest; + let url; + if (isBuild && domains) { + const base = domains[locale]; + url = joinPaths(base, localeUrl.replace(`/${rest.locale}`, '')); + } else { + if (site) { + url = joinPaths(site, localeUrl); + } else { + url = localeUrl; + } + } + + if (shouldAppendForwardSlash(rest.trailingSlash, rest.format)) { + return appendForwardSlash(url); } else { - return locale; + return url; } } interface GetLocalesRelativeUrlList extends GetLocaleOptions { base: string; + path?: string; locales: Locales; trailingSlash: AstroConfig['trailingSlash']; format: AstroConfig['build']['format']; routing?: RoutingStrategies; defaultLocale: string; + domains: Record | undefined; } export function getLocaleRelativeUrlList({ @@ -100,7 +114,7 @@ export function getLocaleRelativeUrlList({ format, path, prependWith, - normalizeLocale = false, + normalizeLocale = true, routing = 'pathname-prefix-other-locales', defaultLocale, }: GetLocalesRelativeUrlList) { @@ -124,16 +138,58 @@ export function getLocaleRelativeUrlList({ } interface GetLocalesAbsoluteUrlList extends GetLocalesRelativeUrlList { - site?: string; + site?: AstroConfig['site']; + isBuild: boolean; } -export function getLocaleAbsoluteUrlList({ site, ...rest }: GetLocalesAbsoluteUrlList) { - const locales = getLocaleRelativeUrlList(rest); - return locales.map((locale) => { - if (site) { - return joinPaths(site, locale); +export function getLocaleAbsoluteUrlList({ + base, + locales: _locales, + trailingSlash, + format, + path, + prependWith, + normalizeLocale = true, + routing = 'pathname-prefix-other-locales', + defaultLocale, + isBuild, + domains, + site, +}: GetLocalesAbsoluteUrlList) { + const locales = toPaths(_locales); + return locales.map((currentLocale) => { + const pathsToJoin = []; + const normalizedLocale = normalizeLocale ? normalizeTheLocale(currentLocale) : currentLocale; + const domainBase = domains ? domains[currentLocale] : undefined; + if (isBuild && domainBase) { + if (domainBase) { + pathsToJoin.push(domainBase); + } else { + pathsToJoin.push(site); + } + pathsToJoin.push(base); + pathsToJoin.push(prependWith); } else { - return locale; + if (site) { + pathsToJoin.push(site); + } + pathsToJoin.push(base); + pathsToJoin.push(prependWith); + if ( + routing === 'pathname-prefix-always' || + routing === 'pathname-prefix-always-no-redirect' + ) { + pathsToJoin.push(normalizedLocale); + } else if (currentLocale !== defaultLocale) { + pathsToJoin.push(normalizedLocale); + } + } + + pathsToJoin.push(path); + if (shouldAppendForwardSlash(trailingSlash, format)) { + return appendForwardSlash(joinPaths(...pathsToJoin)); + } else { + return joinPaths(...pathsToJoin); } }); } diff --git a/packages/astro/src/i18n/middleware.ts b/packages/astro/src/i18n/middleware.ts index a7c50dc82d53..9fabff13af41 100644 --- a/packages/astro/src/i18n/middleware.ts +++ b/packages/astro/src/i18n/middleware.ts @@ -1,9 +1,16 @@ import { appendForwardSlash, joinPaths } from '@astrojs/internal-helpers/path'; -import type { Locales, MiddlewareHandler, RouteData, SSRManifest } from '../@types/astro.js'; +import type { + APIContext, + Locales, + MiddlewareHandler, + RouteData, + SSRManifest, +} from '../@types/astro.js'; import type { PipelineHookFunction } from '../core/pipeline.js'; import { getPathByLocale, normalizeTheLocale } from './index.js'; import { shouldAppendForwardSlash } from '../core/build/util.js'; import { ROUTE_DATA_SYMBOL } from '../core/constants.js'; +import type { SSRManifestI18n } from '../core/app/types.js'; const routeDataSymbol = Symbol.for(ROUTE_DATA_SYMBOL); @@ -25,6 +32,13 @@ function pathnameHasLocale(pathname: string, locales: Locales): boolean { return false; } +type MiddlewareOptions = { + i18n: SSRManifest['i18n']; + base: SSRManifest['base']; + trailingSlash: SSRManifest['trailingSlash']; + buildFormat: SSRManifest['buildFormat']; +}; + export function createI18nMiddleware( i18n: SSRManifest['i18n'], base: SSRManifest['base'], @@ -33,62 +47,130 @@ export function createI18nMiddleware( ): MiddlewareHandler { if (!i18n) return (_, next) => next(); + const prefixAlways = ( + url: URL, + response: Response, + context: APIContext + ): Response | undefined => { + if (url.pathname === base + '/' || url.pathname === base) { + if (shouldAppendForwardSlash(trailingSlash, buildFormat)) { + return context.redirect(`${appendForwardSlash(joinPaths(base, i18n.defaultLocale))}`); + } else { + return context.redirect(`${joinPaths(base, i18n.defaultLocale)}`); + } + } + + // Astro can't know where the default locale is supposed to be, so it returns a 404 with no content. + else if (!pathnameHasLocale(url.pathname, i18n.locales)) { + return new Response(null, { + status: 404, + headers: response.headers, + }); + } + + return undefined; + }; + + const prefixOtherLocales = (url: URL, response: Response): Response | undefined => { + const pathnameContainsDefaultLocale = url.pathname.includes(`/${i18n.defaultLocale}`); + if (pathnameContainsDefaultLocale) { + const newLocation = url.pathname.replace(`/${i18n.defaultLocale}`, ''); + response.headers.set('Location', newLocation); + return new Response(null, { + status: 404, + headers: response.headers, + }); + } + + return undefined; + }; + + /** + * We return a 404 if: + * - the current path isn't a root. e.g. / or / + * - the URL doesn't contain a locale + * @param url + * @param response + */ + const prefixAlwaysNoRedirect = (url: URL, response: Response): Response | undefined => { + // We return a 404 if: + // - the current path isn't a root. e.g. / or / + // - the URL doesn't contain a locale + const isRoot = url.pathname === base + '/' || url.pathname === base; + if (!(isRoot || pathnameHasLocale(url.pathname, i18n.locales))) { + return new Response(null, { + status: 404, + headers: response.headers, + }); + } + + return undefined; + }; + return async (context, next) => { const routeData: RouteData | undefined = Reflect.get(context.request, routeDataSymbol); // If the route we're processing is not a page, then we ignore it if (routeData?.type !== 'page' && routeData?.type !== 'fallback') { return await next(); } + const currentLocale = context.currentLocale; const url = context.url; const { locales, defaultLocale, fallback, routing } = i18n; const response = await next(); if (response instanceof Response) { - const pathnameContainsDefaultLocale = url.pathname.includes(`/${defaultLocale}`); switch (i18n.routing) { + case 'domains-prefix-other-locales': { + if (localeHasntDomain(i18n, currentLocale)) { + const result = prefixOtherLocales(url, response); + if (result) { + return result; + } + } + break; + } case 'pathname-prefix-other-locales': { - if (pathnameContainsDefaultLocale) { - const newLocation = url.pathname.replace(`/${defaultLocale}`, ''); - response.headers.set('Location', newLocation); - return new Response(null, { - status: 404, - headers: response.headers, - }); + const result = prefixOtherLocales(url, response); + if (result) { + return result; + } + break; + } + + case 'domains-prefix-other-no-redirect': { + if (localeHasntDomain(i18n, currentLocale)) { + const result = prefixAlwaysNoRedirect(url, response); + if (result) { + return result; + } } break; } case 'pathname-prefix-always-no-redirect': { - // We return a 404 if: - // - the current path isn't a root. e.g. / or / - // - the URL doesn't contain a locale - const isRoot = url.pathname === base + '/' || url.pathname === base; - if (!(isRoot || pathnameHasLocale(url.pathname, i18n.locales))) { - return new Response(null, { - status: 404, - headers: response.headers, - }); + const result = prefixAlwaysNoRedirect(url, response); + if (result) { + return result; } break; } case 'pathname-prefix-always': { - if (url.pathname === base + '/' || url.pathname === base) { - if (shouldAppendForwardSlash(trailingSlash, buildFormat)) { - return context.redirect(`${appendForwardSlash(joinPaths(base, i18n.defaultLocale))}`); - } else { - return context.redirect(`${joinPaths(base, i18n.defaultLocale)}`); - } + const result = prefixAlways(url, response, context); + if (result) { + return result; } - - // Astro can't know where the default locale is supposed to be, so it returns a 404 with no content. - else if (!pathnameHasLocale(url.pathname, i18n.locales)) { - return new Response(null, { - status: 404, - headers: response.headers, - }); + break; + } + case 'domains-prefix-always': { + if (localeHasntDomain(i18n, currentLocale)) { + const result = prefixAlways(url, response, context); + if (result) { + return result; + } } + break; } } @@ -138,3 +220,17 @@ export function createI18nMiddleware( export const i18nPipelineHook: PipelineHookFunction = (ctx) => { Reflect.set(ctx.request, routeDataSymbol, ctx.route); }; + +/** + * 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/vite-plugin-i18n.ts b/packages/astro/src/i18n/vite-plugin-i18n.ts index 2ce015c26521..7aa4d327b223 100644 --- a/packages/astro/src/i18n/vite-plugin-i18n.ts +++ b/packages/astro/src/i18n/vite-plugin-i18n.ts @@ -13,6 +13,7 @@ export interface I18nInternalConfig extends Pick, Pick { i18n: AstroConfig['i18n']; + isBuild: boolean; } export default function astroInternationalization({ @@ -28,8 +29,15 @@ export default function astroInternationalization({ return { name: 'astro:i18n', enforce: 'pre', - config(config) { - const i18nConfig: I18nInternalConfig = { base, format, site, trailingSlash, i18n }; + config(config, { command }) { + const i18nConfig: I18nInternalConfig = { + base, + format, + site, + trailingSlash, + i18n, + isBuild: command === 'build', + }; return { define: { __ASTRO_INTERNAL_I18N_CONFIG__: JSON.stringify(i18nConfig), diff --git a/packages/astro/src/integrations/astroFeaturesValidation.ts b/packages/astro/src/integrations/astroFeaturesValidation.ts index 8bae77846c29..ce0f1b9c41ee 100644 --- a/packages/astro/src/integrations/astroFeaturesValidation.ts +++ b/packages/astro/src/integrations/astroFeaturesValidation.ts @@ -1,4 +1,5 @@ import type { + AstroAdapterFeatures, AstroAssetsFeature, AstroConfig, AstroFeatureMap, @@ -32,6 +33,7 @@ export function validateSupportedFeatures( adapterName: string, featureMap: AstroFeatureMap, config: AstroConfig, + adapterFeatures: AstroAdapterFeatures | undefined, logger: Logger ): ValidationResult { const { @@ -39,6 +41,7 @@ export function validateSupportedFeatures( serverOutput = UNSUPPORTED, staticOutput = UNSUPPORTED, hybridOutput = UNSUPPORTED, + i18nDomains = UNSUPPORTED, } = featureMap; const validationResult: ValidationResult = {}; @@ -67,6 +70,24 @@ export function validateSupportedFeatures( ); validationResult.assets = validateAssetsFeature(assets, adapterName, config, logger); + if (i18nDomains) { + validationResult.i18nDomains = validateSupportKind( + i18nDomains, + adapterName, + logger, + 'i18nDomains', + () => { + return config?.output === 'server' && !config?.site; + } + ); + if (adapterFeatures?.functionPerRoute) { + logger.error( + 'config', + 'The Astro feature `i18nDomains` is incompatible with the Adapter feature `functionPerRoute`' + ); + } + } + return validationResult; } diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts index d082c438fbaa..63d526ad4c52 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -257,6 +257,8 @@ export async function runHookConfigDone({ adapter.name, adapter.supportedAstroFeatures, settings.config, + // SAFETY: we checked before if it's not present, and we throw an error + adapter.adapterFeatures, logger ); for (const [featureName, supported] of Object.entries(validationResult)) { @@ -503,11 +505,3 @@ export function isFunctionPerRouteEnabled(adapter: AstroAdapter | undefined): bo return false; } } - -export function isEdgeMiddlewareEnabled(adapter: AstroAdapter | undefined): boolean { - if (adapter?.adapterFeatures?.edgeMiddleware === true) { - return true; - } else { - return false; - } -} diff --git a/packages/astro/src/runtime/server/endpoint.ts b/packages/astro/src/runtime/server/endpoint.ts index afafb21c7dad..49f00224e61d 100644 --- a/packages/astro/src/runtime/server/endpoint.ts +++ b/packages/astro/src/runtime/server/endpoint.ts @@ -42,6 +42,12 @@ export async function renderEndpoint( const response = await handler.call(mod, context); // Endpoints explicitly returning 404 or 500 response status should // NOT be subject to rerouting to 404.astro or 500.astro. - response.headers.set(REROUTE_DIRECTIVE_HEADER, 'no'); + if (response.status === 404 || response.status === 500) { + // Only `Response.redirect` headers are immutable, therefore a `try..catch` is not necessary. + // Note: `Response.redirect` can only be called with HTTP status codes: 301, 302, 303, 307, 308. + // Source: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#parameters + response.headers.set(REROUTE_DIRECTIVE_HEADER, 'no'); + } + return response; } diff --git a/packages/astro/src/virtual-modules/i18n.ts b/packages/astro/src/virtual-modules/i18n.ts index aff49a492dc0..f71068667787 100644 --- a/packages/astro/src/virtual-modules/i18n.ts +++ b/packages/astro/src/virtual-modules/i18n.ts @@ -2,9 +2,10 @@ import * as I18nInternals from '../i18n/index.js'; import type { I18nInternalConfig } from '../i18n/vite-plugin-i18n.js'; export { normalizeTheLocale, toCodes, toPaths } from '../i18n/index.js'; -// @ts-expect-error -const { trailingSlash, format, site, i18n } = __ASTRO_INTERNAL_I18N_CONFIG__ as I18nInternalConfig; -const { defaultLocale, locales, routing } = i18n!; +const { trailingSlash, format, site, i18n, isBuild } = + // @ts-expect-error + __ASTRO_INTERNAL_I18N_CONFIG__ as I18nInternalConfig; +const { defaultLocale, locales, routing, domains } = i18n!; const base = import.meta.env.BASE_URL; export type GetLocaleOptions = I18nInternals.GetLocaleOptions; @@ -40,6 +41,7 @@ export const getRelativeLocaleUrl = (locale: string, path?: string, options?: Ge defaultLocale, locales, routing, + domains, ...options, }); @@ -68,7 +70,7 @@ export const getRelativeLocaleUrl = (locale: string, path?: string, options?: Ge * getAbsoluteLocaleUrl("es_US", "getting-started", { prependWith: "blog", normalizeLocale: false }); // https://example.com/blog/es_US/getting-started * ``` */ -export const getAbsoluteLocaleUrl = (locale: string, path = '', options?: GetLocaleOptions) => +export const getAbsoluteLocaleUrl = (locale: string, path?: string, options?: GetLocaleOptions) => I18nInternals.getLocaleAbsoluteUrl({ locale, path, @@ -79,6 +81,8 @@ export const getAbsoluteLocaleUrl = (locale: string, path = '', options?: GetLoc defaultLocale, locales, routing, + domains, + isBuild, ...options, }); @@ -97,6 +101,7 @@ export const getRelativeLocaleUrlList = (path?: string, options?: GetLocaleOptio defaultLocale, locales, routing, + domains, ...options, }); @@ -116,6 +121,8 @@ export const getAbsoluteLocaleUrlList = (path?: string, options?: GetLocaleOptio defaultLocale, locales, routing, + domains, + isBuild, ...options, }); diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index f1162b7bc967..ba33c3ebd44b 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -121,6 +121,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest routing: settings.config.i18n.routing, defaultLocale: settings.config.i18n.defaultLocale, locales: settings.config.i18n.locales, + domainLookupTable: {}, }; } return { diff --git a/packages/astro/src/vite-plugin-astro-server/response.ts b/packages/astro/src/vite-plugin-astro-server/response.ts index ac36e703b072..75649a714482 100644 --- a/packages/astro/src/vite-plugin-astro-server/response.ts +++ b/packages/astro/src/vite-plugin-astro-server/response.ts @@ -62,16 +62,10 @@ export async function writeWebResponse(res: http.ServerResponse, webResponse: Re res.setHeader('set-cookie', setCookieHeaders); } - const _headers = Object.fromEntries(headers.entries()); + const _headers: http.OutgoingHttpHeaders = Object.fromEntries(headers.entries()); - // Undici 5.20.0+ includes a `getSetCookie` helper that returns an array of all the `set-cookies` headers. - // Previously, `headers.entries()` would already have these merged, but it seems like this isn't the case anymore. if (headers.has('set-cookie')) { - if ('getSetCookie' in headers && typeof headers.getSetCookie === 'function') { - _headers['set-cookie'] = headers.getSetCookie().toString(); - } else { - _headers['set-cookie'] = headers.get('set-cookie')!; - } + _headers['set-cookie'] = headers.getSetCookie(); } res.writeHead(status, _headers); diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 3c2eea14dc96..9f9951b7e313 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -309,7 +309,7 @@ export async function handleRoute({ const onRequest: MiddlewareHandler = middleware.onRequest; if (config.i18n) { const i18Middleware = createI18nMiddleware( - config.i18n, + manifest.i18n, config.base, config.trailingSlash, config.build.format diff --git a/packages/astro/src/vite-plugin-markdown/content-entry-type.ts b/packages/astro/src/vite-plugin-markdown/content-entry-type.ts index dc32969747da..131422298d3f 100644 --- a/packages/astro/src/vite-plugin-markdown/content-entry-type.ts +++ b/packages/astro/src/vite-plugin-markdown/content-entry-type.ts @@ -1,10 +1,11 @@ +import { fileURLToPath } from 'node:url'; import type { ContentEntryType } from '../@types/astro.js'; -import { parseFrontmatter } from '../content/utils.js'; +import { safeParseFrontmatter } from '../content/utils.js'; export const markdownContentEntryType: ContentEntryType = { extensions: ['.md'], - async getEntryInfo({ contents }: { contents: string }) { - const parsed = parseFrontmatter(contents); + async getEntryInfo({ contents, fileUrl }: { contents: string; fileUrl: URL }) { + const parsed = safeParseFrontmatter(contents, fileURLToPath(fileUrl)); return { data: parsed.data, body: parsed.content, diff --git a/packages/astro/src/vite-plugin-markdown/images.ts b/packages/astro/src/vite-plugin-markdown/images.ts index 1488ef6fc8c4..751deff21518 100644 --- a/packages/astro/src/vite-plugin-markdown/images.ts +++ b/packages/astro/src/vite-plugin-markdown/images.ts @@ -4,7 +4,10 @@ export function getMarkdownCodeForImages(imagePaths: MarkdownImagePath[], html: return ` import { getImage } from "astro:assets"; ${imagePaths - .map((entry) => `import Astro__${entry.safeName} from ${JSON.stringify(entry.raw)};`) + .map((entry) => { + const prefix = entry.raw.includes('/') ? '' : './'; + return `import Astro__${entry.safeName} from ${JSON.stringify(prefix + entry.raw)};`; + }) .join('\n')} const images = async function(html) { @@ -23,7 +26,7 @@ export function getMarkdownCodeForImages(imagePaths: MarkdownImagePath[], html: const matchKey = ${rawUrl} + '_' + occurrenceCounter; const imageProps = JSON.parse(match[1].replace(/"/g, '"')); const { src, ...props } = imageProps; - + imageSources[matchKey] = await getImage({src: Astro__${entry.safeName}, ...props}); occurrenceCounter++; } diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index cc138149b1c4..445b69d31034 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -3,14 +3,14 @@ import { InvalidAstroDataError, type MarkdownProcessor, } from '@astrojs/markdown-remark'; -import matter from 'gray-matter'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { Plugin } from 'vite'; import { normalizePath } from 'vite'; import type { AstroSettings } from '../@types/astro.js'; -import { AstroError, AstroErrorData, MarkdownError } from '../core/errors/index.js'; +import { AstroError, AstroErrorData } from '../core/errors/index.js'; +import { safeParseFrontmatter } from '../content/utils.js'; import type { Logger } from '../core/logger/core.js'; import { isMarkdownFile } from '../core/util.js'; import { shorthash } from '../runtime/server/shorthash.js'; @@ -23,33 +23,6 @@ interface AstroPluginOptions { logger: Logger; } -function safeMatter(source: string, id: string) { - try { - return matter(source); - } catch (err: any) { - const markdownError = new MarkdownError({ - name: 'MarkdownError', - message: err.message, - stack: err.stack, - location: { - file: id, - }, - }); - - if (err.name === 'YAMLException') { - markdownError.setLocation({ - file: id, - line: err.mark.line, - column: err.mark.column, - }); - - markdownError.setMessage(err.reason); - } - - throw markdownError; - } -} - const astroServerRuntimeModulePath = normalizePath( fileURLToPath(new URL('../runtime/server/index.js', import.meta.url)) ); @@ -75,7 +48,7 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug if (isMarkdownFile(id)) { const { fileId, fileUrl } = getFileInfo(id, settings.config); const rawFile = await fs.promises.readFile(fileId, 'utf-8'); - const raw = safeMatter(rawFile, id); + const raw = safeParseFrontmatter(rawFile, id); const fileURL = pathToFileURL(fileId); diff --git a/packages/astro/test/astro-get-static-paths.test.js b/packages/astro/test/astro-get-static-paths.test.js index 66aa5b94d042..3e35d6c856ea 100644 --- a/packages/astro/test/astro-get-static-paths.test.js +++ b/packages/astro/test/astro-get-static-paths.test.js @@ -10,6 +10,7 @@ describe('getStaticPaths - build calls', () => { fixture = await loadFixture({ root: './fixtures/astro-get-static-paths/', site: 'https://mysite.dev/', + trailingSlash: 'never', base: '/blog', }); await fixture.build(); @@ -29,7 +30,7 @@ describe('getStaticPaths - build calls', () => { const html = await fixture.readFile('/food/tacos/index.html'); const $ = cheerio.load(html); - expect($('#url').text()).to.equal('/blog/food/tacos/'); + expect($('#url').text()).to.equal('/blog/food/tacos'); }); }); diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js index fbaedd9a1e16..447079fa2dca 100644 --- a/packages/astro/test/core-image.test.js +++ b/packages/astro/test/core-image.test.js @@ -480,7 +480,14 @@ describe('astro:image', () => { it('Adds the tags', () => { let $img = $('img'); - expect($img).to.have.a.lengthOf(7); + expect($img).to.have.a.lengthOf(8); + }); + + it('image in cc folder is processed', () => { + let $imgs = $('img'); + let $blogfolderimg = $($imgs[7]); + expect($blogfolderimg.attr('src')).to.include('blogfolder.jpg'); + expect($blogfolderimg.attr('src').endsWith('f=webp')).to.equal(true); }); it('has proper source for directly used image', () => { diff --git a/packages/astro/test/fixtures/core-image/src/content/blog/blogfolder.jpg b/packages/astro/test/fixtures/core-image/src/content/blog/blogfolder.jpg new file mode 100644 index 000000000000..e859ac3c992f Binary files /dev/null and b/packages/astro/test/fixtures/core-image/src/content/blog/blogfolder.jpg differ diff --git a/packages/astro/test/fixtures/core-image/src/content/blog/one.md b/packages/astro/test/fixtures/core-image/src/content/blog/one.md index 8c6522534185..1ddcce919440 100644 --- a/packages/astro/test/fixtures/core-image/src/content/blog/one.md +++ b/packages/astro/test/fixtures/core-image/src/content/blog/one.md @@ -12,3 +12,5 @@ refinedImage: ../../assets/penguin1.jpg # A post text here + +![](blogfolder.jpg) diff --git a/packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs b/packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs index 58eb50540ec1..7a612488d4ca 100644 --- a/packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs +++ b/packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs @@ -13,5 +13,5 @@ export default defineConfig({ prefixDefaultLocale: true } }, - base: "/new-site" + base: "/new-site" }) diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs b/packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs new file mode 100644 index 000000000000..d8fe7b4a5599 --- /dev/null +++ b/packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs @@ -0,0 +1,24 @@ +import { defineConfig} from "astro/config"; + +export default defineConfig({ + output: "server", + trailingSlash: "never", + i18n: { + defaultLocale: 'en', + locales: [ + 'en', 'pt', 'it' + ], + domains: { + pt: "https://example.pt", + it: "http://it.example.com" + }, + routing: { + prefixDefaultLocale: true, + redirectToDefaultLocale: false + } + }, + experimental: { + i18nDomains: true + }, + site: "https://example.com", +}) diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/package.json b/packages/astro/test/fixtures/i18n-routing-subdomain/package.json new file mode 100644 index 000000000000..931425fa6206 --- /dev/null +++ b/packages/astro/test/fixtures/i18n-routing-subdomain/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/i18n-routing-subdomain", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/blog/[id].astro b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/blog/[id].astro new file mode 100644 index 000000000000..97b41230d6e9 --- /dev/null +++ b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/blog/[id].astro @@ -0,0 +1,18 @@ +--- +export function getStaticPaths() { + return [ + {params: {id: '1'}, props: { content: "Hello world" }}, + {params: {id: '2'}, props: { content: "Eat Something" }}, + {params: {id: '3'}, props: { content: "How are you?" }}, + ]; +} +const { content } = Astro.props; +--- + + + Astro + + +{content} + + diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/index.astro b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/index.astro new file mode 100644 index 000000000000..3e50ac6bf3cb --- /dev/null +++ b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/index.astro @@ -0,0 +1,8 @@ + + + Astro + + +Hello + + diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/start.astro b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/start.astro new file mode 100644 index 000000000000..990baecd9a8c --- /dev/null +++ b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/start.astro @@ -0,0 +1,8 @@ + + + Astro + + +Start + + diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/index.astro b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/index.astro new file mode 100644 index 000000000000..d138455a3e3f --- /dev/null +++ b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getRelativeLocaleUrl, getAbsoluteLocaleUrl, getPathByLocale, getLocaleByPath } from "astro:i18n"; + +let absoluteLocaleUrl_pt = getAbsoluteLocaleUrl("pt", "about"); +let absoluteLocaleUrl_it = getAbsoluteLocaleUrl("it"); + +--- + + + + Astro + + + Virtual module doesn't break + + Absolute URL pt: {absoluteLocaleUrl_pt} + Absolute URL it: {absoluteLocaleUrl_it} + + diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/blog/[id].astro b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/blog/[id].astro new file mode 100644 index 000000000000..e37f83a30243 --- /dev/null +++ b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/blog/[id].astro @@ -0,0 +1,18 @@ +--- +export function getStaticPaths() { + return [ + {params: {id: '1'}, props: { content: "Hola mundo" }}, + {params: {id: '2'}, props: { content: "Eat Something" }}, + {params: {id: '3'}, props: { content: "How are you?" }}, + ]; +} +const { content } = Astro.props; +--- + + + Astro + + +{content} + + diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/start.astro b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/start.astro new file mode 100644 index 000000000000..5a4a84c2cf0c --- /dev/null +++ b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/start.astro @@ -0,0 +1,8 @@ + + + Astro + + +Oi essa e start + + diff --git a/packages/astro/test/fixtures/i18n-routing/astro.config.mjs b/packages/astro/test/fixtures/i18n-routing/astro.config.mjs index 42559e778a30..8868d2b1f665 100644 --- a/packages/astro/test/fixtures/i18n-routing/astro.config.mjs +++ b/packages/astro/test/fixtures/i18n-routing/astro.config.mjs @@ -11,6 +11,6 @@ export default defineConfig({ path: "spanish", codes: ["es", "es-SP"] } - ] + ], } }) diff --git a/packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro b/packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro index 58141fec05ce..4b0a97b6eed5 100644 --- a/packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro +++ b/packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro @@ -2,7 +2,7 @@ --- export function getStaticPaths() { return [ - { id: "lorem" } + { params: {id: "lorem"}} ] } const currentLocale = Astro.currentLocale; diff --git a/packages/astro/test/fixtures/i18n-routing/src/pages/virtual-module.astro b/packages/astro/test/fixtures/i18n-routing/src/pages/virtual-module.astro index ca33030dbed0..f65356e8fab1 100644 --- a/packages/astro/test/fixtures/i18n-routing/src/pages/virtual-module.astro +++ b/packages/astro/test/fixtures/i18n-routing/src/pages/virtual-module.astro @@ -1,10 +1,12 @@ --- -import { getRelativeLocaleUrl, getPathByLocale, getLocaleByPath } from "astro:i18n"; +import { getRelativeLocaleUrl, getAbsoluteLocaleUrl, getPathByLocale, getLocaleByPath } from "astro:i18n"; let about = getRelativeLocaleUrl("pt", "about"); let spanish = getRelativeLocaleUrl("es", "about"); let spainPath = getPathByLocale("es-SP"); let localeByPath = getLocaleByPath("spanish"); +let italianAbout = getAbsoluteLocaleUrl("it", "about"); + --- @@ -18,5 +20,6 @@ let localeByPath = getLocaleByPath("spanish"); About spanish: {spanish} Spain path: {spainPath} Preferred path: {localeByPath} + About it: {italianAbout} diff --git a/packages/astro/test/i18n-routing.test.js b/packages/astro/test/i18n-routing.test.js index da9fdc7c8316..b3d496ff41ba 100644 --- a/packages/astro/test/i18n-routing.test.js +++ b/packages/astro/test/i18n-routing.test.js @@ -29,6 +29,34 @@ describe('astro:i18n virtual module', () => { expect(text).includes('About spanish: /spanish/about'); expect(text).includes('Spain path: spanish'); expect(text).includes('Preferred path: es'); + expect(text).includes('About it: /it/about'); + }); + + 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); + expect(response.status).to.equal(200); + + let html = await response.text(); + let $ = cheerio.load(html); + + console.log(html); + expect($('body').text()).includes("Virtual module doesn't break"); + expect($('body').text()).includes('Absolute URL pt: https://example.pt/about'); + expect($('body').text()).includes('Absolute URL it: http://it.example.com/'); + }); }); }); describe('[DEV] i18n routing', () => { @@ -1613,4 +1641,67 @@ describe('i18n routing does not break assets and endpoints', () => { expect(await response.text()).includes('lorem'); }); }); + + 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(), + }); + 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); + expect(response.status).to.equal(200); + expect(await response.text()).includes('Oi essa e start\n'); + }); + + 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); + expect(response.status).to.equal(200); + expect(await response.text()).includes('Oi essa e start\n'); + }); + + 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); + expect(response.status).to.equal(200); + expect(await response.text()).includes('Oi essa e start\n'); + }); + + 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); + expect(response.status).to.equal(200); + expect(await response.text()).includes('Oi essa e start\n'); + }); + }); }); diff --git a/packages/astro/test/units/config/config-validate.test.js b/packages/astro/test/units/config/config-validate.test.js index 477bc4bedac3..d2efbbafba4c 100644 --- a/packages/astro/test/units/config/config-validate.test.js +++ b/packages/astro/test/units/config/config-validate.test.js @@ -213,5 +213,132 @@ describe('Config Validation', () => { 'The option `i18n.redirectToDefaultLocale` is only useful when the `i18n.prefixDefaultLocale` is set to `true`. Remove the option `i18n.redirectToDefaultLocale`, or change its value to `true`.' ); }); + + it('errors if a domains key does not exist', async () => { + const configError = await validateConfig( + { + output: 'server', + i18n: { + defaultLocale: 'en', + locales: ['es', 'en'], + domains: { + lorem: 'https://example.com', + }, + }, + }, + process.cwd() + ).catch((err) => err); + expect(configError instanceof z.ZodError).to.equal(true); + expect(configError.errors[0].message).to.equal( + "The locale `lorem` key in the `i18n.domains` record doesn't exist in the `i18n.locales` array." + ); + }); + + it('errors if a domains value is not an URL', async () => { + const configError = await validateConfig( + { + output: 'server', + i18n: { + defaultLocale: 'en', + locales: ['es', 'en'], + domains: { + en: 'www.example.com', + }, + }, + }, + process.cwd() + ).catch((err) => err); + expect(configError instanceof z.ZodError).to.equal(true); + expect(configError.errors[0].message).to.equal( + "The domain value must be a valid URL, and it has to start with 'https' or 'http'." + ); + }); + + it('errors if a domains value is not an URL with incorrect protocol', async () => { + const configError = await validateConfig( + { + output: 'server', + i18n: { + defaultLocale: 'en', + locales: ['es', 'en'], + domains: { + en: 'tcp://www.example.com', + }, + }, + }, + process.cwd() + ).catch((err) => err); + expect(configError instanceof z.ZodError).to.equal(true); + expect(configError.errors[0].message).to.equal( + "The domain value must be a valid URL, and it has to start with 'https' or 'http'." + ); + }); + + it('errors if a domain is a URL with a pathname that is not the home', async () => { + const configError = await validateConfig( + { + output: 'server', + i18n: { + defaultLocale: 'en', + locales: ['es', 'en'], + domains: { + en: 'https://www.example.com/blog/page/', + }, + }, + }, + process.cwd() + ).catch((err) => err); + expect(configError instanceof z.ZodError).to.equal(true); + expect(configError.errors[0].message).to.equal( + "The URL `https://www.example.com/blog/page/` must contain only the origin. A subsequent pathname isn't allowed here. Remove `/blog/page/`." + ); + }); + + it('errors if domains is enabled but site is not provided', async () => { + const configError = await validateConfig( + { + output: 'server', + i18n: { + defaultLocale: 'en', + locales: ['es', 'en'], + domains: { + en: 'https://www.example.com/', + }, + }, + experimental: { + i18nDomains: true, + }, + }, + process.cwd() + ).catch((err) => err); + expect(configError instanceof z.ZodError).to.equal(true); + expect(configError.errors[0].message).to.equal( + "The option `site` isn't set. When using the 'domains' strategy for `i18n`, `site` is required to create absolute URLs for locales that aren't mapped to a domain." + ); + }); + + it('errors if domains is enabled but the `output` is not "server"', async () => { + const configError = await validateConfig( + { + output: 'static', + i18n: { + defaultLocale: 'en', + locales: ['es', 'en'], + domains: { + en: 'https://www.example.com/', + }, + }, + experimental: { + i18nDomains: true, + }, + site: 'https://foo.org', + }, + process.cwd() + ).catch((err) => err); + expect(configError instanceof z.ZodError).to.equal(true); + expect(configError.errors[0].message).to.equal( + 'Domain support is only available when `output` is `"server"`.' + ); + }); }); }); diff --git a/packages/astro/test/units/i18n/astro_i18n.test.js b/packages/astro/test/units/i18n/astro_i18n.test.js index 011193accdeb..e3364924af8f 100644 --- a/packages/astro/test/units/i18n/astro_i18n.test.js +++ b/packages/astro/test/units/i18n/astro_i18n.test.js @@ -142,18 +142,16 @@ describe('getLocaleRelativeUrl', () => { * @type {import("../../../dist/@types").AstroUserConfig} */ const config = { - experimental: { - i18n: { - defaultLocale: 'en', - locales: [ - 'en', - 'es', - { - path: 'italiano', - codes: ['it', 'it-VA'], - }, - ], - }, + i18n: { + defaultLocale: 'en', + locales: [ + 'en', + 'es', + { + path: 'italiano', + codes: ['it', 'it-VA'], + }, + ], }, }; // directory format @@ -161,7 +159,7 @@ describe('getLocaleRelativeUrl', () => { getLocaleRelativeUrl({ locale: 'en', base: '/blog', - ...config.experimental.i18n, + ...config.i18n, trailingSlash: 'never', format: 'directory', }) @@ -170,7 +168,7 @@ describe('getLocaleRelativeUrl', () => { getLocaleRelativeUrl({ locale: 'es', base: '/blog/', - ...config.experimental.i18n, + ...config.i18n, trailingSlash: 'always', format: 'directory', }) @@ -180,7 +178,7 @@ describe('getLocaleRelativeUrl', () => { getLocaleRelativeUrl({ locale: 'it-VA', base: '/blog/', - ...config.experimental.i18n, + ...config.i18n, trailingSlash: 'always', format: 'file', }) @@ -190,7 +188,7 @@ describe('getLocaleRelativeUrl', () => { getLocaleRelativeUrl({ locale: 'en', base: '/blog/', - ...config.experimental.i18n, + ...config.i18n, trailingSlash: 'ignore', format: 'directory', }) @@ -201,7 +199,7 @@ describe('getLocaleRelativeUrl', () => { getLocaleRelativeUrl({ locale: 'en', base: '/blog', - ...config.experimental.i18n, + ...config.i18n, trailingSlash: 'never', format: 'file', }) @@ -210,7 +208,7 @@ describe('getLocaleRelativeUrl', () => { getLocaleRelativeUrl({ locale: 'es', base: '/blog/', - ...config.experimental.i18n, + ...config.i18n, trailingSlash: 'always', format: 'file', }) @@ -221,7 +219,7 @@ describe('getLocaleRelativeUrl', () => { locale: 'en', // ignore + file => no trailing slash base: '/blog', - ...config.experimental.i18n, + ...config.i18n, trailingSlash: 'ignore', format: 'file', }) @@ -461,7 +459,7 @@ describe('getLocaleRelativeUrlList', () => { trailingSlash: 'never', format: 'directory', }) - ).to.have.members(['/blog', '/blog/en_US', '/blog/es', '/blog/italiano']); + ).to.have.members(['/blog', '/blog/en-us', '/blog/es', '/blog/italiano']); }); it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: always]', () => { @@ -494,7 +492,7 @@ describe('getLocaleRelativeUrlList', () => { trailingSlash: 'always', format: 'directory', }) - ).to.have.members(['/blog/', '/blog/en_US/', '/blog/es/', '/blog/italiano/']); + ).to.have.members(['/blog/', '/blog/en-us/', '/blog/es/', '/blog/italiano/']); }); it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: always]', () => { @@ -519,7 +517,7 @@ describe('getLocaleRelativeUrlList', () => { trailingSlash: 'always', format: 'file', }) - ).to.have.members(['/blog/', '/blog/en_US/', '/blog/es/']); + ).to.have.members(['/blog/', '/blog/en-us/', '/blog/es/']); }); it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: never]', () => { @@ -544,7 +542,7 @@ describe('getLocaleRelativeUrlList', () => { trailingSlash: 'never', format: 'file', }) - ).to.have.members(['/blog', '/blog/en_US', '/blog/es']); + ).to.have.members(['/blog', '/blog/en-us', '/blog/es']); }); it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: ignore]', () => { @@ -569,7 +567,7 @@ describe('getLocaleRelativeUrlList', () => { trailingSlash: 'ignore', format: 'file', }) - ).to.have.members(['/blog', '/blog/en_US', '/blog/es']); + ).to.have.members(['/blog', '/blog/en-us', '/blog/es']); }); it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: ignore]', () => { @@ -594,7 +592,7 @@ describe('getLocaleRelativeUrlList', () => { trailingSlash: 'ignore', format: 'directory', }) - ).to.have.members(['/blog/', '/blog/en_US/', '/blog/es/']); + ).to.have.members(['/blog/', '/blog/en-us/', '/blog/es/']); }); it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: never, routingStategy: pathname-prefix-always]', () => { @@ -620,7 +618,7 @@ describe('getLocaleRelativeUrlList', () => { trailingSlash: 'never', format: 'directory', }) - ).to.have.members(['/blog/en', '/blog/en_US', '/blog/es']); + ).to.have.members(['/blog/en', '/blog/en-us', '/blog/es']); }); it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: never, routingStategy: pathname-prefix-always-no-redirect]', () => { @@ -646,456 +644,806 @@ describe('getLocaleRelativeUrlList', () => { trailingSlash: 'never', format: 'directory', }) - ).to.have.members(['/blog/en', '/blog/en_US', '/blog/es']); + ).to.have.members(['/blog/en', '/blog/en-us', '/blog/es']); }); }); describe('getLocaleAbsoluteUrl', () => { - it('should correctly return the URL with the base', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ - const config = { - base: '/blog', - experimental: { - i18n: { - defaultLocale: 'en', - locales: [ - 'en', - 'en_US', - 'es', - { - path: 'italiano', - codes: ['it', 'it-VA'], + describe('with [prefix-other-locales]', () => { + it('should correctly return the URL with the base', () => { + /** + * + * @type {import("../../../dist/@types").AstroUserConfig} + */ + const config = { + base: '/blog', + experimental: { + i18n: { + defaultLocale: 'en', + locales: [ + 'en', + 'en_US', + 'es', + { + path: 'italiano', + codes: ['it', 'it-VA'], + }, + ], + domains: { + es: 'https://es.example.com', }, - ], + routingStrategy: 'prefix-other-locales', + }, }, - }, - }; - - // directory format - expect( - getLocaleAbsoluteUrl({ - locale: 'en', - base: '/blog/', - trailingSlash: 'always', - format: 'directory', - site: 'https://example.com', - ...config.experimental.i18n, - }) - ).to.eq('https://example.com/blog/'); - expect( - getLocaleAbsoluteUrl({ - locale: 'es', - base: '/blog/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'directory', - site: 'https://example.com', - }) - ).to.eq('https://example.com/blog/es/'); - - expect( - getLocaleAbsoluteUrl({ - locale: 'en_US', - base: '/blog/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'directory', - site: 'https://example.com', - }) - ).to.throw; - - // file format - expect( - getLocaleAbsoluteUrl({ - locale: 'en', - base: '/blog/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'file', - site: 'https://example.com', - }) - ).to.eq('https://example.com/blog/'); - expect( - getLocaleAbsoluteUrl({ - locale: 'es', - base: '/blog/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'file', - site: 'https://example.com', - }) - ).to.eq('https://example.com/blog/es/'); - - expect( - getLocaleAbsoluteUrl({ - locale: 'en_US', - base: '/blog/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'file', - site: 'https://example.com', - }) - ).to.throw; - expect( - getLocaleAbsoluteUrl({ - locale: 'it-VA', - base: '/blog/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'file', - site: 'https://example.com', - }) - ).to.eq('https://example.com/blog/italiano/'); + }; + + // directory format + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog/', + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + ...config.experimental.i18n, + }) + ).to.eq('https://example.com/blog/'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/es/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + isBuild: true, + }) + ).to.eq('https://es.example.com/blog/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + }) + ).to.throw; + + // file format + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/es/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + }) + ).to.throw; + expect( + getLocaleAbsoluteUrl({ + locale: 'it-VA', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/italiano/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + }) + ).to.throw; + + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + isBuild: true, + }) + ).to.eq('https://es.example.com/blog/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + prependWith: 'some-name', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + path: 'first-post', + isBuild: true, + }) + ).to.eq('https://es.example.com/blog/some-name/first-post/'); + + // en isn't mapped to a domain + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog/', + prependWith: 'some-name', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + path: 'first-post', + isBuild: true, + }) + ).to.eq('/blog/some-name/first-post/'); + }); }); - - it('should correctly return the URL without base', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ - const config = { - experimental: { - i18n: { - defaultLocale: 'en', - locales: [ - 'en', - 'es', - { - path: 'italiano', - codes: ['it', 'it-VA'], + describe('with [prefix-always]', () => { + it('should correctly return the URL with the base', () => { + /** + * + * @type {import("../../../dist/@types").AstroUserConfig} + */ + const config = { + base: '/blog', + experimental: { + i18n: { + defaultLocale: 'en', + locales: ['en', 'en_US', 'es'], + domains: { + es: 'https://es.example.com', }, - ], + routing: 'pathname-prefix-always', + }, }, - }, - }; - - expect( - getLocaleAbsoluteUrl({ - locale: 'en', - base: '/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'directory', - site: 'https://example.com', - }) - ).to.eq('https://example.com/'); - expect( - getLocaleAbsoluteUrl({ - locale: 'es', - base: '/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'directory', - site: 'https://example.com', - }) - ).to.eq('https://example.com/es/'); - expect( - getLocaleAbsoluteUrl({ - locale: 'it-VA', - base: '/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'directory', - site: 'https://example.com', - }) - ).to.eq('https://example.com/italiano/'); - }); - - it('should correctly handle the trailing slash', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ - const config = { - experimental: { + }; + + // directory format + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog/', + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + ...config.experimental.i18n, + }) + ).to.eq('https://example.com/blog/en/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/es/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + }) + ).to.throw; + + // file format + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/en/'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/es/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + }) + ).to.throw; + + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + isBuild: true, + }) + ).to.eq('https://es.example.com/blog/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + prependWith: 'some-name', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + path: 'first-post', + isBuild: true, + }) + ).to.eq('https://es.example.com/blog/some-name/first-post/'); + }); + it('should correctly return the URL without base', () => { + /** + * + * @type {import("../../../dist/@types").AstroUserConfig} + */ + const config = { + experimental: { + i18n: { + defaultLocale: 'en', + locales: ['en', 'es'], + routing: 'pathname-prefix-always', + }, + }, + }; + + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/en/'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/es/'); + }); + + it('should correctly handle the trailing slash', () => { + /** + * + * @type {import("../../../dist/@types").AstroUserConfig} + */ + const config = { i18n: { defaultLocale: 'en', locales: ['en', 'es'], + routing: 'pathname-prefix-always', }, - }, - }; - // directory format - expect( - getLocaleAbsoluteUrl({ - locale: 'en', + }; + // directory format + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog', + ...config.i18n, + trailingSlash: 'never', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/en'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.i18n, + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/es/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog/', + ...config.i18n, + trailingSlash: 'ignore', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/en/'); + + // directory file + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog', + ...config.i18n, + trailingSlash: 'never', + format: 'file', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/en'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/es/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + // ignore + file => no trailing slash + base: '/blog', + ...config.i18n, + trailingSlash: 'ignore', + format: 'file', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/en'); + }); + + it('should normalize locales', () => { + /** + * + * @type {import("../../../dist/@types").AstroUserConfig} + */ + const config = { base: '/blog', - ...config.experimental.i18n, - trailingSlash: 'never', - format: 'directory', - site: 'https://example.com', - }) - ).to.eq('https://example.com/blog'); - expect( - getLocaleAbsoluteUrl({ - locale: 'es', - base: '/blog/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'directory', - site: 'https://example.com', - }) - ).to.eq('https://example.com/blog/es/'); - - expect( - getLocaleAbsoluteUrl({ - locale: 'en', - base: '/blog/', - ...config.experimental.i18n, - trailingSlash: 'ignore', - format: 'directory', - site: 'https://example.com', - }) - ).to.eq('https://example.com/blog/'); - - // directory file - expect( - getLocaleAbsoluteUrl({ - locale: 'en', + experimental: { + i18n: { + defaultLocale: 'en', + locales: ['en', 'en_US', 'en_AU'], + routingStrategy: 'pathname-prefix-always', + }, + }, + }; + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + }) + ).to.eq('/blog/en-us/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_AU', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + }) + ).to.eq('/blog/en-au/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + normalizeLocale: true, + }) + ).to.eq('/blog/en-us/'); + }); + + it('should return the default locale when routing strategy is [pathname-prefix-always]', () => { + /** + * + * @type {import("../../../dist/@types").AstroUserConfig} + */ + const config = { base: '/blog', - ...config.experimental.i18n, - trailingSlash: 'never', - format: 'file', - site: 'https://example.com', - }) - ).to.eq('https://example.com/blog'); - expect( - getLocaleAbsoluteUrl({ - locale: 'es', - base: '/blog/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'file', - site: 'https://example.com', - }) - ).to.eq('https://example.com/blog/es/'); - - expect( - getLocaleAbsoluteUrl({ - locale: 'en', - // ignore + file => no trailing slash + experimental: { + i18n: { + defaultLocale: 'en', + locales: ['en', 'es', 'en_US', 'en_AU'], + routing: 'pathname-prefix-always', + }, + }, + }; + + // directory format + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog/', + trailingSlash: 'always', + site: 'https://example.com', + format: 'directory', + ...config.experimental.i18n, + }) + ).to.eq('https://example.com/blog/en/'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + site: 'https://example.com', + trailingSlash: 'always', + format: 'directory', + }) + ).to.eq('https://example.com/blog/es/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + site: 'https://example.com', + trailingSlash: 'always', + format: 'directory', + }) + ).to.throw; + + // file format + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog/', + ...config.experimental.i18n, + site: 'https://example.com', + trailingSlash: 'always', + format: 'file', + }) + ).to.eq('https://example.com/blog/en/'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + site: 'https://example.com', + trailingSlash: 'always', + format: 'file', + }) + ).to.eq('https://example.com/blog/es/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + site: 'https://example.com', + trailingSlash: 'always', + format: 'file', + }) + ).to.throw; + }); + + it('should return the default locale when routing strategy is [pathname-prefix-always-no-redirect]', () => { + /** + * + * @type {import("../../../dist/@types").AstroUserConfig} + */ + const config = { base: '/blog', - ...config.experimental.i18n, - trailingSlash: 'ignore', - format: 'file', - site: 'https://example.com', - }) - ).to.eq('https://example.com/blog'); - }); - - it('should normalize locales', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ - const config = { - base: '/blog', - experimental: { - i18n: { - defaultLocale: 'en', - locales: ['en', 'en_US', 'en_AU'], + experimental: { + i18n: { + defaultLocale: 'en', + locales: ['en', 'es', 'en_US', 'en_AU'], + routing: 'pathname-prefix-always-no-redirect', + }, }, - }, - }; - - expect( - getLocaleAbsoluteUrl({ - locale: 'en_US', - base: '/blog/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'directory', - }) - ).to.eq('/blog/en-us/'); - - expect( - getLocaleAbsoluteUrl({ - locale: 'en_AU', - base: '/blog/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'directory', - }) - ).to.eq('/blog/en-au/'); - - expect( - getLocaleAbsoluteUrl({ - locale: 'en_US', - base: '/blog/', - ...config.experimental.i18n, - trailingSlash: 'always', - format: 'directory', - normalizeLocale: true, - }) - ).to.eq('/blog/en-us/'); - }); - - it('should return the default locale when routing strategy is [pathname-prefix-always]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ - const config = { - base: '/blog', - experimental: { - i18n: { - defaultLocale: 'en', - locales: ['en', 'es', 'en_US', 'en_AU'], - routing: 'pathname-prefix-always', + }; + + // directory format + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog/', + trailingSlash: 'always', + site: 'https://example.com', + format: 'directory', + ...config.experimental.i18n, + }) + ).to.eq('https://example.com/blog/en/'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + site: 'https://example.com', + trailingSlash: 'always', + format: 'directory', + }) + ).to.eq('https://example.com/blog/es/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + site: 'https://example.com', + trailingSlash: 'always', + format: 'directory', + }) + ).to.throw; + + // file format + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog/', + ...config.experimental.i18n, + site: 'https://example.com', + trailingSlash: 'always', + format: 'file', + }) + ).to.eq('https://example.com/blog/en/'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + site: 'https://example.com', + trailingSlash: 'always', + format: 'file', + }) + ).to.eq('https://example.com/blog/es/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + site: 'https://example.com', + trailingSlash: 'always', + format: 'file', + }) + ).to.throw; + }); + it('should correctly return the URL without base', () => { + /** + * + * @type {import("../../../dist/@types").AstroUserConfig} + */ + const config = { + experimental: { + i18n: { + defaultLocale: 'en', + locales: [ + 'en', + 'es', + { + path: 'italiano', + codes: ['it', 'it-VA'], + }, + ], + routingStrategy: 'prefix-other-locales', + }, }, - }, - }; - - // directory format - expect( - getLocaleAbsoluteUrl({ - locale: 'en', - base: '/blog/', - trailingSlash: 'always', - site: 'https://example.com', - format: 'directory', - ...config.experimental.i18n, - }) - ).to.eq('https://example.com/blog/en/'); - expect( - getLocaleAbsoluteUrl({ - locale: 'es', - base: '/blog/', - ...config.experimental.i18n, - site: 'https://example.com', - trailingSlash: 'always', - format: 'directory', - }) - ).to.eq('https://example.com/blog/es/'); - - expect( - getLocaleAbsoluteUrl({ - locale: 'en_US', - base: '/blog/', - ...config.experimental.i18n, - site: 'https://example.com', - trailingSlash: 'always', - format: 'directory', - }) - ).to.throw; - - // file format - expect( - getLocaleAbsoluteUrl({ - locale: 'en', - base: '/blog/', - ...config.experimental.i18n, - site: 'https://example.com', - trailingSlash: 'always', - format: 'file', - }) - ).to.eq('https://example.com/blog/en/'); - expect( - getLocaleAbsoluteUrl({ - locale: 'es', - base: '/blog/', - ...config.experimental.i18n, - site: 'https://example.com', - trailingSlash: 'always', - format: 'file', - }) - ).to.eq('https://example.com/blog/es/'); - - expect( - getLocaleAbsoluteUrl({ - locale: 'en_US', - base: '/blog/', - ...config.experimental.i18n, - site: 'https://example.com', - trailingSlash: 'always', - format: 'file', - }) - ).to.throw; - }); - - it('should return the default locale when routing strategy is [pathname-prefix-always-no-redirect]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ - const config = { - base: '/blog', - experimental: { - i18n: { - defaultLocale: 'en', - locales: ['en', 'es', 'en_US', 'en_AU'], - routing: 'pathname-prefix-always-no-redirect', + }; + + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/es/'); + expect( + getLocaleAbsoluteUrl({ + locale: 'it-VA', + base: '/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/italiano/'); + }); + + it('should correctly handle the trailing slash', () => { + /** + * + * @type {import("../../../dist/@types").AstroUserConfig} + */ + const config = { + experimental: { + i18n: { + defaultLocale: 'en', + locales: ['en', 'es'], + routingStrategy: 'prefix-other-locales', + }, }, - }, - }; - - // directory format - expect( - getLocaleAbsoluteUrl({ - locale: 'en', - base: '/blog/', - trailingSlash: 'always', - site: 'https://example.com', - format: 'directory', - ...config.experimental.i18n, - }) - ).to.eq('https://example.com/blog/en/'); - expect( - getLocaleAbsoluteUrl({ - locale: 'es', - base: '/blog/', - ...config.experimental.i18n, - site: 'https://example.com', - trailingSlash: 'always', - format: 'directory', - }) - ).to.eq('https://example.com/blog/es/'); - - expect( - getLocaleAbsoluteUrl({ - locale: 'en_US', - base: '/blog/', - ...config.experimental.i18n, - site: 'https://example.com', - trailingSlash: 'always', - format: 'directory', - }) - ).to.throw; - - // file format - expect( - getLocaleAbsoluteUrl({ - locale: 'en', - base: '/blog/', - ...config.experimental.i18n, - site: 'https://example.com', - trailingSlash: 'always', - format: 'file', - }) - ).to.eq('https://example.com/blog/en/'); - expect( - getLocaleAbsoluteUrl({ - locale: 'es', - base: '/blog/', - ...config.experimental.i18n, - site: 'https://example.com', - trailingSlash: 'always', - format: 'file', - }) - ).to.eq('https://example.com/blog/es/'); - - expect( - getLocaleAbsoluteUrl({ - locale: 'en_US', - base: '/blog/', - ...config.experimental.i18n, - site: 'https://example.com', - trailingSlash: 'always', - format: 'file', - }) - ).to.throw; + }; + // directory format + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog', + ...config.experimental.i18n, + trailingSlash: 'never', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/es/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'ignore', + format: 'directory', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/'); + + // directory file + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + base: '/blog', + ...config.experimental.i18n, + trailingSlash: 'never', + format: 'file', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog'); + expect( + getLocaleAbsoluteUrl({ + locale: 'es', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'file', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog/es/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en', + // ignore + file => no trailing slash + base: '/blog', + ...config.experimental.i18n, + trailingSlash: 'ignore', + format: 'file', + site: 'https://example.com', + }) + ).to.eq('https://example.com/blog'); + }); + + it('should normalize locales', () => { + /** + * + * @type {import("../../../dist/@types").AstroUserConfig} + */ + const config = { + base: '/blog', + experimental: { + i18n: { + defaultLocale: 'en', + locales: ['en', 'en_US', 'en_AU'], + routingStrategy: 'prefix-other-locales', + }, + }, + }; + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + }) + ).to.eq('/blog/en-us/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_AU', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + }) + ).to.eq('/blog/en-au/'); + + expect( + getLocaleAbsoluteUrl({ + locale: 'en_US', + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'always', + format: 'directory', + normalizeLocale: true, + }) + ).to.eq('/blog/en-us/'); + }); }); }); @@ -1133,7 +1481,7 @@ describe('getLocaleAbsoluteUrlList', () => { }) ).to.have.members([ 'https://example.com/blog', - 'https://example.com/blog/en_US', + 'https://example.com/blog/en-us', 'https://example.com/blog/es', 'https://example.com/blog/italiano', ]); @@ -1164,7 +1512,7 @@ describe('getLocaleAbsoluteUrlList', () => { }) ).to.have.members([ 'https://example.com/blog/', - 'https://example.com/blog/en_US/', + 'https://example.com/blog/en-us/', 'https://example.com/blog/es/', ]); }); @@ -1202,7 +1550,7 @@ describe('getLocaleAbsoluteUrlList', () => { }) ).to.have.members([ 'https://example.com/blog/', - 'https://example.com/blog/en_US/', + 'https://example.com/blog/en-us/', 'https://example.com/blog/es/', 'https://example.com/blog/italiano/', ]); @@ -1233,7 +1581,7 @@ describe('getLocaleAbsoluteUrlList', () => { }) ).to.have.members([ 'https://example.com/blog', - 'https://example.com/blog/en_US', + 'https://example.com/blog/en-us', 'https://example.com/blog/es', ]); }); @@ -1263,7 +1611,7 @@ describe('getLocaleAbsoluteUrlList', () => { }) ).to.have.members([ 'https://example.com/blog', - 'https://example.com/blog/en_US', + 'https://example.com/blog/en-us', 'https://example.com/blog/es', ]); }); @@ -1293,7 +1641,7 @@ describe('getLocaleAbsoluteUrlList', () => { }) ).to.have.members([ 'https://example.com/blog/', - 'https://example.com/blog/en_US/', + 'https://example.com/blog/en-us/', 'https://example.com/blog/es/', ]); }); @@ -1324,7 +1672,7 @@ describe('getLocaleAbsoluteUrlList', () => { }) ).to.have.members([ 'https://example.com/blog/en/', - 'https://example.com/blog/en_US/', + 'https://example.com/blog/en-us/', 'https://example.com/blog/es/', ]); }); @@ -1355,10 +1703,45 @@ describe('getLocaleAbsoluteUrlList', () => { }) ).to.have.members([ 'https://example.com/blog/en/', - 'https://example.com/blog/en_US/', + 'https://example.com/blog/en-us/', 'https://example.com/blog/es/', ]); }); + + it('should retrieve the correct list of base URLs, swapped with the correct domain', () => { + /** + * + * @type {import("../../../dist/@types").AstroUserConfig} + */ + const config = { + experimental: { + i18n: { + defaultLocale: 'en', + locales: ['en', 'en_US', 'es'], + routingStrategy: 'pathname-prefix-always', + domains: { + es: 'https://es.example.com', + en: 'https://example.uk', + }, + }, + }, + }; + // directory format + expect( + getLocaleAbsoluteUrlList({ + base: '/blog/', + ...config.experimental.i18n, + trailingSlash: 'ignore', + format: 'directory', + site: 'https://example.com', + isBuild: true, + }) + ).to.have.members([ + 'https://example.uk/blog/', + 'https://example.com/blog/en-us/', + 'https://es.example.com/blog/', + ]); + }); }); describe('parse accept-header', () => { diff --git a/packages/astro/test/units/integrations/api.test.js b/packages/astro/test/units/integrations/api.test.js index ff700833ef84..882570db2589 100644 --- a/packages/astro/test/units/integrations/api.test.js +++ b/packages/astro/test/units/integrations/api.test.js @@ -125,6 +125,7 @@ describe('Astro feature map', function () { { output: 'hybrid', }, + {}, defaultLogger ); expect(result['hybridOutput']).to.be.true; @@ -137,6 +138,7 @@ describe('Astro feature map', function () { { output: 'hybrid', }, + {}, defaultLogger ); expect(result['hybridOutput']).to.be.false; @@ -149,6 +151,7 @@ describe('Astro feature map', function () { { output: 'hybrid', }, + {}, defaultLogger ); expect(result['hybridOutput']).to.be.false; @@ -162,6 +165,7 @@ describe('Astro feature map', function () { { output: 'static', }, + {}, defaultLogger ); expect(result['staticOutput']).to.be.true; @@ -174,6 +178,7 @@ describe('Astro feature map', function () { { output: 'static', }, + {}, defaultLogger ); expect(result['staticOutput']).to.be.false; @@ -187,6 +192,7 @@ describe('Astro feature map', function () { { output: 'hybrid', }, + {}, defaultLogger ); expect(result['hybridOutput']).to.be.true; @@ -201,6 +207,7 @@ describe('Astro feature map', function () { { output: 'hybrid', }, + {}, defaultLogger ); expect(result['hybridOutput']).to.be.false; @@ -214,6 +221,7 @@ describe('Astro feature map', function () { { output: 'server', }, + {}, defaultLogger ); expect(result['serverOutput']).to.be.true; @@ -228,6 +236,7 @@ describe('Astro feature map', function () { { output: 'server', }, + {}, defaultLogger ); expect(result['serverOutput']).to.be.false; @@ -251,6 +260,7 @@ describe('Astro feature map', function () { }, }, }, + {}, defaultLogger ); expect(result['assets']).to.be.true; @@ -271,6 +281,7 @@ describe('Astro feature map', function () { }, }, }, + {}, defaultLogger ); expect(result['assets']).to.be.true; @@ -292,6 +303,7 @@ describe('Astro feature map', function () { }, }, }, + {}, defaultLogger ); expect(result['assets']).to.be.false; diff --git a/packages/astro/test/units/routing/endpoints.test.js b/packages/astro/test/units/routing/endpoints.test.js new file mode 100644 index 000000000000..fde7a81daddb --- /dev/null +++ b/packages/astro/test/units/routing/endpoints.test.js @@ -0,0 +1,91 @@ +import { + createBasicSettings, + createFs, + createRequestAndResponse, + defaultLogger, +} from '../test-utils.js'; +import { fileURLToPath } from 'node:url'; +import { expect } from 'chai'; +import { createContainer } from '../../../dist/core/dev/container.js'; +import testAdapter from '../../test-adapter.js'; + +const root = new URL('../../fixtures/api-routes/', import.meta.url); +const fileSystem = { + '/src/pages/response-redirect.ts': `export const GET = ({ url }) => Response.redirect("https://example.com/destination", 307)`, + '/src/pages/response.ts': `export const GET = ({ url }) => new Response(null, { headers: { Location: "https://example.com/destination" }, status: 307 })`, + '/src/pages/not-found.ts': `export const GET = ({ url }) => new Response('empty', { headers: { "Content-Type": "text/plain" }, status: 404 })`, + '/src/pages/internal-error.ts': `export const GET = ({ url }) => new Response('something went wrong', { headers: { "Content-Type": "text/plain" }, status: 500 })`, +}; + +describe('endpoints', () => { + let container; + let settings; + + before(async () => { + const fs = createFs(fileSystem, root); + settings = await createBasicSettings({ + root: fileURLToPath(root), + output: 'server', + adapter: testAdapter(), + }); + container = await createContainer({ + fs, + settings, + logger: defaultLogger, + }); + }); + + after(async () => { + await container.close(); + }); + + it('should return a redirect response with location header', async () => { + const { req, res, done } = createRequestAndResponse({ + method: 'GET', + url: '/response-redirect', + }); + container.handle(req, res); + await done; + const headers = res.getHeaders(); + expect(headers).to.deep.include({ location: 'https://example.com/destination' }); + expect(headers).not.to.deep.include({ 'x-astro-reroute': 'no' }); + expect(res.statusCode).to.equal(307); + }); + + it('should return a response with location header', async () => { + const { req, res, done } = createRequestAndResponse({ + method: 'GET', + url: '/response', + }); + container.handle(req, res); + await done; + const headers = res.getHeaders(); + expect(headers).to.deep.include({ location: 'https://example.com/destination' }); + expect(headers).not.to.deep.include({ 'x-astro-reroute': 'no' }); + expect(res.statusCode).to.equal(307); + }); + + it('should append reroute header for HTTP status 404', async () => { + const { req, res, done } = createRequestAndResponse({ + method: 'GET', + url: '/not-found', + }); + container.handle(req, res); + await done; + const headers = res.getHeaders(); + expect(headers).to.deep.include({ 'x-astro-reroute': 'no' }); + expect(res.statusCode).to.equal(404); + }); + + it('should append reroute header for HTTP status 500', async () => { + const { req, res, done } = createRequestAndResponse({ + method: 'GET', + url: '/internal-error', + }); + container.handle(req, res); + await done; + const headers = res.getHeaders(); + expect(headers).to.deep.include({ 'x-astro-reroute': 'no' }); + expect(res.statusCode).to.equal(500); + }); +}); diff --git a/packages/astro/test/units/vite-plugin-astro-server/response.test.js b/packages/astro/test/units/vite-plugin-astro-server/response.test.js new file mode 100644 index 000000000000..a47769556e40 --- /dev/null +++ b/packages/astro/test/units/vite-plugin-astro-server/response.test.js @@ -0,0 +1,63 @@ +import { + createBasicSettings, + createFs, + createRequestAndResponse, + defaultLogger, +} from '../test-utils.js'; +import { fileURLToPath } from 'node:url'; +import { expect } from 'chai'; +import { createContainer } from '../../../dist/core/dev/container.js'; +import testAdapter from '../../test-adapter.js'; + +const root = new URL('../../fixtures/api-routes/', import.meta.url); +const fileSystem = { + '/src/pages/index.js': `export const GET = () => { + const headers = new Headers(); + headers.append('x-single', 'single'); + headers.append('x-triple', 'one'); + headers.append('x-triple', 'two'); + headers.append('x-triple', 'three'); + headers.append('Set-cookie', 'hello'); + headers.append('Set-Cookie', 'world'); + return new Response(null, { headers }); + }`, +}; + +describe('endpoints', () => { + let container; + let settings; + + before(async () => { + const fs = createFs(fileSystem, root); + settings = await createBasicSettings({ + root: fileURLToPath(root), + output: 'server', + adapter: testAdapter(), + }); + container = await createContainer({ + fs, + settings, + logger: defaultLogger, + }); + }); + + after(async () => { + await container.close(); + }); + + it('Headers with multiple values (set-cookie special case)', async () => { + const { req, res, done } = createRequestAndResponse({ + method: 'GET', + url: '/', + }); + container.handle(req, res); + await done; + const headers = res.getHeaders(); + expect(headers).to.deep.equal({ + 'access-control-allow-origin': '*', + 'x-single': 'single', + 'x-triple': 'one, two, three', + 'set-cookie': ['hello', 'world'], + }); + }); +}); diff --git a/packages/astro/types.d.ts b/packages/astro/types.d.ts index 638ad762e81b..7eaa4823c636 100644 --- a/packages/astro/types.d.ts +++ b/packages/astro/types.d.ts @@ -1,5 +1,6 @@ import './astro-jsx'; -import { AstroBuiltinAttributes } from './dist/@types/astro.js'; +import type { AstroBuiltinAttributes } from './dist/@types/astro.js'; +import type { Simplify } from './dist/type-utils.js'; /** Any supported HTML or SVG element name, as defined by the HTML specification */ export type HTMLTag = keyof astroHTML.JSX.DefinedIntrinsicElements; @@ -20,3 +21,5 @@ type PolymorphicAttributes

= Omit

= PolymorphicAttributes< Omit & { as: NonNullable } >; + +export type ComponentProps any> = Simplify[0]>; diff --git a/packages/create-astro/src/actions/typescript.ts b/packages/create-astro/src/actions/typescript.ts index efff6099453e..78f75daf523e 100644 --- a/packages/create-astro/src/actions/typescript.ts +++ b/packages/create-astro/src/actions/typescript.ts @@ -1,5 +1,4 @@ import type { Context } from './context.js'; - import { color } from '@astrojs/cli-kit'; import { readFile, rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json index 7c4fc4713b5d..7af2e1b1544f 100644 --- a/packages/integrations/markdoc/package.json +++ b/packages/integrations/markdoc/package.json @@ -59,8 +59,7 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "mocha --exit --timeout 20000", - "test:match": "mocha --timeout 20000 -g" + "test": "astro-scripts test \"test/**/*.test.js\"" }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", @@ -78,16 +77,12 @@ }, "devDependencies": { "@astrojs/markdown-remark": "workspace:*", - "@types/chai": "^4.3.10", "@types/html-escaper": "^3.0.2", "@types/markdown-it": "^13.0.6", - "@types/mocha": "^10.0.4", "astro": "workspace:*", "astro-scripts": "workspace:*", - "chai": "^4.3.7", "devalue": "^4.3.2", "linkedom": "^0.16.4", - "mocha": "^10.2.0", "vite": "^5.0.12" }, "engines": { diff --git a/packages/integrations/markdoc/test/content-collections.test.js b/packages/integrations/markdoc/test/content-collections.test.js index aad389e0ccd2..cfd1c80c7e2c 100644 --- a/packages/integrations/markdoc/test/content-collections.test.js +++ b/packages/integrations/markdoc/test/content-collections.test.js @@ -1,7 +1,8 @@ import { parse as parseDevalue } from 'devalue'; -import { expect } from 'chai'; import { loadFixture, fixLineEndings } from '../../../astro/test/test-utils.js'; import markdoc from '../dist/index.js'; +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; function formatPost(post) { return { @@ -36,19 +37,18 @@ describe('Markdoc - Content Collections', () => { it('loads entry', async () => { const res = await baseFixture.fetch('/entry.json'); const post = parseDevalue(await res.text()); - expect(formatPost(post)).to.deep.equal(post1Entry); + assert.deepEqual(formatPost(post), post1Entry); }); it('loads collection', async () => { const res = await baseFixture.fetch('/collection.json'); const posts = parseDevalue(await res.text()); - expect(posts).to.not.be.null; + assert.notEqual(posts, null); - expect(posts.sort().map((post) => formatPost(post))).to.deep.equal([ - post1Entry, - post2Entry, - post3Entry, - ]); + assert.deepEqual( + posts.sort().map((post) => formatPost(post)), + [post1Entry, post2Entry, post3Entry] + ); }); }); @@ -60,18 +60,17 @@ describe('Markdoc - Content Collections', () => { it('loads entry', async () => { const res = await baseFixture.readFile('/entry.json'); const post = parseDevalue(res); - expect(formatPost(post)).to.deep.equal(post1Entry); + assert.deepEqual(formatPost(post), post1Entry); }); it('loads collection', async () => { const res = await baseFixture.readFile('/collection.json'); const posts = parseDevalue(res); - expect(posts).to.not.be.null; - expect(posts.sort().map((post) => formatPost(post))).to.deep.equal([ - post1Entry, - post2Entry, - post3Entry, - ]); + assert.notEqual(posts, null); + assert.deepEqual( + posts.sort().map((post) => formatPost(post)), + [post1Entry, post2Entry, post3Entry] + ); }); }); }); diff --git a/packages/integrations/markdoc/test/headings.test.js b/packages/integrations/markdoc/test/headings.test.js index 5468e8c6b3d4..b87f6408e60c 100644 --- a/packages/integrations/markdoc/test/headings.test.js +++ b/packages/integrations/markdoc/test/headings.test.js @@ -1,6 +1,7 @@ import { parseHTML } from 'linkedom'; -import { expect } from 'chai'; import { loadFixture } from '../../../astro/test/test-utils.js'; +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; async function getFixture(name) { return await loadFixture({ @@ -195,20 +196,20 @@ const depthToHeadingMap = { /** @param {Document} document */ function idTest(document) { for (const [depth, info] of Object.entries(depthToHeadingMap)) { - expect(document.querySelector(`h${depth}`)?.getAttribute('id')).to.equal(info.slug); + assert.equal(document.querySelector(`h${depth}`)?.getAttribute('id'), info.slug); } } /** @param {Document} document */ function tocTest(document) { const toc = document.querySelector('[data-toc] > ul'); - expect(toc.children).to.have.lengthOf(Object.keys(depthToHeadingMap).length); + assert.equal(toc.children.length, Object.keys(depthToHeadingMap).length); for (const [depth, info] of Object.entries(depthToHeadingMap)) { const linkEl = toc.querySelector(`a[href="#${info.slug}"]`); - expect(linkEl).to.exist; - expect(linkEl.getAttribute('data-depth')).to.equal(depth); - expect(linkEl.textContent.trim()).to.equal(info.text); + assert.ok(linkEl); + assert.equal(linkEl.getAttribute('data-depth'), depth); + assert.equal(linkEl.textContent.trim(), info.text); } } @@ -217,6 +218,6 @@ function astroComponentTest(document) { const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); for (const heading of headings) { - expect(heading.hasAttribute('data-custom-heading')).to.be.true; + assert.equal(heading.hasAttribute('data-custom-heading'), true); } } diff --git a/packages/integrations/markdoc/test/image-assets.test.js b/packages/integrations/markdoc/test/image-assets.test.js index 7339960dd202..ae576b5cfd27 100644 --- a/packages/integrations/markdoc/test/image-assets.test.js +++ b/packages/integrations/markdoc/test/image-assets.test.js @@ -1,6 +1,7 @@ import { parseHTML } from 'linkedom'; -import { expect } from 'chai'; import { loadFixture } from '../../../astro/test/test-utils.js'; +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; const root = new URL('./fixtures/image-assets/', import.meta.url); @@ -28,14 +29,15 @@ describe('Markdoc - Image assets', () => { const res = await baseFixture.fetch('/'); const html = await res.text(); const { document } = parseHTML(html); - expect(document.querySelector('#public > img')?.src).to.equal('/favicon.svg'); + assert.equal(document.querySelector('#public > img')?.src, '/favicon.svg'); }); it('transforms relative image paths to optimized path', async () => { const res = await baseFixture.fetch('/'); const html = await res.text(); const { document } = parseHTML(html); - expect(document.querySelector('#relative > img')?.src).to.match( + assert.match( + document.querySelector('#relative > img')?.src, /\/_image\?href=.*%2Fsrc%2Fassets%2Frelative%2Foar.jpg%3ForigWidth%3D420%26origHeight%3D630%26origFormat%3Djpg&f=webp/ ); }); @@ -44,7 +46,8 @@ describe('Markdoc - Image assets', () => { const res = await baseFixture.fetch('/'); const html = await res.text(); const { document } = parseHTML(html); - expect(document.querySelector('#alias > img')?.src).to.match( + assert.match( + document.querySelector('#alias > img')?.src, /\/_image\?href=.*%2Fsrc%2Fassets%2Falias%2Fcityscape.jpg%3ForigWidth%3D420%26origHeight%3D280%26origFormat%3Djpg&f=webp/ ); }); @@ -58,19 +61,19 @@ describe('Markdoc - Image assets', () => { it('uses public/ image paths unchanged', async () => { const html = await baseFixture.readFile('/index.html'); const { document } = parseHTML(html); - expect(document.querySelector('#public > img')?.src).to.equal('/favicon.svg'); + assert.equal(document.querySelector('#public > img')?.src, '/favicon.svg'); }); it('transforms relative image paths to optimized path', async () => { const html = await baseFixture.readFile('/index.html'); const { document } = parseHTML(html); - expect(document.querySelector('#relative > img')?.src).to.match(/^\/_astro\/oar.*\.webp$/); + assert.match(document.querySelector('#relative > img')?.src, /^\/_astro\/oar.*\.webp$/); }); it('transforms aliased image paths to optimized path', async () => { const html = await baseFixture.readFile('/index.html'); const { document } = parseHTML(html); - expect(document.querySelector('#alias > img')?.src).to.match(/^\/_astro\/cityscape.*\.webp$/); + assert.match(document.querySelector('#alias > img')?.src, /^\/_astro\/cityscape.*\.webp$/); }); }); }); diff --git a/packages/integrations/markdoc/test/propagated-assets.test.js b/packages/integrations/markdoc/test/propagated-assets.test.js index 4326233c1d45..652c60014a41 100644 --- a/packages/integrations/markdoc/test/propagated-assets.test.js +++ b/packages/integrations/markdoc/test/propagated-assets.test.js @@ -1,6 +1,7 @@ import { parseHTML } from 'linkedom'; -import { expect } from 'chai'; import { loadFixture } from '../../../astro/test/test-utils.js'; +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; describe('Markdoc - propagated assets', () => { let fixture; @@ -44,23 +45,23 @@ describe('Markdoc - propagated assets', () => { let styleContents; if (mode === 'dev') { const styles = stylesDocument.querySelectorAll('style'); - expect(styles).to.have.lengthOf(1); + assert.equal(styles.length, 1); styleContents = styles[0].textContent; } else { const links = stylesDocument.querySelectorAll('link[rel="stylesheet"]'); - expect(links).to.have.lengthOf(1); + assert.equal(links.length, 1); styleContents = await fixture.readFile(links[0].href); } - expect(styleContents).to.include('--color-base-purple: 269, 79%;'); + assert.equal(styleContents.includes('--color-base-purple: 269, 79%;'), true); }); it('[fails] Does not bleed styles to other page', async () => { if (mode === 'dev') { const styles = scriptsDocument.querySelectorAll('style'); - expect(styles).to.have.lengthOf(0); + assert.equal(styles.length, 0); } else { const links = scriptsDocument.querySelectorAll('link[rel="stylesheet"]'); - expect(links).to.have.lengthOf(0); + assert.equal(links.length, 0); } }); }); diff --git a/packages/integrations/markdoc/test/render-html.test.js b/packages/integrations/markdoc/test/render-html.test.js index a0c38ace3127..d83a916071ce 100644 --- a/packages/integrations/markdoc/test/render-html.test.js +++ b/packages/integrations/markdoc/test/render-html.test.js @@ -1,6 +1,7 @@ import { parseHTML } from 'linkedom'; -import { expect } from 'chai'; import { loadFixture } from '../../../astro/test/test-utils.js'; +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; async function getFixture(name) { return await loadFixture({ @@ -91,24 +92,24 @@ function renderSimpleChecks(html) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - expect(h2.textContent).to.equal('Simple post header'); + assert.equal(h2.textContent, 'Simple post header'); const spanInsideH2 = document.querySelector('h2 > span'); - expect(spanInsideH2.textContent).to.equal('post'); - expect(spanInsideH2.className).to.equal('inside-h2'); - expect(spanInsideH2.style.color).to.equal('fuscia'); + assert.equal(spanInsideH2.textContent, 'post'); + assert.equal(spanInsideH2.className, 'inside-h2'); + assert.equal(spanInsideH2.style.color, 'fuscia'); const p1 = document.querySelector('article > p:nth-of-type(1)'); - expect(p1.children.length).to.equal(1); - expect(p1.textContent).to.equal('This is a simple Markdoc post.'); + assert.equal(p1.children.length, 1); + assert.equal(p1.textContent, 'This is a simple Markdoc post.'); const p2 = document.querySelector('article > p:nth-of-type(2)'); - expect(p2.children.length).to.equal(0); - expect(p2.textContent).to.equal('This is a paragraph!'); + assert.equal(p2.children.length, 0); + assert.equal(p2.textContent, 'This is a paragraph!'); const p3 = document.querySelector('article > p:nth-of-type(3)'); - expect(p3.children.length).to.equal(1); - expect(p3.textContent).to.equal('This is a span inside a paragraph!'); + assert.equal(p3.children.length, 1); + assert.equal(p3.textContent, 'This is a span inside a paragraph!'); } /** @param {string} html */ @@ -116,47 +117,47 @@ function renderNestedHTMLChecks(html) { const { document } = parseHTML(html); const p1 = document.querySelector('p:nth-of-type(1)'); - expect(p1.id).to.equal('p1'); - expect(p1.textContent).to.equal('before inner after'); - expect(p1.children.length).to.equal(1); + assert.equal(p1.id, 'p1'); + assert.equal(p1.textContent, 'before inner after'); + assert.equal(p1.children.length, 1); const p1Span1 = p1.querySelector('span'); - expect(p1Span1.textContent).to.equal('inner'); - expect(p1Span1.id).to.equal('inner1'); - expect(p1Span1.className).to.equal('inner-class'); - expect(p1Span1.style.color).to.equal('hotpink'); + assert.equal(p1Span1.textContent, 'inner'); + assert.equal(p1Span1.id, 'inner1'); + assert.equal(p1Span1.className, 'inner-class'); + assert.equal(p1Span1.style.color, 'hotpink'); const p2 = document.querySelector('p:nth-of-type(2)'); - expect(p2.id).to.equal('p2'); - expect(p2.textContent).to.equal('\n before\n inner\n after\n'); - expect(p2.children.length).to.equal(1); + assert.equal(p2.id, 'p2'); + assert.equal(p2.textContent, '\n before\n inner\n after\n'); + assert.equal(p2.children.length, 1); const divL1 = document.querySelector('div:nth-of-type(1)'); - expect(divL1.id).to.equal('div-l1'); - expect(divL1.children.length).to.equal(2); + assert.equal(divL1.id, 'div-l1'); + assert.equal(divL1.children.length, 2); const divL2_1 = divL1.querySelector('div:nth-of-type(1)'); - expect(divL2_1.id).to.equal('div-l2-1'); - expect(divL2_1.children.length).to.equal(1); + assert.equal(divL2_1.id, 'div-l2-1'); + assert.equal(divL2_1.children.length, 1); const p3 = divL2_1.querySelector('p:nth-of-type(1)'); - expect(p3.id).to.equal('p3'); - expect(p3.textContent).to.equal('before inner after'); - expect(p3.children.length).to.equal(1); + assert.equal(p3.id, 'p3'); + assert.equal(p3.textContent, 'before inner after'); + assert.equal(p3.children.length, 1); const divL2_2 = divL1.querySelector('div:nth-of-type(2)'); - expect(divL2_2.id).to.equal('div-l2-2'); - expect(divL2_2.children.length).to.equal(2); + assert.equal(divL2_2.id, 'div-l2-2'); + assert.equal(divL2_2.children.length, 2); const p4 = divL2_2.querySelector('p:nth-of-type(1)'); - expect(p4.id).to.equal('p4'); - expect(p4.textContent).to.equal('before inner after'); - expect(p4.children.length).to.equal(1); + assert.equal(p4.id, 'p4'); + assert.equal(p4.textContent, 'before inner after'); + assert.equal(p4.children.length, 1); const p5 = divL2_2.querySelector('p:nth-of-type(2)'); - expect(p5.id).to.equal('p5'); - expect(p5.textContent).to.equal('before inner after'); - expect(p5.children.length).to.equal(1); + assert.equal(p5.id, 'p5'); + assert.equal(p5.textContent, 'before inner after'); + assert.equal(p5.children.length, 1); } /** @@ -172,17 +173,17 @@ function renderRandomlyCasedHTMLAttributesChecks(html) { // all four 's which had randomly cased variants of colspan/rowspan should all be rendered lowercased at this point - expect(td1.getAttribute('colspan')).to.equal('3'); - expect(td1.getAttribute('rowspan')).to.equal('2'); + assert.equal(td1.getAttribute('colspan'), '3'); + assert.equal(td1.getAttribute('rowspan'), '2'); - expect(td2.getAttribute('colspan')).to.equal('3'); - expect(td2.getAttribute('rowspan')).to.equal('2'); + assert.equal(td2.getAttribute('colspan'), '3'); + assert.equal(td2.getAttribute('rowspan'), '2'); - expect(td3.getAttribute('colspan')).to.equal('3'); - expect(td3.getAttribute('rowspan')).to.equal('2'); + assert.equal(td3.getAttribute('colspan'), '3'); + assert.equal(td3.getAttribute('rowspan'), '2'); - expect(td4.getAttribute('colspan')).to.equal('3'); - expect(td4.getAttribute('rowspan')).to.equal('2'); + assert.equal(td4.getAttribute('colspan'), '3'); + assert.equal(td4.getAttribute('rowspan'), '2'); } /** @@ -193,95 +194,95 @@ function renderComponentsHTMLChecks(html) { const { document } = parseHTML(html); const naturalP1 = document.querySelector('article > p:nth-of-type(1)'); - expect(naturalP1.textContent).to.equal('This is a inline mark in regular Markdown markup.'); - expect(naturalP1.children.length).to.equal(1); + assert.equal(naturalP1.textContent, 'This is a inline mark in regular Markdown markup.'); + assert.equal(naturalP1.children.length, 1); const p1 = document.querySelector('article > p:nth-of-type(2)'); - expect(p1.id).to.equal('p1'); - expect(p1.textContent).to.equal('This is a inline mark under some HTML'); - expect(p1.children.length).to.equal(1); + assert.equal(p1.id, 'p1'); + assert.equal(p1.textContent, 'This is a inline mark under some HTML'); + assert.equal(p1.children.length, 1); assertInlineMark(p1.children[0]); const div1p1 = document.querySelector('article > #div1 > p:nth-of-type(1)'); - expect(div1p1.id).to.equal('div1-p1'); - expect(div1p1.textContent).to.equal('This is a inline mark under some HTML'); - expect(div1p1.children.length).to.equal(1); + assert.equal(div1p1.id, 'div1-p1'); + assert.equal(div1p1.textContent, 'This is a inline mark under some HTML'); + assert.equal(div1p1.children.length, 1); assertInlineMark(div1p1.children[0]); const div1p2 = document.querySelector('article > #div1 > p:nth-of-type(2)'); - expect(div1p2.id).to.equal('div1-p2'); - expect(div1p2.textContent).to.equal('This is a inline mark under some HTML'); - expect(div1p2.children.length).to.equal(1); + assert.equal(div1p2.id, 'div1-p2'); + assert.equal(div1p2.textContent, 'This is a inline mark under some HTML'); + assert.equal(div1p2.children.length, 1); const div1p2span1 = div1p2.querySelector('span'); - expect(div1p2span1.id).to.equal('div1-p2-span1'); - expect(div1p2span1.textContent).to.equal('inline mark'); - expect(div1p2span1.children.length).to.equal(1); + assert.equal(div1p2span1.id, 'div1-p2-span1'); + assert.equal(div1p2span1.textContent, 'inline mark'); + assert.equal(div1p2span1.children.length, 1); assertInlineMark(div1p2span1.children[0]); const aside1 = document.querySelector('article > aside:nth-of-type(1)'); const aside1Title = aside1.querySelector('p.title'); - expect(aside1Title.textContent.trim()).to.equal('Aside One'); + assert.equal(aside1Title.textContent.trim(), 'Aside One'); const aside1Section = aside1.querySelector('section'); const aside1SectionP1 = aside1Section.querySelector('p:nth-of-type(1)'); - expect(aside1SectionP1.textContent).to.equal( + assert.equal( + aside1SectionP1.textContent, "I'm a Markdown paragraph inside an top-level aside tag" ); const aside1H2_1 = aside1Section.querySelector('h2:nth-of-type(1)'); - expect(aside1H2_1.id).to.equal('im-an-h2-via-markdown-markup'); // automatic slug - expect(aside1H2_1.textContent).to.equal("I'm an H2 via Markdown markup"); + assert.equal(aside1H2_1.id, 'im-an-h2-via-markdown-markup'); // automatic slug + assert.equal(aside1H2_1.textContent, "I'm an H2 via Markdown markup"); const aside1H2_2 = aside1Section.querySelector('h2:nth-of-type(2)'); - expect(aside1H2_2.id).to.equal('h-two'); - expect(aside1H2_2.textContent).to.equal("I'm an H2 via HTML markup"); + assert.equal(aside1H2_2.id, 'h-two'); + assert.equal(aside1H2_2.textContent, "I'm an H2 via HTML markup"); const aside1SectionP2 = aside1Section.querySelector('p:nth-of-type(2)'); - expect(aside1SectionP2.textContent).to.equal('Markdown bold vs HTML bold'); - expect(aside1SectionP2.children.length).to.equal(2); + assert.equal(aside1SectionP2.textContent, 'Markdown bold vs HTML bold'); + assert.equal(aside1SectionP2.children.length, 2); const aside1SectionP2Strong1 = aside1SectionP2.querySelector('strong:nth-of-type(1)'); - expect(aside1SectionP2Strong1.textContent).to.equal('Markdown bold'); + assert.equal(aside1SectionP2Strong1.textContent, 'Markdown bold'); const aside1SectionP2Strong2 = aside1SectionP2.querySelector('strong:nth-of-type(2)'); - expect(aside1SectionP2Strong2.textContent).to.equal('HTML bold'); + assert.equal(aside1SectionP2Strong2.textContent, 'HTML bold'); const article = document.querySelector('article'); - expect(article.textContent).to.contain('RENDERED'); - expect(article.textContent).to.not.contain('NOT RENDERED'); + assert.equal(article.textContent.includes('RENDERED'), true); + assert.notEqual(article.textContent.includes('NOT RENDERED'), true); const section1 = document.querySelector('article > #section1'); const section1div1 = section1.querySelector('#div1'); const section1Aside1 = section1div1.querySelector('aside:nth-of-type(1)'); const section1Aside1Title = section1Aside1.querySelector('p.title'); - expect(section1Aside1Title.textContent.trim()).to.equal('Nested un-indented Aside'); + assert.equal(section1Aside1Title.textContent.trim(), 'Nested un-indented Aside'); const section1Aside1Section = section1Aside1.querySelector('section'); const section1Aside1SectionP1 = section1Aside1Section.querySelector('p:nth-of-type(1)'); - expect(section1Aside1SectionP1.textContent).to.equal('regular Markdown markup'); + assert.equal(section1Aside1SectionP1.textContent, 'regular Markdown markup'); const section1Aside1SectionP4 = section1Aside1Section.querySelector('p:nth-of-type(2)'); - expect(section1Aside1SectionP4.textContent).to.equal('nested inline mark content'); - expect(section1Aside1SectionP4.children.length).to.equal(1); + assert.equal(section1Aside1SectionP4.textContent, 'nested inline mark content'); + assert.equal(section1Aside1SectionP4.children.length, 1); assertInlineMark(section1Aside1SectionP4.children[0]); const section1div2 = section1.querySelector('#div2'); const section1Aside2 = section1div2.querySelector('aside:nth-of-type(1)'); const section1Aside2Title = section1Aside2.querySelector('p.title'); - expect(section1Aside2Title.textContent.trim()).to.equal('Nested indented Aside 💀'); + assert.equal(section1Aside2Title.textContent.trim(), 'Nested indented Aside 💀'); const section1Aside2Section = section1Aside2.querySelector('section'); const section1Aside2SectionP1 = section1Aside2Section.querySelector('p:nth-of-type(1)'); - expect(section1Aside2SectionP1.textContent).to.equal('regular Markdown markup'); + assert.equal(section1Aside2SectionP1.textContent, 'regular Markdown markup'); const section1Aside1SectionP5 = section1Aside2Section.querySelector('p:nth-of-type(2)'); - expect(section1Aside1SectionP5.id).to.equal('p5'); - expect(section1Aside1SectionP5.children.length).to.equal(1); + assert.equal(section1Aside1SectionP5.id, 'p5'); + assert.equal(section1Aside1SectionP5.children.length, 1); const section1Aside1SectionP5Span1 = section1Aside1SectionP5.children[0]; - expect(section1Aside1SectionP5Span1.textContent).to.equal('inline mark'); - expect(section1Aside1SectionP5Span1.children.length).to.equal(1); + assert.equal(section1Aside1SectionP5Span1.textContent, 'inline mark'); + assert.equal(section1Aside1SectionP5Span1.children.length, 1); const section1Aside1SectionP5Span1Span1 = section1Aside1SectionP5Span1.children[0]; - expect(section1Aside1SectionP5Span1Span1.textContent).to.equal(' mark'); + assert.equal(section1Aside1SectionP5Span1Span1.textContent, ' mark'); } /** @param {HTMLElement | null | undefined} el */ function assertInlineMark(el) { - expect(el).to.not.be.null; - expect(el).to.not.be.undefined; - expect(el.children.length).to.equal(0); - expect(el.textContent).to.equal('inline mark'); - expect(el.className).to.equal('mark'); - expect(el.style.color).to.equal('hotpink'); + assert.ok(el); + assert.equal(el.children.length, 0); + assert.equal(el.textContent, 'inline mark'); + assert.equal(el.className, 'mark'); + assert.equal(el.style.color, 'hotpink'); } diff --git a/packages/integrations/markdoc/test/render.test.js b/packages/integrations/markdoc/test/render.test.js index f1760a8e61f8..e5b8da74fab1 100644 --- a/packages/integrations/markdoc/test/render.test.js +++ b/packages/integrations/markdoc/test/render.test.js @@ -1,6 +1,7 @@ import { parseHTML } from 'linkedom'; -import { expect } from 'chai'; import { loadFixture } from '../../../astro/test/test-utils.js'; +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; async function getFixture(name) { return await loadFixture({ @@ -146,78 +147,76 @@ describe('Markdoc - render', () => { function renderNullChecks(html) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - expect(h2.textContent).to.equal('Post with render null'); - expect(h2.parentElement?.tagName).to.equal('BODY'); + assert.equal(h2.textContent, 'Post with render null'); + assert.equal(h2.parentElement?.tagName, 'BODY'); } /** @param {string} html */ function renderComponentsChecks(html) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - expect(h2.textContent).to.equal('Post with components'); + assert.equal(h2.textContent, 'Post with components'); // Renders custom shortcode component const marquee = document.querySelector('marquee'); - expect(marquee).to.not.be.null; - expect(marquee.hasAttribute('data-custom-marquee')).to.equal(true); + assert.notEqual(marquee, null); + assert.equal(marquee.hasAttribute('data-custom-marquee'), true); // Renders Astro Code component const pre = document.querySelector('pre'); - expect(pre).to.not.be.null; - expect(pre.className).to.equal('astro-code github-dark'); + assert.notEqual(pre, null); + assert.equal(pre.className, 'astro-code github-dark'); } /** @param {string} html */ function renderIndentedComponentsChecks(html) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - expect(h2.textContent).to.equal('Post with indented components'); + assert.equal(h2.textContent, 'Post with indented components'); // Renders custom shortcode components const marquees = document.querySelectorAll('marquee'); - expect(marquees.length).to.equal(2); + assert.equal(marquees.length, 2); // Renders h3 const h3 = document.querySelector('h3'); - expect(h3.textContent).to.equal('I am an h3!'); + assert.equal(h3.textContent, 'I am an h3!'); // Renders Astro Code component const pre = document.querySelector('pre'); - expect(pre).to.not.be.null; - expect(pre.className).to.equal('astro-code github-dark'); + assert.notEqual(pre, null); + assert.equal(pre.className, 'astro-code github-dark'); } /** @param {string} html */ function renderConfigChecks(html) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - expect(h2.textContent).to.equal('Post with config'); + assert.equal(h2.textContent, 'Post with config'); const textContent = html; - expect(textContent).to.not.include('Hello'); - expect(textContent).to.include('Hola'); - expect(textContent).to.include(`Konnichiwa`); + assert.notEqual(textContent.includes('Hello'), true); + assert.equal(textContent.includes('Hola'), true); + assert.equal(textContent.includes('Konnichiwa'), true); const runtimeVariable = document.querySelector('#runtime-variable'); - expect(runtimeVariable?.textContent?.trim()).to.equal('working!'); + assert.equal(runtimeVariable?.textContent?.trim(), 'working!'); } /** @param {string} html */ function renderSimpleChecks(html) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - expect(h2.textContent).to.equal('Simple post'); + assert.equal(h2.textContent, 'Simple post'); const p = document.querySelector('p'); - expect(p.textContent).to.equal('This is a simple Markdoc post.'); + assert.equal(p.textContent, 'This is a simple Markdoc post.'); } /** @param {string} html */ function renderWithRootFolderContainingSpace(html) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - expect(h2.textContent).to.equal('Simple post with root folder containing a space'); + assert.equal(h2.textContent, 'Simple post with root folder containing a space'); const p = document.querySelector('p'); - expect(p.textContent).to.equal( - 'This is a simple Markdoc post with root folder containing a space.' - ); + assert.equal(p.textContent, 'This is a simple Markdoc post with root folder containing a space.'); } diff --git a/packages/integrations/markdoc/test/syntax-highlighting.test.js b/packages/integrations/markdoc/test/syntax-highlighting.test.js index 1530e0c825f0..1d0f6127ba9d 100644 --- a/packages/integrations/markdoc/test/syntax-highlighting.test.js +++ b/packages/integrations/markdoc/test/syntax-highlighting.test.js @@ -1,10 +1,11 @@ import { parseHTML } from 'linkedom'; -import { expect } from 'chai'; import Markdoc from '@markdoc/markdoc'; import shiki from '../dist/extensions/shiki.js'; import prism from '../dist/extensions/prism.js'; import { setupConfig } from '../dist/runtime.js'; import { isHTMLString } from 'astro/runtime/server/index.js'; +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; const entry = ` \`\`\`ts @@ -24,13 +25,13 @@ describe('Markdoc - syntax highlighting', () => { const ast = Markdoc.parse(entry); const content = Markdoc.transform(ast, await getConfigExtendingShiki()); - expect(content.children).to.have.lengthOf(2); + assert.equal(content.children.length, 2); for (const codeBlock of content.children) { - expect(isHTMLString(codeBlock)).to.be.true; + assert.equal(isHTMLString(codeBlock), true); const pre = parsePreTag(codeBlock); - expect(pre.classList).to.include('astro-code'); - expect(pre.classList).to.include('github-dark'); + assert.equal(pre.classList.contains('astro-code'), true); + assert.equal(pre.classList.contains('github-dark'), true); } }); it('transforms with `theme` property', async () => { @@ -41,13 +42,13 @@ describe('Markdoc - syntax highlighting', () => { theme: 'dracula', }) ); - expect(content.children).to.have.lengthOf(2); + assert.equal(content.children.length, 2); for (const codeBlock of content.children) { - expect(isHTMLString(codeBlock)).to.be.true; + assert.equal(isHTMLString(codeBlock), true); const pre = parsePreTag(codeBlock); - expect(pre.classList).to.include('astro-code'); - expect(pre.classList).to.include('dracula'); + assert.equal(pre.classList.contains('astro-code'), true); + assert.equal(pre.classList.contains('dracula'), true); } }); it('transforms with `wrap` property', async () => { @@ -58,13 +59,13 @@ describe('Markdoc - syntax highlighting', () => { wrap: true, }) ); - expect(content.children).to.have.lengthOf(2); + assert.equal(content.children.length, 2); for (const codeBlock of content.children) { - expect(isHTMLString(codeBlock)).to.be.true; + assert.equal(isHTMLString(codeBlock), true); const pre = parsePreTag(codeBlock); - expect(pre.getAttribute('style')).to.include('white-space: pre-wrap'); - expect(pre.getAttribute('style')).to.include('word-wrap: break-word'); + assert.equal(pre.getAttribute('style').includes('white-space: pre-wrap'), true); + assert.equal(pre.getAttribute('style').includes('word-wrap: break-word'), true); } }); }); @@ -77,17 +78,17 @@ describe('Markdoc - syntax highlighting', () => { }); const content = Markdoc.transform(ast, config); - expect(content.children).to.have.lengthOf(2); + assert.equal(content.children.length, 2); const [tsBlock, cssBlock] = content.children; - expect(isHTMLString(tsBlock)).to.be.true; - expect(isHTMLString(cssBlock)).to.be.true; + assert.equal(isHTMLString(tsBlock), true); + assert.equal(isHTMLString(cssBlock), true); const preTs = parsePreTag(tsBlock); - expect(preTs.classList).to.include('language-ts'); + assert.equal(preTs.classList.contains('language-ts'), true); const preCss = parsePreTag(cssBlock); - expect(preCss.classList).to.include('language-css'); + assert.equal(preCss.classList.contains('language-css'), true); }); }); }); @@ -109,6 +110,6 @@ async function getConfigExtendingShiki(config) { function parsePreTag(html) { const { document } = parseHTML(html); const pre = document.querySelector('pre'); - expect(pre).to.exist; + assert.ok(pre); return pre; } diff --git a/packages/integrations/markdoc/test/variables.test.js b/packages/integrations/markdoc/test/variables.test.js index 90d5fe276320..c26ca3c45b7d 100644 --- a/packages/integrations/markdoc/test/variables.test.js +++ b/packages/integrations/markdoc/test/variables.test.js @@ -1,7 +1,8 @@ import { parseHTML } from 'linkedom'; -import { expect } from 'chai'; import { loadFixture } from '../../../astro/test/test-utils.js'; import markdoc from '../dist/index.js'; +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; const root = new URL('./fixtures/variables/', import.meta.url); @@ -30,12 +31,10 @@ describe('Markdoc - Variables', () => { const res = await baseFixture.fetch('/'); const html = await res.text(); const { document } = parseHTML(html); - expect(document.querySelector('h1')?.textContent).to.equal('Processed by schema: Test entry'); - expect(document.getElementById('id')?.textContent?.trim()).to.equal('id: entry.mdoc'); - expect(document.getElementById('slug')?.textContent?.trim()).to.equal('slug: entry'); - expect(document.getElementById('collection')?.textContent?.trim()).to.equal( - 'collection: blog' - ); + assert.equal(document.querySelector('h1')?.textContent, 'Processed by schema: Test entry'); + assert.equal(document.getElementById('id')?.textContent?.trim(), 'id: entry.mdoc'); + assert.equal(document.getElementById('slug')?.textContent?.trim(), 'slug: entry'); + assert.equal(document.getElementById('collection')?.textContent?.trim(), 'collection: blog'); }); }); @@ -47,12 +46,10 @@ describe('Markdoc - Variables', () => { it('has expected entry properties', async () => { const html = await baseFixture.readFile('/index.html'); const { document } = parseHTML(html); - expect(document.querySelector('h1')?.textContent).to.equal('Processed by schema: Test entry'); - expect(document.getElementById('id')?.textContent?.trim()).to.equal('id: entry.mdoc'); - expect(document.getElementById('slug')?.textContent?.trim()).to.equal('slug: entry'); - expect(document.getElementById('collection')?.textContent?.trim()).to.equal( - 'collection: blog' - ); + assert.equal(document.querySelector('h1')?.textContent, 'Processed by schema: Test entry'); + assert.equal(document.getElementById('id')?.textContent?.trim(), 'id: entry.mdoc'); + assert.equal(document.getElementById('slug')?.textContent?.trim(), 'slug: entry'); + assert.equal(document.getElementById('collection')?.textContent?.trim(), 'collection: blog'); }); }); }); diff --git a/packages/integrations/mdx/CHANGELOG.md b/packages/integrations/mdx/CHANGELOG.md index 05f9534e2b34..f1da065f650f 100644 --- a/packages/integrations/mdx/CHANGELOG.md +++ b/packages/integrations/mdx/CHANGELOG.md @@ -1,5 +1,12 @@ # @astrojs/mdx +## 2.1.1 + +### Patch Changes + +- Updated dependencies [[`44c957f893c6bf5f5b7c78301de7b21c5975584d`](https://github.com/withastro/astro/commit/44c957f893c6bf5f5b7c78301de7b21c5975584d)]: + - @astrojs/markdown-remark@4.2.1 + ## 2.1.0 ### Minor Changes diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json index ee2e3394014a..27ba130eed9b 100644 --- a/packages/integrations/mdx/package.json +++ b/packages/integrations/mdx/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/mdx", "description": "Add support for MDX pages in your Astro site", - "version": "2.1.0", + "version": "2.1.1", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", @@ -30,8 +30,7 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "mocha --exit --timeout 20000", - "test:match": "mocha --timeout 20000 -g" + "test": "astro-scripts test --timeout 50000 \"test/**/*.test.js\"" }, "dependencies": { "@astrojs/markdown-remark": "workspace:*", @@ -61,12 +60,10 @@ "@types/yargs-parser": "^21.0.3", "astro": "workspace:*", "astro-scripts": "workspace:*", - "chai": "^4.3.7", "cheerio": "1.0.0-rc.12", "linkedom": "^0.16.4", "mdast-util-mdx": "^3.0.0", "mdast-util-to-string": "^4.0.0", - "mocha": "^10.2.0", "reading-time": "^1.5.0", "rehype-mathjax": "^5.0.0", "rehype-pretty-code": "^0.10.2", diff --git a/packages/integrations/mdx/test/css-head-mdx.test.js b/packages/integrations/mdx/test/css-head-mdx.test.js index ed1c6d1d68ce..8b5764625086 100644 --- a/packages/integrations/mdx/test/css-head-mdx.test.js +++ b/packages/integrations/mdx/test/css-head-mdx.test.js @@ -1,7 +1,8 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; import { parseHTML } from 'linkedom'; +import { describe, it, before } from 'node:test'; +import * as assert from 'node:assert/strict'; import { loadFixture } from '../../../astro/test/test-utils.js'; import * as cheerio from 'cheerio'; @@ -27,10 +28,10 @@ describe('Head injection w/ MDX', () => { const { document } = parseHTML(html); const links = document.querySelectorAll('head link[rel=stylesheet]'); - expect(links).to.have.a.lengthOf(1); + assert.equal(links.length, 1); const scripts = document.querySelectorAll('head script[type=module]'); - expect(scripts).to.have.a.lengthOf(1); + assert.equal(scripts.length, 1); }); it('injects into the head for content collections', async () => { @@ -38,7 +39,7 @@ describe('Head injection w/ MDX', () => { const { document } = parseHTML(html); const links = document.querySelectorAll('head link[rel=stylesheet]'); - expect(links).to.have.a.lengthOf(1); + assert.equal(links.length, 1); }); it('injects content from a component using Content#render()', async () => { @@ -46,10 +47,10 @@ describe('Head injection w/ MDX', () => { const { document } = parseHTML(html); const links = document.querySelectorAll('head link[rel=stylesheet]'); - expect(links).to.have.a.lengthOf(1); + assert.equal(links.length, 1); const scripts = document.querySelectorAll('head script[type=module]'); - expect(scripts).to.have.a.lengthOf(2); + assert.equal(scripts.length, 2); }); it('Using component using slots.render() API', async () => { @@ -57,7 +58,7 @@ describe('Head injection w/ MDX', () => { const { document } = parseHTML(html); const links = document.querySelectorAll('head link[rel=stylesheet]'); - expect(links).to.have.a.lengthOf(1); + assert.equal(links.length, 1); }); it('Using component but no layout', async () => { @@ -66,10 +67,10 @@ describe('Head injection w/ MDX', () => { const $ = cheerio.load(html); const headLinks = $('head link[rel=stylesheet]'); - expect(headLinks).to.have.a.lengthOf(1); + assert.equal(headLinks.length, 1); const bodyLinks = $('body link[rel=stylesheet]'); - expect(bodyLinks).to.have.a.lengthOf(0); + assert.equal(bodyLinks.length, 0); }); it('JSX component rendering Astro children within head buffering phase', async () => { @@ -78,10 +79,10 @@ describe('Head injection w/ MDX', () => { const $ = cheerio.load(html); const headLinks = $('head link[rel=stylesheet]'); - expect(headLinks).to.have.a.lengthOf(1); + assert.equal(headLinks.length, 1); const bodyLinks = $('body link[rel=stylesheet]'); - expect(bodyLinks).to.have.a.lengthOf(0); + assert.equal(bodyLinks.length, 0); }); it('Injection caused by delayed slots', async () => { @@ -91,10 +92,10 @@ describe('Head injection w/ MDX', () => { const $ = cheerio.load(html); const headLinks = $('head link[rel=stylesheet]'); - expect(headLinks).to.have.a.lengthOf(1); + assert.equal(headLinks.length, 1); const bodyLinks = $('body link[rel=stylesheet]'); - expect(bodyLinks).to.have.a.lengthOf(0); + assert.equal(bodyLinks.length, 0); }); }); }); diff --git a/packages/integrations/mdx/test/invalid-mdx-component.test.js b/packages/integrations/mdx/test/invalid-mdx-component.test.js index 4f96a9fa20be..4621b73da613 100644 --- a/packages/integrations/mdx/test/invalid-mdx-component.test.js +++ b/packages/integrations/mdx/test/invalid-mdx-component.test.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +import { describe, it, before } from 'node:test'; +import * as assert from 'node:assert/strict'; import { loadFixture } from '../../../astro/test/test-utils.js'; import mdx from '../dist/index.js'; @@ -28,8 +29,9 @@ describe('MDX component with runtime error', () => { }); it('Throws the right error', async () => { - expect(error).to.exist; - expect(error?.hint).to.match( + assert.ok(error); + assert.match( + error?.hint, /This issue often occurs when your MDX component encounters runtime errors/ ); }); diff --git a/packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.js b/packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.js index eab2c61b089c..c9f846a3ae22 100644 --- a/packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.js +++ b/packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.js @@ -1,6 +1,7 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; +import { describe, it } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -21,8 +22,9 @@ describe('MDX with Astro Markdown remark-rehype config', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - expect(document.querySelector('#footnote-label').textContent).to.equal('Catatan kaki'); - expect(document.querySelector('.data-footnote-backref').getAttribute('aria-label')).to.equal( + assert.equal(document.querySelector('#footnote-label').textContent, 'Catatan kaki'); + assert.equal( + document.querySelector('.data-footnote-backref').getAttribute('aria-label'), 'Kembali ke konten' ); }); @@ -49,8 +51,9 @@ describe('MDX with Astro Markdown remark-rehype config', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - expect(document.querySelector('#footnote-label').textContent).to.equal('Catatan kaki'); - expect(document.querySelector('.data-footnote-backref').getAttribute('aria-label')).to.equal( + assert.equal(document.querySelector('#footnote-label').textContent, 'Catatan kaki'); + assert.equal( + document.querySelector('.data-footnote-backref').getAttribute('aria-label'), 'Kembali ke konten' ); }); @@ -77,8 +80,9 @@ describe('MDX with Astro Markdown remark-rehype config', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - expect(document.querySelector('#footnote-label').textContent).to.equal('Catatan kaki'); - expect(document.querySelector('.data-footnote-backref').getAttribute('aria-label')).to.equal( + assert.equal(document.querySelector('#footnote-label').textContent, 'Catatan kaki'); + assert.equal( + document.querySelector('.data-footnote-backref').getAttribute('aria-label'), 'Back to reference 1' ); }); diff --git a/packages/integrations/mdx/test/mdx-component.test.js b/packages/integrations/mdx/test/mdx-component.test.js index e2c8417f9f0e..3bb213001ed9 100644 --- a/packages/integrations/mdx/test/mdx-component.test.js +++ b/packages/integrations/mdx/test/mdx-component.test.js @@ -1,6 +1,7 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; +import { describe, it, before, after } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -26,8 +27,8 @@ describe('MDX Component', () => { const h1 = document.querySelector('h1'); const foo = document.querySelector('#foo'); - expect(h1.textContent).to.equal('Hello component!'); - expect(foo.textContent).to.equal('bar'); + assert.equal(h1.textContent, 'Hello component!'); + assert.equal(foo.textContent, 'bar'); }); it('supports glob imports - ', async () => { @@ -37,8 +38,8 @@ describe('MDX Component', () => { const h1 = document.querySelector('[data-default-export] h1'); const foo = document.querySelector('[data-default-export] #foo'); - expect(h1.textContent).to.equal('Hello component!'); - expect(foo.textContent).to.equal('bar'); + assert.equal(h1.textContent, 'Hello component!'); + assert.equal(foo.textContent, 'bar'); }); it('supports glob imports - ', async () => { @@ -48,8 +49,8 @@ describe('MDX Component', () => { const h1 = document.querySelector('[data-content-export] h1'); const foo = document.querySelector('[data-content-export] #foo'); - expect(h1.textContent).to.equal('Hello component!'); - expect(foo.textContent).to.equal('bar'); + assert.equal(h1.textContent, 'Hello component!'); + assert.equal(foo.textContent, 'bar'); }); describe('with ', () => { @@ -60,8 +61,8 @@ describe('MDX Component', () => { const h1 = document.querySelector('h1'); const p = document.querySelector('p'); - expect(h1.textContent).to.equal('MDX containing '); - expect(p.textContent).to.equal('bar'); + assert.equal(h1.textContent, 'MDX containing '); + assert.equal(p.textContent, 'bar'); }); it('supports glob imports - ', async () => { @@ -71,8 +72,8 @@ describe('MDX Component', () => { const h = document.querySelector('[data-default-export] [data-file="WithFragment.mdx"] h1'); const p = document.querySelector('[data-default-export] [data-file="WithFragment.mdx"] p'); - expect(h.textContent).to.equal('MDX containing '); - expect(p.textContent).to.equal('bar'); + assert.equal(h.textContent, 'MDX containing '); + assert.equal(p.textContent, 'bar'); }); it('supports glob imports - ', async () => { @@ -82,8 +83,8 @@ describe('MDX Component', () => { const h = document.querySelector('[data-content-export] [data-file="WithFragment.mdx"] h1'); const p = document.querySelector('[data-content-export] [data-file="WithFragment.mdx"] p'); - expect(h.textContent).to.equal('MDX containing '); - expect(p.textContent).to.equal('bar'); + assert.equal(h.textContent, 'MDX containing '); + assert.equal(p.textContent, 'bar'); }); }); }); @@ -102,7 +103,7 @@ describe('MDX Component', () => { it('supports top-level imports', async () => { const res = await fixture.fetch('/'); - expect(res.status).to.equal(200); + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); @@ -110,14 +111,14 @@ describe('MDX Component', () => { const h1 = document.querySelector('h1'); const foo = document.querySelector('#foo'); - expect(h1.textContent).to.equal('Hello component!'); - expect(foo.textContent).to.equal('bar'); + assert.equal(h1.textContent, 'Hello component!'); + assert.equal(foo.textContent, 'bar'); }); it('supports glob imports - ', async () => { const res = await fixture.fetch('/glob'); - expect(res.status).to.equal(200); + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); @@ -125,14 +126,14 @@ describe('MDX Component', () => { const h1 = document.querySelector('[data-default-export] h1'); const foo = document.querySelector('[data-default-export] #foo'); - expect(h1.textContent).to.equal('Hello component!'); - expect(foo.textContent).to.equal('bar'); + assert.equal(h1.textContent, 'Hello component!'); + assert.equal(foo.textContent, 'bar'); }); it('supports glob imports - ', async () => { const res = await fixture.fetch('/glob'); - expect(res.status).to.equal(200); + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); @@ -140,14 +141,15 @@ describe('MDX Component', () => { const h1 = document.querySelector('[data-content-export] h1'); const foo = document.querySelector('[data-content-export] #foo'); - expect(h1.textContent).to.equal('Hello component!'); - expect(foo.textContent).to.equal('bar'); + assert.equal(h1.textContent, 'Hello component!'); + assert.equal(foo.textContent, 'bar'); }); describe('with ', () => { it('supports top-level imports', async () => { const res = await fixture.fetch('/w-fragment'); - expect(res.status).to.equal(200); + + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); @@ -155,13 +157,14 @@ describe('MDX Component', () => { const h1 = document.querySelector('h1'); const p = document.querySelector('p'); - expect(h1.textContent).to.equal('MDX containing '); - expect(p.textContent).to.equal('bar'); + assert.equal(h1.textContent, 'MDX containing '); + assert.equal(p.textContent, 'bar'); }); it('supports glob imports - ', async () => { const res = await fixture.fetch('/glob'); - expect(res.status).to.equal(200); + + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); @@ -169,13 +172,14 @@ describe('MDX Component', () => { const h = document.querySelector('[data-default-export] [data-file="WithFragment.mdx"] h1'); const p = document.querySelector('[data-default-export] [data-file="WithFragment.mdx"] p'); - expect(h.textContent).to.equal('MDX containing '); - expect(p.textContent).to.equal('bar'); + assert.equal(h.textContent, 'MDX containing '); + assert.equal(p.textContent, 'bar'); }); it('supports glob imports - ', async () => { const res = await fixture.fetch('/glob'); - expect(res.status).to.equal(200); + + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); @@ -183,8 +187,8 @@ describe('MDX Component', () => { const h = document.querySelector('[data-content-export] [data-file="WithFragment.mdx"] h1'); const p = document.querySelector('[data-content-export] [data-file="WithFragment.mdx"] p'); - expect(h.textContent).to.equal('MDX containing '); - expect(p.textContent).to.equal('bar'); + assert.equal(h.textContent, 'MDX containing '); + assert.equal(p.textContent, 'bar'); }); }); }); diff --git a/packages/integrations/mdx/test/mdx-escape.test.js b/packages/integrations/mdx/test/mdx-escape.test.js index d2a4e79ca738..f758dc50f81a 100644 --- a/packages/integrations/mdx/test/mdx-escape.test.js +++ b/packages/integrations/mdx/test/mdx-escape.test.js @@ -1,6 +1,7 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; +import { describe, it, before } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -20,13 +21,13 @@ describe('MDX frontmatter', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - expect(document.body.textContent).to.not.include(' { const html = await fixture.readFile('/html-tag/index.html'); const { document } = parseHTML(html); - expect(document.body.textContent).to.not.include(' { it('remark supports custom vfile data - get title', async () => { const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json')); const titles = frontmatterByPage.map((frontmatter = {}) => frontmatter.title); - expect(titles).to.contain('Page 1'); - expect(titles).to.contain('Page 2'); + assert.equal(titles.includes('Page 1'), true); + assert.equal(titles.includes('Page 2'), true); }); it('rehype supports custom vfile data - reading time', async () => { @@ -26,18 +27,24 @@ describe('MDX frontmatter injection', () => { const readingTimes = frontmatterByPage.map( (frontmatter = {}) => frontmatter.injectedReadingTime ); - expect(readingTimes.length).to.be.greaterThan(0); + assert.equal(readingTimes.length > 0, true); for (let readingTime of readingTimes) { - expect(readingTime).to.not.be.null; - expect(readingTime.text).match(/^\d+ min read/); + assert.notEqual(readingTime, null); + assert.match(readingTime.text, /^\d+ min read/); } }); it('allow user frontmatter mutation', async () => { const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json')); const descriptions = frontmatterByPage.map((frontmatter = {}) => frontmatter.description); - expect(descriptions).to.contain('Processed by remarkDescription plugin: Page 1 description'); - expect(descriptions).to.contain('Processed by remarkDescription plugin: Page 2 description'); + assert.equal( + descriptions.includes('Processed by remarkDescription plugin: Page 1 description'), + true + ); + assert.equal( + descriptions.includes('Processed by remarkDescription plugin: Page 2 description'), + true + ); }); it('passes injected frontmatter to layouts', async () => { @@ -47,7 +54,7 @@ describe('MDX frontmatter injection', () => { const title1 = parseHTML(html1).document.querySelector('title'); const title2 = parseHTML(html2).document.querySelector('title'); - expect(title1.innerHTML).to.equal('Page 1'); - expect(title2.innerHTML).to.equal('Page 2'); + assert.equal(title1.innerHTML, 'Page 1'); + assert.equal(title2.innerHTML, 'Page 2'); }); }); diff --git a/packages/integrations/mdx/test/mdx-frontmatter.test.js b/packages/integrations/mdx/test/mdx-frontmatter.test.js index 539fdbf9960f..94ddc5e21968 100644 --- a/packages/integrations/mdx/test/mdx-frontmatter.test.js +++ b/packages/integrations/mdx/test/mdx-frontmatter.test.js @@ -1,6 +1,7 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; +import { describe, it, before } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -16,12 +17,12 @@ describe('MDX frontmatter', () => { await fixture.build(); }); it('builds when "frontmatter.property" is in JSX expression', async () => { - expect(true).to.equal(true); + assert.equal(true, true); }); it('extracts frontmatter to "frontmatter" export', async () => { const { titles } = JSON.parse(await fixture.readFile('/glob.json')); - expect(titles).to.include('Using YAML frontmatter'); + assert.equal(titles.includes('Using YAML frontmatter'), true); }); it('renders layout from "layout" frontmatter property', async () => { @@ -30,7 +31,7 @@ describe('MDX frontmatter', () => { const layoutParagraph = document.querySelector('[data-layout-rendered]'); - expect(layoutParagraph).to.not.be.null; + assert.notEqual(layoutParagraph, null); }); it('passes frontmatter to layout via "content" and "frontmatter" props', async () => { @@ -40,8 +41,8 @@ describe('MDX frontmatter', () => { const contentTitle = document.querySelector('[data-content-title]'); const frontmatterTitle = document.querySelector('[data-frontmatter-title]'); - expect(contentTitle.textContent).to.equal('Using YAML frontmatter'); - expect(frontmatterTitle.textContent).to.equal('Using YAML frontmatter'); + assert.equal(contentTitle.textContent, 'Using YAML frontmatter'); + assert.equal(frontmatterTitle.textContent, 'Using YAML frontmatter'); }); it('passes headings to layout via "headings" prop', async () => { @@ -52,9 +53,9 @@ describe('MDX frontmatter', () => { (el) => el.textContent ); - expect(headingSlugs.length).to.be.greaterThan(0); - expect(headingSlugs).to.contain('section-1'); - expect(headingSlugs).to.contain('section-2'); + assert.equal(headingSlugs.length > 0, true); + assert.equal(headingSlugs.includes('section-1'), true); + assert.equal(headingSlugs.includes('section-2'), true); }); it('passes "file" and "url" to layout', async () => { @@ -66,12 +67,13 @@ describe('MDX frontmatter', () => { const file = document.querySelector('[data-file]')?.textContent; const url = document.querySelector('[data-url]')?.textContent; - expect(frontmatterFile?.endsWith('with-headings.mdx')).to.equal( + assert.equal( + frontmatterFile?.endsWith('with-headings.mdx'), true, '"file" prop does not end with correct path or is undefined' ); - expect(frontmatterUrl).to.equal('/with-headings'); - expect(file).to.equal(frontmatterFile); - expect(url).to.equal(frontmatterUrl); + assert.equal(frontmatterUrl, '/with-headings'); + assert.equal(file, frontmatterFile); + assert.equal(url, frontmatterUrl); }); }); diff --git a/packages/integrations/mdx/test/mdx-get-headings.test.js b/packages/integrations/mdx/test/mdx-get-headings.test.js index 5b415c70f064..5e5b318ea469 100644 --- a/packages/integrations/mdx/test/mdx-get-headings.test.js +++ b/packages/integrations/mdx/test/mdx-get-headings.test.js @@ -2,7 +2,8 @@ import { rehypeHeadingIds } from '@astrojs/markdown-remark'; import mdx from '@astrojs/mdx'; import { visit } from 'unist-util-visit'; -import { expect } from 'chai'; +import { describe, it, before } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -24,17 +25,18 @@ describe('MDX getHeadings', () => { const h2Ids = document.querySelectorAll('h2').map((el) => el?.id); const h3Ids = document.querySelectorAll('h3').map((el) => el?.id); - expect(document.querySelector('h1').id).to.equal('heading-test'); - expect(h2Ids).to.contain('section-1'); - expect(h2Ids).to.contain('section-2'); - expect(h3Ids).to.contain('subsection-1'); - expect(h3Ids).to.contain('subsection-2'); + assert.equal(document.querySelector('h1').id, 'heading-test'); + assert.equal(h2Ids.includes('section-1'), true); + assert.equal(h2Ids.includes('section-2'), true); + assert.equal(h3Ids.includes('subsection-1'), true); + assert.equal(h3Ids.includes('subsection-2'), true); }); it('generates correct getHeadings() export', async () => { const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json')); // TODO: make this a snapshot test :) - expect(JSON.stringify(headingsByPage['./test.mdx'])).to.equal( + assert.equal( + JSON.stringify(headingsByPage['./test.mdx']), JSON.stringify([ { depth: 1, slug: 'heading-test', text: 'Heading test' }, { depth: 2, slug: 'section-1', text: 'Section 1' }, @@ -47,7 +49,8 @@ describe('MDX getHeadings', () => { it('generates correct getHeadings() export for JSX expressions', async () => { const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json')); - expect(JSON.stringify(headingsByPage['./test-with-jsx-expressions.mdx'])).to.equal( + assert.equal( + JSON.stringify(headingsByPage['./test-with-jsx-expressions.mdx']), JSON.stringify([ { depth: 1, @@ -91,18 +94,20 @@ describe('MDX heading IDs can be customized by user plugins', () => { const { document } = parseHTML(html); const h1 = document.querySelector('h1'); - expect(h1?.textContent).to.equal('Heading test'); - expect(h1?.getAttribute('id')).to.equal('0'); + assert.equal(h1?.textContent, 'Heading test'); + assert.equal(h1?.getAttribute('id'), '0'); const headingIDs = document.querySelectorAll('h1,h2,h3').map((el) => el.id); - expect(JSON.stringify(headingIDs)).to.equal( + assert.equal( + JSON.stringify(headingIDs), JSON.stringify(Array.from({ length: headingIDs.length }, (_, idx) => String(idx))) ); }); it('generates correct getHeadings() export', async () => { const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json')); - expect(JSON.stringify(headingsByPage['./test.mdx'])).to.equal( + assert.equal( + JSON.stringify(headingsByPage['./test.mdx']), JSON.stringify([ { depth: 1, slug: '0', text: 'Heading test' }, { depth: 2, slug: '1', text: 'Section 1' }, @@ -145,8 +150,8 @@ describe('MDX heading IDs can be injected before user plugins', () => { const { document } = parseHTML(html); const h1 = document.querySelector('h1'); - expect(h1?.textContent).to.equal('Heading test heading-test'); - expect(h1?.id).to.equal('heading-test'); + assert.equal(h1?.textContent, 'Heading test heading-test'); + assert.equal(h1?.id, 'heading-test'); }); }); @@ -168,18 +173,19 @@ describe('MDX headings with frontmatter', () => { const h3Ids = document.querySelectorAll('h3').map((el) => el?.id); - expect(document.querySelector('h1').id).to.equal('the-frontmatter-title'); - expect(document.querySelector('h2').id).to.equal('frontmattertitle'); - expect(h3Ids).to.contain('keyword-2'); - expect(h3Ids).to.contain('tag-1'); - expect(document.querySelector('h4').id).to.equal('item-2'); - expect(document.querySelector('h5').id).to.equal('nested-item-3'); - expect(document.querySelector('h6').id).to.equal('frontmatterunknown'); + assert.equal(document.querySelector('h1').id, 'the-frontmatter-title'); + assert.equal(document.querySelector('h2').id, 'frontmattertitle'); + assert.equal(h3Ids.includes('keyword-2'), true); + assert.equal(h3Ids.includes('tag-1'), true); + assert.equal(document.querySelector('h4').id, 'item-2'); + assert.equal(document.querySelector('h5').id, 'nested-item-3'); + assert.equal(document.querySelector('h6').id, 'frontmatterunknown'); }); it('generates correct getHeadings() export', async () => { const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json')); - expect(JSON.stringify(headingsByPage['./test-with-frontmatter.mdx'])).to.equal( + assert.equal( + JSON.stringify(headingsByPage['./test-with-frontmatter.mdx']), JSON.stringify([ { depth: 1, slug: 'the-frontmatter-title', text: 'The Frontmatter Title' }, { depth: 2, slug: 'frontmattertitle', text: 'frontmatter.title' }, diff --git a/packages/integrations/mdx/test/mdx-get-static-paths.test.js b/packages/integrations/mdx/test/mdx-get-static-paths.test.js index c5a34f7deaf4..153ddad7cbe1 100644 --- a/packages/integrations/mdx/test/mdx-get-static-paths.test.js +++ b/packages/integrations/mdx/test/mdx-get-static-paths.test.js @@ -1,6 +1,7 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; +import { describe, it, before } from 'node:test'; +import * as assert from 'node:assert/strict'; import { loadFixture } from '../../../astro/test/test-utils.js'; import * as cheerio from 'cheerio'; @@ -21,11 +22,12 @@ describe('getStaticPaths', () => { const html = await fixture.readFile('/one/index.html'); const $ = cheerio.load(html); - expect($('p').text()).to.equal('First mdx file'); - expect($('#one').text()).to.equal('hello', 'Frontmatter included'); - expect($('#url').text()).to.equal('src/content/1.mdx', 'url is included'); - expect($('#file').text()).to.contain( - 'fixtures/mdx-get-static-paths/src/content/1.mdx', + assert.equal($('p').text(), 'First mdx file'); + assert.equal($('#one').text(), 'hello', 'Frontmatter included'); + assert.equal($('#url').text(), 'src/content/1.mdx', 'url is included'); + assert.equal( + $('#file').text().includes('fixtures/mdx-get-static-paths/src/content/1.mdx'), + true, 'file is included' ); }); diff --git a/packages/integrations/mdx/test/mdx-images.test.js b/packages/integrations/mdx/test/mdx-images.test.js index 950b54581d2c..e070f235864f 100644 --- a/packages/integrations/mdx/test/mdx-images.test.js +++ b/packages/integrations/mdx/test/mdx-images.test.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +import { describe, it, before, after } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -22,41 +23,41 @@ describe('MDX Page', () => { describe('Optimized images in MDX', () => { it('works', async () => { const res = await fixture.fetch('/'); - expect(res.status).to.equal(200); + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); const imgs = document.getElementsByTagName('img'); - expect(imgs.length).to.equal(4); + assert.equal(imgs.length, 4); // Image using a relative path - expect(imgs.item(0).src.startsWith('/_image')).to.be.true; + assert.equal(imgs.item(0).src.startsWith('/_image'), true); // Image using an aliased path - expect(imgs.item(1).src.startsWith('/_image')).to.be.true; + assert.equal(imgs.item(1).src.startsWith('/_image'), true); // Image with title - expect(imgs.item(2).title).to.equal('Houston title'); + assert.equal(imgs.item(2).title, 'Houston title'); // Image with spaces in the path - expect(imgs.item(3).src.startsWith('/_image')).to.be.true; + assert.equal(imgs.item(3).src.startsWith('/_image'), true); }); for (const route of imageTestRoutes) { it(`supports img component - ${route}`, async () => { const res = await fixture.fetch(`/${route}`); - expect(res.status).to.equal(200); + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); const imgs = document.getElementsByTagName('img'); - expect(imgs.length).to.equal(2); + assert.equal(imgs.length, 2); const assetsImg = imgs.item(0); - expect(assetsImg.src.startsWith('/_image')).to.be.true; - expect(assetsImg.hasAttribute('data-my-image')).to.be.true; + assert.equal(assetsImg.src.startsWith('/_image'), true); + assert.equal(assetsImg.hasAttribute('data-my-image'), true); const publicImg = imgs.item(1); - expect(publicImg.src).to.equal('/favicon.svg'); - expect(publicImg.hasAttribute('data-my-image')).to.be.true; + assert.equal(publicImg.src, '/favicon.svg'); + assert.equal(publicImg.hasAttribute('data-my-image'), true); }); } }); diff --git a/packages/integrations/mdx/test/mdx-infinite-loop.test.js b/packages/integrations/mdx/test/mdx-infinite-loop.test.js index c76c242703e6..a13554fbe4ee 100644 --- a/packages/integrations/mdx/test/mdx-infinite-loop.test.js +++ b/packages/integrations/mdx/test/mdx-infinite-loop.test.js @@ -1,6 +1,7 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; +import { describe, it, before } from 'node:test'; +import * as assert from 'node:assert/strict'; import { loadFixture } from '../../../astro/test/test-utils.js'; describe('MDX Infinite Loop', () => { @@ -24,7 +25,7 @@ describe('MDX Infinite Loop', () => { }); it('does not hang forever if an error is thrown', async () => { - expect(!!err).to.be.true; + assert.equal(!!err, true); }); }); }); diff --git a/packages/integrations/mdx/test/mdx-math.test.js b/packages/integrations/mdx/test/mdx-math.test.js index 52ee94a46919..cc6169c554c9 100644 --- a/packages/integrations/mdx/test/mdx-math.test.js +++ b/packages/integrations/mdx/test/mdx-math.test.js @@ -1,5 +1,6 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; +import { describe, it } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; import remarkMath from 'remark-math'; @@ -25,10 +26,14 @@ describe('MDX math', () => { const { document } = parseHTML(html); const mjxContainer = document.querySelector('mjx-container[jax="SVG"]'); - expect(mjxContainer).to.not.be.null; + assert.notEqual(mjxContainer, null); const mjxStyle = document.querySelector('style').innerHTML; - expect(mjxStyle).to.include('mjx-container[jax="SVG"]', 'style should not be html-escaped'); + assert.equal( + mjxStyle.includes('mjx-container[jax="SVG"]'), + true, + 'style should not be html-escaped' + ); }); it('works with chtml', async () => { @@ -55,10 +60,14 @@ describe('MDX math', () => { const { document } = parseHTML(html); const mjxContainer = document.querySelector('mjx-container[jax="CHTML"]'); - expect(mjxContainer).to.not.be.null; + assert.notEqual(mjxContainer, null); const mjxStyle = document.querySelector('style').innerHTML; - expect(mjxStyle).to.include('mjx-container[jax="CHTML"]', 'style should not be html-escaped'); + assert.equal( + mjxStyle.includes('mjx-container[jax="CHTML"]'), + true, + 'style should not be html-escaped' + ); }); }); }); diff --git a/packages/integrations/mdx/test/mdx-namespace.test.js b/packages/integrations/mdx/test/mdx-namespace.test.js index 486910ca2551..04c07817d435 100644 --- a/packages/integrations/mdx/test/mdx-namespace.test.js +++ b/packages/integrations/mdx/test/mdx-namespace.test.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +import { describe, it, before, after } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -23,8 +24,8 @@ describe('MDX Namespace', () => { const island = document.querySelector('astro-island'); const component = document.querySelector('#component'); - expect(island).not.undefined; - expect(component.textContent).equal('Hello world'); + assert.notEqual(island, undefined); + assert.equal(component.textContent, 'Hello world'); }); it('works for star', async () => { @@ -34,8 +35,8 @@ describe('MDX Namespace', () => { const island = document.querySelector('astro-island'); const component = document.querySelector('#component'); - expect(island).not.undefined; - expect(component.textContent).equal('Hello world'); + assert.notEqual(island, undefined); + assert.equal(component.textContent, 'Hello world'); }); }); @@ -53,7 +54,7 @@ describe('MDX Namespace', () => { it('works for object', async () => { const res = await fixture.fetch('/object'); - expect(res.status).to.equal(200); + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); @@ -61,14 +62,14 @@ describe('MDX Namespace', () => { const island = document.querySelector('astro-island'); const component = document.querySelector('#component'); - expect(island).not.undefined; - expect(component.textContent).equal('Hello world'); + assert.notEqual(island, undefined); + assert.equal(component.textContent, 'Hello world'); }); it('works for star', async () => { const res = await fixture.fetch('/star'); - expect(res.status).to.equal(200); + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); @@ -76,8 +77,8 @@ describe('MDX Namespace', () => { const island = document.querySelector('astro-island'); const component = document.querySelector('#component'); - expect(island).not.undefined; - expect(component.textContent).equal('Hello world'); + assert.notEqual(island, undefined); + assert.equal(component.textContent, 'Hello world'); }); }); }); diff --git a/packages/integrations/mdx/test/mdx-optimize.test.js b/packages/integrations/mdx/test/mdx-optimize.test.js index 2e67a7064972..7d9a68c95091 100644 --- a/packages/integrations/mdx/test/mdx-optimize.test.js +++ b/packages/integrations/mdx/test/mdx-optimize.test.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +import { describe, it, before } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -17,31 +18,33 @@ describe('MDX optimize', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - expect(document.querySelector('h1').textContent).include('MDX page'); - expect(document.querySelector('p').textContent).include( - 'I once heard a very inspirational quote:' + assert.equal(document.querySelector('h1').textContent.includes('MDX page'), true); + assert.equal( + document.querySelector('p').textContent.includes('I once heard a very inspirational quote:'), + true ); const blockquote = document.querySelector('blockquote.custom-blockquote'); - expect(blockquote).to.not.be.null; - expect(blockquote.textContent).to.include('I like pancakes'); + assert.notEqual(blockquote, null); + assert.equal(blockquote.textContent.includes('I like pancakes'), true); const code = document.querySelector('pre.astro-code'); - expect(code).to.not.be.null; - expect(code.textContent).to.include(`const pancakes = 'yummy'`); + assert.notEqual(code, null); + assert.equal(code.textContent.includes(`const pancakes = 'yummy'`), true); }); it('renders an Astro page that imports MDX fine', async () => { const html = await fixture.readFile('/import/index.html'); const { document } = parseHTML(html); - expect(document.querySelector('h1').textContent).include('Astro page'); - expect(document.querySelector('p').textContent).include( - 'I once heard a very inspirational quote:' + assert.equal(document.querySelector('h1').textContent.includes('Astro page'), true); + assert.equal( + document.querySelector('p').textContent.includes('I once heard a very inspirational quote:'), + true ); const blockquote = document.querySelector('blockquote.custom-blockquote'); - expect(blockquote).to.not.be.null; - expect(blockquote.textContent).to.include('I like pancakes'); + assert.notEqual(blockquote, null); + assert.equal(blockquote.textContent.includes('I like pancakes'), true); }); }); diff --git a/packages/integrations/mdx/test/mdx-page.test.js b/packages/integrations/mdx/test/mdx-page.test.js index 752987012758..6a97fb7366e8 100644 --- a/packages/integrations/mdx/test/mdx-page.test.js +++ b/packages/integrations/mdx/test/mdx-page.test.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +import { describe, it, before, after } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -24,7 +25,7 @@ describe('MDX Page', () => { const h1 = document.querySelector('h1'); - expect(h1.textContent).to.equal('Hello page!'); + assert.equal(h1.textContent, 'Hello page!'); }); it('injects style imports when layout is not applied', async () => { @@ -33,7 +34,7 @@ describe('MDX Page', () => { const stylesheet = document.querySelector('link[rel="stylesheet"]'); - expect(stylesheet).to.not.be.null; + assert.notEqual(stylesheet, null); }); }); @@ -51,14 +52,14 @@ describe('MDX Page', () => { it('works', async () => { const res = await fixture.fetch('/'); - expect(res.status).to.equal(200); + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); const h1 = document.querySelector('h1'); - expect(h1.textContent).to.equal('Hello page!'); + assert.equal(h1.textContent, 'Hello page!'); }); }); }); diff --git a/packages/integrations/mdx/test/mdx-plugins.test.js b/packages/integrations/mdx/test/mdx-plugins.test.js index 324e00c9ccd2..ae98b48998de 100644 --- a/packages/integrations/mdx/test/mdx-plugins.test.js +++ b/packages/integrations/mdx/test/mdx-plugins.test.js @@ -1,6 +1,7 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; +import { describe, it, before } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; import remarkToc from 'remark-toc'; @@ -22,7 +23,7 @@ describe('MDX plugins', () => { const html = await fixture.readFile(FILE); const { document } = parseHTML(html); - expect(selectTocLink(document)).to.not.be.null; + assert.notEqual(selectTocLink(document), null); }); it('Applies GFM by default', async () => { @@ -33,7 +34,7 @@ describe('MDX plugins', () => { const html = await fixture.readFile(FILE); const { document } = parseHTML(html); - expect(selectGfmLink(document)).to.not.be.null; + assert.notEqual(selectGfmLink(document), null); }); it('Applies SmartyPants by default', async () => { @@ -45,8 +46,8 @@ describe('MDX plugins', () => { const { document } = parseHTML(html); const quote = selectSmartypantsQuote(document); - expect(quote).to.not.be.null; - expect(quote.textContent).to.contain('“Smartypants” is — awesome'); + assert.notEqual(quote, null); + assert.equal(quote.textContent.includes('“Smartypants” is — awesome'), true); }); it('supports custom rehype plugins', async () => { @@ -60,7 +61,7 @@ describe('MDX plugins', () => { const html = await fixture.readFile(FILE); const { document } = parseHTML(html); - expect(selectRehypeExample(document)).to.not.be.null; + assert.notEqual(selectRehypeExample(document), null); }); it('supports custom rehype plugins with namespaced attributes', async () => { @@ -74,7 +75,7 @@ describe('MDX plugins', () => { const html = await fixture.readFile(FILE); const { document } = parseHTML(html); - expect(selectRehypeSvg(document)).to.not.be.null; + assert.notEqual(selectRehypeSvg(document), null); }); it('extends markdown config by default', async () => { @@ -89,8 +90,8 @@ describe('MDX plugins', () => { const html = await fixture.readFile(FILE); const { document } = parseHTML(html); - expect(selectRemarkExample(document)).to.not.be.null; - expect(selectRehypeExample(document)).to.not.be.null; + assert.notEqual(selectRemarkExample(document), null); + assert.notEqual(selectRehypeExample(document), null); }); it('ignores string-based plugins in markdown config', async () => { @@ -104,7 +105,7 @@ describe('MDX plugins', () => { const html = await fixture.readFile(FILE); const { document } = parseHTML(html); - expect(selectTocLink(document)).to.be.null; + assert.equal(selectTocLink(document), null); }); for (const extendMarkdownConfig of [true, false]) { @@ -131,20 +132,21 @@ describe('MDX plugins', () => { const html = await fixture.readFile(FILE); const { document } = parseHTML(html); - expect(selectRemarkExample(document, 'MDX remark plugins not applied.')).to.not.be.null; - expect(selectRehypeExample(document, 'MDX rehype plugins not applied.')).to.not.be.null; + assert.notEqual(selectRemarkExample(document, 'MDX remark plugins not applied.'), null); + assert.notEqual(selectRehypeExample(document, 'MDX rehype plugins not applied.'), null); }); it('Handles Markdown plugins', async () => { const html = await fixture.readFile(FILE); const { document } = parseHTML(html); - expect( + assert.equal( selectTocLink( document, '`remarkToc` plugin applied unexpectedly. Should override Markdown config.' - ) - ).to.be.null; + ), + null + ); }); it('Handles gfm', async () => { @@ -152,9 +154,9 @@ describe('MDX plugins', () => { const { document } = parseHTML(html); if (extendMarkdownConfig === true) { - expect(selectGfmLink(document), 'Does not respect `markdown.gfm` option.').to.be.null; + assert.equal(selectGfmLink(document), null, 'Does not respect `markdown.gfm` option.'); } else { - expect(selectGfmLink(document), 'Respects `markdown.gfm` unexpectedly.').to.not.be.null; + assert.notEqual(selectGfmLink(document), null, 'Respects `markdown.gfm` unexpectedly.'); } }); @@ -165,12 +167,16 @@ describe('MDX plugins', () => { const quote = selectSmartypantsQuote(document); if (extendMarkdownConfig === true) { - expect(quote.textContent, 'Does not respect `markdown.smartypants` option.').to.contain( - '"Smartypants" is -- awesome' + assert.equal( + quote.textContent.includes('"Smartypants" is -- awesome'), + true, + 'Does not respect `markdown.smartypants` option.' ); } else { - expect(quote.textContent, 'Respects `markdown.smartypants` unexpectedly.').to.contain( - '“Smartypants” is — awesome' + assert.equal( + quote.textContent.includes('“Smartypants” is — awesome'), + true, + 'Respects `markdown.smartypants` unexpectedly.' ); } }); @@ -189,7 +195,7 @@ describe('MDX plugins', () => { const html = await fixture.readFile(FILE); const { document } = parseHTML(html); - expect(selectRecmaExample(document)).to.not.be.null; + assert.notEqual(selectRecmaExample(document), null); }); }); diff --git a/packages/integrations/mdx/test/mdx-plus-react.test.js b/packages/integrations/mdx/test/mdx-plus-react.test.js index d0f6095c1d48..eb5955282984 100644 --- a/packages/integrations/mdx/test/mdx-plus-react.test.js +++ b/packages/integrations/mdx/test/mdx-plus-react.test.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +import { describe, it, before } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -32,18 +33,18 @@ describe('MDX and React', () => { const p = document.querySelector('p'); - expect(p.textContent).to.equal('Hello world'); + assert.equal(p.textContent, 'Hello world'); }); it('mdx renders fine', async () => { const html = await fixture.readFile('/post/index.html'); const { document } = parseHTML(html); const h = document.querySelector('#testing'); - expect(h.textContent).to.equal('Testing'); + assert.equal(h.textContent, 'Testing'); }); it('does not get a invalid hook call warning', () => { const errors = unhook(); - expect(errors).to.have.a.lengthOf(0); + assert.equal(errors.length === 0, true); }); }); diff --git a/packages/integrations/mdx/test/mdx-script-style-raw.test.js b/packages/integrations/mdx/test/mdx-script-style-raw.test.js index 99c17ed8d6ff..cac3e782306d 100644 --- a/packages/integrations/mdx/test/mdx-script-style-raw.test.js +++ b/packages/integrations/mdx/test/mdx-script-style-raw.test.js @@ -1,5 +1,6 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; +import { describe, it, before, after } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -24,20 +25,22 @@ describe('MDX script style raw', () => { it('works with with raw script and style strings', async () => { const res = await fixture.fetch('/index.html'); - expect(res.status).to.equal(200); + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); const scriptContent = document.getElementById('test-script').innerHTML; - expect(scriptContent).to.include( - "console.log('raw script')", + assert.equal( + scriptContent.includes("console.log('raw script')"), + true, 'script should not be html-escaped' ); const styleContent = document.getElementById('test-style').innerHTML; - expect(styleContent).to.include( - 'h1[id="script-style-raw"]', + assert.equal( + styleContent.includes('h1[id="script-style-raw"]'), + true, 'style should not be html-escaped' ); }); @@ -55,14 +58,16 @@ describe('MDX script style raw', () => { const { document } = parseHTML(html); const scriptContent = document.getElementById('test-script').innerHTML; - expect(scriptContent).to.include( - "console.log('raw script')", + assert.equal( + scriptContent.includes("console.log('raw script')"), + true, 'script should not be html-escaped' ); const styleContent = document.getElementById('test-style').innerHTML; - expect(styleContent).to.include( - 'h1[id="script-style-raw"]', + assert.equal( + styleContent.includes('h1[id="script-style-raw"]'), + true, 'style should not be html-escaped' ); }); diff --git a/packages/integrations/mdx/test/mdx-slots.test.js b/packages/integrations/mdx/test/mdx-slots.test.js index f0557cc4a139..e5c5f77d4ae0 100644 --- a/packages/integrations/mdx/test/mdx-slots.test.js +++ b/packages/integrations/mdx/test/mdx-slots.test.js @@ -1,6 +1,7 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; +import { describe, it, before, after } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -27,9 +28,9 @@ describe('MDX slots', () => { const defaultSlot = document.querySelector('[data-default-slot]'); const namedSlot = document.querySelector('[data-named-slot]'); - expect(h1.textContent).to.equal('Hello slotted component!'); - expect(defaultSlot.textContent).to.equal('Default content.'); - expect(namedSlot.textContent).to.equal('Content for named slot.'); + assert.equal(h1.textContent, 'Hello slotted component!'); + assert.equal(defaultSlot.textContent, 'Default content.'); + assert.equal(namedSlot.textContent, 'Content for named slot.'); }); it('supports glob imports - ', async () => { @@ -40,9 +41,9 @@ describe('MDX slots', () => { const defaultSlot = document.querySelector('[data-default-export] [data-default-slot]'); const namedSlot = document.querySelector('[data-default-export] [data-named-slot]'); - expect(h1.textContent).to.equal('Hello slotted component!'); - expect(defaultSlot.textContent).to.equal('Default content.'); - expect(namedSlot.textContent).to.equal('Content for named slot.'); + assert.equal(h1.textContent, 'Hello slotted component!'); + assert.equal(defaultSlot.textContent, 'Default content.'); + assert.equal(namedSlot.textContent, 'Content for named slot.'); }); it('supports glob imports - ', async () => { @@ -53,9 +54,9 @@ describe('MDX slots', () => { const defaultSlot = document.querySelector('[data-content-export] [data-default-slot]'); const namedSlot = document.querySelector('[data-content-export] [data-named-slot]'); - expect(h1.textContent).to.equal('Hello slotted component!'); - expect(defaultSlot.textContent).to.equal('Default content.'); - expect(namedSlot.textContent).to.equal('Content for named slot.'); + assert.equal(h1.textContent, 'Hello slotted component!'); + assert.equal(defaultSlot.textContent, 'Default content.'); + assert.equal(namedSlot.textContent, 'Content for named slot.'); }); }); @@ -73,7 +74,7 @@ describe('MDX slots', () => { it('supports top-level imports', async () => { const res = await fixture.fetch('/'); - expect(res.status).to.equal(200); + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); @@ -82,15 +83,15 @@ describe('MDX slots', () => { const defaultSlot = document.querySelector('[data-default-slot]'); const namedSlot = document.querySelector('[data-named-slot]'); - expect(h1.textContent).to.equal('Hello slotted component!'); - expect(defaultSlot.textContent).to.equal('Default content.'); - expect(namedSlot.textContent).to.equal('Content for named slot.'); + assert.equal(h1.textContent, 'Hello slotted component!'); + assert.equal(defaultSlot.textContent, 'Default content.'); + assert.equal(namedSlot.textContent, 'Content for named slot.'); }); it('supports glob imports - ', async () => { const res = await fixture.fetch('/glob'); - expect(res.status).to.equal(200); + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); @@ -99,15 +100,15 @@ describe('MDX slots', () => { const defaultSlot = document.querySelector('[data-default-export] [data-default-slot]'); const namedSlot = document.querySelector('[data-default-export] [data-named-slot]'); - expect(h1.textContent).to.equal('Hello slotted component!'); - expect(defaultSlot.textContent).to.equal('Default content.'); - expect(namedSlot.textContent).to.equal('Content for named slot.'); + assert.equal(h1.textContent, 'Hello slotted component!'); + assert.equal(defaultSlot.textContent, 'Default content.'); + assert.equal(namedSlot.textContent, 'Content for named slot.'); }); it('supports glob imports - ', async () => { const res = await fixture.fetch('/glob'); - expect(res.status).to.equal(200); + assert.equal(res.status, 200); const html = await res.text(); const { document } = parseHTML(html); @@ -116,9 +117,9 @@ describe('MDX slots', () => { const defaultSlot = document.querySelector('[data-content-export] [data-default-slot]'); const namedSlot = document.querySelector('[data-content-export] [data-named-slot]'); - expect(h1.textContent).to.equal('Hello slotted component!'); - expect(defaultSlot.textContent).to.equal('Default content.'); - expect(namedSlot.textContent).to.equal('Content for named slot.'); + assert.equal(h1.textContent, 'Hello slotted component!'); + assert.equal(defaultSlot.textContent, 'Default content.'); + assert.equal(namedSlot.textContent, 'Content for named slot.'); }); }); }); diff --git a/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js b/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js index 40281cffd1c4..e5ce04126744 100644 --- a/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js +++ b/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js @@ -1,6 +1,7 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; +import { describe, it } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; import shikiTwoslash from 'remark-shiki-twoslash'; @@ -24,8 +25,8 @@ describe('MDX syntax highlighting', () => { const { document } = parseHTML(html); const shikiCodeBlock = document.querySelector('pre.astro-code'); - expect(shikiCodeBlock).to.not.be.null; - expect(shikiCodeBlock.getAttribute('style')).to.contain('background-color:#24292e'); + assert.notEqual(shikiCodeBlock, null); + assert.equal(shikiCodeBlock.getAttribute('style').includes('background-color:#24292e'), true); }); it('respects markdown.shikiConfig.theme', async () => { @@ -45,8 +46,8 @@ describe('MDX syntax highlighting', () => { const { document } = parseHTML(html); const shikiCodeBlock = document.querySelector('pre.astro-code'); - expect(shikiCodeBlock).to.not.be.null; - expect(shikiCodeBlock.getAttribute('style')).to.contain('background-color:#282A36'); + assert.notEqual(shikiCodeBlock, null); + assert.equal(shikiCodeBlock.getAttribute('style').includes('background-color:#282A36'), true); }); }); @@ -65,7 +66,7 @@ describe('MDX syntax highlighting', () => { const { document } = parseHTML(html); const prismCodeBlock = document.querySelector('pre.language-astro'); - expect(prismCodeBlock).to.not.be.null; + assert.notEqual(prismCodeBlock, null); }); for (const extendMarkdownConfig of [true, false]) { @@ -88,9 +89,9 @@ describe('MDX syntax highlighting', () => { const { document } = parseHTML(html); const shikiCodeBlock = document.querySelector('pre.astro-code'); - expect(shikiCodeBlock, 'Markdown config syntaxHighlight used unexpectedly').to.be.null; + assert.equal(shikiCodeBlock, null, 'Markdown config syntaxHighlight used unexpectedly'); const prismCodeBlock = document.querySelector('pre.language-astro'); - expect(prismCodeBlock).to.not.be.null; + assert.notEqual(prismCodeBlock, null); }); } }); @@ -113,7 +114,7 @@ describe('MDX syntax highlighting', () => { const { document } = parseHTML(html); const twoslashCodeBlock = document.querySelector('pre.shiki'); - expect(twoslashCodeBlock).to.not.be.null; + assert.notEqual(twoslashCodeBlock, null); }); it('supports custom highlighter - rehype-pretty-code', async () => { @@ -140,6 +141,6 @@ describe('MDX syntax highlighting', () => { await fixture.build(); const html = await fixture.readFile('/index.html'); - expect(html).to.include('style="background-color:#000000"'); + assert.equal(html.includes('style="background-color:#000000"'), true); }); }); diff --git a/packages/integrations/mdx/test/mdx-url-export.test.js b/packages/integrations/mdx/test/mdx-url-export.test.js index 76d6709f0257..6ab475070184 100644 --- a/packages/integrations/mdx/test/mdx-url-export.test.js +++ b/packages/integrations/mdx/test/mdx-url-export.test.js @@ -1,6 +1,7 @@ import mdx from '@astrojs/mdx'; -import { expect } from 'chai'; +import { describe, it, before } from 'node:test'; +import * as assert from 'node:assert/strict'; import { loadFixture } from '../../../astro/test/test-utils.js'; describe('MDX url export', () => { @@ -17,12 +18,12 @@ describe('MDX url export', () => { it('generates correct urls in glob result', async () => { const { urls } = JSON.parse(await fixture.readFile('/pages.json')); - expect(urls).to.include('/test-1'); - expect(urls).to.include('/test-2'); + assert.equal(urls.includes('/test-1'), true); + assert.equal(urls.includes('/test-2'), true); }); it('respects "export url" overrides in glob result', async () => { const { urls } = JSON.parse(await fixture.readFile('/pages.json')); - expect(urls).to.include('/AH!'); + assert.equal(urls.includes('/AH!'), true); }); }); diff --git a/packages/integrations/mdx/test/mdx-vite-env-vars.test.js b/packages/integrations/mdx/test/mdx-vite-env-vars.test.js index f61b3292f82e..0be5f735ffcd 100644 --- a/packages/integrations/mdx/test/mdx-vite-env-vars.test.js +++ b/packages/integrations/mdx/test/mdx-vite-env-vars.test.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +import { describe, it, before } from 'node:test'; +import * as assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -15,31 +16,38 @@ describe('MDX - Vite env vars', () => { const html = await fixture.readFile('/vite-env-vars/index.html'); const { document } = parseHTML(html); - expect(document.querySelector('h1')?.innerHTML).to.contain('import.meta.env.SITE'); - expect(document.querySelector('code')?.innerHTML).to.contain('import.meta.env.SITE'); - expect(document.querySelector('pre')?.innerHTML).to.contain('import.meta.env.SITE'); + assert.equal(document.querySelector('h1')?.innerHTML.includes('import.meta.env.SITE'), true); + assert.equal(document.querySelector('code')?.innerHTML.includes('import.meta.env.SITE'), true); + assert.equal(document.querySelector('pre')?.innerHTML.includes('import.meta.env.SITE'), true); }); it('Allows referencing `import.meta.env` in frontmatter', async () => { const { title = '' } = JSON.parse(await fixture.readFile('/frontmatter.json')); - expect(title).to.contain('import.meta.env.SITE'); + assert.equal(title.includes('import.meta.env.SITE'), true); }); it('Transforms `import.meta.env` in {JSX expressions}', async () => { const html = await fixture.readFile('/vite-env-vars/index.html'); const { document } = parseHTML(html); - expect(document.querySelector('[data-env-site]')?.innerHTML).to.contain( - 'https://mdx-is-neat.com/blog/cool-post' + assert.equal( + document + .querySelector('[data-env-site]') + ?.innerHTML.includes('https://mdx-is-neat.com/blog/cool-post'), + true ); }); it('Transforms `import.meta.env` in variable exports', async () => { const html = await fixture.readFile('/vite-env-vars/index.html'); const { document } = parseHTML(html); - expect(document.querySelector('[data-env-variable-exports]')?.innerHTML).to.contain( - 'MODE works' + assert.equal( + document.querySelector('[data-env-variable-exports]')?.innerHTML.includes('MODE works'), + true ); - expect(document.querySelector('[data-env-variable-exports-unknown]')?.innerHTML).to.contain( - 'exports: ””' // NOTE: these double quotes are special unicode quotes emitted in the HTML file + assert.equal( + document + .querySelector('[data-env-variable-exports-unknown]') + ?.innerHTML.includes('exports: ””'), // NOTE: these double quotes are special unicode quotes emitted in the HTML file + true ); }); it('Transforms `import.meta.env` in HTML attributes', async () => { @@ -47,11 +55,11 @@ describe('MDX - Vite env vars', () => { const { document } = parseHTML(html); const dataAttrDump = document.querySelector('[data-env-dump]'); - expect(dataAttrDump).to.not.be.null; + assert.notEqual(dataAttrDump, null); - expect(dataAttrDump.getAttribute('data-env-prod')).to.not.be.null; - expect(dataAttrDump.getAttribute('data-env-dev')).to.be.null; - expect(dataAttrDump.getAttribute('data-env-base-url')).to.equal('/'); - expect(dataAttrDump.getAttribute('data-env-mode')).to.equal('production'); + assert.notEqual(dataAttrDump.getAttribute('data-env-prod'), null); + assert.equal(dataAttrDump.getAttribute('data-env-dev'), null); + assert.equal(dataAttrDump.getAttribute('data-env-base-url'), '/'); + assert.equal(dataAttrDump.getAttribute('data-env-mode'), 'production'); }); }); diff --git a/packages/integrations/mdx/test/remark-imgattr.test.js b/packages/integrations/mdx/test/remark-imgattr.test.js index 7bedc3e91868..02f155fd4e3a 100644 --- a/packages/integrations/mdx/test/remark-imgattr.test.js +++ b/packages/integrations/mdx/test/remark-imgattr.test.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +import { describe, it, before, after } from 'node:test'; +import * as assert from 'node:assert/strict'; import * as cheerio from 'cheerio'; import { loadFixture } from '../../../astro/test/test-utils.js'; @@ -34,16 +35,14 @@ describe('Testing remark plugins for image processing', () => { it(' has correct attributes', async () => { let $img = $('img'); - expect($img.attr('id')).to.equal('test'); - expect($img.attr('sizes')).to.equal('(min-width: 600px) 600w, 300w'); - expect($img.attr('srcset')).to.not.be.empty; + assert.equal($img.attr('id'), 'test'); + assert.equal($img.attr('sizes'), '(min-width: 600px) 600w, 300w'); + assert.ok($img.attr('srcset')); }); it(' was processed properly', async () => { let $img = $('img'); - expect(new URL($img.attr('src'), 'http://example.com').searchParams.get('w')).to.equal( - '300' - ); + assert.equal(new URL($img.attr('src'), 'http://example.com').searchParams.get('w'), '300'); }); }); }); diff --git a/packages/integrations/node/src/index.ts b/packages/integrations/node/src/index.ts index e7d655403be1..61ea5ad7471b 100644 --- a/packages/integrations/node/src/index.ts +++ b/packages/integrations/node/src/index.ts @@ -18,6 +18,7 @@ export function getAdapter(options: Options): AstroAdapter { isSharpCompatible: true, isSquooshCompatible: true, }, + i18nDomains: 'experimental', }, }; } diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json index 1390903304a7..98b39ff8fc0f 100644 --- a/packages/integrations/react/package.json +++ b/packages/integrations/react/package.json @@ -43,7 +43,7 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "mocha --exit --timeout 20000" + "test": "astro-scripts test \"test/**/*.test.js\"" }, "dependencies": { "@vitejs/plugin-react": "^4.2.0", @@ -54,9 +54,7 @@ "@types/react-dom": "^18.2.15", "astro": "workspace:*", "astro-scripts": "workspace:*", - "chai": "^4.3.7", "cheerio": "1.0.0-rc.12", - "mocha": "^10.2.0", "react": "^18.1.0", "react-dom": "^18.1.0", "vite": "^5.0.12" diff --git a/packages/integrations/react/test/parsed-react-children.test.js b/packages/integrations/react/test/parsed-react-children.test.js index 1c845836f37f..5417705f0896 100644 --- a/packages/integrations/react/test/parsed-react-children.test.js +++ b/packages/integrations/react/test/parsed-react-children.test.js @@ -1,15 +1,16 @@ -import { expect } from 'chai'; +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; import convert from '../vnode-children.js'; describe('experimental react children', () => { it('has undefined as children for direct children', () => { const [imgVNode] = convert(''); - expect(imgVNode.props).to.deep.include({ children: undefined }); + assert.deepStrictEqual(imgVNode.props.children, undefined); }); it('has undefined as children for nested children', () => { const [divVNode] = convert('

'); const [imgVNode] = divVNode.props.children; - expect(imgVNode.props).to.deep.include({ children: undefined }); + assert.deepStrictEqual(imgVNode.props.children, undefined); }); }); diff --git a/packages/integrations/react/test/react-component.test.js b/packages/integrations/react/test/react-component.test.js index 96d60fd54d20..3e07c6d5907c 100644 --- a/packages/integrations/react/test/react-component.test.js +++ b/packages/integrations/react/test/react-component.test.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +import assert from 'node:assert/strict'; +import { describe, it, before, after } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; import { isWindows, loadFixture } from '../../../astro/test/test-utils.js'; @@ -21,50 +22,51 @@ describe('React Components', () => { const $ = cheerioLoad(html); // test 1: basic component renders - expect($('#react-static').text()).to.equal('Hello static!'); + assert.equal($('#react-static').text(), 'Hello static!'); // test 2: no reactroot - expect($('#react-static').attr('data-reactroot')).to.equal(undefined); + assert.equal($('#react-static').attr('data-reactroot'), undefined); // test 3: Can use function components - expect($('#arrow-fn-component')).to.have.lengthOf(1); + assert.equal($('#arrow-fn-component').length, 1); // test 4: Can use spread for components - expect($('#component-spread-props')).to.have.lengthOf(1); + assert.equal($('#component-spread-props').length, 1); // test 5: spread props renders - expect($('#component-spread-props').text(), 'Hello world!'); + assert.equal($('#component-spread-props').text(), 'Hello world!'); // test 6: Can use TS components - expect($('.ts-component')).to.have.lengthOf(1); + assert.equal($('.ts-component').length, 1); // test 7: Can use Pure components - expect($('#pure')).to.have.lengthOf(1); + assert.equal($('#pure').length, 1); // test 8: Check number of islands - expect($('astro-island[uid]')).to.have.lengthOf(9); + assert.equal($('astro-island[uid]').length, 9); // test 9: Check island deduplication const uniqueRootUIDs = new Set($('astro-island').map((i, el) => $(el).attr('uid'))); - expect(uniqueRootUIDs.size).to.equal(8); + assert.equal(uniqueRootUIDs.size, 8); // test 10: Should properly render children passed as props const islandsWithChildren = $('.with-children'); - expect(islandsWithChildren).to.have.lengthOf(2); - expect($(islandsWithChildren[0]).html()).to.equal( + assert.equal(islandsWithChildren.length, 2); + assert.equal( + $(islandsWithChildren[0]).html(), $(islandsWithChildren[1]).find('astro-slot').html() ); // test 11: Should generate unique React.useId per island const islandsWithId = $('.react-use-id'); - expect(islandsWithId).to.have.lengthOf(2); - expect($(islandsWithId[0]).attr('id')).to.not.equal($(islandsWithId[1]).attr('id')); + assert.equal(islandsWithId.length, 2); + assert.notEqual($(islandsWithId[0]).attr('id'), $(islandsWithId[1]).attr('id')); }); it('Can load Vue', async () => { const html = await fixture.readFile('/index.html'); const $ = cheerioLoad(html); - expect($('#vue-h2').text()).to.equal('Hasta la vista, baby'); + assert.equal($('#vue-h2').text(), 'Hasta la vista, baby'); }); it('Can use a pragma comment', async () => { @@ -72,7 +74,7 @@ describe('React Components', () => { const $ = cheerioLoad(html); // test 1: rendered the PragmaComment component - expect($('.pragma-comment')).to.have.lengthOf(2); + assert.equal($('.pragma-comment').length, 2); }); // TODO: is this still a relevant test? @@ -83,35 +85,35 @@ describe('React Components', () => { const div = $('#research'); // test 1: has the hydration attr - expect(div.attr('data-reactroot')).to.be.ok; + assert.ok(div.attr('data-reactroot')); // test 2: renders correctly - expect(div.html()).to.equal('foo bar 1'); + assert.equal(div.html(), 'foo bar 1'); }); it('Can load Suspense-using components', async () => { const html = await fixture.readFile('/suspense/index.html'); const $ = cheerioLoad(html); - expect($('#client #lazy')).to.have.lengthOf(1); - expect($('#server #lazy')).to.have.lengthOf(1); + assert.equal($('#client #lazy').length, 1); + assert.equal($('#server #lazy').length, 1); }); it('Can pass through props with cloneElement', async () => { const html = await fixture.readFile('/index.html'); const $ = cheerioLoad(html); - expect($('#cloned').text()).to.equal('Cloned With Props'); + assert.equal($('#cloned').text(), 'Cloned With Props'); }); it('Children are parsed as React components, can be manipulated', async () => { const html = await fixture.readFile('/children/index.html'); const $ = cheerioLoad(html); - expect($('#one .with-children-count').text()).to.equal('2'); + assert.equal($('#one .with-children-count').text(), '2'); }); it('Client children passes option to the client', async () => { const html = await fixture.readFile('/children/index.html'); const $ = cheerioLoad(html); - expect($('[data-react-children]')).to.have.lengthOf(1); + assert.equal($('[data-react-children]').length, 1); }); }); @@ -136,18 +138,20 @@ describe('React Components', () => { for (const script of $('script').toArray()) { const { src } = script.attribs; if (!src) continue; - expect((await fixture.fetch(src)).status, `404: ${src}`).to.equal(200); + assert.equal((await fixture.fetch(src)).status, 200, `404: ${src}`); } }); // TODO: move this to separate dev test? it.skip('Throws helpful error message on window SSR', async () => { const html = await fixture.fetch('/window/index.html'); - expect(html).to.include( - `[/window] - The window object is not available during server-side rendering (SSR). - Try using \`import.meta.env.SSR\` to write SSR-friendly code. - https://docs.astro.build/reference/api-reference/#importmeta` + assert.ok( + (await html.text()).includes( + `[/window] + The window object is not available during server-side rendering (SSR). + Try using \`import.meta.env.SSR\` to write SSR-friendly code. + https://docs.astro.build/reference/api-reference/#importmeta` + ) ); }); @@ -168,7 +172,7 @@ describe('React Components', () => { const jsxRuntime = component.imports.filter((i) => i.specifier.includes('jsx-runtime')); // test 1: react/jsx-runtime is used for the component - expect(jsxRuntime).to.be.ok; + assert.ok(jsxRuntime); }); it('When a nested component throws it does not crash the server', async () => { diff --git a/packages/integrations/sitemap/package.json b/packages/integrations/sitemap/package.json index c50359c73aa1..2bc76067d39b 100644 --- a/packages/integrations/sitemap/package.json +++ b/packages/integrations/sitemap/package.json @@ -30,7 +30,7 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "mocha --timeout 20000" + "test": "astro-scripts test \"test/**/*.test.js\"" }, "dependencies": { "sitemap": "^7.1.1", @@ -40,8 +40,6 @@ "@astrojs/node": "workspace:*", "astro": "workspace:*", "astro-scripts": "workspace:*", - "chai": "^4.3.7", - "mocha": "^10.2.0", "xml2js": "0.6.2" }, "publishConfig": { diff --git a/packages/integrations/sitemap/test/base-path.test.js b/packages/integrations/sitemap/test/base-path.test.js index a90f28c30275..dd80fd29acae 100644 --- a/packages/integrations/sitemap/test/base-path.test.js +++ b/packages/integrations/sitemap/test/base-path.test.js @@ -1,5 +1,6 @@ import { loadFixture, readXML } from './test-utils.js'; -import { expect } from 'chai'; +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; describe('URLs with base path', () => { /** @type {import('./test-utils').Fixture} */ @@ -17,7 +18,7 @@ describe('URLs with base path', () => { it('Base path is concatenated correctly', async () => { const data = await readXML(fixture.readFile('/client/sitemap-0.xml')); const urls = data.urlset.url; - expect(urls[0].loc[0]).to.equal('http://example.com/base/one/'); + assert.equal(urls[0].loc[0], 'http://example.com/base/one/'); }); }); @@ -33,7 +34,7 @@ describe('URLs with base path', () => { it('Base path is concatenated correctly', async () => { const data = await readXML(fixture.readFile('/sitemap-0.xml')); const urls = data.urlset.url; - expect(urls[0].loc[0]).to.equal('http://example.com/base/123/'); + assert.equal(urls[0].loc[0], 'http://example.com/base/123/'); }); }); }); diff --git a/packages/integrations/sitemap/test/filter.test.js b/packages/integrations/sitemap/test/filter.test.js index b2623248170a..57faa47f7e86 100644 --- a/packages/integrations/sitemap/test/filter.test.js +++ b/packages/integrations/sitemap/test/filter.test.js @@ -1,5 +1,6 @@ import { loadFixture, readXML } from './test-utils.js'; -import { expect } from 'chai'; +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; import { sitemap } from './fixtures/static/deps.mjs'; describe('Filter support', () => { @@ -22,7 +23,7 @@ describe('Filter support', () => { it('Just one page is added', async () => { const data = await readXML(fixture.readFile('/sitemap-0.xml')); const urls = data.urlset.url; - expect(urls.length).to.equal(1); + assert.equal(urls.length, 1); }); }); @@ -42,7 +43,7 @@ describe('Filter support', () => { it('Just one page is added', async () => { const data = await readXML(fixture.readFile('/client/sitemap-0.xml')); const urls = data.urlset.url; - expect(urls.length).to.equal(1); + assert.equal(urls.length, 1); }); }); }); diff --git a/packages/integrations/sitemap/test/routes.test.js b/packages/integrations/sitemap/test/routes.test.js index 909580dd9ce5..326cb64ef996 100644 --- a/packages/integrations/sitemap/test/routes.test.js +++ b/packages/integrations/sitemap/test/routes.test.js @@ -1,5 +1,6 @@ import { loadFixture, readXML } from './test-utils.js'; -import { expect } from 'chai'; +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; describe('routes', () => { /** @type {import('./test-utils.js').Fixture} */ @@ -17,10 +18,10 @@ describe('routes', () => { }); it('does not include endpoints', async () => { - expect(urls).to.not.include('http://example.com/endpoint.json'); + assert.equal(urls.includes('http://example.com/endpoint.json'), false); }); it('does not include redirects', async () => { - expect(urls).to.not.include('http://example.com/redirect'); + assert.equal(urls.includes('http://example.com/redirect'), false); }); }); diff --git a/packages/integrations/sitemap/test/ssr.test.js b/packages/integrations/sitemap/test/ssr.test.js index e6f8412d56fc..d4bbd5264244 100644 --- a/packages/integrations/sitemap/test/ssr.test.js +++ b/packages/integrations/sitemap/test/ssr.test.js @@ -1,5 +1,6 @@ import { loadFixture, readXML } from './test-utils.js'; -import { expect } from 'chai'; +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; describe('SSR support', () => { /** @type {import('./test-utils.js').Fixture} */ @@ -16,7 +17,7 @@ describe('SSR support', () => { const data = await readXML(fixture.readFile('/client/sitemap-0.xml')); const urls = data.urlset.url; - expect(urls[0].loc[0]).to.equal('http://example.com/one/'); - expect(urls[1].loc[0]).to.equal('http://example.com/two/'); + assert.equal(urls[0].loc[0], 'http://example.com/one/'); + assert.equal(urls[1].loc[0], 'http://example.com/two/'); }); }); diff --git a/packages/integrations/sitemap/test/staticPaths.test.js b/packages/integrations/sitemap/test/staticPaths.test.js index 603af163d10b..0e73537656bf 100644 --- a/packages/integrations/sitemap/test/staticPaths.test.js +++ b/packages/integrations/sitemap/test/staticPaths.test.js @@ -1,5 +1,6 @@ import { loadFixture, readXML } from './test-utils.js'; -import { expect } from 'chai'; +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; describe('getStaticPaths support', () => { /** @type {import('./test-utils.js').Fixture} */ @@ -19,24 +20,24 @@ describe('getStaticPaths support', () => { }); it('requires zero config for getStaticPaths', async () => { - expect(urls).to.include('http://example.com/one/'); - expect(urls).to.include('http://example.com/two/'); + assert.equal(urls.includes('http://example.com/one/'), true); + assert.equal(urls.includes('http://example.com/two/'), true); }); it('does not include 404 pages', () => { - expect(urls).to.not.include('http://example.com/404/'); + assert.equal(urls.includes('http://example.com/404/'), false); }); it('does not include nested 404 pages', () => { - expect(urls).to.not.include('http://example.com/de/404/'); + assert.equal(urls.includes('http://example.com/de/404/'), false); }); it('includes numerical pages', () => { - expect(urls).to.include('http://example.com/123/'); + assert.equal(urls.includes('http://example.com/123/'), true); }); it('should render the endpoint', async () => { const page = await fixture.readFile('./it/manifest'); - expect(page).to.contain('I\'m a route in the "it" language.'); + assert.match(page, /I\'m a route in the "it" language./); }); }); diff --git a/packages/integrations/sitemap/test/trailing-slash.test.js b/packages/integrations/sitemap/test/trailing-slash.test.js index a393fb9f12f5..bdedf7687391 100644 --- a/packages/integrations/sitemap/test/trailing-slash.test.js +++ b/packages/integrations/sitemap/test/trailing-slash.test.js @@ -1,5 +1,6 @@ import { loadFixture, readXML } from './test-utils.js'; -import { expect } from 'chai'; +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; describe('Trailing slash', () => { /** @type {import('./test-utils').Fixture} */ @@ -21,7 +22,7 @@ describe('Trailing slash', () => { it('URLs end with trailing slash', async () => { const data = await readXML(fixture.readFile('/sitemap-0.xml')); const urls = data.urlset.url; - expect(urls[0].loc[0]).to.equal('http://example.com/one/'); + assert.equal(urls[0].loc[0], 'http://example.com/one/'); }); }); @@ -40,7 +41,7 @@ describe('Trailing slash', () => { it('URLs do not end with trailing slash', async () => { const data = await readXML(fixture.readFile('/sitemap-0.xml')); const urls = data.urlset.url; - expect(urls[0].loc[0]).to.equal('http://example.com/one'); + assert.equal(urls[0].loc[0], 'http://example.com/one'); }); }); }); @@ -57,7 +58,7 @@ describe('Trailing slash', () => { it('URLs do no end with trailing slash', async () => { const data = await readXML(fixture.readFile('/sitemap-0.xml')); const urls = data.urlset.url; - expect(urls[0].loc[0]).to.equal('http://example.com/one'); + assert.equal(urls[0].loc[0], 'http://example.com/one'); }); describe('with base path', () => { before(async () => { @@ -72,7 +73,7 @@ describe('Trailing slash', () => { it('URLs do not end with trailing slash', async () => { const data = await readXML(fixture.readFile('/sitemap-0.xml')); const urls = data.urlset.url; - expect(urls[0].loc[0]).to.equal('http://example.com/base/one'); + assert.equal(urls[0].loc[0], 'http://example.com/base/one'); }); }); }); @@ -89,7 +90,7 @@ describe('Trailing slash', () => { it('URLs end with trailing slash', async () => { const data = await readXML(fixture.readFile('/sitemap-0.xml')); const urls = data.urlset.url; - expect(urls[0].loc[0]).to.equal('http://example.com/one/'); + assert.equal(urls[0].loc[0], 'http://example.com/one/'); }); describe('with base path', () => { before(async () => { @@ -104,7 +105,7 @@ describe('Trailing slash', () => { it('URLs end with trailing slash', async () => { const data = await readXML(fixture.readFile('/sitemap-0.xml')); const urls = data.urlset.url; - expect(urls[0].loc[0]).to.equal('http://example.com/base/one/'); + assert.equal(urls[0].loc[0], 'http://example.com/base/one/'); }); }); }); diff --git a/packages/integrations/tailwind/package.json b/packages/integrations/tailwind/package.json index c07f65963be2..add6641eab9b 100644 --- a/packages/integrations/tailwind/package.json +++ b/packages/integrations/tailwind/package.json @@ -30,7 +30,7 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "mocha --exit --timeout 20000 test/" + "test": "astro-scripts test \"test/**/*.test.js\"" }, "dependencies": { "autoprefixer": "^10.4.15", @@ -40,8 +40,6 @@ "devDependencies": { "astro": "workspace:*", "astro-scripts": "workspace:*", - "chai": "^4.3.7", - "mocha": "^10.2.0", "tailwindcss": "^3.3.5", "vite": "^5.0.12" }, diff --git a/packages/integrations/tailwind/test/basic.test.js b/packages/integrations/tailwind/test/basic.test.js index e5275c81e9d6..6e544eb8c70d 100644 --- a/packages/integrations/tailwind/test/basic.test.js +++ b/packages/integrations/tailwind/test/basic.test.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +import * as assert from 'node:assert/strict'; +import { describe, it, before } from 'node:test'; import { loadFixture } from '../../../astro/test/test-utils.js'; describe('Basic', () => { @@ -25,9 +26,12 @@ describe('Basic', () => { } } - expect(css).to.include('box-sizing:border-box;'); // base css - expect(css).to.include('text-red-500'); // class css - expect(css).to.match(/\.a\[data-astro-cid-.*?\] \.b\[data-astro-cid-.*?\]/); // nesting + assert.equal(css.includes('box-sizing:border-box;'), true); // base css + assert.equal(css.includes('text-red-500'), true); // class css + assert.equal( + new RegExp(/\.a\[data-astro-cid-.*?\] \.b\[data-astro-cid-.*?\]/).test(css), + true + ); // nesting }); }); }); diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index f1c75e4b4d9e..11efdc16f62d 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -82,6 +82,7 @@ function getAdapter({ isSharpCompatible: true, isSquooshCompatible: true, }, + i18nDomains: 'experimental', }, }; } diff --git a/packages/markdown/remark/CHANGELOG.md b/packages/markdown/remark/CHANGELOG.md index c71de000f32b..550830e081ce 100644 --- a/packages/markdown/remark/CHANGELOG.md +++ b/packages/markdown/remark/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/markdown-remark +## 4.2.1 + +### Patch Changes + +- [#9866](https://github.com/withastro/astro/pull/9866) [`44c957f893c6bf5f5b7c78301de7b21c5975584d`](https://github.com/withastro/astro/commit/44c957f893c6bf5f5b7c78301de7b21c5975584d) Thanks [@ktym4a](https://github.com/ktym4a)! - Fixes a bug where non-UTF-8 file names are not displayed when using relative paths in markdowns. + ## 4.2.0 ### Minor Changes diff --git a/packages/markdown/remark/package.json b/packages/markdown/remark/package.json index e38b8e792440..ee081361df04 100644 --- a/packages/markdown/remark/package.json +++ b/packages/markdown/remark/package.json @@ -1,6 +1,6 @@ { "name": "@astrojs/markdown-remark", - "version": "4.2.0", + "version": "4.2.1", "type": "module", "author": "withastro", "license": "MIT", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e76d8195cf21..307fc01b65ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,13 +128,13 @@ importers: examples/basics: dependencies: astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro examples/blog: dependencies: '@astrojs/mdx': - specifier: ^2.1.0 + specifier: ^2.1.1 version: link:../../packages/integrations/mdx '@astrojs/rss': specifier: ^4.0.4 @@ -143,13 +143,13 @@ importers: specifier: ^3.0.5 version: link:../../packages/integrations/sitemap astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro examples/component: devDependencies: astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro examples/framework-alpine: @@ -164,7 +164,7 @@ importers: specifier: ^3.13.3 version: 3.13.3 astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro examples/framework-lit: @@ -176,7 +176,7 @@ importers: specifier: ^0.2.1 version: 0.2.1 astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro lit: specifier: ^2.8.0 @@ -200,7 +200,7 @@ importers: specifier: ^4.0.8 version: link:../../packages/integrations/vue astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -230,7 +230,7 @@ importers: specifier: ^1.2.1 version: 1.2.1(preact@10.19.3) astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -248,7 +248,7 @@ importers: specifier: ^18.2.15 version: 18.2.18 astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro react: specifier: ^18.2.0 @@ -263,7 +263,7 @@ importers: specifier: ^4.0.1 version: link:../../packages/integrations/solid astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro solid-js: specifier: ^1.8.5 @@ -275,7 +275,7 @@ importers: specifier: ^5.0.3 version: link:../../packages/integrations/svelte astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro svelte: specifier: ^4.2.5 @@ -287,7 +287,7 @@ importers: specifier: ^4.0.8 version: link:../../packages/integrations/vue astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro vue: specifier: ^3.3.8 @@ -299,13 +299,13 @@ importers: specifier: ^8.1.0 version: link:../../packages/integrations/node astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro examples/integration: devDependencies: astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro examples/middleware: @@ -314,7 +314,7 @@ importers: specifier: ^8.1.0 version: link:../../packages/integrations/node astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro html-minifier: specifier: ^4.0.0 @@ -327,19 +327,19 @@ importers: examples/minimal: dependencies: astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro examples/non-html-pages: dependencies: astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro examples/portfolio: dependencies: astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro examples/ssr: @@ -351,7 +351,7 @@ importers: specifier: ^5.0.3 version: link:../../packages/integrations/svelte astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro svelte: specifier: ^4.2.5 @@ -360,7 +360,7 @@ importers: examples/starlog: dependencies: astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro sass: specifier: ^1.69.5 @@ -378,7 +378,7 @@ importers: specifier: ^5.1.0 version: link:../../packages/integrations/tailwind astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro examples/with-markdoc: @@ -387,16 +387,16 @@ importers: specifier: ^0.8.3 version: link:../../packages/integrations/markdoc astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro examples/with-markdown-plugins: dependencies: '@astrojs/markdown-remark': - specifier: ^4.2.0 + specifier: ^4.2.1 version: link:../../packages/markdown/remark astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro hast-util-select: specifier: ^6.0.2 @@ -417,19 +417,19 @@ importers: examples/with-markdown-shiki: dependencies: astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro examples/with-mdx: dependencies: '@astrojs/mdx': - specifier: ^2.1.0 + specifier: ^2.1.1 version: link:../../packages/integrations/mdx '@astrojs/preact': specifier: ^3.1.0 version: link:../../packages/integrations/preact astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -444,7 +444,7 @@ importers: specifier: ^0.5.0 version: 0.5.0(nanostores@0.9.5)(preact@10.19.3) astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro nanostores: specifier: ^0.9.5 @@ -456,7 +456,7 @@ importers: examples/with-tailwindcss: dependencies: '@astrojs/mdx': - specifier: ^2.1.0 + specifier: ^2.1.1 version: link:../../packages/integrations/mdx '@astrojs/tailwind': specifier: ^5.1.0 @@ -465,7 +465,7 @@ importers: specifier: ^1.6.3 version: 1.6.4 astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro autoprefixer: specifier: ^10.4.15 @@ -483,7 +483,7 @@ importers: examples/with-vitest: dependencies: astro: - specifier: ^4.2.6 + specifier: ^4.2.8 version: link:../../packages/astro vitest: specifier: ^1.2.1 @@ -2900,6 +2900,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/i18n-routing-subdomain: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/import-ts-with-js: dependencies: astro: @@ -3926,36 +3932,24 @@ importers: '@astrojs/markdown-remark': specifier: workspace:* version: link:../../markdown/remark - '@types/chai': - specifier: ^4.3.10 - version: 4.3.11 '@types/html-escaper': specifier: ^3.0.2 version: 3.0.2 '@types/markdown-it': specifier: ^13.0.6 version: 13.0.7 - '@types/mocha': - specifier: ^10.0.4 - version: 10.0.6 astro: specifier: workspace:* version: link:../../astro astro-scripts: specifier: workspace:* version: link:../../../scripts - chai: - specifier: ^4.3.7 - version: 4.3.10 devalue: specifier: ^4.3.2 version: 4.3.2 linkedom: specifier: ^0.16.4 version: 0.16.6 - mocha: - specifier: ^10.2.0 - version: 10.2.0 vite: specifier: ^5.0.12 version: 5.0.12(@types/node@18.19.4)(sass@1.69.6) @@ -4146,9 +4140,6 @@ importers: astro-scripts: specifier: workspace:* version: link:../../../scripts - chai: - specifier: ^4.3.7 - version: 4.3.10 cheerio: specifier: 1.0.0-rc.12 version: 1.0.0-rc.12 @@ -4161,9 +4152,6 @@ importers: mdast-util-to-string: specifier: ^4.0.0 version: 4.0.0 - mocha: - specifier: ^10.2.0 - version: 10.2.0 reading-time: specifier: ^1.5.0 version: 1.5.0 @@ -4554,15 +4542,9 @@ importers: astro-scripts: specifier: workspace:* version: link:../../../scripts - chai: - specifier: ^4.3.7 - version: 4.3.10 cheerio: specifier: 1.0.0-rc.12 version: 1.0.0-rc.12 - mocha: - specifier: ^10.2.0 - version: 10.2.0 react: specifier: ^18.1.0 version: 18.2.0 @@ -4612,12 +4594,6 @@ importers: astro-scripts: specifier: workspace:* version: link:../../../scripts - chai: - specifier: ^4.3.7 - version: 4.3.10 - mocha: - specifier: ^10.2.0 - version: 10.2.0 xml2js: specifier: 0.6.2 version: 0.6.2 @@ -4705,12 +4681,6 @@ importers: astro-scripts: specifier: workspace:* version: link:../../../scripts - chai: - specifier: ^4.3.7 - version: 4.3.10 - mocha: - specifier: ^10.2.0 - version: 10.2.0 tailwindcss: specifier: ^3.3.5 version: 3.4.0