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 6c60ea2dcd62..62211ea1f937 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -92,7 +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: 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 9581e3b6456d..56f4ad6fbffe 100644 --- a/packages/astro/src/core/astro-islands-hashes.ts +++ b/packages/astro/src/core/astro-islands-hashes.ts @@ -1,11 +1,10 @@ // 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=" -]; \ 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/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..74c7c2f67652 100644 --- a/packages/astro/src/core/csp/common.ts +++ b/packages/astro/src/core/csp/common.ts @@ -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,19 @@ 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'); +} 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/csp.ts b/packages/astro/src/runtime/server/render/csp.ts new file mode 100644 index 000000000000..143bf0746b9a --- /dev/null +++ b/packages/astro/src/runtime/server/render/csp.ts @@ -0,0 +1,28 @@ +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 Object.entries(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 bc7a70f9b965..7989cb7acb5d 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 './csp.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/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/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..fc5703c0a016 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] = Object.entries(manifest.astroIslandHashes).find( + ([name, _]) => name === 'astro-island-styles', + ); + astroStyleHash = `sha256-${astroStyleHash}`; + + let [, astroIsland] = Object.entries(manifest.astroIslandHashes).find(([name, _]) => name === 'astro-island'); + astroIsland = `sha256-${astroIsland}`; + + 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..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'); @@ -115,7 +115,8 @@ 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.set(basename.slice(0, basename.indexOf('.')), hash); } } for (const entrypoint of entryPoints) { @@ -127,13 +128,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.sort(); - const entries = hashes.map((hash) => `"${hash}"`); + 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.join(',\n ')} -];`; +export const ASTRO_ISLAND_HASHES = ${entries};`; await fs.promises.writeFile( path.join( fileURLToPath(import.meta.url),