summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/wet-foxes-walk.md28
-rw-r--r--packages/astro/src/core/build/generate.ts15
-rw-r--r--packages/astro/src/core/build/static-build.ts4
-rw-r--r--packages/astro/src/core/fs/index.ts7
-rw-r--r--packages/astro/src/core/render-context.ts2
-rw-r--r--packages/astro/src/core/render/params-and-props.ts2
-rw-r--r--packages/astro/test/fixtures/ssr-params/src/pages/[category].astro8
-rw-r--r--packages/astro/test/fixtures/ssr-params/src/pages/東西/[category].astro5
-rw-r--r--packages/astro/test/params.test.js126
-rw-r--r--packages/astro/test/ssr-params.test.js52
10 files changed, 182 insertions, 67 deletions
diff --git a/.changeset/wet-foxes-walk.md b/.changeset/wet-foxes-walk.md
new file mode 100644
index 000000000..1b0b47c0f
--- /dev/null
+++ b/.changeset/wet-foxes-walk.md
@@ -0,0 +1,28 @@
+---
+'astro': major
+---
+
+`params` passed in `getStaticPaths` are no longer automatically decoded.
+
+### [changed]: `params` aren't decoded anymore.
+In Astro v4.x, `params` in ` were automatically decoded using `decodeURIComponent`.
+
+Astro v5.0 doesn't automatically decode `params` in `getStaticPaths` anymore, so you'll need to manually decode them yourself if needed
+
+#### What should I do?
+If you were relying on the automatic decode, you'll need to manually decode it using `decodeURI`.
+
+Note that the use of [`decodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent)) is discouraged for `getStaticPaths` because it decodes more characters than it should, for example `/`, `?`, `#` and more.
+
+```diff
+---
+export function getStaticPaths() {
+ return [
++ { params: { id: decodeURI("%5Bpage%5D") } },
+- { params: { id: "%5Bpage%5D" } },
+ ]
+}
+
+const { id } = Astro.params;
+---
+```
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index b85b728f4..66a42807f 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -419,7 +419,7 @@ async function generatePath(
});
const renderContext = await RenderContext.create({
pipeline,
- pathname,
+ pathname: pathname,
request,
routeData: route,
});
@@ -469,8 +469,11 @@ async function generatePath(
body = Buffer.from(await response.arrayBuffer());
}
- const outFolder = getOutFolder(pipeline.settings, pathname, route);
- const outFile = getOutFile(config, outFolder, pathname, route);
+ // We encode the path because some paths will received encoded characters, e.g. /[page] VS /%5Bpage%5D.
+ // Node.js decodes the paths, so to avoid a clash between paths, do encode paths again, so we create the correct files and folders requested by the user.
+ const encodedPath = encodeURI(pathname);
+ const outFolder = getOutFolder(pipeline.settings, encodedPath, route);
+ const outFile = getOutFile(config, outFolder, encodedPath, route);
if (route.distURL) {
route.distURL.push(outFile);
} else {
@@ -484,13 +487,13 @@ async function generatePath(
function getPrettyRouteName(route: RouteData): string {
if (isRelativePath(route.component)) {
return route.route;
- } else if (route.component.includes('node_modules/')) {
+ }
+ if (route.component.includes('node_modules/')) {
// For routes from node_modules (usually injected by integrations),
// prettify it by only grabbing the part after the last `node_modules/`
return /.*node_modules\/(.+)/.exec(route.component)?.[1] ?? route.component;
- } else {
- return route.component;
}
+ return route.component;
}
/**
diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts
index 06cd4bb27..4e5414bbb 100644
--- a/packages/astro/src/core/build/static-build.ts
+++ b/packages/astro/src/core/build/static-build.ts
@@ -383,7 +383,7 @@ async function cleanServerOutput(
}),
);
- removeEmptyDirs(out);
+ removeEmptyDirs(fileURLToPath(out));
}
// Clean out directly if the outDir is outside of root
@@ -447,7 +447,7 @@ async function ssrMoveAssets(opts: StaticBuildOptions) {
return fs.promises.rename(currentUrl, clientUrl);
}),
);
- removeEmptyDirs(serverAssets);
+ removeEmptyDirs(fileURLToPath(serverAssets));
}
}
diff --git a/packages/astro/src/core/fs/index.ts b/packages/astro/src/core/fs/index.ts
index 1f9f79f4e..b9e3154c7 100644
--- a/packages/astro/src/core/fs/index.ts
+++ b/packages/astro/src/core/fs/index.ts
@@ -1,19 +1,16 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
-import { appendForwardSlash } from '../path.js';
const isWindows = process.platform === 'win32';
-export function removeEmptyDirs(root: URL): void {
- const dir = fileURLToPath(root);
+export function removeEmptyDirs(dir: string): void {
if (!fs.statSync(dir).isDirectory()) return;
let files = fs.readdirSync(dir);
if (files.length > 0) {
files.map((file) => {
- const url = new URL(`./${file}`, appendForwardSlash(root.toString()));
- removeEmptyDirs(url);
+ removeEmptyDirs(path.join(dir, file));
});
files = fs.readdirSync(dir);
}
diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts
index 0d458511c..5a20b795d 100644
--- a/packages/astro/src/core/render-context.ts
+++ b/packages/astro/src/core/render-context.ts
@@ -77,7 +77,7 @@ export class RenderContext {
pipeline,
locals,
sequence(...pipeline.internalMiddleware, middleware ?? pipelineMiddleware),
- pathname,
+ decodeURI(pathname),
request,
routeData,
status,
diff --git a/packages/astro/src/core/render/params-and-props.ts b/packages/astro/src/core/render/params-and-props.ts
index 43d36218e..5eabbc0d4 100644
--- a/packages/astro/src/core/render/params-and-props.ts
+++ b/packages/astro/src/core/render/params-and-props.ts
@@ -74,7 +74,7 @@ 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(decodeURIComponent(pathname));
+ const paramsMatch = route.pattern.exec(pathname);
if (!paramsMatch) return {};
const params: Params = {};
route.params.forEach((key, i) => {
diff --git a/packages/astro/test/fixtures/ssr-params/src/pages/[category].astro b/packages/astro/test/fixtures/ssr-params/src/pages/[category].astro
index bdaa1f965..d5bdfd3a6 100644
--- a/packages/astro/test/fixtures/ssr-params/src/pages/[category].astro
+++ b/packages/astro/test/fixtures/ssr-params/src/pages/[category].astro
@@ -1,5 +1,13 @@
---
const { category } = Astro.params
+export function getStaticPaths() {
+ return [
+ { params: { category: "%23something" } },
+ { params: { category: "%2Fsomething" } },
+ { params: { category: "%3Fsomething" } },
+ { params: { category: "[page]" } },
+ ]
+}
---
<html>
<head>
diff --git a/packages/astro/test/fixtures/ssr-params/src/pages/東西/[category].astro b/packages/astro/test/fixtures/ssr-params/src/pages/東西/[category].astro
index bdaa1f965..8aaf4de24 100644
--- a/packages/astro/test/fixtures/ssr-params/src/pages/東西/[category].astro
+++ b/packages/astro/test/fixtures/ssr-params/src/pages/東西/[category].astro
@@ -1,4 +1,9 @@
---
+export function getStaticPaths() {
+ return [
+ { params: { category: "food" } },
+ ]
+}
const { category } = Astro.params
---
<html>
diff --git a/packages/astro/test/params.test.js b/packages/astro/test/params.test.js
new file mode 100644
index 000000000..14addbb96
--- /dev/null
+++ b/packages/astro/test/params.test.js
@@ -0,0 +1,126 @@
+import assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import * as cheerio from 'cheerio';
+import testAdapter from './test-adapter.js';
+import { loadFixture } from './test-utils.js';
+
+describe('Astro.params in SSR', () => {
+ /** @type {import('./test-utils.js').Fixture} */
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/ssr-params/',
+ adapter: testAdapter(),
+ output: 'server',
+ base: '/users/houston/',
+ });
+ await fixture.build();
+ });
+
+ it('Params are passed to component', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/users/houston/food');
+ const response = await app.render(request);
+ assert.equal(response.status, 200);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ assert.equal($('.category').text(), 'food');
+ });
+
+ describe('Non-english characters in the URL', () => {
+ it('Params are passed to component', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/users/houston/東西/food');
+ const response = await app.render(request);
+ assert.equal(response.status, 200);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ assert.equal($('.category').text(), 'food');
+ });
+ });
+
+ it('It uses encodeURI/decodeURI to decode parameters', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/users/houston/[page]');
+ const response = await app.render(request);
+ assert.equal(response.status, 200);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ assert.equal($('.category').text(), '[page]');
+ });
+
+ it('It accepts encoded URLs, and the params decoded', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/users/houston/%5Bpage%5D');
+ const response = await app.render(request);
+ assert.equal(response.status, 200);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ assert.equal($('.category').text(), '[page]');
+ });
+
+ it("It doesn't encode/decode URI characters such as %23 (#)", async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/users/houston/%23something');
+ const response = await app.render(request);
+ assert.equal(response.status, 200);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ assert.equal($('.category').text(), '%23something');
+ });
+ it("It doesn't encode/decode URI characters such as %2F (/)", async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/users/houston/%2Fsomething');
+ const response = await app.render(request);
+ assert.equal(response.status, 200);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ assert.equal($('.category').text(), '%2Fsomething');
+ });
+
+ it("It doesn't encode/decode URI characters such as %3F (?)", async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/users/houston/%3Fsomething');
+ const response = await app.render(request);
+ assert.equal(response.status, 200);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ assert.equal($('.category').text(), '%3Fsomething');
+ });
+});
+
+describe('Astro.params in static mode', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/ssr-params/',
+ });
+ await fixture.build();
+ });
+
+ it('It creates files that have square brackets in their URL', async () => {
+ const html = await fixture.readFile(encodeURI('/[page]/index.html'));
+ const $ = cheerio.load(html);
+ assert.equal($('.category').text(), '[page]');
+ });
+
+ it("It doesn't encode/decode URI characters such as %23 (#)", async () => {
+ const html = await fixture.readFile(encodeURI('/%23something/index.html'));
+ const $ = cheerio.load(html);
+ assert.equal($('.category').text(), '%23something');
+ });
+
+ it("It doesn't encode/decode URI characters such as %2F (/)", async () => {
+ const html = await fixture.readFile(encodeURI('/%2Fsomething/index.html'));
+ const $ = cheerio.load(html);
+ assert.equal($('.category').text(), '%2Fsomething');
+ });
+
+ it("It doesn't encode/decode URI characters such as %3F (?)", async () => {
+ const html = await fixture.readFile(encodeURI('/%3Fsomething/index.html'));
+ const $ = cheerio.load(html);
+ assert.equal($('.category').text(), '%3Fsomething');
+ });
+});
diff --git a/packages/astro/test/ssr-params.test.js b/packages/astro/test/ssr-params.test.js
deleted file mode 100644
index 13c071e7d..000000000
--- a/packages/astro/test/ssr-params.test.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import assert from 'node:assert/strict';
-import { before, describe, it } from 'node:test';
-import * as cheerio from 'cheerio';
-import testAdapter from './test-adapter.js';
-import { loadFixture } from './test-utils.js';
-
-describe('Astro.params in SSR', () => {
- /** @type {import('./test-utils.js').Fixture} */
- let fixture;
-
- before(async () => {
- fixture = await loadFixture({
- root: './fixtures/ssr-params/',
- adapter: testAdapter(),
- output: 'server',
- base: '/users/houston/',
- });
- await fixture.build();
- });
-
- it('Params are passed to component', async () => {
- const app = await fixture.loadTestAdapterApp();
- const request = new Request('http://example.com/users/houston/food');
- const response = await app.render(request);
- assert.equal(response.status, 200);
- const html = await response.text();
- const $ = cheerio.load(html);
- assert.equal($('.category').text(), 'food');
- });
-
- describe('Non-english characters in the URL', () => {
- it('Params are passed to component', async () => {
- const app = await fixture.loadTestAdapterApp();
- const request = new Request('http://example.com/users/houston/東西/food');
- const response = await app.render(request);
- assert.equal(response.status, 200);
- const html = await response.text();
- const $ = cheerio.load(html);
- assert.equal($('.category').text(), 'food');
- });
- });
-
- it('No double URL decoding', async () => {
- const app = await fixture.loadTestAdapterApp();
- const request = new Request('http://example.com/users/houston/%25%23%3F');
- const response = await app.render(request);
- assert.equal(response.status, 200);
- const html = await response.text();
- const $ = cheerio.load(html);
- assert.equal($('.category').text(), '%#?');
- });
-});