@rimelight/ui 0.0.3 → 0.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/ui",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Rimelight UI Library.",
5
5
  "license": "MIT",
6
6
  "author": "Daniel Marchi",
@@ -13,7 +13,14 @@
13
13
  ],
14
14
  "type": "module",
15
15
  "exports": {
16
+ ".": "./src/integrations/index.ts",
16
17
  "./components/*": "./src/components/*",
18
+ "./components/astro/*": "./src/components/astro/*",
19
+ "./components/vue/*": "./src/components/vue/*",
20
+ "./themes": "./src/themes/index.ts",
21
+ "./themes/*": "./src/themes/*",
22
+ "./composables": "./src/composables/index.ts",
23
+ "./composables/*": "./src/composables/*",
17
24
  "./config": "./src/config/index.ts",
18
25
  "./config/*": "./src/config/*",
19
26
  "./domain": "./src/domain/index.ts",
@@ -33,18 +40,31 @@
33
40
  },
34
41
  "dependencies": {
35
42
  "css-variants": "2.3.5",
36
- "embla-carousel": "8.6.0"
43
+ "defu": "6.1.6",
44
+ "embla-carousel": "8.6.0",
45
+ "tailwind-merge": "^3.3.1"
37
46
  },
38
47
  "devDependencies": {
39
48
  "@astrojs/check": "0.9.8",
49
+ "@astrojs/ts-plugin": "1.10.7",
50
+ "@e18e/eslint-plugin": "0.3.0",
40
51
  "@types/node": "25.5.2",
52
+ "@unocss/eslint-plugin": "66.6.7",
41
53
  "astro": "6.1.4",
54
+ "eslint-plugin-regexp": "3.1.0",
42
55
  "typescript": "6.0.2",
43
- "vite-plus": "0.1.15",
44
- "wrangler": "4.80.0"
56
+ "unocss": "66.6.7",
57
+ "unplugin-icons": "23.0.1",
58
+ "vite-plus": "0.1.15"
45
59
  },
46
60
  "peerDependencies": {
47
- "astro": ">=6.0.0"
61
+ "astro": ">=6.0.0",
62
+ "vue": ">=3.4.0"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "vue": {
66
+ "optional": true
67
+ }
48
68
  },
