From 054dc68c35df9d9a2cd3d9126e46d9753081e91e Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 25 Apr 2025 15:46:26 +0100 Subject: [PATCH 1/4] feat(csp): inject astro island script hash --- packages/astro/src/core/app/types.ts | 7 ++- .../astro/src/core/astro-islands-hashes.ts | 40 ++++++++++++---- packages/astro/src/core/base-pipeline.ts | 12 +++-- packages/astro/src/core/csp/common.ts | 41 +++++++++++++--- packages/astro/src/core/csp/middleware.ts | 11 +++++ packages/astro/src/core/render-context.ts | 1 + .../astro/src/runtime/server/render/common.ts | 13 ++--- .../astro/src/runtime/server/render/head.ts | 30 +++++------- packages/astro/src/runtime/server/scripts.ts | 9 ++-- packages/astro/src/types/public/internal.ts | 6 ++- packages/astro/test/csp.test.js | 26 +++++++++- .../astro/test/fixtures/csp/astro.config.mjs | 6 ++- packages/astro/test/fixtures/csp/package.json | 5 +- .../test/fixtures/csp/src/components/Text.jsx | 5 ++ .../test/fixtures/csp/src/pages/react.astro | 18 +++++++ pnpm-lock.yaml | 48 +++++++++++++++---- scripts/cmd/prebuild.js | 17 ++++--- 17 files changed, 224 insertions(+), 71 deletions(-) create mode 100644 packages/astro/src/core/csp/middleware.ts create mode 100644 packages/astro/test/fixtures/csp/src/components/Text.jsx create mode 100644 packages/astro/test/fixtures/csp/src/pages/react.astro diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 6c60ea2dcd62..49cec063f79f 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -92,7 +92,12 @@ export type SSRManifest = { * When enabled, Astro tracks the hashes of script and styles, and eventually it will render the `` tag */ shouldInjectCspMetaTags: boolean; - astroIslandHashes: string[]; + astroIslandHashes: AstroIslandHashes[]; +}; + +export type AstroIslandHashes = { + name: string; + hash: string; }; export type SSRActions = { diff --git a/packages/astro/src/core/astro-islands-hashes.ts b/packages/astro/src/core/astro-islands-hashes.ts index 9581e3b6456d..bb7467c88a58 100644 --- a/packages/astro/src/core/astro-islands-hashes.ts +++ b/packages/astro/src/core/astro-islands-hashes.ts @@ -1,11 +1,35 @@ // This file is code-generated, please don't change it manually export const ASTRO_ISLAND_HASHES = [ - "GI/D8grziRZwfj/Mqmn+dcgU/i8sylHSR/IfobqcUT4=", - "HDWxd14AUw8OvjrhhRRyyZFHCGnzxXGDrg59Qi8ayhc=", - "XN6a2Vn8uvpBr/WhdYPdK0jVeCzlcOD2XYaP10veV4Y=", - "ZR0ZAU8UNTzLmo/ApeWH0y1mVLT+XtFkvZ5nw32W8jI=", - "cSNmhdbFlyTDRozeu9HPjo+B2S4QAeMp0RO41PqgAcA=", - "mH3H4wSoDVWMXJKrmeBKYJQMdAZQ3dArB2N66JomkzI=", - "mH3H4wSoDVWMXJKrmeBKYJQMdAZQ3dArB2N66JomkzI=", - "s81ZcLcyAa7P/Jh5M5hUxYthTGwW+iZY3e6aHrQ8H9E=" + { + "hash": "mH3H4wSoDVWMXJKrmeBKYJQMdAZQ3dArB2N66JomkzI=", + "name": "astro-island" + }, + { + "hash": "mH3H4wSoDVWMXJKrmeBKYJQMdAZQ3dArB2N66JomkzI=", + "name": "astro-island" + }, + { + "hash": "GI/D8grziRZwfj/Mqmn+dcgU/i8sylHSR/IfobqcUT4=", + "name": "idle" + }, + { + "hash": "XN6a2Vn8uvpBr/WhdYPdK0jVeCzlcOD2XYaP10veV4Y=", + "name": "load" + }, + { + "hash": "HDWxd14AUw8OvjrhhRRyyZFHCGnzxXGDrg59Qi8ayhc=", + "name": "media" + }, + { + "hash": "ZR0ZAU8UNTzLmo/ApeWH0y1mVLT+XtFkvZ5nw32W8jI=", + "name": "only" + }, + { + "hash": "cSNmhdbFlyTDRozeu9HPjo+B2S4QAeMp0RO41PqgAcA=", + "name": "visible" + }, + { + "hash": "s81ZcLcyAa7P/Jh5M5hUxYthTGwW+iZY3e6aHrQ8H9E=", + "name": "astro-island-styles" + } ]; \ No newline at end of file diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts index bbaf7da6cda3..7e3174a48c46 100644 --- a/packages/astro/src/core/base-pipeline.ts +++ b/packages/astro/src/core/base-pipeline.ts @@ -20,6 +20,7 @@ import { NOOP_MIDDLEWARE_FN } from './middleware/noop-middleware.js'; import { sequence } from './middleware/sequence.js'; import { RouteCache } from './render/route-cache.js'; import { createDefaultRoutes } from './routing/default.js'; +import { createCSPMiddleware } from './csp/middleware.js'; /** * The `Pipeline` represents the static parts of rendering that do not change between requests. @@ -112,11 +113,16 @@ export abstract class Pipeline { else if (this.middleware) { const middlewareInstance = await this.middleware(); const onRequest = middlewareInstance.onRequest ?? NOOP_MIDDLEWARE_FN; + const internalMiddlewares = [onRequest]; if (this.manifest.checkOrigin) { - this.resolvedMiddleware = sequence(createOriginCheckMiddleware(), onRequest); - } else { - this.resolvedMiddleware = onRequest; + // this middleware must be placed at the beginning because it needs to block incoming requests + internalMiddlewares.unshift(createOriginCheckMiddleware()); } + if (this.manifest.shouldInjectCspMetaTags) { + // this middleware must be placed at the end because it needs to inject the CSP headers + internalMiddlewares.push(createCSPMiddleware()); + } + this.resolvedMiddleware = sequence(...internalMiddlewares); return this.resolvedMiddleware; } else { this.resolvedMiddleware = NOOP_MIDDLEWARE_FN; diff --git a/packages/astro/src/core/csp/common.ts b/packages/astro/src/core/csp/common.ts index 97d63c271ddf..5f3e6d458c8d 100644 --- a/packages/astro/src/core/csp/common.ts +++ b/packages/astro/src/core/csp/common.ts @@ -1,4 +1,4 @@ -import type { AstroConfig } from '../../types/public/index.js'; +import type { AstroConfig, SSRResult } from '../../types/public/index.js'; import type { BuildInternals } from '../build/internal.js'; import crypto from 'node:crypto'; import type { AstroSettings } from '../../types/astro.js'; @@ -12,9 +12,7 @@ export function trackStyleHashes(internals: BuildInternals): string[] { for (const [_, page] of internals.pagesByViteID.entries()) { for (const style of page.styles) { if (style.sheet.type === 'inline') { - clientStyleHashes.push( - crypto.createHash('sha256').update(style.sheet.content).digest('base64'), - ); + clientStyleHashes.push(generateHash(style.sheet.content)); } } } @@ -26,15 +24,46 @@ export function trackScriptHashes(internals: BuildInternals, settings: AstroSett const clientScriptHashes: string[] = []; for (const script of internals.inlinedScripts.values()) { - clientScriptHashes.push(crypto.createHash('sha256').update(script).digest('base64')); + clientScriptHashes.push(generateHash(script)); } for (const script of settings.scripts) { const { content, stage } = script; if (stage === 'head-inline' || stage === 'before-hydration') { - clientScriptHashes.push(crypto.createHash('sha256').update(content).digest('base64')); + clientScriptHashes.push(generateHash(content)); } } return clientScriptHashes; } + +function generateHash(content: string): string { + return crypto.createHash('sha256').update(content).digest('base64'); +} + +export function renderCspContent(result: SSRResult): string { + const finalScriptHashes = new Set(); + const finalStyleHashes = new Set(); + + for (const scriptHash of result.clientScriptHashes) { + finalScriptHashes.add(`'sha256-${scriptHash}'`); + } + + for (const styleHash of result.clientStyleHashes) { + finalStyleHashes.add(`'sha256-${styleHash}'`); + } + + if (result.renderers.length > 0) { + for (const { name, hash } of result.astroIslandHashes) { + if (name === 'astro-island-styles') { + finalStyleHashes.add(`'sha256-${hash}'`); + } else { + finalScriptHashes.add(`'sha256-${hash}'`); + } + } + } + + const scriptSrc = `style-src 'self' ${Array.from(finalStyleHashes).join(' ')};`; + const styleSrc = `script-src 'self' ${Array.from(finalScriptHashes).join(' ')};`; + return `${scriptSrc} ${styleSrc}`; +} diff --git a/packages/astro/src/core/csp/middleware.ts b/packages/astro/src/core/csp/middleware.ts new file mode 100644 index 000000000000..f05a8443b75f --- /dev/null +++ b/packages/astro/src/core/csp/middleware.ts @@ -0,0 +1,11 @@ +import type { MiddlewareHandler } from '../../types/public/index.js'; + +export function createCSPMiddleware(): MiddlewareHandler { + return async (_, next) => { + const response = await next(); + + // Do something with the response + + return response; + }; +} diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index b877b4c859fe..81610f42635a 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -465,6 +465,7 @@ export class RenderContext { shouldInjectCspMetaTags: manifest.shouldInjectCspMetaTags, clientScriptHashes: manifest.clientScriptHashes, clientStyleHashes: manifest.clientStyleHashes, + astroIslandHashes: manifest.astroIslandHashes, }; return result; diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts index 90d0c1e5756a..de70c6127e82 100644 --- a/packages/astro/src/runtime/server/render/common.ts +++ b/packages/astro/src/runtime/server/render/common.ts @@ -3,7 +3,6 @@ import type { SSRResult } from '../../../types/public/internal.js'; import type { HTMLBytes, HTMLString } from '../escape.js'; import { markHTMLString } from '../escape.js'; import { - type PrescriptType, determineIfNeedsHydrationScript, determinesIfNeedsDirectiveScript, getPrescripts, @@ -65,13 +64,11 @@ function stringifyChunk( let needsDirectiveScript = hydration && determinesIfNeedsDirectiveScript(result, hydration.directive); - let prescriptType: PrescriptType = needsHydrationScript - ? 'both' - : needsDirectiveScript - ? 'directive' - : null; - if (prescriptType) { - let prescripts = getPrescripts(result, prescriptType, hydration.directive); + if (needsHydrationScript) { + let prescripts = getPrescripts(result, 'both', hydration.directive); + return markHTMLString(prescripts); + } else if (needsDirectiveScript) { + let prescripts = getPrescripts(result, 'directive', hydration.directive); return markHTMLString(prescripts); } else { return ''; diff --git a/packages/astro/src/runtime/server/render/head.ts b/packages/astro/src/runtime/server/render/head.ts index bc7a70f9b965..2281db0fe1a0 100644 --- a/packages/astro/src/runtime/server/render/head.ts +++ b/packages/astro/src/runtime/server/render/head.ts @@ -3,6 +3,7 @@ import { markHTMLString } from '../escape.js'; import type { MaybeRenderHeadInstruction, RenderHeadInstruction } from './instruction.js'; import { createRenderInstruction } from './instruction.js'; import { renderElement } from './util.js'; +import { renderCspContent } from '../../../core/csp/common.js'; // Filter out duplicate elements in our set const uniqueElements = (item: any, index: number, all: any[]) => { @@ -51,26 +52,19 @@ export function renderAllHeadContent(result: SSRResult) { } } - const hashes = []; - if (result.shouldInjectCspMetaTags) { - for (const scriptHash of [...result.clientScriptHashes, ...result.clientStyleHashes]) { - hashes.push( - renderElement( - 'meta', - { - props: { - 'http-equiv': 'content-security-policy', - content: scriptHash, - }, - children: '', - }, - false, - ), - ); - } + content += renderElement( + 'meta', + { + props: { + 'http-equiv': 'content-security-policy', + content: renderCspContent(result), + }, + children: '', + }, + false, + ); } - content += hashes.join('\n'); return markHTMLString(content); } diff --git a/packages/astro/src/runtime/server/scripts.ts b/packages/astro/src/runtime/server/scripts.ts index 4756a0adb510..e77947de9168 100644 --- a/packages/astro/src/runtime/server/scripts.ts +++ b/packages/astro/src/runtime/server/scripts.ts @@ -18,7 +18,7 @@ export function determinesIfNeedsDirectiveScript(result: SSRResult, directive: s return true; } -export type PrescriptType = null | 'both' | 'directive'; +export type PrescriptType = 'both' | 'directive'; function getDirectiveScriptText(result: SSRResult, directive: string): string { const clientDirectives = result.clientDirectives; @@ -31,8 +31,8 @@ function getDirectiveScriptText(result: SSRResult, directive: string): string { export function getPrescripts(result: SSRResult, type: PrescriptType, directive: string): string { // Note that this is a classic script, not a module script. - // This is so that it executes immediate, and when the browser encounters - // an astro-island element the callbacks will fire immediately, causing the JS + // This is so that it executes immediately, and when the browser encounters + // an astro-island element, the callbacks will fire immediately, causing the JS // deps to be loaded immediately. switch (type) { case 'both': @@ -41,8 +41,5 @@ export function getPrescripts(result: SSRResult, type: PrescriptType, directive: }`; case 'directive': return ``; - case null: - break; } - return ''; } diff --git a/packages/astro/src/types/public/internal.ts b/packages/astro/src/types/public/internal.ts index cb29517a2970..50d202389a5d 100644 --- a/packages/astro/src/types/public/internal.ts +++ b/packages/astro/src/types/public/internal.ts @@ -7,6 +7,7 @@ import type { Params } from './common.js'; import type { AstroConfig, RedirectConfig } from './config.js'; import type { AstroGlobal, AstroGlobalPartial } from './context.js'; import type { AstroRenderer } from './integrations.js'; +import type { SSRManifest } from '../../core/app/types.js'; export type { SSRManifest } from '../../core/app/types.js'; @@ -250,8 +251,9 @@ export interface SSRResult { * Whether Astro should inject the CSP tag into the head of the component. */ shouldInjectCspMetaTags: boolean; - clientScriptHashes: string[]; - clientStyleHashes: string[]; + clientScriptHashes: SSRManifest['clientScriptHashes']; + clientStyleHashes: SSRManifest['clientStyleHashes']; + astroIslandHashes: SSRManifest['astroIslandHashes']; } /** diff --git a/packages/astro/test/csp.test.js b/packages/astro/test/csp.test.js index 23ae039a305d..64b3baa1e6fc 100644 --- a/packages/astro/test/csp.test.js +++ b/packages/astro/test/csp.test.js @@ -31,10 +31,32 @@ describe('CSP', () => { const response = await app.render(request); const $ = cheerio.load(await response.text()); + const meta = $('meta[http-equiv="Content-Security-Policy"]'); for (const hash of manifest.clientStyleHashes) { - let meta = $('meta[http-equiv="Content-Security-Policy"][content="' + hash + '"]'); - assert.equal(meta.length, 1, `Should have a CSP meta tag for ${hash}`); + assert.match( + meta.attr('content'), + new RegExp(`'sha256-${hash}'`), + `Should have a CSP meta tag for ${hash}`, + ); } + + let astroStyleHash = manifest.astroIslandHashes.find( + ({ name }) => name === 'astro-island-styles', + ); + astroStyleHash = `sha256-${astroStyleHash.hash}`; + + let astroIsland = manifest.astroIslandHashes.find(({ name }) => name === 'astro-island'); + astroIsland = `sha256-${astroIsland.hash}`; + + assert.ok( + meta.attr('content').includes(astroStyleHash), + `Should have a CSP meta tag for ${astroStyleHash}`, + ); + + assert.ok( + meta.attr('content').includes(astroIsland), + `Should have a CSP meta tag for ${astroIsland}`, + ); } else { assert.fail('Should have the manifest'); } diff --git a/packages/astro/test/fixtures/csp/astro.config.mjs b/packages/astro/test/fixtures/csp/astro.config.mjs index 7184336cde47..9ea073c7d5f6 100644 --- a/packages/astro/test/fixtures/csp/astro.config.mjs +++ b/packages/astro/test/fixtures/csp/astro.config.mjs @@ -1,8 +1,12 @@ import { defineConfig } from 'astro/config'; +import react from '@astrojs/react'; export default defineConfig({ experimental: { csp: true, - } + }, + integrations: [ + react() + ], }); diff --git a/packages/astro/test/fixtures/csp/package.json b/packages/astro/test/fixtures/csp/package.json index 60390ccd8f87..4965f2c6fe5d 100644 --- a/packages/astro/test/fixtures/csp/package.json +++ b/packages/astro/test/fixtures/csp/package.json @@ -3,6 +3,9 @@ "version": "0.0.0", "private": true, "dependencies": { - "astro": "workspace:*" + "astro": "workspace:*", + "@astrojs/react": "workspace:*", + "react": "^19.1.0", + "react-dom": "^19.1.0" } } diff --git a/packages/astro/test/fixtures/csp/src/components/Text.jsx b/packages/astro/test/fixtures/csp/src/components/Text.jsx new file mode 100644 index 000000000000..5317786a7d94 --- /dev/null +++ b/packages/astro/test/fixtures/csp/src/components/Text.jsx @@ -0,0 +1,5 @@ + + +export function Text() { + return "Text" +} diff --git a/packages/astro/test/fixtures/csp/src/pages/react.astro b/packages/astro/test/fixtures/csp/src/pages/react.astro new file mode 100644 index 000000000000..934af5df38f6 --- /dev/null +++ b/packages/astro/test/fixtures/csp/src/pages/react.astro @@ -0,0 +1,18 @@ +--- +import {Text} from "../components/Text.jsx" +--- + + + + + + + Index + + +
+

React

+ +
+ + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a63ebed7a8d1..13f5bf53479e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2789,9 +2789,18 @@ importers: packages/astro/test/fixtures/csp: dependencies: + '@astrojs/react': + specifier: workspace:* + version: link:../../../../integrations/react astro: specifier: workspace:* version: link:../../.. + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) packages/astro/test/fixtures/csrf-check-origin: dependencies: @@ -4322,7 +4331,7 @@ importers: version: 1.0.2 drizzle-orm: specifier: ^0.31.2 - version: 0.31.4(@cloudflare/workers-types@4.20250327.0)(@libsql/client@0.15.2)(@types/react@18.3.20)(react@19.0.0) + version: 0.31.4(@cloudflare/workers-types@4.20250327.0)(@libsql/client@0.15.2)(@types/react@18.3.20)(react@19.1.0) github-slugger: specifier: ^2.0.0 version: 2.0.0 @@ -4774,7 +4783,7 @@ importers: version: link:../../astro-prism '@markdoc/markdoc': specifier: ^0.5.1 - version: 0.5.1(@types/react@18.3.20)(react@19.0.0) + version: 0.5.1(@types/react@18.3.20)(react@19.1.0) esbuild: specifier: ^0.25.0 version: 0.25.0 @@ -5688,7 +5697,7 @@ importers: version: link:../../internal-helpers '@vercel/analytics': specifier: ^1.5.0 - version: 1.5.0(react@19.0.0)(svelte@5.28.2)(vue@3.5.13(typescript@5.8.3)) + version: 1.5.0(react@19.1.0)(svelte@5.28.2)(vue@3.5.13(typescript@5.8.3)) '@vercel/edge': specifier: ^1.2.1 version: 1.2.1 @@ -11067,6 +11076,11 @@ packages: peerDependencies: react: ^19.0.0 + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -11079,6 +11093,10 @@ packages: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -11295,6 +11313,9 @@ packages: scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} @@ -13717,12 +13738,12 @@ snapshots: - encoding - supports-color - '@markdoc/markdoc@0.5.1(@types/react@18.3.20)(react@19.0.0)': + '@markdoc/markdoc@0.5.1(@types/react@18.3.20)(react@19.1.0)': optionalDependencies: '@types/linkify-it': 3.0.5 '@types/markdown-it': 12.2.3 '@types/react': 18.3.20 - react: 19.0.0 + react: 19.1.0 '@mdx-js/mdx@3.1.0(acorn@8.14.1)': dependencies: @@ -14375,9 +14396,9 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vercel/analytics@1.5.0(react@19.0.0)(svelte@5.28.2)(vue@3.5.13(typescript@5.8.3))': + '@vercel/analytics@1.5.0(react@19.1.0)(svelte@5.28.2)(vue@3.5.13(typescript@5.8.3))': optionalDependencies: - react: 19.0.0 + react: 19.1.0 svelte: 5.28.2 vue: 3.5.13(typescript@5.8.3) @@ -15292,12 +15313,12 @@ snapshots: dotenv@8.6.0: {} - drizzle-orm@0.31.4(@cloudflare/workers-types@4.20250327.0)(@libsql/client@0.15.2)(@types/react@18.3.20)(react@19.0.0): + drizzle-orm@0.31.4(@cloudflare/workers-types@4.20250327.0)(@libsql/client@0.15.2)(@types/react@18.3.20)(react@19.1.0): optionalDependencies: '@cloudflare/workers-types': 4.20250327.0 '@libsql/client': 0.15.2 '@types/react': 18.3.20 - react: 19.0.0 + react: 19.1.0 dset@3.1.4: {} @@ -17693,6 +17714,11 @@ snapshots: react: 19.0.0 scheduler: 0.25.0 + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + react-refresh@0.17.0: {} react@18.3.1: @@ -17701,6 +17727,8 @@ snapshots: react@19.0.0: {} + react@19.1.0: {} + read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -18041,6 +18069,8 @@ snapshots: scheduler@0.25.0: {} + scheduler@0.26.0: {} + scslre@0.3.0: dependencies: '@eslint-community/regexpp': 4.12.1 diff --git a/scripts/cmd/prebuild.js b/scripts/cmd/prebuild.js index 1ee505500308..3941607c521d 100644 --- a/scripts/cmd/prebuild.js +++ b/scripts/cmd/prebuild.js @@ -115,7 +115,11 @@ export default \`${generatedCode}\`;`; const url = getPrebuildURL(filepath, result.dev); await fs.promises.writeFile(url, mod, 'utf-8'); const hash = crypto.createHash('sha256').update(code).digest('base64'); - hashes.push(hash); + const basename = path.basename(filepath); + hashes.push({ + hash, + name: basename.slice(0, basename.indexOf('.')), + }); } } for (const entrypoint of entryPoints) { @@ -127,13 +131,14 @@ export default \`${generatedCode}\`;`; 'utf-8', ); const styleContent = fileContent.match(ASTRO_ISLAND_STYLE_REGEX)[1]; - hashes.push(crypto.createHash('sha256').update(styleContent).digest('base64')); + hashes.push({ + hash: crypto.createHash('sha256').update(styleContent).digest('base64'), + name: 'astro-island-styles', + }); hashes.sort(); - const entries = hashes.map((hash) => `"${hash}"`); + const entries = JSON.stringify(hashes, null, 2); const content = `// This file is code-generated, please don't change it manually -export const ASTRO_ISLAND_HASHES = [ - ${entries.join(',\n ')} -];`; +export const ASTRO_ISLAND_HASHES = ${entries};`; await fs.promises.writeFile( path.join( fileURLToPath(import.meta.url), From 3573b9f0e89c729e87292c3bbb6ba10642c6c685 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 28 Apr 2025 11:07:16 +0100 Subject: [PATCH 2/4] chore: move the rendering inside the runtime folder --- packages/astro/src/core/csp/common.ts | 97 +++++++------- .../astro/src/runtime/server/render/csp.ts | 49 +++++++ .../astro/src/runtime/server/render/head.ts | 121 ++++++++++++++---- 3 files changed, 198 insertions(+), 69 deletions(-) create mode 100644 packages/astro/src/runtime/server/render/csp.ts diff --git a/packages/astro/src/core/csp/common.ts b/packages/astro/src/core/csp/common.ts index 5f3e6d458c8d..31805da8e3f2 100644 --- a/packages/astro/src/core/csp/common.ts +++ b/packages/astro/src/core/csp/common.ts @@ -1,18 +1,37 @@ -import type { AstroConfig, SSRResult } from '../../types/public/index.js'; +import type { AstroConfig } from '../../types/public/index.js'; import type { BuildInternals } from '../build/internal.js'; import crypto from 'node:crypto'; import type { AstroSettings } from '../../types/astro.js'; -export function shouldTrackCspHashes(config: AstroConfig): boolean { - return config.experimental?.csp === true; +export function shouldTrackCspHashes( + config: AstroConfig, +): boolean { + return ( + config.experimental + ?.csp === true + ); } -export function trackStyleHashes(internals: BuildInternals): string[] { - const clientStyleHashes: string[] = []; - for (const [_, page] of internals.pagesByViteID.entries()) { +export function trackStyleHashes( + internals: BuildInternals, +): string[] { + const clientStyleHashes: string[] = + []; + for (const [ + _, + page, + ] of internals.pagesByViteID.entries()) { for (const style of page.styles) { - if (style.sheet.type === 'inline') { - clientStyleHashes.push(generateHash(style.sheet.content)); + if ( + style.sheet.type === + 'inline' + ) { + clientStyleHashes.push( + generateHash( + style.sheet + .content, + ), + ); } } } @@ -20,50 +39,42 @@ export function trackStyleHashes(internals: BuildInternals): string[] { return clientStyleHashes; } -export function trackScriptHashes(internals: BuildInternals, settings: AstroSettings): string[] { - const clientScriptHashes: string[] = []; +export function trackScriptHashes( + internals: BuildInternals, + settings: AstroSettings, +): string[] { + const clientScriptHashes: string[] = + []; for (const script of internals.inlinedScripts.values()) { - clientScriptHashes.push(generateHash(script)); + clientScriptHashes.push( + generateHash(script), + ); } for (const script of settings.scripts) { - const { content, stage } = script; - if (stage === 'head-inline' || stage === 'before-hydration') { - clientScriptHashes.push(generateHash(content)); + const { content, stage } = + script; + if ( + stage === + 'head-inline' || + stage === + 'before-hydration' + ) { + clientScriptHashes.push( + generateHash(content), + ); } } return clientScriptHashes; } -function generateHash(content: string): string { - return crypto.createHash('sha256').update(content).digest('base64'); -} - -export function renderCspContent(result: SSRResult): string { - const finalScriptHashes = new Set(); - const finalStyleHashes = new Set(); - - for (const scriptHash of result.clientScriptHashes) { - finalScriptHashes.add(`'sha256-${scriptHash}'`); - } - - for (const styleHash of result.clientStyleHashes) { - finalStyleHashes.add(`'sha256-${styleHash}'`); - } - - if (result.renderers.length > 0) { - for (const { name, hash } of result.astroIslandHashes) { - if (name === 'astro-island-styles') { - finalStyleHashes.add(`'sha256-${hash}'`); - } else { - finalScriptHashes.add(`'sha256-${hash}'`); - } - } - } - - const scriptSrc = `style-src 'self' ${Array.from(finalStyleHashes).join(' ')};`; - const styleSrc = `script-src 'self' ${Array.from(finalScriptHashes).join(' ')};`; - return `${scriptSrc} ${styleSrc}`; +function generateHash( + content: string, +): string { + return crypto + .createHash('sha256') + .update(content) + .digest('base64'); } diff --git a/packages/astro/src/runtime/server/render/csp.ts b/packages/astro/src/runtime/server/render/csp.ts new file mode 100644 index 000000000000..eb3d1395736c --- /dev/null +++ b/packages/astro/src/runtime/server/render/csp.ts @@ -0,0 +1,49 @@ +import type { SSRResult } from '../../../types/public/index.js'; + +export function renderCspContent( + result: SSRResult, +): string { + const finalScriptHashes = + new Set(); + const finalStyleHashes = + new Set(); + + for (const scriptHash of result.clientScriptHashes) { + finalScriptHashes.add( + `'sha256-${scriptHash}'`, + ); + } + + for (const styleHash of result.clientStyleHashes) { + finalStyleHashes.add( + `'sha256-${styleHash}'`, + ); + } + + if ( + result.renderers.length > + 0 + ) { + for (const { + name, + hash, + } of result.astroIslandHashes) { + if ( + name === + 'astro-island-styles' + ) { + finalStyleHashes.add( + `'sha256-${hash}'`, + ); + } else { + finalScriptHashes.add( + `'sha256-${hash}'`, + ); + } + } + } + + const scriptSrc = `style-src 'self' ${Array.from(finalStyleHashes).join(' ')};`; + const styleSrc = `script-src 'self' ${Array.from(finalScriptHashes).join(' ')};`; + return `${scriptSrc} ${styleSrc}`; +} diff --git a/packages/astro/src/runtime/server/render/head.ts b/packages/astro/src/runtime/server/render/head.ts index 2281db0fe1a0..895e9cab40d1 100644 --- a/packages/astro/src/runtime/server/render/head.ts +++ b/packages/astro/src/runtime/server/render/head.ts @@ -1,42 +1,92 @@ import type { SSRResult } from '../../../types/public/internal.js'; import { markHTMLString } from '../escape.js'; -import type { MaybeRenderHeadInstruction, RenderHeadInstruction } from './instruction.js'; +import type { + MaybeRenderHeadInstruction, + RenderHeadInstruction, +} from './instruction.js'; import { createRenderInstruction } from './instruction.js'; import { renderElement } from './util.js'; -import { renderCspContent } from '../../../core/csp/common.js'; +import { renderCspContent } from './csp.js'; // Filter out duplicate elements in our set -const uniqueElements = (item: any, index: number, all: any[]) => { - const props = JSON.stringify(item.props); - const children = item.children; +const uniqueElements = ( + item: any, + index: number, + all: any[], +) => { + const props = + JSON.stringify( + item.props, + ); + const children = + item.children; return ( - index === all.findIndex((i) => JSON.stringify(i.props) === props && i.children == children) + index === + all.findIndex( + (i) => + JSON.stringify( + i.props, + ) === props && + i.children == + children, + ) ); }; -export function renderAllHeadContent(result: SSRResult) { +export function renderAllHeadContent( + result: SSRResult, +) { result._metadata.hasRenderedHead = true; - const styles = Array.from(result.styles) + const styles = Array.from( + result.styles, + ) .filter(uniqueElements) .map((style) => - style.props.rel === 'stylesheet' - ? renderElement('link', style) - : renderElement('style', style), + style.props.rel === + 'stylesheet' + ? renderElement( + 'link', + style, + ) + : renderElement( + 'style', + style, + ), ); // Clear result.styles so that any new styles added will be inlined. result.styles.clear(); - const scripts = Array.from(result.scripts) + const scripts = Array.from( + result.scripts, + ) .filter(uniqueElements) .map((script) => { - if (result.userAssetsBase) { + if ( + result.userAssetsBase + ) { script.props.src = - (result.base === '/' ? '' : result.base) + result.userAssetsBase + script.props.src; + (result.base === '/' + ? '' + : result.base) + + result.userAssetsBase + + script.props.src; } - return renderElement('script', script, false); + return renderElement( + 'script', + script, + false, + ); }); - const links = Array.from(result.links) + const links = Array.from( + result.links, + ) .filter(uniqueElements) - .map((link) => renderElement('link', link, false)); + .map((link) => + renderElement( + 'link', + link, + false, + ), + ); // Order styles -> links -> scripts similar to src/content/runtime.ts // The order is usually fine as the ordering between these groups are mutually exclusive, @@ -44,21 +94,34 @@ export function renderAllHeadContent(result: SSRResult) { // consist of CSS modules which should naturally take precedence over CSS styles, so the // order will still work. In prod, all CSS are stylesheet links. // In the future, it may be better to have only an array of head elements to avoid these assumptions. - let content = styles.join('\n') + links.join('\n') + scripts.join('\n'); + let content = + styles.join('\n') + + links.join('\n') + + scripts.join('\n'); - if (result._metadata.extraHead.length > 0) { - for (const part of result._metadata.extraHead) { + if ( + result._metadata.extraHead + .length > 0 + ) { + for (const part of result + ._metadata.extraHead) { content += part; } } - if (result.shouldInjectCspMetaTags) { + if ( + result.shouldInjectCspMetaTags + ) { content += renderElement( 'meta', { props: { - 'http-equiv': 'content-security-policy', - content: renderCspContent(result), + 'http-equiv': + 'content-security-policy', + content: + renderCspContent( + result, + ), }, children: '', }, @@ -66,11 +129,15 @@ export function renderAllHeadContent(result: SSRResult) { ); } - return markHTMLString(content); + return markHTMLString( + content, + ); } export function renderHead(): RenderHeadInstruction { - return createRenderInstruction({ type: 'head' }); + return createRenderInstruction( + { type: 'head' }, + ); } // This function is called by Astro components that do not contain a component @@ -80,5 +147,7 @@ export function renderHead(): RenderHeadInstruction { export function maybeRenderHead(): MaybeRenderHeadInstruction { // This is an instruction informing the page rendering that head might need rendering. // This allows the page to deduplicate head injections. - return createRenderInstruction({ type: 'maybe-head' }); + return createRenderInstruction( + { type: 'maybe-head' }, + ); } From 9feb22f99b32a41b51ccfc44b178d2db071cc02a Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 28 Apr 2025 11:22:32 +0100 Subject: [PATCH 3/4] chore: refactor codegen again --- packages/astro/src/container/index.ts | 2 +- packages/astro/src/core/app/types.ts | 7 +- .../astro/src/core/astro-islands-hashes.ts | 43 ++----- packages/astro/src/core/csp/common.ts | 68 +++------- .../astro/src/runtime/server/render/csp.ts | 41 ++---- .../astro/src/runtime/server/render/head.ts | 119 ++++-------------- packages/astro/src/types/public/config.ts | 5 +- scripts/cmd/prebuild.js | 19 ++- 8 files changed, 71 insertions(+), 233 deletions(-) diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts index b570dd8531c5..acacdf22b8d3 100644 --- a/packages/astro/src/container/index.ts +++ b/packages/astro/src/container/index.ts @@ -164,7 +164,7 @@ function createManifest( clientScriptHashes: manifest?.clientScriptHashes ?? [], clientStyleHashes: manifest?.clientStyleHashes ?? [], shouldInjectCspMetaTags: manifest?.shouldInjectCspMetaTags ?? false, - astroIslandHashes: manifest?.astroIslandHashes ?? [], + astroIslandHashes: manifest?.astroIslandHashes ?? {}, }; } diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 49cec063f79f..62211ea1f937 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -92,12 +92,7 @@ export type SSRManifest = { * When enabled, Astro tracks the hashes of script and styles, and eventually it will render the `` tag */ shouldInjectCspMetaTags: boolean; - astroIslandHashes: AstroIslandHashes[]; -}; - -export type AstroIslandHashes = { - name: string; - hash: string; + astroIslandHashes: Record; }; export type SSRActions = { diff --git a/packages/astro/src/core/astro-islands-hashes.ts b/packages/astro/src/core/astro-islands-hashes.ts index bb7467c88a58..56f4ad6fbffe 100644 --- a/packages/astro/src/core/astro-islands-hashes.ts +++ b/packages/astro/src/core/astro-islands-hashes.ts @@ -1,35 +1,10 @@ // This file is code-generated, please don't change it manually -export const ASTRO_ISLAND_HASHES = [ - { - "hash": "mH3H4wSoDVWMXJKrmeBKYJQMdAZQ3dArB2N66JomkzI=", - "name": "astro-island" - }, - { - "hash": "mH3H4wSoDVWMXJKrmeBKYJQMdAZQ3dArB2N66JomkzI=", - "name": "astro-island" - }, - { - "hash": "GI/D8grziRZwfj/Mqmn+dcgU/i8sylHSR/IfobqcUT4=", - "name": "idle" - }, - { - "hash": "XN6a2Vn8uvpBr/WhdYPdK0jVeCzlcOD2XYaP10veV4Y=", - "name": "load" - }, - { - "hash": "HDWxd14AUw8OvjrhhRRyyZFHCGnzxXGDrg59Qi8ayhc=", - "name": "media" - }, - { - "hash": "ZR0ZAU8UNTzLmo/ApeWH0y1mVLT+XtFkvZ5nw32W8jI=", - "name": "only" - }, - { - "hash": "cSNmhdbFlyTDRozeu9HPjo+B2S4QAeMp0RO41PqgAcA=", - "name": "visible" - }, - { - "hash": "s81ZcLcyAa7P/Jh5M5hUxYthTGwW+iZY3e6aHrQ8H9E=", - "name": "astro-island-styles" - } -]; \ No newline at end of file +export const ASTRO_ISLAND_HASHES = { + "astro-island": "p9VbHs/ClkQc+x63XdUjvCAgeWxA4ZGvpebJtMn9jbs=", + "idle": "BF0290pkb3jxQsE7z00xR8Imp8X34FLC88L0lkMnrGw=", + "load": "QzWFZi+FLIx23tnm9SBU4aEgx4x8DsuASP07mfqol/c=", + "media": "0chmwFk0zaA528yFfGV7J9ppIpdfTPPULncDF3WG7Zs=", + "only": "eIXWvAmxkr251LJZkjniEK5LcPF3NkapbJepohwYRIc=", + "visible": "Q2BPg90ZMplYY+FSdApNErhpWafg2hcRRbndmvxuL/Q=", + "astro-island-styles": "s81ZcLcyAa7P/Jh5M5hUxYthTGwW+iZY3e6aHrQ8H9E=" +}; \ No newline at end of file diff --git a/packages/astro/src/core/csp/common.ts b/packages/astro/src/core/csp/common.ts index 31805da8e3f2..74c7c2f67652 100644 --- a/packages/astro/src/core/csp/common.ts +++ b/packages/astro/src/core/csp/common.ts @@ -3,35 +3,16 @@ import type { BuildInternals } from '../build/internal.js'; import crypto from 'node:crypto'; import type { AstroSettings } from '../../types/astro.js'; -export function shouldTrackCspHashes( - config: AstroConfig, -): boolean { - return ( - config.experimental - ?.csp === true - ); +export function shouldTrackCspHashes(config: AstroConfig): boolean { + return config.experimental?.csp === true; } -export function trackStyleHashes( - internals: BuildInternals, -): string[] { - const clientStyleHashes: string[] = - []; - for (const [ - _, - page, - ] of internals.pagesByViteID.entries()) { +export function trackStyleHashes(internals: BuildInternals): string[] { + const clientStyleHashes: string[] = []; + for (const [_, page] of internals.pagesByViteID.entries()) { for (const style of page.styles) { - if ( - style.sheet.type === - 'inline' - ) { - clientStyleHashes.push( - generateHash( - style.sheet - .content, - ), - ); + if (style.sheet.type === 'inline') { + clientStyleHashes.push(generateHash(style.sheet.content)); } } } @@ -39,42 +20,23 @@ export function trackStyleHashes( return clientStyleHashes; } -export function trackScriptHashes( - internals: BuildInternals, - settings: AstroSettings, -): string[] { - const clientScriptHashes: string[] = - []; +export function trackScriptHashes(internals: BuildInternals, settings: AstroSettings): string[] { + const clientScriptHashes: string[] = []; for (const script of internals.inlinedScripts.values()) { - clientScriptHashes.push( - generateHash(script), - ); + clientScriptHashes.push(generateHash(script)); } for (const script of settings.scripts) { - const { content, stage } = - script; - if ( - stage === - 'head-inline' || - stage === - 'before-hydration' - ) { - clientScriptHashes.push( - generateHash(content), - ); + const { content, stage } = script; + if (stage === 'head-inline' || stage === 'before-hydration') { + clientScriptHashes.push(generateHash(content)); } } return clientScriptHashes; } -function generateHash( - content: string, -): string { - return crypto - .createHash('sha256') - .update(content) - .digest('base64'); +function generateHash(content: string): string { + return crypto.createHash('sha256').update(content).digest('base64'); } diff --git a/packages/astro/src/runtime/server/render/csp.ts b/packages/astro/src/runtime/server/render/csp.ts index eb3d1395736c..143bf0746b9a 100644 --- a/packages/astro/src/runtime/server/render/csp.ts +++ b/packages/astro/src/runtime/server/render/csp.ts @@ -1,44 +1,23 @@ import type { SSRResult } from '../../../types/public/index.js'; -export function renderCspContent( - result: SSRResult, -): string { - const finalScriptHashes = - new Set(); - const finalStyleHashes = - new Set(); +export function renderCspContent(result: SSRResult): string { + const finalScriptHashes = new Set(); + const finalStyleHashes = new Set(); for (const scriptHash of result.clientScriptHashes) { - finalScriptHashes.add( - `'sha256-${scriptHash}'`, - ); + finalScriptHashes.add(`'sha256-${scriptHash}'`); } for (const styleHash of result.clientStyleHashes) { - finalStyleHashes.add( - `'sha256-${styleHash}'`, - ); + finalStyleHashes.add(`'sha256-${styleHash}'`); } - if ( - result.renderers.length > - 0 - ) { - for (const { - name, - hash, - } of result.astroIslandHashes) { - if ( - name === - 'astro-island-styles' - ) { - finalStyleHashes.add( - `'sha256-${hash}'`, - ); + if (result.renderers.length > 0) { + for (const [ name, hash ] of Object.entries(result.astroIslandHashes)) { + if (name === 'astro-island-styles') { + finalStyleHashes.add(`'sha256-${hash}'`); } else { - finalScriptHashes.add( - `'sha256-${hash}'`, - ); + finalScriptHashes.add(`'sha256-${hash}'`); } } } diff --git a/packages/astro/src/runtime/server/render/head.ts b/packages/astro/src/runtime/server/render/head.ts index 895e9cab40d1..7989cb7acb5d 100644 --- a/packages/astro/src/runtime/server/render/head.ts +++ b/packages/astro/src/runtime/server/render/head.ts @@ -1,92 +1,42 @@ import type { SSRResult } from '../../../types/public/internal.js'; import { markHTMLString } from '../escape.js'; -import type { - MaybeRenderHeadInstruction, - RenderHeadInstruction, -} from './instruction.js'; +import type { MaybeRenderHeadInstruction, RenderHeadInstruction } from './instruction.js'; import { createRenderInstruction } from './instruction.js'; import { renderElement } from './util.js'; import { renderCspContent } from './csp.js'; // Filter out duplicate elements in our set -const uniqueElements = ( - item: any, - index: number, - all: any[], -) => { - const props = - JSON.stringify( - item.props, - ); - const children = - item.children; +const uniqueElements = (item: any, index: number, all: any[]) => { + const props = JSON.stringify(item.props); + const children = item.children; return ( - index === - all.findIndex( - (i) => - JSON.stringify( - i.props, - ) === props && - i.children == - children, - ) + index === all.findIndex((i) => JSON.stringify(i.props) === props && i.children == children) ); }; -export function renderAllHeadContent( - result: SSRResult, -) { +export function renderAllHeadContent(result: SSRResult) { result._metadata.hasRenderedHead = true; - const styles = Array.from( - result.styles, - ) + const styles = Array.from(result.styles) .filter(uniqueElements) .map((style) => - style.props.rel === - 'stylesheet' - ? renderElement( - 'link', - style, - ) - : renderElement( - 'style', - style, - ), + style.props.rel === 'stylesheet' + ? renderElement('link', style) + : renderElement('style', style), ); // Clear result.styles so that any new styles added will be inlined. result.styles.clear(); - const scripts = Array.from( - result.scripts, - ) + const scripts = Array.from(result.scripts) .filter(uniqueElements) .map((script) => { - if ( - result.userAssetsBase - ) { + if (result.userAssetsBase) { script.props.src = - (result.base === '/' - ? '' - : result.base) + - result.userAssetsBase + - script.props.src; + (result.base === '/' ? '' : result.base) + result.userAssetsBase + script.props.src; } - return renderElement( - 'script', - script, - false, - ); + return renderElement('script', script, false); }); - const links = Array.from( - result.links, - ) + const links = Array.from(result.links) .filter(uniqueElements) - .map((link) => - renderElement( - 'link', - link, - false, - ), - ); + .map((link) => renderElement('link', link, false)); // Order styles -> links -> scripts similar to src/content/runtime.ts // The order is usually fine as the ordering between these groups are mutually exclusive, @@ -94,34 +44,21 @@ export function renderAllHeadContent( // consist of CSS modules which should naturally take precedence over CSS styles, so the // order will still work. In prod, all CSS are stylesheet links. // In the future, it may be better to have only an array of head elements to avoid these assumptions. - let content = - styles.join('\n') + - links.join('\n') + - scripts.join('\n'); + let content = styles.join('\n') + links.join('\n') + scripts.join('\n'); - if ( - result._metadata.extraHead - .length > 0 - ) { - for (const part of result - ._metadata.extraHead) { + if (result._metadata.extraHead.length > 0) { + for (const part of result._metadata.extraHead) { content += part; } } - if ( - result.shouldInjectCspMetaTags - ) { + if (result.shouldInjectCspMetaTags) { content += renderElement( 'meta', { props: { - 'http-equiv': - 'content-security-policy', - content: - renderCspContent( - result, - ), + 'http-equiv': 'content-security-policy', + content: renderCspContent(result), }, children: '', }, @@ -129,15 +66,11 @@ export function renderAllHeadContent( ); } - return markHTMLString( - content, - ); + return markHTMLString(content); } export function renderHead(): RenderHeadInstruction { - return createRenderInstruction( - { type: 'head' }, - ); + return createRenderInstruction({ type: 'head' }); } // This function is called by Astro components that do not contain a component @@ -147,7 +80,5 @@ export function renderHead(): RenderHeadInstruction { export function maybeRenderHead(): MaybeRenderHeadInstruction { // This is an instruction informing the page rendering that head might need rendering. // This allows the page to deduplicate head injections. - return createRenderInstruction( - { type: 'maybe-head' }, - ); + return createRenderInstruction({ type: 'maybe-head' }); } diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index 39178cf7ae8c..c8393f577906 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -2229,12 +2229,11 @@ export interface ViteUserConfig extends OriginalViteUserConfig { */ headingIdCompat?: boolean; - /** - * + * */ // TODO: add docs once we are reaching the end - csp?: boolean, + csp?: boolean; /** * @name experimental.preserveScriptOrder diff --git a/scripts/cmd/prebuild.js b/scripts/cmd/prebuild.js index 3941607c521d..4b26aa529bd5 100644 --- a/scripts/cmd/prebuild.js +++ b/scripts/cmd/prebuild.js @@ -43,7 +43,7 @@ export default async function prebuild(...args) { return outURL; } - const hashes = []; + const hashes = new Map(); async function prebuildFile(filepath) { let tscode = await fs.promises.readFile(filepath, 'utf-8'); @@ -116,10 +116,7 @@ export default \`${generatedCode}\`;`; await fs.promises.writeFile(url, mod, 'utf-8'); const hash = crypto.createHash('sha256').update(code).digest('base64'); const basename = path.basename(filepath); - hashes.push({ - hash, - name: basename.slice(0, basename.indexOf('.')), - }); + hashes.set(basename.slice(0, basename.indexOf('.')), hash); } } for (const entrypoint of entryPoints) { @@ -131,12 +128,12 @@ export default \`${generatedCode}\`;`; 'utf-8', ); const styleContent = fileContent.match(ASTRO_ISLAND_STYLE_REGEX)[1]; - hashes.push({ - hash: crypto.createHash('sha256').update(styleContent).digest('base64'), - name: 'astro-island-styles', - }); - hashes.sort(); - const entries = JSON.stringify(hashes, null, 2); + hashes.set( + 'astro-island-styles', + crypto.createHash('sha256').update(styleContent).digest('base64'), + ); + + const entries = JSON.stringify(Object.fromEntries(hashes.entries()), null, 2); const content = `// This file is code-generated, please don't change it manually export const ASTRO_ISLAND_HASHES = ${entries};`; await fs.promises.writeFile( From 342014def161c32d86d3211cb1d59c46b6c6e941 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 28 Apr 2025 11:45:04 +0100 Subject: [PATCH 4/4] chore: refactor test --- packages/astro/test/csp.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/astro/test/csp.test.js b/packages/astro/test/csp.test.js index 64b3baa1e6fc..fc5703c0a016 100644 --- a/packages/astro/test/csp.test.js +++ b/packages/astro/test/csp.test.js @@ -40,13 +40,13 @@ describe('CSP', () => { ); } - let astroStyleHash = manifest.astroIslandHashes.find( - ({ name }) => name === 'astro-island-styles', + let [, astroStyleHash] = Object.entries(manifest.astroIslandHashes).find( + ([name, _]) => name === 'astro-island-styles', ); - astroStyleHash = `sha256-${astroStyleHash.hash}`; + astroStyleHash = `sha256-${astroStyleHash}`; - let astroIsland = manifest.astroIslandHashes.find(({ name }) => name === 'astro-island'); - astroIsland = `sha256-${astroIsland.hash}`; + let [, astroIsland] = Object.entries(manifest.astroIslandHashes).find(([name, _]) => name === 'astro-island'); + astroIsland = `sha256-${astroIsland}`; assert.ok( meta.attr('content').includes(astroStyleHash),