From c9487138d6d8fd39c8c8512239b6724cf2b275ff Mon Sep 17 00:00:00 2001 From: pilcrowOnPaper <80624252+pilcrowOnPaper@users.noreply.github.com> Date: Thu, 16 Nov 2023 08:39:41 +0900 Subject: [PATCH 1/5] Cancel response stream when connection closes (#9071) * cancel stream on close * add changeset * add test * Update .changeset/modern-ways-develop.md Co-authored-by: Sarah Rainsberger --------- Co-authored-by: lilnasy <69170106+lilnasy@users.noreply.github.com> Co-authored-by: Sarah Rainsberger --- .changeset/modern-ways-develop.md | 5 + .../integrations/node/src/nodeMiddleware.ts | 12 +- .../node/src/response-iterator.ts | 228 ------------------ .../integrations/node/test/api-route.test.js | 19 ++ .../test/fixtures/api-route/src/pages/hash.ts | 2 +- .../fixtures/api-route/src/pages/streaming.ts | 22 ++ 6 files changed, 55 insertions(+), 233 deletions(-) create mode 100644 .changeset/modern-ways-develop.md delete mode 100644 packages/integrations/node/src/response-iterator.ts create mode 100644 packages/integrations/node/test/fixtures/api-route/src/pages/streaming.ts diff --git a/.changeset/modern-ways-develop.md b/.changeset/modern-ways-develop.md new file mode 100644 index 0000000000000..0378abc1aeecb --- /dev/null +++ b/.changeset/modern-ways-develop.md @@ -0,0 +1,5 @@ +--- +'@astrojs/node': patch +--- + +Fixes a bug where the response stream would not cancel when the connection closed diff --git a/packages/integrations/node/src/nodeMiddleware.ts b/packages/integrations/node/src/nodeMiddleware.ts index ddaa95deb8bdb..7f242809ee0ed 100644 --- a/packages/integrations/node/src/nodeMiddleware.ts +++ b/packages/integrations/node/src/nodeMiddleware.ts @@ -1,8 +1,6 @@ import type { NodeApp } from 'astro/app/node'; import type { ServerResponse } from 'node:http'; -import type { Readable } from 'stream'; import { createOutgoingHttpHeaders } from './createOutgoingHttpHeaders.js'; -import { responseIterator } from './response-iterator.js'; import type { ErrorHandlerParams, Options, RequestHandlerParams } from './types.js'; // Disable no-unused-vars to avoid breaking signature change @@ -79,8 +77,14 @@ async function writeWebResponse(app: NodeApp, res: ServerResponse, webResponse: res.writeHead(status, nodeHeaders); if (webResponse.body) { try { - for await (const chunk of responseIterator(webResponse) as unknown as Readable) { - res.write(chunk); + const reader = webResponse.body.getReader(); + res.on("close", () => { + reader.cancel(); + }) + let result = await reader.read(); + while (!result.done) { + res.write(result.value); + result = await reader.read(); } } catch (err: any) { console.error(err?.stack || err?.message || String(err)); diff --git a/packages/integrations/node/src/response-iterator.ts b/packages/integrations/node/src/response-iterator.ts deleted file mode 100644 index b79c3a85345b4..0000000000000 --- a/packages/integrations/node/src/response-iterator.ts +++ /dev/null @@ -1,228 +0,0 @@ -/** - * Original sources: - * - https://github.com/kmalakoff/response-iterator/blob/master/src/index.ts - * - https://github.com/apollographql/apollo-client/blob/main/src/utilities/common/responseIterator.ts - */ - -import { AstroError } from 'astro/errors'; -import type { ReadableStreamDefaultReadResult } from 'node:stream/web'; -import { Readable as NodeReadableStream } from 'stream'; - -interface NodeStreamIterator { - next(): Promise>; - [Symbol.asyncIterator]?(): AsyncIterator; -} - -interface PromiseIterator { - next(): Promise>; - [Symbol.asyncIterator]?(): AsyncIterator; -} - -interface ReaderIterator { - next(): Promise>; - [Symbol.asyncIterator]?(): AsyncIterator; -} - -const canUseSymbol = typeof Symbol === 'function' && typeof Symbol.for === 'function'; - -const canUseAsyncIteratorSymbol = canUseSymbol && Symbol.asyncIterator; - -function isBuffer(value: any): value is Buffer { - return ( - value?.constructor != null && - typeof value.constructor.isBuffer === 'function' && - value.constructor.isBuffer(value) - ); -} - -function isNodeResponse(value: any): value is Response { - return !!(value as Response).body; -} - -function isReadableStream(value: any): value is ReadableStream { - return !!(value as ReadableStream).getReader; -} - -function isAsyncIterableIterator(value: any): value is AsyncIterableIterator { - return !!( - canUseAsyncIteratorSymbol && (value as AsyncIterableIterator)[Symbol.asyncIterator] - ); -} - -function isStreamableBlob(value: any): value is Blob { - return !!(value as Blob).stream; -} - -function isBlob(value: any): value is Blob { - return !!(value as Blob).arrayBuffer; -} - -function isNodeReadableStream(value: any): value is NodeReadableStream { - return !!(value as NodeReadableStream).pipe; -} - -function readerIterator(reader: ReadableStreamDefaultReader): AsyncIterableIterator { - const iterator: ReaderIterator = { - //@ts-expect-error - next() { - return reader.read(); - }, - }; - - if (canUseAsyncIteratorSymbol) { - iterator[Symbol.asyncIterator] = function (): AsyncIterator { - //@ts-expect-error - return this; - }; - } - - return iterator as AsyncIterableIterator; -} - -function promiseIterator(promise: Promise): AsyncIterableIterator { - let resolved = false; - - const iterator: PromiseIterator = { - next(): Promise> { - if (resolved) - return Promise.resolve({ - value: undefined, - done: true, - }); - resolved = true; - return new Promise(function (resolve, reject) { - promise - .then(function (value) { - resolve({ value: value as unknown as T, done: false }); - }) - .catch(reject); - }); - }, - }; - - if (canUseAsyncIteratorSymbol) { - iterator[Symbol.asyncIterator] = function (): AsyncIterator { - return this; - }; - } - - return iterator as AsyncIterableIterator; -} - -function nodeStreamIterator(stream: NodeReadableStream): AsyncIterableIterator { - let cleanup: (() => void) | null = null; - let error: Error | null = null; - let done = false; - const data: unknown[] = []; - - const waiting: [ - ( - value: - | IteratorResult - | PromiseLike> - ) => void, - (reason?: any) => void, - ][] = []; - - function onData(chunk: any) { - if (error) return; - if (waiting.length) { - const shiftedArr = waiting.shift(); - if (Array.isArray(shiftedArr) && shiftedArr[0]) { - return shiftedArr[0]({ value: chunk, done: false }); - } - } - data.push(chunk); - } - function onError(err: Error) { - error = err; - const all = waiting.slice(); - all.forEach(function (pair) { - pair[1](err); - }); - !cleanup || cleanup(); - } - function onEnd() { - done = true; - const all = waiting.slice(); - all.forEach(function (pair) { - pair[0]({ value: undefined, done: true }); - }); - !cleanup || cleanup(); - } - - cleanup = function () { - cleanup = null; - stream.removeListener('data', onData); - stream.removeListener('error', onError); - stream.removeListener('end', onEnd); - stream.removeListener('finish', onEnd); - stream.removeListener('close', onEnd); - }; - stream.on('data', onData); - stream.on('error', onError); - stream.on('end', onEnd); - stream.on('finish', onEnd); - stream.on('close', onEnd); - - function getNext(): Promise> { - return new Promise(function (resolve, reject) { - if (error) return reject(error); - if (data.length) return resolve({ value: data.shift() as T, done: false }); - if (done) return resolve({ value: undefined, done: true }); - waiting.push([resolve, reject]); - }); - } - - const iterator: NodeStreamIterator = { - next(): Promise> { - return getNext(); - }, - }; - - if (canUseAsyncIteratorSymbol) { - iterator[Symbol.asyncIterator] = function (): AsyncIterator { - return this; - }; - } - - return iterator as AsyncIterableIterator; -} - -function asyncIterator(source: AsyncIterableIterator): AsyncIterableIterator { - const iterator = source[Symbol.asyncIterator](); - return { - next(): Promise> { - return iterator.next(); - }, - [Symbol.asyncIterator](): AsyncIterableIterator { - return this; - }, - }; -} - -export function responseIterator(response: Response | Buffer): AsyncIterableIterator { - let body: unknown = response; - - if (isNodeResponse(response)) body = response.body; - - if (isBuffer(body)) body = NodeReadableStream.from(body); - - if (isAsyncIterableIterator(body)) return asyncIterator(body); - - if (isReadableStream(body)) return readerIterator(body.getReader()); - - // this errors without casting to ReadableStream - // because Blob.stream() returns a NodeJS ReadableStream - if (isStreamableBlob(body)) { - return readerIterator((body.stream() as unknown as ReadableStream).getReader()); - } - - if (isBlob(body)) return promiseIterator(body.arrayBuffer()); - - if (isNodeReadableStream(body)) return nodeStreamIterator(body); - - throw new AstroError( - 'Unknown body type for responseIterator. Please pass a streamable response.' - ); -} diff --git a/packages/integrations/node/test/api-route.test.js b/packages/integrations/node/test/api-route.test.js index c830eee2d6c81..7d9422ab4d0cc 100644 --- a/packages/integrations/node/test/api-route.test.js +++ b/packages/integrations/node/test/api-route.test.js @@ -89,4 +89,23 @@ describe('API routes', () => { let [out] = await done; expect(new Uint8Array(out.buffer)).to.deep.equal(expectedDigest); }); + + it('Can bail on streaming', async () => { + const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + let { req, res, done } = createRequestAndResponse({ + url: '/streaming', + }); + + let locals = { cancelledByTheServer: false }; + + handler(req, res, () => {}, locals); + req.send(); + + await new Promise((resolve) => setTimeout(resolve, 500)); + res.emit("close"); + + await done; + + expect(locals).to.deep.include({ cancelledByTheServer: true }); + }); }); diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/hash.ts b/packages/integrations/node/test/fixtures/api-route/src/pages/hash.ts index fbf44c5478bca..3f1b236de76b3 100644 --- a/packages/integrations/node/test/fixtures/api-route/src/pages/hash.ts +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/hash.ts @@ -1,6 +1,6 @@ import crypto from 'node:crypto'; -export async function post({ request }: { request: Request }) { +export async function POST({ request }: { request: Request }) { const hash = crypto.createHash('sha256'); const iterable = request.body as unknown as AsyncIterable; diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/streaming.ts b/packages/integrations/node/test/fixtures/api-route/src/pages/streaming.ts new file mode 100644 index 0000000000000..9ecb884bf89b1 --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/streaming.ts @@ -0,0 +1,22 @@ +export const GET = ({ locals }) => { + let sentChunks = 0; + + const readableStream = new ReadableStream({ + async pull(controller) { + if (sentChunks === 3) return controller.close(); + else sentChunks++; + + await new Promise(resolve => setTimeout(resolve, 1000)); + controller.enqueue(new TextEncoder().encode('hello\n')); + }, + cancel() { + locals.cancelledByTheServer = true; + } + }); + + return new Response(readableStream, { + headers: { + "Content-Type": "text/event-stream" + } + }); +} From 1862fb44eb1008e17222130ff74a7a589aacecfd Mon Sep 17 00:00:00 2001 From: pilcrowOnPaper Date: Wed, 15 Nov 2023 23:41:21 +0000 Subject: [PATCH 2/5] [ci] format --- packages/integrations/node/src/nodeMiddleware.ts | 4 ++-- packages/integrations/node/test/api-route.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/integrations/node/src/nodeMiddleware.ts b/packages/integrations/node/src/nodeMiddleware.ts index 7f242809ee0ed..4fd0a4bc23f5d 100644 --- a/packages/integrations/node/src/nodeMiddleware.ts +++ b/packages/integrations/node/src/nodeMiddleware.ts @@ -78,9 +78,9 @@ async function writeWebResponse(app: NodeApp, res: ServerResponse, webResponse: if (webResponse.body) { try { const reader = webResponse.body.getReader(); - res.on("close", () => { + res.on('close', () => { reader.cancel(); - }) + }); let result = await reader.read(); while (!result.done) { res.write(result.value); diff --git a/packages/integrations/node/test/api-route.test.js b/packages/integrations/node/test/api-route.test.js index 7d9422ab4d0cc..313819188015b 100644 --- a/packages/integrations/node/test/api-route.test.js +++ b/packages/integrations/node/test/api-route.test.js @@ -102,7 +102,7 @@ describe('API routes', () => { req.send(); await new Promise((resolve) => setTimeout(resolve, 500)); - res.emit("close"); + res.emit('close'); await done; From ac5633b8f615fe90ea419e00c5c771d00783a6e2 Mon Sep 17 00:00:00 2001 From: brandonsdebt <124833708+brandonsdebt@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:47:46 +0100 Subject: [PATCH 3/5] Add compatibility with cloudflare node (#8925) --- .changeset/calm-ducks-divide.md | 5 +++++ packages/integrations/react/server.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/calm-ducks-divide.md diff --git a/.changeset/calm-ducks-divide.md b/.changeset/calm-ducks-divide.md new file mode 100644 index 0000000000000..1cacaaf5a79c3 --- /dev/null +++ b/.changeset/calm-ducks-divide.md @@ -0,0 +1,5 @@ +--- +"@astrojs/react": patch +--- + +Uses `node:stream` during server rendering for compatibility with Cloudflare diff --git a/packages/integrations/react/server.js b/packages/integrations/react/server.js index 26596289eb8ce..05ee66c6a8177 100644 --- a/packages/integrations/react/server.js +++ b/packages/integrations/react/server.js @@ -53,7 +53,7 @@ async function check(Component, props, children) { } async function getNodeWritable() { - let nodeStreamBuiltinModuleName = 'stream'; + let nodeStreamBuiltinModuleName = 'node:stream'; let { Writable } = await import(/* @vite-ignore */ nodeStreamBuiltinModuleName); return Writable; } From 8366cd777530e6e0740f8a9c535f3c4382ffb053 Mon Sep 17 00:00:00 2001 From: "Houston (Bot)" <108291165+astrobot-houston@users.noreply.github.com> Date: Thu, 16 Nov 2023 05:50:47 -0800 Subject: [PATCH 4/5] [ci] release (#9107) Co-authored-by: github-actions[bot] --- .changeset/calm-ducks-divide.md | 5 -- .changeset/empty-turtles-wave.md | 5 -- .changeset/fresh-garlics-film.md | 5 -- .changeset/modern-ways-develop.md | 5 -- .changeset/twelve-mails-drive.md | 5 -- examples/basics/package.json | 2 +- examples/blog/package.json | 2 +- examples/component/package.json | 2 +- examples/framework-alpine/package.json | 2 +- examples/framework-lit/package.json | 2 +- examples/framework-multiple/package.json | 6 +- examples/framework-preact/package.json | 2 +- examples/framework-react/package.json | 4 +- examples/framework-solid/package.json | 2 +- examples/framework-svelte/package.json | 4 +- examples/framework-vue/package.json | 2 +- examples/hackernews/package.json | 4 +- examples/integration/package.json | 2 +- examples/middleware/package.json | 4 +- examples/minimal/package.json | 2 +- examples/non-html-pages/package.json | 2 +- examples/portfolio/package.json | 2 +- examples/ssr/package.json | 6 +- examples/view-transitions/package.json | 4 +- examples/with-markdoc/package.json | 2 +- examples/with-markdown-plugins/package.json | 2 +- examples/with-markdown-shiki/package.json | 2 +- examples/with-mdx/package.json | 2 +- examples/with-nanostores/package.json | 2 +- examples/with-tailwindcss/package.json | 2 +- examples/with-vite-plugin-pwa/package.json | 2 +- examples/with-vitest/package.json | 2 +- packages/astro/CHANGELOG.md | 8 +++ packages/astro/package.json | 2 +- packages/integrations/node/CHANGELOG.md | 6 ++ packages/integrations/node/package.json | 2 +- packages/integrations/react/CHANGELOG.md | 6 ++ packages/integrations/react/package.json | 2 +- packages/integrations/svelte/CHANGELOG.md | 6 ++ packages/integrations/svelte/package.json | 2 +- pnpm-lock.yaml | 72 ++++++++++----------- 41 files changed, 102 insertions(+), 101 deletions(-) delete mode 100644 .changeset/calm-ducks-divide.md delete mode 100644 .changeset/empty-turtles-wave.md delete mode 100644 .changeset/fresh-garlics-film.md delete mode 100644 .changeset/modern-ways-develop.md delete mode 100644 .changeset/twelve-mails-drive.md diff --git a/.changeset/calm-ducks-divide.md b/.changeset/calm-ducks-divide.md deleted file mode 100644 index 1cacaaf5a79c3..0000000000000 --- a/.changeset/calm-ducks-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@astrojs/react": patch ---- - -Uses `node:stream` during server rendering for compatibility with Cloudflare diff --git a/.changeset/empty-turtles-wave.md b/.changeset/empty-turtles-wave.md deleted file mode 100644 index 3605ca3d645dd..0000000000000 --- a/.changeset/empty-turtles-wave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -The `routingStrategy` `prefix-always` should not force its logic to endpoints. This fixes some regression with `astro:assets` and `@astrojs/rss`. diff --git a/.changeset/fresh-garlics-film.md b/.changeset/fresh-garlics-film.md deleted file mode 100644 index 12a20ddf8548e..0000000000000 --- a/.changeset/fresh-garlics-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -In the dev overlay, when there's too many plugins enabled at once, some of the plugins will now be hidden in a separate sub menu to avoid the bar becoming too long diff --git a/.changeset/modern-ways-develop.md b/.changeset/modern-ways-develop.md deleted file mode 100644 index 0378abc1aeecb..0000000000000 --- a/.changeset/modern-ways-develop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@astrojs/node': patch ---- - -Fixes a bug where the response stream would not cancel when the connection closed diff --git a/.changeset/twelve-mails-drive.md b/.changeset/twelve-mails-drive.md deleted file mode 100644 index b8ff5d6e58f41..0000000000000 --- a/.changeset/twelve-mails-drive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@astrojs/svelte': patch ---- - -Adds experimental support for Svelte 5 diff --git a/examples/basics/package.json b/examples/basics/package.json index a45c4bb8609dc..1be44bed6e74d 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^3.5.4" + "astro": "^3.5.5" } } diff --git a/examples/blog/package.json b/examples/blog/package.json index 818a563c3dd3b..afee1f0281c66 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -14,6 +14,6 @@ "@astrojs/mdx": "^1.1.5", "@astrojs/rss": "^3.0.0", "@astrojs/sitemap": "^3.0.3", - "astro": "^3.5.4" + "astro": "^3.5.5" } } diff --git a/examples/component/package.json b/examples/component/package.json index 61ffc785f6c76..2c781217268ba 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^3.5.4" + "astro": "^3.5.5" }, "peerDependencies": { "astro": "^3.0.0" diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index 91db63ef8d61f..88c5f32295e46 100644 --- a/examples/framework-alpine/package.json +++ b/examples/framework-alpine/package.json @@ -14,6 +14,6 @@ "@astrojs/alpinejs": "^0.3.1", "@types/alpinejs": "^3.7.2", "alpinejs": "^3.12.3", - "astro": "^3.5.4" + "astro": "^3.5.5" } } diff --git a/examples/framework-lit/package.json b/examples/framework-lit/package.json index 429947f0c21e4..97f9a4dcb29c9 100644 --- a/examples/framework-lit/package.json +++ b/examples/framework-lit/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/lit": "^3.0.3", "@webcomponents/template-shadowroot": "^0.2.1", - "astro": "^3.5.4", + "astro": "^3.5.5", "lit": "^2.8.0" } } diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index e2383aa0c6966..af251b7d8796c 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -12,11 +12,11 @@ }, "dependencies": { "@astrojs/preact": "^3.0.1", - "@astrojs/react": "^3.0.4", + "@astrojs/react": "^3.0.5", "@astrojs/solid-js": "^3.0.2", - "@astrojs/svelte": "^4.0.3", + "@astrojs/svelte": "^4.0.4", "@astrojs/vue": "^3.0.4", - "astro": "^3.5.4", + "astro": "^3.5.5", "preact": "^10.17.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index 37495defecfb1..c8059410c45ec 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.0.1", "@preact/signals": "^1.2.1", - "astro": "^3.5.4", + "astro": "^3.5.5", "preact": "^10.17.1" } } diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index 5bda39a5b1f46..79b09fc45d4c9 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -11,10 +11,10 @@ "astro": "astro" }, "dependencies": { - "@astrojs/react": "^3.0.4", + "@astrojs/react": "^3.0.5", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", - "astro": "^3.5.4", + "astro": "^3.5.5", "react": "^18.2.0", "react-dom": "^18.2.0" } diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index bdbbe509cbeb3..8b1a9a3157f06 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/solid-js": "^3.0.2", - "astro": "^3.5.4", + "astro": "^3.5.5", "solid-js": "^1.7.11" } } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index e81c59b88396e..3badefefbdd66 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -11,8 +11,8 @@ "astro": "astro" }, "dependencies": { - "@astrojs/svelte": "^4.0.3", - "astro": "^3.5.4", + "@astrojs/svelte": "^4.0.4", + "astro": "^3.5.5", "svelte": "^4.2.0" } } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index e0599b48bf941..99a6b678f005b 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/vue": "^3.0.4", - "astro": "^3.5.4", + "astro": "^3.5.5", "vue": "^3.3.4" } } diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index a06ee5c12db6a..fa19009ab647b 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -11,7 +11,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/node": "^6.0.3", - "astro": "^3.5.4" + "@astrojs/node": "^6.0.4", + "astro": "^3.5.5" } } diff --git a/examples/integration/package.json b/examples/integration/package.json index e3618421286e8..262ba7aeb1cbb 100644 --- a/examples/integration/package.json +++ b/examples/integration/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^3.5.4" + "astro": "^3.5.5" }, "peerDependencies": { "astro": "^3.0.0" diff --git a/examples/middleware/package.json b/examples/middleware/package.json index 0fff77bec1f7d..7c3da323eecd0 100644 --- a/examples/middleware/package.json +++ b/examples/middleware/package.json @@ -12,8 +12,8 @@ "server": "node dist/server/entry.mjs" }, "dependencies": { - "@astrojs/node": "^6.0.3", - "astro": "^3.5.4", + "@astrojs/node": "^6.0.4", + "astro": "^3.5.5", "html-minifier": "^4.0.0" } } diff --git a/examples/minimal/package.json b/examples/minimal/package.json index 273841273d627..9577132a2dc16 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^3.5.4" + "astro": "^3.5.5" } } diff --git a/examples/non-html-pages/package.json b/examples/non-html-pages/package.json index ee311ea5efa4f..9b8a6e24814d0 100644 --- a/examples/non-html-pages/package.json +++ b/examples/non-html-pages/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^3.5.4" + "astro": "^3.5.5" } } diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index 815e227cb6bb8..375bb8ff5bfd3 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^3.5.4" + "astro": "^3.5.5" } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index f3df907438553..04c0d698420e0 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -12,9 +12,9 @@ "server": "node dist/server/entry.mjs" }, "dependencies": { - "@astrojs/node": "^6.0.3", - "@astrojs/svelte": "^4.0.3", - "astro": "^3.5.4", + "@astrojs/node": "^6.0.4", + "@astrojs/svelte": "^4.0.4", + "astro": "^3.5.5", "svelte": "^4.2.0" } } diff --git a/examples/view-transitions/package.json b/examples/view-transitions/package.json index 492b84de73b30..b2904039ce435 100644 --- a/examples/view-transitions/package.json +++ b/examples/view-transitions/package.json @@ -11,7 +11,7 @@ }, "devDependencies": { "@astrojs/tailwind": "^5.0.2", - "@astrojs/node": "^6.0.3", - "astro": "^3.5.4" + "@astrojs/node": "^6.0.4", + "astro": "^3.5.5" } } diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json index 7056542a12c00..8447aa0cff28d 100644 --- a/examples/with-markdoc/package.json +++ b/examples/with-markdoc/package.json @@ -12,6 +12,6 @@ }, "dependencies": { "@astrojs/markdoc": "^0.7.2", - "astro": "^3.5.4" + "astro": "^3.5.5" } } diff --git a/examples/with-markdown-plugins/package.json b/examples/with-markdown-plugins/package.json index 94f2afbc167bd..ad51e3f987f17 100644 --- a/examples/with-markdown-plugins/package.json +++ b/examples/with-markdown-plugins/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/markdown-remark": "^3.5.0", - "astro": "^3.5.4", + "astro": "^3.5.5", "hast-util-select": "^5.0.5", "rehype-autolink-headings": "^6.1.1", "rehype-slug": "^5.1.0", diff --git a/examples/with-markdown-shiki/package.json b/examples/with-markdown-shiki/package.json index 1b20b750a5f75..f44759d761d98 100644 --- a/examples/with-markdown-shiki/package.json +++ b/examples/with-markdown-shiki/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^3.5.4" + "astro": "^3.5.5" } } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index 9426890ce2f39..13f77a46eded0 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/mdx": "^1.1.5", "@astrojs/preact": "^3.0.1", - "astro": "^3.5.4", + "astro": "^3.5.5", "preact": "^10.17.1" } } diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index 61752e8644963..14ae5ab8d6114 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.0.1", "@nanostores/preact": "^0.5.0", - "astro": "^3.5.4", + "astro": "^3.5.5", "nanostores": "^0.9.3", "preact": "^10.17.1" } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index bcaacaf24ca90..3a3e83dd3e7c5 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -14,7 +14,7 @@ "@astrojs/mdx": "^1.1.5", "@astrojs/tailwind": "^5.0.2", "@types/canvas-confetti": "^1.6.0", - "astro": "^3.5.4", + "astro": "^3.5.5", "autoprefixer": "^10.4.15", "canvas-confetti": "^1.6.0", "postcss": "^8.4.28", diff --git a/examples/with-vite-plugin-pwa/package.json b/examples/with-vite-plugin-pwa/package.json index 610c97eec408b..adc9a8032c2c0 100644 --- a/examples/with-vite-plugin-pwa/package.json +++ b/examples/with-vite-plugin-pwa/package.json @@ -11,7 +11,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^3.5.4", + "astro": "^3.5.5", "vite-plugin-pwa": "0.16.4", "workbox-window": "^7.0.0" } diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index 7850755c86b09..5259966bb9e71 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -12,7 +12,7 @@ "test": "vitest" }, "dependencies": { - "astro": "^3.5.4", + "astro": "^3.5.5", "vitest": "^0.34.2" } } diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 8aa69c0a23a40..a3061833944f2 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,13 @@ # astro +## 3.5.5 + +### Patch Changes + +- [#9091](https://github.com/withastro/astro/pull/9091) [`536c6c9fd`](https://github.com/withastro/astro/commit/536c6c9fd3d65d1a60bbc8b924c5939f27541d41) Thanks [@ematipico](https://github.com/ematipico)! - The `routingStrategy` `prefix-always` should not force its logic to endpoints. This fixes some regression with `astro:assets` and `@astrojs/rss`. + +- [#9102](https://github.com/withastro/astro/pull/9102) [`60e8210b0`](https://github.com/withastro/astro/commit/60e8210b0ce5bc512aff72a32322ba7937a411b0) Thanks [@Princesseuh](https://github.com/Princesseuh)! - In the dev overlay, when there's too many plugins enabled at once, some of the plugins will now be hidden in a separate sub menu to avoid the bar becoming too long + ## 3.5.4 ### Patch Changes diff --git a/packages/astro/package.json b/packages/astro/package.json index 8c94bc1dce74b..1cb40861cddba 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "astro", - "version": "3.5.4", + "version": "3.5.5", "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/integrations/node/CHANGELOG.md b/packages/integrations/node/CHANGELOG.md index 1b319f62dfda8..8ef4b80ac4737 100644 --- a/packages/integrations/node/CHANGELOG.md +++ b/packages/integrations/node/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/node +## 6.0.4 + +### Patch Changes + +- [#9071](https://github.com/withastro/astro/pull/9071) [`c9487138d`](https://github.com/withastro/astro/commit/c9487138d6d8fd39c8c8512239b6724cf2b275ff) Thanks [@pilcrowOnPaper](https://github.com/pilcrowOnPaper)! - Fixes a bug where the response stream would not cancel when the connection closed + ## 6.0.3 ### Patch Changes diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json index 09d26aa4fbc4b..b50b3e1757f02 100644 --- a/packages/integrations/node/package.json +++ b/packages/integrations/node/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/node", "description": "Deploy your site to a Node.js server", - "version": "6.0.3", + "version": "6.0.4", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/packages/integrations/react/CHANGELOG.md b/packages/integrations/react/CHANGELOG.md index b74b2c69648be..5b127b75337eb 100644 --- a/packages/integrations/react/CHANGELOG.md +++ b/packages/integrations/react/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/react +## 3.0.5 + +### Patch Changes + +- [#8925](https://github.com/withastro/astro/pull/8925) [`ac5633b8f`](https://github.com/withastro/astro/commit/ac5633b8f615fe90ea419e00c5c771d00783a6e2) Thanks [@brandonsdebt](https://github.com/brandonsdebt)! - Uses `node:stream` during server rendering for compatibility with Cloudflare + ## 3.0.4 ### Patch Changes diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json index 074d963e85b15..49d357a946f7c 100644 --- a/packages/integrations/react/package.json +++ b/packages/integrations/react/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/react", "description": "Use React components within Astro", - "version": "3.0.4", + "version": "3.0.5", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/packages/integrations/svelte/CHANGELOG.md b/packages/integrations/svelte/CHANGELOG.md index 38a378c2b5284..2681a45c0461b 100644 --- a/packages/integrations/svelte/CHANGELOG.md +++ b/packages/integrations/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/svelte +## 4.0.4 + +### Patch Changes + +- [#9098](https://github.com/withastro/astro/pull/9098) [`a600c1483`](https://github.com/withastro/astro/commit/a600c14837fd18c4c4c3330c0195cd47b0b73df9) Thanks [@bluwy](https://github.com/bluwy)! - Adds experimental support for Svelte 5 + ## 4.0.3 ### Patch Changes diff --git a/packages/integrations/svelte/package.json b/packages/integrations/svelte/package.json index 65c8522f91823..4c7e8bb63486c 100644 --- a/packages/integrations/svelte/package.json +++ b/packages/integrations/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@astrojs/svelte", - "version": "4.0.3", + "version": "4.0.4", "description": "Use Svelte components within Astro", "type": "module", "types": "./dist/index.d.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37616480c6b29..68aed44e57bef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,7 +125,7 @@ importers: examples/basics: dependencies: astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro examples/blog: @@ -140,13 +140,13 @@ importers: specifier: ^3.0.3 version: link:../../packages/integrations/sitemap astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro examples/component: devDependencies: astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro examples/framework-alpine: @@ -161,7 +161,7 @@ importers: specifier: ^3.12.3 version: 3.13.2 astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro examples/framework-lit: @@ -173,7 +173,7 @@ importers: specifier: ^0.2.1 version: 0.2.1 astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro lit: specifier: ^2.8.0 @@ -185,19 +185,19 @@ importers: specifier: ^3.0.1 version: link:../../packages/integrations/preact '@astrojs/react': - specifier: ^3.0.4 + specifier: ^3.0.5 version: link:../../packages/integrations/react '@astrojs/solid-js': specifier: ^3.0.2 version: link:../../packages/integrations/solid '@astrojs/svelte': - specifier: ^4.0.3 + specifier: ^4.0.4 version: link:../../packages/integrations/svelte '@astrojs/vue': specifier: ^3.0.4 version: link:../../packages/integrations/vue astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro preact: specifier: ^10.17.1 @@ -227,7 +227,7 @@ importers: specifier: ^1.2.1 version: 1.2.1(preact@10.18.1) astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro preact: specifier: ^10.17.1 @@ -236,7 +236,7 @@ importers: examples/framework-react: dependencies: '@astrojs/react': - specifier: ^3.0.4 + specifier: ^3.0.5 version: link:../../packages/integrations/react '@types/react': specifier: ^18.2.21 @@ -245,7 +245,7 @@ importers: specifier: ^18.2.7 version: 18.2.14 astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro react: specifier: ^18.2.0 @@ -260,7 +260,7 @@ importers: specifier: ^3.0.2 version: link:../../packages/integrations/solid astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro solid-js: specifier: ^1.7.11 @@ -269,10 +269,10 @@ importers: examples/framework-svelte: dependencies: '@astrojs/svelte': - specifier: ^4.0.3 + specifier: ^4.0.4 version: link:../../packages/integrations/svelte astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro svelte: specifier: ^4.2.0 @@ -284,7 +284,7 @@ importers: specifier: ^3.0.4 version: link:../../packages/integrations/vue astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro vue: specifier: ^3.3.4 @@ -293,25 +293,25 @@ importers: examples/hackernews: dependencies: '@astrojs/node': - specifier: ^6.0.3 + specifier: ^6.0.4 version: link:../../packages/integrations/node astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro examples/integration: devDependencies: astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro examples/middleware: dependencies: '@astrojs/node': - specifier: ^6.0.3 + specifier: ^6.0.4 version: link:../../packages/integrations/node astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro html-minifier: specifier: ^4.0.0 @@ -320,31 +320,31 @@ importers: examples/minimal: dependencies: astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro examples/non-html-pages: dependencies: astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro examples/portfolio: dependencies: astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro examples/ssr: dependencies: '@astrojs/node': - specifier: ^6.0.3 + specifier: ^6.0.4 version: link:../../packages/integrations/node '@astrojs/svelte': - specifier: ^4.0.3 + specifier: ^4.0.4 version: link:../../packages/integrations/svelte astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro svelte: specifier: ^4.2.0 @@ -353,13 +353,13 @@ importers: examples/view-transitions: devDependencies: '@astrojs/node': - specifier: ^6.0.3 + specifier: ^6.0.4 version: link:../../packages/integrations/node '@astrojs/tailwind': specifier: ^5.0.2 version: link:../../packages/integrations/tailwind astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro examples/with-markdoc: @@ -368,7 +368,7 @@ importers: specifier: ^0.7.2 version: link:../../packages/integrations/markdoc astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro examples/with-markdown-plugins: @@ -377,7 +377,7 @@ importers: specifier: ^3.5.0 version: link:../../packages/markdown/remark astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro hast-util-select: specifier: ^5.0.5 @@ -398,7 +398,7 @@ importers: examples/with-markdown-shiki: dependencies: astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro examples/with-mdx: @@ -410,7 +410,7 @@ importers: specifier: ^3.0.1 version: link:../../packages/integrations/preact astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro preact: specifier: ^10.17.1 @@ -425,7 +425,7 @@ importers: specifier: ^0.5.0 version: 0.5.0(nanostores@0.9.4)(preact@10.18.1) astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro nanostores: specifier: ^0.9.3 @@ -446,7 +446,7 @@ importers: specifier: ^1.6.0 version: 1.6.2 astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro autoprefixer: specifier: ^10.4.15 @@ -464,7 +464,7 @@ importers: examples/with-vite-plugin-pwa: dependencies: astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro vite-plugin-pwa: specifier: 0.16.4 @@ -476,7 +476,7 @@ importers: examples/with-vitest: dependencies: astro: - specifier: ^3.5.4 + specifier: ^3.5.5 version: link:../../packages/astro vitest: specifier: ^0.34.2 From e3dce215a5ea06bcff1b21027e5613e6518c69d4 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 16 Nov 2023 08:54:10 -0600 Subject: [PATCH 5/5] feat(i18n): add `Astro.currentLocale` (#9101) --- .changeset/quick-toes-peel.md | 5 ++ packages/astro/src/@types/astro.ts | 10 +++ packages/astro/src/core/app/index.ts | 8 ++- packages/astro/src/core/build/generate.ts | 4 +- packages/astro/src/core/endpoint/index.ts | 28 ++++++-- packages/astro/src/core/middleware/index.ts | 2 + packages/astro/src/core/pipeline.ts | 10 +-- packages/astro/src/core/render/context.ts | 24 +++++++ packages/astro/src/core/render/core.ts | 2 + packages/astro/src/core/render/result.ts | 27 +++++++- .../src/vite-plugin-astro-server/route.ts | 7 +- .../src/pages/en/start.astro | 5 ++ .../src/pages/pt/start.astro | 4 ++ .../src/pages/current-locale.astro | 12 ++++ .../i18n-routing/src/pages/dynamic/[id].astro | 19 ++++++ .../i18n-routing/src/pages/pt/start.astro | 4 ++ ...8-routing.test.js => i18n-routing.test.js} | 67 +++++++++++++++++++ 17 files changed, 222 insertions(+), 16 deletions(-) create mode 100644 .changeset/quick-toes-peel.md create mode 100644 packages/astro/test/fixtures/i18n-routing/src/pages/current-locale.astro create mode 100644 packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro rename packages/astro/test/{i18-routing.test.js => i18n-routing.test.js} (93%) diff --git a/.changeset/quick-toes-peel.md b/.changeset/quick-toes-peel.md new file mode 100644 index 0000000000000..25d5c13c7130b --- /dev/null +++ b/.changeset/quick-toes-peel.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Add a new property `Astro.currentLocale`, available when `i18n` is enabled. diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index f15cf7d096303..6477f738396ca 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -2112,6 +2112,11 @@ interface AstroSharedContext< */ preferredLocaleList: string[] | undefined; + + /** + * The current locale computed from the URL of the request. It matches the locales in `i18n.locales`, and returns `undefined` otherwise. + */ + currentLocale: string | undefined; } export interface APIContext< @@ -2241,6 +2246,11 @@ export interface APIContext< * [quality value]: https://developer.mozilla.org/en-US/docs/Glossary/Quality_values */ preferredLocaleList: string[] | undefined; + + /** + * The current locale computed from the URL of the request. It matches the locales in `i18n.locales`, and returns `undefined` otherwise. + */ + currentLocale: string | undefined; } export type EndpointOutput = diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index ec799011907b2..b297171a4fc46 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -234,7 +234,9 @@ export class App { status, env: this.#pipeline.env, mod: handler as any, - locales: this.#manifest.i18n ? this.#manifest.i18n.locales : undefined, + locales: this.#manifest.i18n?.locales, + routingStrategy: this.#manifest.i18n?.routingStrategy, + defaultLocale: this.#manifest.i18n?.defaultLocale, }); } else { const pathname = prependForwardSlash(this.removeBase(url.pathname)); @@ -269,7 +271,9 @@ export class App { status, mod, env: this.#pipeline.env, - locales: this.#manifest.i18n ? this.#manifest.i18n.locales : undefined, + locales: this.#manifest.i18n?.locales, + routingStrategy: this.#manifest.i18n?.routingStrategy, + defaultLocale: this.#manifest.i18n?.defaultLocale, }); } } diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 02837cf69cc87..20854f779b0e2 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -558,7 +558,9 @@ async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeli route: pageData.route, env: pipeline.getEnvironment(), mod, - locales: i18n ? i18n.locales : undefined, + locales: i18n?.locales, + routingStrategy: i18n?.routingStrategy, + defaultLocale: i18n?.defaultLocale, }); let body: string | Uint8Array; diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index 33c659dcafd5d..80af2358d13be 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -12,7 +12,11 @@ import { ASTRO_VERSION } from '../constants.js'; import { AstroCookies, attachCookiesToResponse } from '../cookies/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { callMiddleware } from '../middleware/callMiddleware.js'; -import { computePreferredLocale, computePreferredLocaleList } from '../render/context.js'; +import { + computeCurrentLocale, + computePreferredLocale, + computePreferredLocaleList, +} from '../render/context.js'; import { type Environment, type RenderContext } from '../render/index.js'; const encoder = new TextEncoder(); @@ -27,6 +31,8 @@ type CreateAPIContext = { props: Record; adapterName?: string; locales: string[] | undefined; + routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined; + defaultLocale: string | undefined; }; /** @@ -41,9 +47,12 @@ export function createAPIContext({ props, adapterName, locales, + routingStrategy, + defaultLocale, }: CreateAPIContext): APIContext { let preferredLocale: string | undefined = undefined; let preferredLocaleList: string[] | undefined = undefined; + let currentLocale: string | undefined = undefined; const context = { cookies: new AstroCookies(request), @@ -83,6 +92,16 @@ export function createAPIContext({ return undefined; }, + get currentLocale(): string | undefined { + if (currentLocale) { + return currentLocale; + } + if (locales) { + currentLocale = computeCurrentLocale(request, locales, routingStrategy, defaultLocale); + } + + return currentLocale; + }, url: new URL(request.url), get clientAddress() { if (clientAddressSymbol in request) { @@ -153,8 +172,7 @@ export async function callEndpoint mod: EndpointHandler, env: Environment, ctx: RenderContext, - onRequest: MiddlewareHandler | undefined, - locales: undefined | string[] + onRequest: MiddlewareHandler | undefined ): Promise { const context = createAPIContext({ request: ctx.request, @@ -162,7 +180,9 @@ export async function callEndpoint props: ctx.props, site: env.site, adapterName: env.adapterName, - locales, + routingStrategy: ctx.routingStrategy, + defaultLocale: ctx.defaultLocale, + locales: ctx.locales, }); let response; diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index 77da30aee2a65..c02761351d3cf 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -35,6 +35,8 @@ function createContext({ request, params, userDefinedLocales = [] }: CreateConte props: {}, site: undefined, locales: userDefinedLocales, + defaultLocale: undefined, + routingStrategy: undefined, }); } diff --git a/packages/astro/src/core/pipeline.ts b/packages/astro/src/core/pipeline.ts index bd203b4374156..87f833ee5cc4f 100644 --- a/packages/astro/src/core/pipeline.ts +++ b/packages/astro/src/core/pipeline.ts @@ -128,6 +128,8 @@ export class Pipeline { site: env.site, adapterName: env.adapterName, locales: renderContext.locales, + routingStrategy: renderContext.routingStrategy, + defaultLocale: renderContext.defaultLocale, }); switch (renderContext.route.type) { @@ -158,13 +160,7 @@ export class Pipeline { } } case 'endpoint': { - return await callEndpoint( - mod as any as EndpointHandler, - env, - renderContext, - onRequest, - renderContext.locales - ); + return await callEndpoint(mod as any as EndpointHandler, env, renderContext, onRequest); } default: throw new Error(`Couldn't find route of type [${renderContext.route.type}]`); diff --git a/packages/astro/src/core/render/context.ts b/packages/astro/src/core/render/context.ts index 851c41bc5ab22..0f0bf39b04654 100644 --- a/packages/astro/src/core/render/context.ts +++ b/packages/astro/src/core/render/context.ts @@ -29,6 +29,8 @@ export interface RenderContext { props: Props; locals?: object; locales: string[] | undefined; + defaultLocale: string | undefined; + routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined; } export type CreateRenderContextArgs = Partial< @@ -60,6 +62,8 @@ export async function createRenderContext( params, props, locales: options.locales, + routingStrategy: options.routingStrategy, + defaultLocale: options.defaultLocale, }; // We define a custom property, so we can check the value passed to locals @@ -208,3 +212,23 @@ export function computePreferredLocaleList(request: Request, locales: string[]) return result; } + +export function computeCurrentLocale( + request: Request, + locales: string[], + routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined, + defaultLocale: string | undefined +): undefined | string { + const requestUrl = new URL(request.url); + for (const segment of requestUrl.pathname.split('/')) { + for (const locale of locales) { + if (normalizeTheLocale(locale) === normalizeTheLocale(segment)) { + return locale; + } + } + } + if (routingStrategy === 'prefix-other-locales') { + return defaultLocale; + } + return undefined; +} diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index da9675f105d11..ed9ea7fdbb508 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -60,6 +60,8 @@ export async function renderPage({ mod, renderContext, env, cookies }: RenderPag cookies, locals: renderContext.locals ?? {}, locales: renderContext.locales, + defaultLocale: renderContext.defaultLocale, + routingStrategy: renderContext.routingStrategy, }); // TODO: Remove in Astro 4.0 diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 91dc545df4c1d..e9c8302a1ed74 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -12,7 +12,11 @@ import { chunkToString } from '../../runtime/server/render/index.js'; import { AstroCookies } from '../cookies/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import type { Logger } from '../logger/core.js'; -import { computePreferredLocale, computePreferredLocaleList } from './context.js'; +import { + computeCurrentLocale, + computePreferredLocale, + computePreferredLocaleList, +} from './context.js'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); const responseSentSymbol = Symbol.for('astro.responseSent'); @@ -47,6 +51,8 @@ export interface CreateResultArgs { locals: App.Locals; cookies?: AstroCookies; locales: string[] | undefined; + defaultLocale: string | undefined; + routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined; } function getFunctionExpression(slot: any) { @@ -148,6 +154,7 @@ export function createResult(args: CreateResultArgs): SSRResult { let cookies: AstroCookies | undefined = args.cookies; let preferredLocale: string | undefined = undefined; let preferredLocaleList: string[] | undefined = undefined; + let currentLocale: string | undefined = undefined; // Create the result object that will be passed into the render function. // This object starts here as an empty shell (not yet the result) but then @@ -218,6 +225,24 @@ export function createResult(args: CreateResultArgs): SSRResult { return undefined; }, + get currentLocale(): string | undefined { + if (currentLocale) { + return currentLocale; + } + if (args.locales) { + currentLocale = computeCurrentLocale( + request, + args.locales, + args.routingStrategy, + args.defaultLocale + ); + if (currentLocale) { + return currentLocale; + } + } + + return undefined; + }, params, props, locals, diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 7468f881907a3..48f89db043a30 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -215,6 +215,9 @@ export async function handleRoute({ env, mod, route, + locales: manifest.i18n?.locales, + routingStrategy: manifest.i18n?.routingStrategy, + defaultLocale: manifest.i18n?.defaultLocale, }); } else { return handle404Response(origin, incomingRequest, incomingResponse); @@ -271,7 +274,9 @@ export async function handleRoute({ route: options.route, mod, env, - locales: i18n ? i18n.locales : undefined, + locales: i18n?.locales, + routingStrategy: i18n?.routingStrategy, + defaultLocale: i18n?.defaultLocale, }); } diff --git a/packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/en/start.astro b/packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/en/start.astro index 990baecd9a8c2..92e189636e789 100644 --- a/packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/en/start.astro +++ b/packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/en/start.astro @@ -1,8 +1,13 @@ +--- +const currentLocale = Astro.currentLocale; +--- Astro Start +Current Locale: {currentLocale ? currentLocale : "none"} + diff --git a/packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/pt/start.astro b/packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/pt/start.astro index 5a4a84c2cf0c2..6f82c3790f441 100644 --- a/packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/pt/start.astro +++ b/packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/pt/start.astro @@ -1,8 +1,12 @@ +--- +const currentLocale = Astro.currentLocale; +--- Astro Oi essa e start +Current Locale: {currentLocale ? currentLocale : "none"} diff --git a/packages/astro/test/fixtures/i18n-routing/src/pages/current-locale.astro b/packages/astro/test/fixtures/i18n-routing/src/pages/current-locale.astro new file mode 100644 index 0000000000000..64af0118bae7f --- /dev/null +++ b/packages/astro/test/fixtures/i18n-routing/src/pages/current-locale.astro @@ -0,0 +1,12 @@ +--- +const currentLocale = Astro.currentLocale; +--- + + + + Astro + + + Current Locale: {currentLocale ? currentLocale : "none"} + + 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 new file mode 100644 index 0000000000000..58141fec05ce1 --- /dev/null +++ b/packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro @@ -0,0 +1,19 @@ + +--- +export function getStaticPaths() { + return [ + { id: "lorem" } + ] +} +const currentLocale = Astro.currentLocale; + +--- + + + + Astro + + +Current Locale: {currentLocale ? currentLocale : "none"} + + diff --git a/packages/astro/test/fixtures/i18n-routing/src/pages/pt/start.astro b/packages/astro/test/fixtures/i18n-routing/src/pages/pt/start.astro index 15a63a7b87f52..9a37428ca626d 100644 --- a/packages/astro/test/fixtures/i18n-routing/src/pages/pt/start.astro +++ b/packages/astro/test/fixtures/i18n-routing/src/pages/pt/start.astro @@ -1,8 +1,12 @@ +--- +const currentLocale = Astro.currentLocale; +--- Astro Hola +Current Locale: {currentLocale ? currentLocale : "none"} diff --git a/packages/astro/test/i18-routing.test.js b/packages/astro/test/i18n-routing.test.js similarity index 93% rename from packages/astro/test/i18-routing.test.js rename to packages/astro/test/i18n-routing.test.js index a7e8b318d3710..f305a5747b26d 100644 --- a/packages/astro/test/i18-routing.test.js +++ b/packages/astro/test/i18n-routing.test.js @@ -991,6 +991,73 @@ describe('[SSR] i18n routing', () => { }); }); }); + + describe('current locale', () => { + describe('with [prefix-other-locales]', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/i18n-routing/', + output: 'server', + adapter: testAdapter(), + }); + await fixture.build(); + app = await fixture.loadTestAdapterApp(); + }); + + it('should return the default locale', async () => { + let request = new Request('http://example.com/current-locale', {}); + let response = await app.render(request); + expect(response.status).to.equal(200); + expect(await response.text()).includes('Current Locale: en'); + }); + + it('should return the default locale of the current URL', async () => { + let request = new Request('http://example.com/pt/start', {}); + let response = await app.render(request); + expect(response.status).to.equal(200); + expect(await response.text()).includes('Current Locale: pt'); + }); + + it('should return the default locale when a route is dynamic', async () => { + let request = new Request('http://example.com/dynamic/lorem', {}); + let response = await app.render(request); + expect(response.status).to.equal(200); + expect(await response.text()).includes('Current Locale: en'); + }); + }); + + describe('with [prefix-always]', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/i18n-routing-prefix-always/', + output: 'server', + adapter: testAdapter(), + }); + await fixture.build(); + app = await fixture.loadTestAdapterApp(); + }); + + it('should return the locale of the current URL (en)', async () => { + let request = new Request('http://example.com/en/start', {}); + let response = await app.render(request); + expect(response.status).to.equal(200); + expect(await response.text()).includes('Current Locale: en'); + }); + + it('should return the locale of the current URL (pt)', async () => { + let request = new Request('http://example.com/pt/start', {}); + let response = await app.render(request); + expect(response.status).to.equal(200); + expect(await response.text()).includes('Current Locale: pt'); + }); + }); + }); }); describe('i18n routing does not break assets and endpoints', () => {