From e3bfd9396969caf35b3b05135539e82aab560c92 Mon Sep 17 00:00:00 2001 From: mtwilliams Date: Thu, 12 Dec 2024 08:29:46 -0500 Subject: [PATCH 1/7] fix(i18n): parse params and props correctly with fallback (#12709) Co-authored-by: Emanuele Stoppa --- .changeset/selfish-paws-play.md | 5 ++ .../astro/src/core/render/params-and-props.ts | 9 ++- .../src/pages/blog/[id].astro | 16 +++- .../src/pages/pt/blog/[id].astro | 10 ++- packages/astro/test/i18n-routing.test.js | 74 ++++++++++++++++++- 5 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 .changeset/selfish-paws-play.md diff --git a/.changeset/selfish-paws-play.md b/.changeset/selfish-paws-play.md new file mode 100644 index 0000000000..6f0e3049b2 --- /dev/null +++ b/.changeset/selfish-paws-play.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes a bug where Astro couldn't correctly parse `params` and `props` when receiving i18n fallback URLs diff --git a/packages/astro/src/core/render/params-and-props.ts b/packages/astro/src/core/render/params-and-props.ts index c1fe318ceb..f8799115b0 100644 --- a/packages/astro/src/core/render/params-and-props.ts +++ b/packages/astro/src/core/render/params-and-props.ts @@ -47,7 +47,8 @@ export async function getProps(opts: GetParamsAndPropsOptions): Promise { base, }); - // The pathname used here comes from the server, which already encored. + if (!staticPaths.length) return {}; + // The pathname used here comes from the server, which already encoded. // Since we decided to not mess up with encoding anymore, we need to decode them back so the parameters can match // the ones expected from the users const params = getParams(route, decodeURI(pathname)); @@ -77,7 +78,11 @@ export function getParams(route: RouteData, pathname: string): Params { if (!route.params.length) return {}; // The RegExp pattern expects a decoded string, but the pathname is encoded // when the URL contains non-English characters. - const paramsMatch = route.pattern.exec(pathname); + const paramsMatch = + route.pattern.exec(pathname) || + route.fallbackRoutes + .map((fallbackRoute) => fallbackRoute.pattern.exec(pathname)) + .find((x) => x); if (!paramsMatch) return {}; const params: Params = {}; route.params.forEach((key, i) => { diff --git a/packages/astro/test/fixtures/i18n-routing-fallback/src/pages/blog/[id].astro b/packages/astro/test/fixtures/i18n-routing-fallback/src/pages/blog/[id].astro index 97b41230d6..f277b93efe 100644 --- a/packages/astro/test/fixtures/i18n-routing-fallback/src/pages/blog/[id].astro +++ b/packages/astro/test/fixtures/i18n-routing-fallback/src/pages/blog/[id].astro @@ -1,18 +1,28 @@ --- +// for SSR +const blogs = { + 1: { content: "Hello world" }, + 2: { content: "Eat Something" }, + 3: { content: "How are you?" }, +} +const id = Astro.params?.id; +const ssrContent = id && blogs[id]?.content; + +// for SSG 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; +const { content } = Astro.props --- Astro -{content} +{content || ssrContent} diff --git a/packages/astro/test/fixtures/i18n-routing-fallback/src/pages/pt/blog/[id].astro b/packages/astro/test/fixtures/i18n-routing-fallback/src/pages/pt/blog/[id].astro index e37f83a302..ed4415fc5b 100644 --- a/packages/astro/test/fixtures/i18n-routing-fallback/src/pages/pt/blog/[id].astro +++ b/packages/astro/test/fixtures/i18n-routing-fallback/src/pages/pt/blog/[id].astro @@ -1,4 +1,12 @@ --- +const blogs = { + 1: { content: "Hola mundo" }, + 2: { content: "Eat Something" }, + 3: { content: "How are you?" }, +} +const id = Astro.params?.id; +const ssrContent = id && blogs[id]?.content; + export function getStaticPaths() { return [ {params: {id: '1'}, props: { content: "Hola mundo" }}, @@ -13,6 +21,6 @@ const { content } = Astro.props; Astro -{content} +{content || ssrContent} diff --git a/packages/astro/test/i18n-routing.test.js b/packages/astro/test/i18n-routing.test.js index 441823fc7a..28f4b05f80 100644 --- a/packages/astro/test/i18n-routing.test.js +++ b/packages/astro/test/i18n-routing.test.js @@ -1,6 +1,6 @@ +import * as cheerio from 'cheerio'; import * as assert from 'node:assert/strict'; import { after, afterEach, before, describe, it } from 'node:test'; -import * as cheerio from 'cheerio'; import testAdapter from './test-adapter.js'; import { loadFixture } from './test-utils.js'; @@ -2000,12 +2000,14 @@ describe('Fallback rewrite dev server', () => { root: './fixtures/i18n-routing-fallback/', i18n: { defaultLocale: 'en', - locales: ['en', 'fr'], + locales: ['en', 'fr', 'es', 'it', 'pt'], routing: { prefixDefaultLocale: false, }, fallback: { fr: 'en', + it: 'en', + es: 'pt', }, fallbackType: 'rewrite', }, @@ -2021,6 +2023,27 @@ describe('Fallback rewrite dev server', () => { assert.match(html, /Hello/); // assert.fail() }); + + it('should render fallback locale paths with path parameters correctly (fr)', async () => { + let response = await fixture.fetch('/fr/blog/1'); + assert.equal(response.status, 200); + const text = await response.text(); + assert.match(text, /Hello world/); + }); + + it('should render fallback locale paths with path parameters correctly (es)', async () => { + let response = await fixture.fetch('/es/blog/1'); + assert.equal(response.status, 200); + const text = await response.text(); + assert.match(text, /Hola mundo/); + }); + + it('should render fallback locale paths with query parameters correctly (it)', async () => { + let response = await fixture.fetch('/it/blog/1'); + assert.equal(response.status, 200); + const text = await response.text(); + assert.match(text, /Hello world/); + }); }); describe('Fallback rewrite SSG', () => { @@ -2032,13 +2055,15 @@ describe('Fallback rewrite SSG', () => { root: './fixtures/i18n-routing-fallback/', i18n: { defaultLocale: 'en', - locales: ['en', 'fr'], + locales: ['en', 'fr', 'es', 'it', 'pt'], routing: { prefixDefaultLocale: false, fallbackType: 'rewrite', }, fallback: { fr: 'en', + it: 'en', + es: 'pt', }, }, }); @@ -2051,6 +2076,21 @@ describe('Fallback rewrite SSG', () => { assert.match(html, /Hello/); // assert.fail() }); + + it('should render fallback locale paths with path parameters correctly (fr)', async () => { + const html = await fixture.readFile('/fr/blog/1/index.html'); + assert.match(html, /Hello world/); + }); + + it('should render fallback locale paths with path parameters correctly (es)', async () => { + const html = await fixture.readFile('/es/blog/1/index.html'); + assert.match(html, /Hola mundo/); + }); + + it('should render fallback locale paths with query parameters correctly (it)', async () => { + const html = await fixture.readFile('/it/blog/1/index.html'); + assert.match(html, /Hello world/); + }); }); describe('Fallback rewrite SSR', () => { @@ -2066,13 +2106,15 @@ describe('Fallback rewrite SSR', () => { adapter: testAdapter(), i18n: { defaultLocale: 'en', - locales: ['en', 'fr'], + locales: ['en', 'fr', 'es', 'it', 'pt'], routing: { prefixDefaultLocale: false, fallbackType: 'rewrite', }, fallback: { fr: 'en', + it: 'en', + es: 'pt', }, }, }); @@ -2087,4 +2129,28 @@ describe('Fallback rewrite SSR', () => { const html = await response.text(); assert.match(html, /Hello/); }); + + it('should render fallback locale paths with path parameters correctly (fr)', async () => { + let request = new Request('http://example.com/new-site/fr/blog/1'); + let response = await app.render(request); + assert.equal(response.status, 200); + const text = await response.text(); + assert.match(text, /Hello world/); + }); + + it('should render fallback locale paths with path parameters correctly (es)', async () => { + let request = new Request('http://example.com/new-site/es/blog/1'); + let response = await app.render(request); + assert.equal(response.status, 200); + const text = await response.text(); + assert.match(text, /Hola mundo/); + }); + + it('should render fallback locale paths with query parameters correctly (it)', async () => { + let request = new Request('http://example.com/new-site/it/blog/1'); + let response = await app.render(request); + assert.equal(response.status, 200); + const text = await response.text(); + assert.match(text, /Hello world/); + }); }); From 799c8676dfba0d281faf2a3f2d9513518b57593b Mon Sep 17 00:00:00 2001 From: mtwilliams Date: Thu, 12 Dec 2024 13:30:38 +0000 Subject: [PATCH 2/7] [ci] format --- packages/astro/test/i18n-routing.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/test/i18n-routing.test.js b/packages/astro/test/i18n-routing.test.js index 28f4b05f80..27491069d3 100644 --- a/packages/astro/test/i18n-routing.test.js +++ b/packages/astro/test/i18n-routing.test.js @@ -1,6 +1,6 @@ -import * as cheerio from 'cheerio'; import * as assert from 'node:assert/strict'; import { after, afterEach, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; import testAdapter from './test-adapter.js'; import { loadFixture } from './test-utils.js'; From 029661daa9b28fd5299d8cc9360025c78f6cd8eb Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Fri, 13 Dec 2024 07:07:21 +0000 Subject: [PATCH 3/7] fix: use atomic writes for data store file operations (#12715) * fix: use atomic writes for data store file operations * Put tmp alongside the target * Implement locking * Refactor * Wording --- .changeset/tame-bags-remember.md | 5 ++ .../astro/src/content/mutable-data-store.ts | 48 +++++++++++++++++-- 2 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 .changeset/tame-bags-remember.md diff --git a/.changeset/tame-bags-remember.md b/.changeset/tame-bags-remember.md new file mode 100644 index 0000000000..3f4f61bce7 --- /dev/null +++ b/.changeset/tame-bags-remember.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes a bug that caused errors in dev when editing sites with large numbers of MDX pages diff --git a/packages/astro/src/content/mutable-data-store.ts b/packages/astro/src/content/mutable-data-store.ts index fdffec7cb8..573adeaaf9 100644 --- a/packages/astro/src/content/mutable-data-store.ts +++ b/packages/astro/src/content/mutable-data-store.ts @@ -9,6 +9,8 @@ import { contentModuleToId } from './utils.js'; const SAVE_DEBOUNCE_MS = 500; +const MAX_DEPTH = 10; + /** * Extends the DataStore with the ability to change entries and write them to disk. * This is kept as a separate class to avoid needing node builtins at runtime, when read-only access is all that is needed. @@ -86,7 +88,7 @@ export class MutableDataStore extends ImmutableDataStore { if (this.#assetImports.size === 0) { try { - await fs.writeFile(filePath, 'export default new Map();'); + await this.#writeFileAtomic(filePath, 'export default new Map();'); } catch (err) { throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err }); } @@ -110,7 +112,7 @@ ${imports.join('\n')} export default new Map([${exports.join(', ')}]); `; try { - await fs.writeFile(filePath, code); + await this.#writeFileAtomic(filePath, code); } catch (err) { throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err }); } @@ -122,7 +124,7 @@ export default new Map([${exports.join(', ')}]); if (this.#moduleImports.size === 0) { try { - await fs.writeFile(filePath, 'export default new Map();'); + await this.#writeFileAtomic(filePath, 'export default new Map();'); } catch (err) { throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err }); } @@ -143,7 +145,7 @@ export default new Map([${exports.join(', ')}]); export default new Map([\n${lines.join(',\n')}]); `; try { - await fs.writeFile(filePath, code); + await this.#writeFileAtomic(filePath, code); } catch (err) { throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err }); } @@ -190,6 +192,42 @@ export default new Map([\n${lines.join(',\n')}]); } } + #writing = new Set(); + #pending = new Set(); + + async #writeFileAtomic(filePath: PathLike, data: string, depth = 0) { + if(depth > MAX_DEPTH) { + // If we hit the max depth, we skip a write to prevent the stack from growing too large + // In theory this means we may miss the latest data, but in practice this will only happen when the file is being written to very frequently + // so it will be saved on the next write. This is unlikely to ever happen in practice, as the writes are debounced. It requires lots of writes to very large files. + return; + } + const fileKey = filePath.toString(); + // If we are already writing this file, instead of writing now, flag it as pending and write it when we're done. + if (this.#writing.has(fileKey)) { + this.#pending.add(fileKey); + return; + } + // Prevent concurrent writes to this file by flagging it as being written + this.#writing.add(fileKey); + + const tempFile = filePath instanceof URL ? new URL(`${filePath.href}.tmp`) : `${filePath}.tmp`; + try { + // Write it to a temporary file first and then move it to prevent partial reads. + await fs.writeFile(tempFile, data); + await fs.rename(tempFile, filePath); + } finally { + // We're done writing. Unflag the file and check if there are any pending writes for this file. + this.#writing.delete(fileKey); + // If there are pending writes, we need to write again to ensure we flush the latest data. + if (this.#pending.has(fileKey)) { + this.#pending.delete(fileKey); + // Call ourself recursively to write the file again + await this.#writeFileAtomic(filePath, data, depth + 1); + } + } + } + scopedStore(collectionName: string): DataStore { return { get: = Record>(key: string) => @@ -298,7 +336,7 @@ export default new Map([\n${lines.join(',\n')}]); return; } try { - await fs.writeFile(filePath, this.toString()); + await this.#writeFileAtomic(filePath, this.toString()); this.#file = filePath; this.#dirty = false; } catch (err) { From 72f30ddbf3febbf0d60896af4073ffc596ac9eef Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Fri, 13 Dec 2024 07:08:08 +0000 Subject: [PATCH 4/7] [ci] format --- packages/astro/src/content/mutable-data-store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/content/mutable-data-store.ts b/packages/astro/src/content/mutable-data-store.ts index 573adeaaf9..7f125ed4b4 100644 --- a/packages/astro/src/content/mutable-data-store.ts +++ b/packages/astro/src/content/mutable-data-store.ts @@ -196,7 +196,7 @@ export default new Map([\n${lines.join(',\n')}]); #pending = new Set(); async #writeFileAtomic(filePath: PathLike, data: string, depth = 0) { - if(depth > MAX_DEPTH) { + if (depth > MAX_DEPTH) { // If we hit the max depth, we skip a write to prevent the stack from growing too large // In theory this means we may miss the latest data, but in practice this will only happen when the file is being written to very frequently // so it will be saved on the next write. This is unlikely to ever happen in practice, as the writes are debounced. It requires lots of writes to very large files. @@ -213,7 +213,7 @@ export default new Map([\n${lines.join(',\n')}]); const tempFile = filePath instanceof URL ? new URL(`${filePath.href}.tmp`) : `${filePath}.tmp`; try { - // Write it to a temporary file first and then move it to prevent partial reads. + // Write it to a temporary file first and then move it to prevent partial reads. await fs.writeFile(tempFile, data); await fs.rename(tempFile, filePath); } finally { From ee66a45b250703a40b34c0a45ae34aefcb14ea44 Mon Sep 17 00:00:00 2001 From: Adam Argyle Date: Fri, 13 Dec 2024 02:40:23 -0800 Subject: [PATCH 5/7] Adds `closedby` to dialog interface (#12728) * Adds `closedby` to dialog interface Standards status https://chromestatus.com/feature/5097714453577728 * Add changeset --------- Co-authored-by: Chris Swithinbank --- .changeset/neat-pumas-accept.md | 5 +++++ packages/astro/astro-jsx.d.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/neat-pumas-accept.md diff --git a/.changeset/neat-pumas-accept.md b/.changeset/neat-pumas-accept.md new file mode 100644 index 0000000000..8babf49e78 --- /dev/null +++ b/.changeset/neat-pumas-accept.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Adds type support for the `closedby` attribute for `` elements diff --git a/packages/astro/astro-jsx.d.ts b/packages/astro/astro-jsx.d.ts index ca54b991ed..39cb40f61b 100644 --- a/packages/astro/astro-jsx.d.ts +++ b/packages/astro/astro-jsx.d.ts @@ -679,6 +679,7 @@ declare namespace astroHTML.JSX { interface DialogHTMLAttributes extends HTMLAttributes { open?: boolean | string | undefined | null; + closedby?: 'none' | 'closerequest' | 'any' | undefined | null; } interface EmbedHTMLAttributes extends HTMLAttributes { From 8b1cecd6b491654ae760a0c75f3270df134c4e25 Mon Sep 17 00:00:00 2001 From: JoeMorgan Date: Fri, 13 Dec 2024 08:59:02 -0500 Subject: [PATCH 6/7] Add inert attribute to boolean list (#12729) * added changeset * added changeset --- .changeset/twelve-donuts-hide.md | 5 +++++ packages/astro/src/runtime/server/render/util.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/twelve-donuts-hide.md diff --git a/.changeset/twelve-donuts-hide.md b/.changeset/twelve-donuts-hide.md new file mode 100644 index 0000000000..504d1b5bea --- /dev/null +++ b/.changeset/twelve-donuts-hide.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +"Added `inert` to htmlBooleanAttributes" diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts index 9c771a0de0..d693ad070f 100644 --- a/packages/astro/src/runtime/server/render/util.ts +++ b/packages/astro/src/runtime/server/render/util.ts @@ -7,7 +7,7 @@ import { HTMLString, markHTMLString } from '../escape.js'; export const voidElementNames = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i; const htmlBooleanAttributes = - /^(?:allowfullscreen|async|autofocus|autoplay|checked|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|selected|itemscope)$/i; + /^(?:allowfullscreen|async|autofocus|autoplay|checked|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|inert|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|selected|itemscope)$/i; const AMPERSAND_REGEX = /&/g; const DOUBLE_QUOTE_REGEX = /"/g; From 901c21f4f09bf61dfbb5ab04db2d7d90120383cb Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 13 Dec 2024 14:37:25 +0000 Subject: [PATCH 7/7] test: make tailwind test more stable (#12732) --- .../tailwind/test/fixtures/basic/tailwind.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/integrations/tailwind/test/fixtures/basic/tailwind.config.js b/packages/integrations/tailwind/test/fixtures/basic/tailwind.config.js index f109096816..278abf14d6 100644 --- a/packages/integrations/tailwind/test/fixtures/basic/tailwind.config.js +++ b/packages/integrations/tailwind/test/fixtures/basic/tailwind.config.js @@ -1,6 +1,7 @@ import path from 'node:path'; +import {fileURLToPath} from "node:url"; /** @type {import('tailwindcss').Config} */ export default { - content: [path.join(__dirname, 'src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}')], + content: [path.join(path.dirname(fileURLToPath(import.meta.url)), 'src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}')], };