diff --git a/.changeset/stale-camels-invent.md b/.changeset/stale-camels-invent.md new file mode 100644 index 0000000000..e3d88bc33a --- /dev/null +++ b/.changeset/stale-camels-invent.md @@ -0,0 +1,5 @@ +--- +'astro': minor +--- + +Allow specifying custom encoding when using a non-html route. Only option before was 'utf-8' and now that is just the default. diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index b2347c4106..c358d63e79 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1085,6 +1085,7 @@ export interface APIContext { export interface EndpointOutput { body: Body; + encoding?: BufferEncoding; } export type APIRoute = ( diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 406186a21d..b6ebf46971 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -391,6 +391,7 @@ async function generatePath( }; let body: string; + let encoding: BufferEncoding | undefined; if (pageData.route.type === 'endpoint') { const result = await callEndpoint(mod as unknown as EndpointHandler, options); @@ -398,6 +399,7 @@ async function generatePath( throw new Error(`Returning a Response from an endpoint is not supported in SSG mode.`); } body = result.body; + encoding = result.encoding; } else { const response = await render(options); @@ -413,5 +415,5 @@ async function generatePath( const outFile = getOutFile(astroConfig, outFolder, pathname, pageData.route.type); pageData.route.distURL = outFile; await fs.promises.mkdir(outFolder, { recursive: true }); - await fs.promises.writeFile(outFile, body, 'utf-8'); + await fs.promises.writeFile(outFile, body, encoding ?? 'utf-8'); } diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index 9e974ee366..73c96ae644 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -21,6 +21,7 @@ type EndpointCallResult = | { type: 'simple'; body: string; + encoding?: BufferEncoding; } | { type: 'response'; @@ -52,5 +53,6 @@ export async function call( return { type: 'simple', body: response.body, + encoding: response.encoding, }; } diff --git a/packages/astro/test/fixtures/non-html-pages/package.json b/packages/astro/test/fixtures/non-html-pages/package.json new file mode 100644 index 0000000000..c3a215d98d --- /dev/null +++ b/packages/astro/test/fixtures/non-html-pages/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/non-html-pages", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/non-html-pages/src/images/placeholder.png b/packages/astro/test/fixtures/non-html-pages/src/images/placeholder.png new file mode 100644 index 0000000000..62841efdbf Binary files /dev/null and b/packages/astro/test/fixtures/non-html-pages/src/images/placeholder.png differ diff --git a/packages/astro/test/fixtures/non-html-pages/src/pages/about.json.ts b/packages/astro/test/fixtures/non-html-pages/src/pages/about.json.ts new file mode 100644 index 0000000000..af61847f3a --- /dev/null +++ b/packages/astro/test/fixtures/non-html-pages/src/pages/about.json.ts @@ -0,0 +1,11 @@ +// Returns the file body for this non-HTML file. +// The content type is based off of the extension in the filename, +// in this case: about.json. +export async function get() { + return { + body: JSON.stringify({ + name: 'Astro', + url: 'https://astro.build/', + }), + }; +} diff --git a/packages/astro/test/fixtures/non-html-pages/src/pages/placeholder.png.ts b/packages/astro/test/fixtures/non-html-pages/src/pages/placeholder.png.ts new file mode 100644 index 0000000000..0c2d3806b2 --- /dev/null +++ b/packages/astro/test/fixtures/non-html-pages/src/pages/placeholder.png.ts @@ -0,0 +1,17 @@ +import { promises as fs } from 'node:fs'; + +import type { APIRoute } from 'astro'; + +export const get: APIRoute = async function get() { + try { + // Image is in the public domain. Sourced from + // https://en.wikipedia.org/wiki/File:Portrait_placeholder.png + const buffer = await fs.readFile('./test/fixtures/non-html-pages/src/images/placeholder.png'); + return { + body: buffer.toString('binary'), + encoding: 'binary', + } as const; + } catch (error: unknown) { + throw new Error(`Something went wrong in placeholder.png route!: ${error as string}`); + } +}; diff --git a/packages/astro/test/non-html-pages.test.js b/packages/astro/test/non-html-pages.test.js new file mode 100644 index 0000000000..e1b89ee6a3 --- /dev/null +++ b/packages/astro/test/non-html-pages.test.js @@ -0,0 +1,38 @@ +import { expect } from 'chai'; +import { loadFixture } from './test-utils.js'; + +describe('Non-HTML Pages', () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ root: './fixtures/non-html-pages/' }); + await fixture.build(); + }); + + describe('json', () => { + it('should match contents', async () => { + const json = JSON.parse(await fixture.readFile('/about.json')); + expect(json).to.have.property('name', 'Astro'); + expect(json).to.have.property('url', 'https://astro.build/'); + }); + }); + + describe('png', () => { + it('should not have had its encoding mangled', async () => { + const buffer = await fixture.readFile('/placeholder.png', 'base64'); + + // Sanity check the first byte + const hex = Buffer.from(buffer, 'base64').toString('hex'); + const firstHexByte = hex.slice(0, 2); + // If we accidentally utf8 encode the png, the first byte (in hex) will be 'c2' + expect(firstHexByte).to.not.equal('c2'); + // and if correctly encoded in binary, it should be '89' + expect(firstHexByte).to.equal('89'); + + // Make sure the whole buffer (in base64) matches this snapshot + expect(buffer).to.equal( + 'iVBORw0KGgoAAAANSUhEUgAAAGQAAACWCAYAAAAouC1GAAAD10lEQVR4Xu3ZbW4iMRCE4c1RuP+ZEEfZFZHIAgHGH9Xtsv3m94yx6qHaM+HrfD7//cOfTQJfgNhYfG8EEC8PQMw8AAHELQGz/XCGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbWe6hpxOp6oIL5dL1fWjL54CpBbhXagz4FiDqCCegZxhLEGiIGaAsQPJwrjhuLXFBiQbwrUtFiCjMZzaMhzEBcMFZSiIG4YDyjAQV4zRKENA3DFGoqSDzIIxCgWQgn9eZb6rpILM1o57qyyUNJCZMTLHFyAFI2s5kBXakYWS0hBAymsYDrISRkZLACn/8j5cGfXUFQqyYjuiWwJIY0Out0W0JAxk5XZEtgQQGtKRgOGt6rEV0pAdxlXU2AKks3U0pDPAiNuVKDREIGQNstP5EXGOyBsCSF/lAOnL7/tuRpYgRPUSKhQaIpIBRBSkahlAVEmK1gFEFKRqGUuQHR951e8i0kMdkP6+SUGu29kVxXJkAUJD+hMQrUBDREGqlgFElaRgHRXGdSsc6oAIEjBbgoYAUpfAbu8i1g3Z7V1EiRFyqANSN02er5Y/Zd0+YJexNUVDdmmJGiNsZAHSPrbCRtYOKFM1ZHWQCIzQkbX64Q5I+1iW3xmFkdKQFUcXIPLvePuCkRhpDVmpJcuArIASjZHakNmfujIwAKk4SpYFmXF0ZWEMachsoysTYyjIDE3JxhgO4owyAsMCxBFlFIYNiBPKSAxAnh57R2PYgLj9/j4SJvQXw5L3LjeM+z2PgBkG4gzx/EXKhEkHmQliRFvSQGaFyEZJAVkB4wYTPb7CQVbCyEAJA1kRImN8hYCsjhHZFDnILhhRKICUvL0eXKM86KUgu7Uj4kyRgeyMoRxfEhAw/neld3x1g4Dx+4DpQQFEcKi/WqIVpQuEdrzXTAcB47haLSjNDQHkGOR6RS1KEwgYZRgtj8PVIGDUYdS2BJD6fJvuKB1dVSC0o8ni56YSFED6Mq66WwpCO6qyf3vxEUpxQwAxAgFDg1HyGFzUEECMQMDQYhy15LAhgBiBgBGD8ent/WNDAIkDeYcCSGzmH1d/9U7yFoR25Eg9owCSk3vxmzsgM4AwrnKV7sfWy4YAAkhuAmaf9rEhtCNfC5D8zA8/8Yby6wyhIYfZhVwASEis7Yu+BKEd7YH23glIb4IB919RHs4QGhKQcsWSgFSElXEpIBkpV3zGAwjjqiK5oEsBCQq2Z9l/4WuAC09sfQEAAAAASUVORK5CYII=' + ); + }); + }); +}); diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index c6150b26bb..59a9253140 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -146,8 +146,8 @@ export async function loadFixture(inlineConfig) { const previewServer = await preview(config, { logging, telemetry, ...opts }); return previewServer; }, - readFile: (filePath) => - fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.outDir), 'utf8'), + readFile: (filePath, encoding) => + fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.outDir), encoding ?? 'utf8'), readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.outDir)), glob: (p) => fastGlob(p, { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e7d1dce1a..e5c2a6f1ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1666,6 +1666,12 @@ importers: packages/astro/test/fixtures/multiple-renderers/renderers/two: specifiers: {} + packages/astro/test/fixtures/non-html-pages: + specifiers: + astro: workspace:* + dependencies: + astro: link:../../.. + packages/astro/test/fixtures/page-format: specifiers: astro: workspace:*