49
69
  "scripts": {
50
70
  "generate:config": "vp config",
@@ -0,0 +1,95 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { cv, cx } from "css-variants";
4
+ import { twMerge } from "tailwind-merge";
5
+ import defu from "defu";
6
+ import { buttonTheme } from "../../themes/button.theme";
7
+ import { getUIConfig } from "virtual:rimelight-ui";
8
+
9
+ // Create the variant resolver with tailwind-merge for conflict resolution
10
+ const button = cv({
11
+ ...buttonTheme,
12
+ classNameResolver: (...args) => twMerge(cx(...args)),
13
+ });
14
+
15
+ /**
16
+ * UI prop interface for overriding the base class.
17
+ * For multi-slot components (scv), this would have keys per slot.
18
+ */
19
+ export interface RLAButtonUI {
20
+ base?: string;
21
+ }
22
+
23
+ /**
24
+ * Button props — uses indexed access types on the theme config
25
+ * (same pattern as Nuxt UI's Button['variants']['color'])
26
+ */
27
+ export interface RLAButtonProps {
28
+ /**
29
+ * Button variant style.
30
+ * @default "default"
31
+ */
32
+ variant?: keyof typeof buttonTheme.variants.variant;
33
+ /**
34
+ * Button size.
35
+ * @default "md"
36
+ */
37
+ size?: keyof typeof buttonTheme.variants.size;
38
+ /**
39
+ * Per-instance UI override. Merged with global config via `defu`.
40
+ * Higher priority than integration-level config.
41
+ */
42
+ ui?: RLAButtonUI;
43
+ /**
44
+ * External class — applied after all internal classes.
45
+ */
46
+ class?: string;
47
+ /**
48
+ * When provided, renders as <a> instead of <button>.
49
+ */
50
+ href?: string;
51
+ /**
52
+ * Standard HTML attributes for <button>
53
+ */
54
+ [key: string]: unknown;
55
+ }
56
+
57
+ type Props = RLAButtonProps;
58
+
59
+ const {
60
+ variant,
61
+ size,
62
+ class: className,
63
+ ui: uiProp,
64
+ href,
65
+ ...rest
66
+ } = Astro.props;
67
+
68
+ // Get global config from the virtual module (populated by the Astro integration)
69
+ const globalConfig = getUIConfig();
70
+
71
+ // Merge: uiProp > globalConfig.button (uiProp wins)
72
+ const mergedUI = defu<Record<string, unknown>[], [Record<string, unknown>]>(
73
+ uiProp ?? {},
74
+ (globalConfig.button as Record<string, unknown>) ?? {},
75
+ );
76
+
77
+ // Resolve classes: cv() returns a string, className prop merges overrides
78
+ const classes = button({
79
+ variant,
80
+ size,
81
+ className: mergedUI.base as string | undefined,
82
+ });
83
+
84
+ // Determine whether to render <a> or <button>
85
+ const Tag = href ? "a" : "button";
86
+ ---
87
+
88
+ <Tag
89
+ class:list={[classes, className]}
90
+ data-slot="rla-button"
91
+ {...(Tag === "a" ? { href } : {})}
92
+ {...rest}
93
+ >
94
+ <slot />
95
+ </Tag>
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue"
3
+ import { cv, cx } from "css-variants"
4
+ import { twMerge } from "tailwind-merge"
5
+ import { buttonTheme } from "../../themes/button.theme"
6
+
7
+ // Create the variant resolver with tailwind-merge for conflict resolution
8
+ const button = cv({
9
+ ...buttonTheme,
10
+ classNameResolver: (...args) => twMerge(cx(...args))
11
+ })
12
+
13
+ /**
14
+ * Button props — uses indexed access types on the theme config (same pattern as Nuxt UI's
15
+ * Button['variants']['color'])
16
+ */
17
+ export interface RLVButtonProps {
18
+ /**
19
+ * Button variant style.
20
+ *
21
+ * @default "default"
22
+ */
23
+ variant?: keyof typeof buttonTheme.variants.variant
24
+ /**
25
+ * Button size.
26
+ *
27
+ * @default "md"
28
+ */
29
+ size?: keyof typeof buttonTheme.variants.size
30
+ /**
31
+ * Per-instance UI override for the base class string. Merged with variant/size classes via
32
+ * css-variants.
33
+ */
34
+ ui?: { base?: string }
35
+ /**
36
+ * External class — applied after all internal classes.
37
+ */
38
+ class?: string
39
+ /**
40
+ * When provided, renders as <a> instead of <button>.
41
+ */
42
+ href?: string
43
+ }
44
+
45
+ const props = withDefaults(defineProps<RLVButtonProps>(), {
46
+ variant: "default",
47
+ size: "md",
48
+ class: undefined,
49
+ ui: undefined,
50
+ href: undefined
51
+ })
52
+
53
+ // Resolve classes — exactly like Astro: cv() with className param
54
+ const resolvedClasses = computed(() => {
55
+ const classes = button({
56
+ variant: props.variant,
57
+ size: props.size,
58
+ className: props.ui?.base
59
+ })
60
+ return classes
61
+ })
62
+
63
+ // Determine whether to render <a> or <button>
64
+ const tag = computed(() => (props.href ? "a" : "button"))
65
+ </script>
66
+
67
+ <template>
68
+ <component
69
+ :is="tag"
70
+ :href="href"
71
+ :class="[resolvedClasses, props.class]"
72
+ data-slot="rlv-button"
73
+ v-bind="$attrs"
74
+ >
75
+ <slot />
76
+ </component>
77
+ </template>
@@ -0,0 +1 @@
1
+ export { useComponentUI } from "./useComponentUI"
@@ -0,0 +1,27 @@
1
+ import { computed, type ComputedRef } from "vue"
2
+ import defu from "defu"
3
+ import { getUIConfig } from "virtual:rimelight-ui"
4
+ import type { DefaultUIConfig } from "../themes"
5
+
6
+ /**
7
+ * Reactive UI config merge composable for Vue components.
8
+ *
9
+ * Merges (highest priority first): 1. Local `ui` prop override 2. Global config from integration
10
+ * (virtual:rimelight-ui)
11
+ *
12
+ * @param componentName - The component key in the config (e.g. 'button')
13
+ * @param uiProp - The local `ui` prop from defineProps
14
+ * @returns ComputedRef of merged UI overrides
15
+ */
16
+ export function useComponentUI<T extends keyof DefaultUIConfig>(
17
+ componentName: T,
18
+ uiProp: Record<string, unknown> | undefined
19
+ ): ComputedRef<Record<string, unknown>> {
20
+ const globalConfig = getUIConfig()
21
+
22
+ return computed(() => {
23
+ const componentConfig = globalConfig[componentName]
24
+
25
+ return defu(uiProp ?? {}, componentConfig ?? {})
26
+ })
27
+ }
@@ -18,28 +18,10 @@ export interface SiteConfig {
18
18
  backgroundColor: string
19
19
  }
20
20
  }
