astro-helmet 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -171,8 +171,8 @@ JSON-LD script tags are rendered with priority `105`, placing them after regular
171
171
 
172
172
  When provided with an array of `headItems`, `astro-helmet` will merge the items together.
173
173
 
174
- `headItems.meta` are deduplicated by `name`, `property` and `http-equiv`.
175
- Meta items later in the array replace earlier items.
174
+ `headItems.meta` are deduplicated by `name`, `property`, `http-equiv` and `charset`.
175
+ Meta items later in the array replace earlier items. Meta tags without any of these keys (e.g. `itemprop`-only tags) are never deduplicated.
176
176
 
177
177
  `title` and `base` items are also deduplicated, with the last item in the array taking precedence.
178
178
 
@@ -270,7 +270,7 @@ function applyPriority(tag: Tag): Required<Tag> {
270
270
  break
271
271
 
272
272
  case 'style':
273
- priority = tag.innerHTML.includes('@import') ? 30 : 51
273
+ priority = tag.innerHTML?.includes('@import') ? 30 : 51
274
274
  break
275
275
 
276
276
  case 'script':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-helmet",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "A document head manager for astro.",
6
6
  "author": "Ryan Voitiskis <ryanvoitiskis@pm.me> (https://ryanvoitiskis.com/)",
@@ -30,24 +30,24 @@
30
30
  "release": "semantic-release"
31
31
  },
32
32
  "devDependencies": {
33
- "@astrojs/check": "^0.9.6",
33
+ "@astrojs/check": "^0.9.8",
34
34
  "@eslint/js": "^9.39.2",
35
35
  "@semantic-release/changelog": "^6.0.3",
36
36
  "@semantic-release/git": "^10.0.1",
37
37
  "@semantic-release/github": "^11.0.1",
38
38
  "@semantic-release/npm": "^12.0.1",
39
- "@vitest/coverage-v8": "^2.1.8",
40
- "astro": "^5.0.3",
39
+ "@vitest/coverage-v8": "^4.1.2",
40
+ "astro": "^6.1.3",
41
41
  "eslint": "^9.39.2",
42
42
  "prettier": "^3.4.2",
43
43
  "prettier-plugin-astro": "^0.14.1",
44
44
  "semantic-release": "^24.2.0",
45
45
  "typescript": "^5.7.2",
46
46
  "typescript-eslint": "^8.55.0",
47
- "vitest": "^2.1.8"
47
+ "vitest": "^4.1.2"
48
48
  },
49
49
  "peerDependencies": {
50
- "astro": "^4.0.0 || ^5.0.0"
50
+ "astro": "^4.0.0 || ^5.0.0 || ^6.0.0"
51
51
  },
52
52
  "repository": {
53
53
  "type": "git",
package/src/Helmet.astro CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { type HeadItems, type Tag, renderHead } from './main'
2
+ import { type HeadItems, type Tag, renderHead, getInlineContent, getExternalResources } from './main'
3
3
 
4
4
  interface Props {
5
5
  headItems: HeadItems | HeadItems[]
@@ -12,6 +12,34 @@ interface Props {
12
12
  const { headItems, options = {} } = Astro.props
13
13
  const { applyPriority, omitHeadTags = false } = options
14
14
  const head = renderHead(headItems, applyPriority)
15
+
16
+ // Register CSP hashes and resources when running on Astro 6+ with CSP enabled.
17
+ // Cast avoids typecheck failures for downstream consumers on Astro 4/5 where
18
+ // `csp` isn't declared on `AstroGlobal`.
19
+ const csp = (Astro as unknown as {
20
+ csp?: {
21
+ insertScriptHash(hash: string): void
22
+ insertStyleHash(hash: string): void
23
+ insertScriptResource(url: string): void
24
+ insertStyleResource(url: string): void
25
+ }
26
+ }).csp
27
+ if (csp) {
28
+ for (const { type, content } of getInlineContent(headItems)) {
29
+ const hashBuffer = await crypto.subtle.digest(
30
+ 'SHA-256',
31
+ new TextEncoder().encode(content)
32
+ )
33
+ const hash = btoa(String.fromCharCode(...new Uint8Array(hashBuffer)))
34
+ if (type === 'script') csp.insertScriptHash(`sha256-${hash}`)
35
+ else csp.insertStyleHash(`sha256-${hash}`)
36
+ }
37
+
38
+ for (const { type, url } of getExternalResources(headItems)) {
39
+ if (type === 'script') csp.insertScriptResource(url)
40
+ else csp.insertStyleResource(url)
41
+ }
42
+ }
15
43
  ---
16
44
 
17
45
  {
package/src/main.ts CHANGED
@@ -183,6 +183,62 @@ function applyPriorityDefault(tag: Tag): Required<Tag> {
183
183
  return { ...tag, priority }
184
184
  }
185
185
 
186
+ export function getInlineContent(
187
+ headItems: HeadItems | HeadItems[]
188
+ ): { type: 'script' | 'style'; content: string }[] {
189
+ const items = Array.isArray(headItems)
190
+ ? mergeHeadItems(headItems.map((i) => normaliseHeadItems(i)))
191
+ : normaliseHeadItems(headItems)
192
+
193
+ const result: { type: 'script' | 'style'; content: string }[] = []
194
+
195
+ for (const style of items.style) {
196
+ if (style.innerHTML) result.push({ type: 'style', content: style.innerHTML })
197
+ }
198
+
199
+ for (const script of items.script) {
200
+ if (script.innerHTML)
201
+ result.push({ type: 'script', content: script.innerHTML })
202
+ }
203
+
204
+ for (const item of items.jsonLd) {
205
+ result.push({
206
+ type: 'script',
207
+ content: escapeJsonLd(
208
+ JSON.stringify({ ...item, '@context': 'https://schema.org' })
209
+ )
210
+ })
211
+ }
212
+
213
+ return result
214
+ }
215
+
216
+ export function getExternalResources(
217
+ headItems: HeadItems | HeadItems[]
218
+ ): { type: 'script' | 'style'; url: string }[] {
219
+ const items = Array.isArray(headItems)
220
+ ? mergeHeadItems(headItems.map((i) => normaliseHeadItems(i)))
221
+ : normaliseHeadItems(headItems)
222
+
223
+ const result: { type: 'script' | 'style'; url: string }[] = []
224
+
225
+ for (const script of items.script) {
226
+ if (script.src) result.push({ type: 'script', url: script.src })
227
+ }
228
+
229
+ for (const link of items.link) {
230
+ if (link.rel === 'stylesheet' && link.href)
231
+ result.push({ type: 'style', url: link.href })
232
+ else if (link.rel === 'preload' && link.href) {
233
+ if (link.as === 'script') result.push({ type: 'script', url: link.href })
234
+ else if (link.as === 'style')
235
+ result.push({ type: 'style', url: link.href })
236
+ }
237
+ }
238
+
239
+ return result
240
+ }
241
+
186
242
  function escapeJsonLd(json: string): string {
187
243
  return json.replace(/<\//g, '<\\/')
188
244
  }