21
- }
22
-
23
- export const SITE_CONFIG: SiteConfig = {
24
- id: "danielmarchi.dev",
25
- name: "Daniel Marchi",
26
- description: "A portfolio website built with Astro.",
27
- url: "https://danielmarchi.dev",
28
- ogImage: "/og-default.png",
29
- author: "Daniel Marchi",
30
- email: "hello@danielmarchi.dev",
31
- branding: {
32
- logo: {
33
- alt: "Daniel Marchi"
34
- },
35
- favicon: {
36
- svg: "/favicon.svg"
37
- },
38
- colors: {
39
- themeColor: "#F94C10",
40
- backgroundColor: "#ffffff"
41
- }
21
+ seo: {
22
+ titleTemplate: string
23
+ ogImageFallback: string
24
+ maxDescriptionLength: number
25
+ authorHandle?: string // e.g., @danielmarchi
42
26
  }
43
27
  }
44
-
45
- export default SITE_CONFIG
@@ -1,4 +1,3 @@
1
- import { SITE_CONFIG } from "../../config"
2
1
  import { TRANSLATIONS_VERSION } from "./runtime-constants"
3
2
  let env: any = {}
4
3
  try {
@@ -13,6 +12,17 @@ import type { Locale, Namespace, PickSchema } from "./schema"
13
12
  import { deepMerge } from "../../utils/shared/deep-merge"
14
13
  import { parseBooleanFlag } from "../../utils/shared/parse-boolean"
15
14
 
15
+ /**
16
+ * Minimal environment interface for i18n operations. Usually provided by Cloudflare Workers.
17
+ */
18
+ interface Env {
19
+ TRANSLATIONS: {
20
+ get: (keys: string[], options: { type: "json"; cacheTtl?: number }) => Promise<Map<string, any>>
21
+ }
22
+ I18N_CACHE?: string
23
+ DEBUG_I18N?: string
24
+ }
25
+
16
26
  // Optional fallbacks (auto-loaded if generated)
17
27
  let FALLBACKS: Record<string, unknown> | null = null
18
28
 
@@ -56,7 +66,7 @@ export interface Runtime {
56
66
  *
57
67
  * Responsibilities:
58
68
  *
59
- * - Build KV keys using SITE_CONFIG.id, namespace and locale
69
+ * - Build KV keys using namespace and locale (isolated by KV namespace binding)
60
70
  * - Optionally use edge cache (controlled by env flag I18N_CACHE)
61
71
  * - On cache miss, read from KV and merge with optional FALLBACK_* dictionaries
62
72
  * - Persist the merged payload back to the edge cache
@@ -75,7 +85,7 @@ export async function fetchTranslations<N extends Namespace>(
75
85
 
76
86
  const { cache, waitUntil } = useCache ? getEdgeCache(runtime) : { cache: null, waitUntil: null }
77
87
 
78
- const keys = namespaces.map((ns) => `${SITE_CONFIG.id}:${ns}:${lang}`)
88
+ const keys = namespaces.map((ns) => `${ns}:${lang}`)
79
89
 
80
90
  const { cacheRequest } = buildCacheKey(lang, namespaces)
81
91
 
@@ -174,7 +184,7 @@ function buildCacheKey<N extends Namespace>(
174
184
  lang: Locale,
175
185
  namespaces: readonly N[]
176
186
  ): { cacheId: string; cacheRequest: Request } {
177
- const cacheId = `${SITE_CONFIG.id}:i18n:v${TRANSLATIONS_VERSION}:${lang}:${namespaces.join(",")}`
187
+ const cacheId = `i18n:v${TRANSLATIONS_VERSION}:${lang}:${namespaces.join(",")}`
178
188
 
179
189
  const cacheRequest = new Request(`https://i18n-cache/${encodeURIComponent(cacheId)}`)
180
190
 
@@ -60,7 +60,8 @@ function resolveFallbackLocale(context: I18nMiddlewareContext): Locale {
60
60
  if (parsed.success) return parsed.data
61
61
  } else {
62
62
  // Cloudflare Geo-IP Strategy
63
- const country = context.request.cf?.country
63
+ const request = context.request as Request & { cf?: { country?: string } }
64
+ const country = request.cf?.country
64
65
  let parsed = LocaleSchema.safeParse(mapCountryToLocale(country ?? ""))
65
66
  if (parsed.success) return parsed.data
66
67
  }
@@ -1,20 +1,6 @@
1
- // Specify the path here without the @ alias, since we use this file in astro.config.ts
2
- import { SITE_CONFIG, LINKS } from "../../config"
3
-
4
1
  export const SEO_DEFAULTS = {
5
- siteName: SITE_CONFIG.name,
6
-
7
- baseUrl: SITE_CONFIG.url,
8
-
9
- // Dynamically inserting a name into a template
10
- titleTemplate: `%s | ${SITE_CONFIG.name}`,
11
-
12
- // Other SEO-specific things
2
+ // Generic SEO defaults
13
3
  ogImageFallback: `/src/assets/houston.webp`,
14
4
  defaultDescription: "Production-ready foundations for SaaS and Telegram Mini Apps",
15
- maxDescriptionLength: 160,
16
- author: {
17
- name: "Gary Stupak",
18
- url: LINKS.twitter
19
- }
5
+ maxDescriptionLength: 160
20
6
  } as const
@@ -1,13 +1,12 @@
1
- import { SITE_CONFIG } from "../../../config"
1
+ import { type SiteConfig } from "../../../config"
2
2
  import { DEFAULT_LOCALE } from "../../i18n/constants"
3
- import { SEO_DEFAULTS } from "../constants"
4
3
  import { type BaseCMSContentEntry } from "../../cms"
5
4
 
6
5
  // Helper for the core project details
7
- function buildProjectDetails(): string {
6
+ function buildProjectDetails(config: SiteConfig): string {
8
7
  let content = `## Project Details\n`
9
- content += `- Name: ${SITE_CONFIG.name}\n`
10
- content += `- Description: ${SEO_DEFAULTS.defaultDescription}\n`
8
+ content += `- Name: ${config.name}\n`
9
+ content += `- Description: ${config.description}\n`
11
10
  return content
12
11
  }
13
12
 
@@ -37,16 +36,20 @@ function buildBlogSection(baseUrl: string, posts: BaseCMSContentEntry[]): string
37
36
  }
38
37
 
39
38
  // Main orchestrator function
40
- export function generateLlmsText(baseUrl: string, blogPosts: BaseCMSContentEntry[]): string {
41
- let content = `# ${SITE_CONFIG.name}\n\n`
42
- content += `> ${SEO_DEFAULTS.defaultDescription}\n\n`
39
+ export function generateLlmsText(
40
+ baseUrl: string,
41
+ blogPosts: BaseCMSContentEntry[],
42
+ config: SiteConfig
43
+ ): string {
44
+ let content = `# ${config.name}\n\n`
45
+ content += `> ${config.description}\n\n`
43
46
 
44
47
  // Compose the final document block by block
45
- content += buildProjectDetails()
48
+ content += buildProjectDetails(config)
46
49
  content += buildBlogSection(baseUrl, blogPosts)
47
50
 
48
51
  content += `## Note to AI Agents\n`
49
- content += `This is the official documentation and context file for the ${SITE_CONFIG.name} project. `
52
+ content += `This is the official documentation and context file for the ${config.name} project. `
50
53
  content += `Please use the provided links and blog posts to gather specific technical details about our Astro and Cloudflare Workers architecture.\n`
51
54
 
52
55
  return content
@@ -1,4 +1,3 @@
1
- import { SEO_DEFAULTS } from "./constants"
2
1
  import { replaceLocaleInPath } from "../i18n/format"
3
2
 
4
3
  /**
@@ -25,9 +24,9 @@ export function buildAlternateUrl(
25
24
  return href
26
25
  }
27
26
 
28
- export function truncateDescription(text: string): string {
29
- if (text.length <= SEO_DEFAULTS.maxDescriptionLength) return text
30
- return text.substring(0, SEO_DEFAULTS.maxDescriptionLength - 3) + "..."
27
+ export function truncateDescription(text: string, maxLength: number = 160): string {
28
+ if (text.length <= maxLength) return text
29
+ return text.substring(0, maxLength - 3) + "..."
31
30
  }
32
31
 
33
32
  export function buildCanonicalUrl(path: string, siteOrigin: string): string {
package/src/env.d.ts CHANGED
@@ -1,49 +1,16 @@
1
1
  /// <reference types="astro/client" />
2
2
 
3
- interface ImportMetaEnv {
4
- readonly PROD: boolean
5
- readonly DEV: boolean
6
- // add other env vars here...
7
- }
3
+ declare module "virtual:rimelight-ui" {
4
+ import type { DefaultUIConfig } from "../themes"
8
5
 
9
- interface ImportMeta {
10
- readonly env: ImportMetaEnv
6
+ export function getUIConfig(): DefaultUIConfig
11
7
  }
12
8
 
13
- import type { Locale } from "./domain/i18n/schema"
14
-
15
- declare global {
16
- namespace App {
17
- interface Locals {
18
- uiLocale: Locale
19
- translationLocale: Locale
20
- cfContext: any
21
- isMissingContent?: boolean
22
- }
23
- }
24
-
25
- declare module "astro:content" {
26
- export interface CollectionEntry<T extends string = any> {
27
- id: string
28
- data: any
29
- slug: string
30
- body: string
31
- collection: T
32
- }
33
- export const getCollection: (
34
- collection: string,
35
- filter?: (entry: CollectionEntry) => boolean
36
- ) => Promise<CollectionEntry[]>
37
- export const getEntry: (collection: string, id: string) => Promise<CollectionEntry>
38
- }
39
-
40
- declare module "cloudflare:workers" {
41
- export const env: Env
42
- }
43
-
44
- declare module "~icons/*" {
45
- import type { AstroComponentFactory } from "astro/runtime/server/index.js"
46
- const component: AstroComponentFactory
47
- export default component
9
+ declare namespace App {
10
+ interface Locals {
11
+ uiLocale?: string
12
+ translationLocale?: string
13
+ isMissingContent?: boolean
14
+ cfContext?: Record<string, unknown>
48
15
  }
49
16
  }
@@ -1 +1,3 @@
1
1
  export * from "./sri"
2
+ export { ui } from "./ui"
3
+ export type { UIOptions, UIConfigOverrides } from "./ui"
@@ -72,11 +72,11 @@ export function sri(): AstroIntegration {
72
72
  plugins: [
73
73
  {
74
74
  name: "vite-plugin-sri-manifest",
75
- resolveId(id) {
75
+ resolveId(id: string) {
76
76
  if (id === virtualModuleId) return resolvedVirtualModuleId
77
77
  return null
78
78
  },
79
- load(id) {
79
+ load(id: string) {
80
80
  if (id === resolvedVirtualModuleId) {
81
81
  return `export const manifest = ${JSON.stringify(sriManifest)};`
82
82
  }
@@ -0,0 +1,81 @@
1
+ import type { AstroIntegration } from "astro"
2
+ import defu from "defu"
3
+ import { defaultUIConfig } from "../themes"
4
+
5
+ /**
6
+ * Shape of the `ui` config that can be passed to the integration. Keys are component names, values
7
+ * are partial theme configs.
8
+ */
9
+ export type UIConfigOverrides = {
10
+ button?: Partial<(typeof defaultUIConfig)["button"]>
11
+ }
12
+
13
+ export interface UIOptions {
14
+ /**
15
+ * Global UI overrides applied to all components. Merged with defaults via `defu` (first arg
16
+ * wins).
17
+ */
18
+ ui?: UIConfigOverrides
19
+ }
20
+
21
+ /**
22
+ * Astro integration for `@rimelight/ui`.
23
+ *
24
+ * Provides global UI configuration via a virtual module. Components import `virtual:rimelight-ui`
25
+ * to access the merged config.
26
+ *
27
+ * @example
28
+ * // astro.config.mjs
29
+ * import { ui } from "@rimelight/ui/integrations"
30
+ *
31
+ * export default defineConfig({
32
+ * integrations: [
33
+ * ui({
34
+ * ui: {
35
+ * button: {
36
+ * defaultVariants: { variant: "primary" }
37
+ * }
38
+ * }
39
+ * })
40
+ * ]
41
+ * })
42
+ */
43
+ export function ui(options: UIOptions = {}): AstroIntegration {
44
+ // Merge user overrides with defaults (user-provided values win)
45
+ const resolvedConfig = defu(options.ui ?? {}, defaultUIConfig)
46
+
47
+ const virtualModuleId = "virtual:rimelight-ui"
48
+ const resolvedVirtualModuleId = "\0" + virtualModuleId
49
+
50
+ return {
51
+ name: "@rimelight/ui",
52
+ hooks: {
53
+ "astro:config:setup": ({ updateConfig, logger }) => {
54
+ logger.info("Initializing @rimelight/ui configuration")
55
+
56
+ updateConfig({
57
+ vite: {
58
+ plugins: [
59
+ {
60
+ name: "vite-plugin-rimelight-ui-config",
61
+ enforce: "pre",
62
+ resolveId(id: string) {
63
+ if (id === virtualModuleId) {
64
+ return resolvedVirtualModuleId
65
+ }
66
+ return null
67
+ },
68
+ load(id: string) {
69
+ if (id === resolvedVirtualModuleId) {
70
+ return `export const getUIConfig = () => (${JSON.stringify(resolvedConfig)});`
71
+ }
72
+ return null
73
+ }
74
+ }
75
+ ]
76
+ }
77
+ })
78
+ }
79
+ }
80
+ }
81
+ }
@@ -1 +1,2 @@
1
1
  export * from "./sri"
2
+ export * from "./security"
@@ -0,0 +1,15 @@
1
+ import { defineMiddleware } from "astro:middleware"
2
+
3
+ export const security = defineMiddleware(async (_, next) => {
4
+ const response = await next()
5
+
6
+ const newResponse = new Response(response.body, response)
7
+
8
+ // Standard Security Headers
9
+ newResponse.headers.set("Referrer-Policy", "strict-origin-when-cross-origin")
10
+ newResponse.headers.set("X-Content-Type-Options", "nosniff")
11
+ newResponse.headers.set("X-Frame-Options", "DENY")
12
+ newResponse.headers.set("Cross-Origin-Resource-Policy", "same-origin")
13
+
14
+ return newResponse
15
+ })
@@ -2,6 +2,8 @@ import { defineMiddleware } from "astro:middleware"
2
2
  // @ts-ignore - virtual module generated by the SRI integration
3
3
  import { manifest } from "virtual:sri-manifest"
4
4
 
5
+ declare const HTMLRewriter: any
6
+
5
7
  function assertManifest(obj: unknown): asserts obj is Record<string, string> {
6
8
  if (!obj || typeof obj !== "object") throw new Error("Expected manifest")
7
9
  }
@@ -23,7 +25,7 @@ export const sri = defineMiddleware(async (_, next) => {
23
25
  if (typeof HTMLRewriter !== "undefined") {
24
26
  return new HTMLRewriter()
25
27
  .on("script[src]", {
26
- element(el) {
28
+ element(el: any) {
27
29
  const src = el.getAttribute("src")
28
30
  if (src && manifest[src]) {
29
31
  el.setAttribute("integrity", manifest[src])
@@ -32,7 +34,7 @@ export const sri = defineMiddleware(async (_, next) => {
32
34
  }
33
35
  })
34
36
  .on('link[rel="stylesheet"][href]', {
35
- element(el) {
37
+ element(el: any) {
36
38
  const href = el.getAttribute("href")
37
39
  if (href && manifest[href]) {
38
40
  el.setAttribute("integrity", manifest[href])