@rimelight/ui 0.0.15 → 0.0.16

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.15",
3
+ "version": "0.0.16",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment UI Library.",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -57,6 +57,7 @@
57
57
  "css-variants": "2.3.5",
58
58
  "defu": "6.1.6",
59
59
  "embla-carousel": "8.6.0",
60
+ "picomatch": "^4.0.4",
60
61
  "tailwind-merge": "^3.3.1",
61
62
  "tailwindcss": "^4.2.3",
62
63
  "turndown": "^7.2.4",
@@ -69,6 +70,7 @@
69
70
  "@astrojs/ts-plugin": "1.10.7",
70
71
  "@e18e/eslint-plugin": "0.3.0",
71
72
  "@types/node": "25.5.2",
73
+ "@types/picomatch": "^4.0.3",
72
74
  "@unocss/eslint-plugin": "66.6.8",
73
75
  "astro": "6.1.8",
74
76
  "eslint-plugin-regexp": "3.1.0",
package/src/env.d.ts CHANGED
@@ -38,5 +38,8 @@ declare module "turndown-plugin-gfm" {
38
38
  declare namespace App {
39
39
  interface Locals {
40
40
  cfContext?: Record<string, unknown>
41
+ // Extended locals used by Starlight Addons for richer type safety
42
+ starlightSidebarTopics?: any
43
+ starlightRoute?: any
41
44
  }
42
45
  }
@@ -1,92 +1,92 @@
1
- import type { AstroIntegration } from "astro"
2
- import { createHash } from "node:crypto"
3
-
4
- interface SRIConfig {
5
- security?: {
6
- csp?: {
7
- scriptDirective?: {
8
- resources?: string[]
9
- }
10
- }
11
- }
12
- }
13
-
14
- function assertSRIConfig(obj: unknown): asserts obj is SRIConfig {
15
- if (!obj || typeof obj !== "object") throw new Error("Expected config")
16
- }
17
-
18
- /**
19
- * Astro SRI Integration Fetches external scripts at build-time and generates hashes for the SSR
20
- * middleware.
21
- */
22
- export function sri(): AstroIntegration {
23
- const sriManifest: Record<string, string> = {}
24
-
25
- return {
26
- name: "sri",
27
- hooks: {
28
- "astro:config:setup": async ({ updateConfig, config, logger }) => {
29
- logger.info("Initializing SRI Discovery...")
30
-
31
- // 1. Identify External Scripts from CSP config
32
- // We look into the custom 'security' block in astro.config.mjs
33
- assertSRIConfig(config)
34
-
35
- const security = config.security
36
- const externalUrls: string[] = []
37
-
38
- if (security?.csp?.scriptDirective?.resources) {
39
- for (const resource of security.csp.scriptDirective.resources) {
40
- if (resource.startsWith("https://")) {
41
- externalUrls.push(resource)
42
- }
43
- }
44
- }
45
-
46
- // 2. Fetch and Hash External Scripts
47
- await Promise.all(
48
- externalUrls.map(async (url) => {
49
- try {
50
- logger.info(`Fetching ${url} for SRI hashing...`)
51
- const response = await fetch(url)
52
- if (!response.ok) throw new Error(`HTTP ${response.status}`)
53
-
54
- const buffer = await response.arrayBuffer()
55
- const hash = createHash("sha384").update(Buffer.from(buffer)).digest("base64")
56
-
57
- sriManifest[url] = `sha384-${hash}`
58
- logger.info(`Successfully hashed ${url}`)
59
- } catch (err: unknown) {
60
- const message = err instanceof Error ? err.message : String(err)
61
- logger.error(`Failed to fetch SRI for ${url}: ${message}`)
62
- }
63
- })
64
- )
65
-
66
- // 3. Provide hashes to the runtime via Vite's virtual module
67
- const virtualModuleId = "virtual:sri-manifest"
68
- const resolvedVirtualModuleId = "\0" + virtualModuleId
69
-
70
- updateConfig({
71
- vite: {
72
- plugins: [
73
- {
74
- name: "vite-plugin-sri-manifest",
75
- resolveId(id: string) {
76
- if (id === virtualModuleId) return resolvedVirtualModuleId
77
- return null
78
- },
79
- load(id: string) {
80
- if (id === resolvedVirtualModuleId) {
81
- return `export const manifest = ${JSON.stringify(sriManifest)};`
82
- }
83
- return null
84
- }
85
- }
86
- ]
87
- }
88
- })
89
- }
90
- }
91
- }
92
- }
1
+ import type { AstroIntegration } from "astro"
2
+ import { createHash } from "node:crypto"
3
+
4
+ interface SRIConfig {
5
+ security?: {
6
+ csp?: {
7
+ scriptDirective?: {
8
+ resources?: string[]
9
+ }
10
+ }
11
+ }
12
+ }
13
+
14
+ function assertSRIConfig(obj: unknown): asserts obj is SRIConfig {
15
+ if (!obj || typeof obj !== "object") throw new Error("Expected config")
16
+ }
17
+
18
+ /**
19
+ * Astro SRI Integration Fetches external scripts at build-time and generates hashes for the SSR
20
+ * middleware.
21
+ */
22
+ export function sri(): AstroIntegration {
23
+ const sriManifest: Record<string, string> = {}
24
+
25
+ return {
26
+ name: "sri",
27
+ hooks: {
28
+ "astro:config:setup": async ({ updateConfig, config, logger }) => {
29
+ logger.info("Initializing SRI Discovery...")
30
+
31
+ // 1. Identify External Scripts from CSP config
32
+ // We look into the custom 'security' block in astro.config.mjs
33
+ assertSRIConfig(config)
34
+
35
+ const security = config.security
36
+ const externalUrls: string[] = []
37
+
38
+ if (security?.csp?.scriptDirective?.resources) {
39
+ for (const resource of security.csp.scriptDirective.resources) {
40
+ if (resource.startsWith("https://")) {
41
+ externalUrls.push(resource)
42
+ }
43
+ }
44
+ }
45
+
46
+ // 2. Fetch and Hash External Scripts
47
+ await Promise.all(
48
+ externalUrls.map(async (url) => {
49
+ try {
50
+ logger.info(`Fetching ${url} for SRI hashing...`)
51
+ const response = await fetch(url)
52
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
53
+
54
+ const buffer = await response.arrayBuffer()
55
+ const hash = createHash("sha384").update(Buffer.from(buffer)).digest("base64")
56
+
57
+ sriManifest[url] = `sha384-${hash}`
58
+ logger.info(`Successfully hashed ${url}`)
59
+ } catch (err: unknown) {
60
+ const message = err instanceof Error ? err.message : String(err)
61
+ logger.error(`Failed to fetch SRI for ${url}: ${message}`)
62
+ }
63
+ })
64
+ )
65
+
66
+ // 3. Provide hashes to the runtime via Vite's virtual module
67
+ const virtualModuleId = "virtual:sri-manifest"
68
+ const resolvedVirtualModuleId = "\0" + virtualModuleId
69
+
70
+ updateConfig({
71
+ vite: {
72
+ plugins: [
73
+ {
74
+ name: "vite-plugin-sri-manifest",
75
+ resolveId(id: string) {
76
+ if (id === virtualModuleId) return resolvedVirtualModuleId
77
+ return null
78
+ },
79
+ load(id: string) {
80
+ if (id === resolvedVirtualModuleId) {
81
+ return `export const manifest = ${JSON.stringify(sriManifest)};`
82
+ }
83
+ return null
84
+ }
85
+ }
86
+ ]
87
+ }
88
+ })
89
+ }
90
+ }
91
+ }
92
+ }
@@ -1,6 +1,12 @@
1
1
  import { defineMiddleware } from "astro:middleware"
2
2
 
3
- export const security = defineMiddleware(async (_, next) => {
3
+ export const security = defineMiddleware(async (context, next) => {
4
+ // HTTP to HTTPS redirect
5
+ if (context.request.url.startsWith("http://")) {
6
+ const httpsUrl = context.request.url.replace(/^http:/, "https:")
7
+ return Response.redirect(httpsUrl, 301)
8
+ }
9
+
4
10
  const response = await next()
5
11
 
6
12
  const newResponse = new Response(response.body, response)
@@ -10,6 +16,7 @@ export const security = defineMiddleware(async (_, next) => {
10
16
  newResponse.headers.set("X-Content-Type-Options", "nosniff")
11
17
  newResponse.headers.set("X-Frame-Options", "DENY")
12
18
  newResponse.headers.set("Cross-Origin-Resource-Policy", "same-origin")
19
+ newResponse.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
13
20
 
14
21
  return newResponse
15
- })
22
+ })
@@ -1,78 +1,78 @@
1
- import { defineMiddleware } from "astro:middleware"
2
- // @ts-ignore - virtual module generated by the SRI integration
3
- import { manifest } from "virtual:sri-manifest"
4
-
5
- declare const HTMLRewriter: any
6
-
7
- function assertManifest(obj: unknown): asserts obj is Record<string, string> {
8
- if (!obj || typeof obj !== "object") throw new Error("Expected manifest")
9
- }
10
-
11
- /**
12
- * SRI Middleware Injects integrity attributes into script tags based on the pre-generated manifest.
13
- * Handles both SSR streaming (via HTMLRewriter) and post-rendering fallback.
14
- */
15
- export const sri = defineMiddleware(async (_, next) => {
16
- const response = await next()
17
- const contentType = response.headers.get("content-type") || ""
18
-
19
- // Only process HTML responses
20
- if (!contentType.includes("text/html")) {
21
- return response
22
- }
23
-
24
- // Use Cloudflare's HTMLRewriter if available (SSR Production/Preview)
25
- if (typeof HTMLRewriter !== "undefined") {
26
- return new HTMLRewriter()
27
- .on("script[src]", {
28
- element(el: any) {
29
- const src = el.getAttribute("src")
30
- if (src && manifest[src]) {
31
- el.setAttribute("integrity", manifest[src])
32
- el.setAttribute("crossorigin", "anonymous")
33
- }
34
- }
35
- })
36
- .on('link[rel="stylesheet"][href]', {
37
- element(el: any) {
38
- const href = el.getAttribute("href")
39
- if (href && manifest[href]) {
40
- el.setAttribute("integrity", manifest[href])
41
- el.setAttribute("crossorigin", "anonymous")
42
- }
43
- }
44
- })
45
- .transform(response)
46
- }
47
-
48
- // Fallback for Dev
49
- let modifiedHtml = await response.text()
50
-
51
- // Simple RegEx replacement for Dev inspection
52
- assertManifest(manifest)
53
-
54
- const entries = Object.entries(manifest)
55
- for (const [url, hash] of entries) {
56
- if (!hash) continue
57
-
58
- // Look for script tags with the specific URL
59
- const scriptRegex = new RegExp(`(<script[^>]+src=["']${url}["'][^>]*)(/?>)`, "g")
60
- modifiedHtml = modifiedHtml.replace(
61
- scriptRegex,
62
- `$1 integrity="${hash}" crossorigin="anonymous"$2`
63
- )
64
-
65
- // Look for link tags with the specific URL
66
- const linkRegex = new RegExp(`(<link[^>]+href=["']${url}["'][^>]*)(/?>)`, "g")
67
- modifiedHtml = modifiedHtml.replace(
68
- linkRegex,
69
- `$1 integrity="${hash}" crossorigin="anonymous"$2`
70
- )
71
- }
72
-
73
- return new Response(modifiedHtml, {
74
- status: response.status,
75
- statusText: response.statusText,
76
- headers: response.headers
77
- })
78
- })
1
+ import { defineMiddleware } from "astro:middleware"
2
+ // @ts-ignore - virtual module generated by the SRI integration
3
+ import { manifest } from "virtual:sri-manifest"
4
+
5
+ declare const HTMLRewriter: any
6
+
7
+ function assertManifest(obj: unknown): asserts obj is Record<string, string> {
8
+ if (!obj || typeof obj !== "object") throw new Error("Expected manifest")
9
+ }
10
+
11
+ /**
12
+ * SRI Middleware Injects integrity attributes into script tags based on the pre-generated manifest.
13
+ * Handles both SSR streaming (via HTMLRewriter) and post-rendering fallback.
14
+ */
15
+ export const sri = defineMiddleware(async (_, next) => {
16
+ const response = await next()
17
+ const contentType = response.headers.get("content-type") || ""
18
+
19
+ // Only process HTML responses
20
+ if (!contentType.includes("text/html")) {
21
+ return response
22
+ }
23
+
24
+ // Use Cloudflare's HTMLRewriter if available (SSR Production/Preview)
25
+ if (typeof HTMLRewriter !== "undefined") {
26
+ return new HTMLRewriter()
27
+ .on("script[src]", {
28
+ element(el: any) {
29
+ const src = el.getAttribute("src")
30
+ if (src && manifest[src]) {
31
+ el.setAttribute("integrity", manifest[src])
32
+ el.setAttribute("crossorigin", "anonymous")
33
+ }
34
+ }
35
+ })
36
+ .on('link[rel="stylesheet"][href]', {
37
+ element(el: any) {
38
+ const href = el.getAttribute("href")
39
+ if (href && manifest[href]) {
40
+ el.setAttribute("integrity", manifest[href])
41
+ el.setAttribute("crossorigin", "anonymous")
42
+ }
43
+ }
44
+ })
45
+ .transform(response)
46
+ }
47
+
48
+ // Fallback for Dev
49
+ let modifiedHtml = await response.text()
50
+
51
+ // Simple RegEx replacement for Dev inspection
52
+ assertManifest(manifest)
53
+
54
+ const entries = Object.entries(manifest)
55
+ for (const [url, hash] of entries) {
56
+ if (!hash) continue
57
+
58
+ // Look for script tags with the specific URL
59
+ const scriptRegex = new RegExp(`(<script[^>]+src=["']${url}["'][^>]*)(/?>)`, "g")
60
+ modifiedHtml = modifiedHtml.replace(
61
+ scriptRegex,
62
+ `$1 integrity="${hash}" crossorigin="anonymous"$2`
63
+ )
64
+
65
+ // Look for link tags with the specific URL
66
+ const linkRegex = new RegExp(`(<link[^>]+href=["']${url}["'][^>]*)(/?>)`, "g")
67
+ modifiedHtml = modifiedHtml.replace(
68
+ linkRegex,
69
+ `$1 integrity="${hash}" crossorigin="anonymous"$2`
70
+ )
71
+ }
72
+
73
+ return new Response(modifiedHtml, {
74
+ status: response.status,
75
+ statusText: response.statusText,
76
+ headers: response.headers
77
+ })
78
+ })
@@ -0,0 +1,89 @@
1
+ ---
2
+ import { Badge, Icon } from '@astrojs/starlight/components'
3
+
4
+ const { hasSidebar } = Astro.locals.starlightRoute
5
+ const { isPageWithTopic, topics } = Astro.locals.starlightSidebarTopics
6
+ ---
7
+
8
+ {hasSidebar && isPageWithTopic && (
9
+ <ul class="starlight-sidebar-topics">
10
+ {topics.map((topic, index) => (
11
+ <li key={index}>
12
+ <a href={topic.link} class:list={{ 'starlight-sidebar-topics-current': topic.isCurrent }}>
13
+ {topic.icon && (
14
+ <div class="starlight-sidebar-topics-icon">
15
+ <Icon name={topic.icon} />
16
+ </div>
17
+ )}
18
+ <div>
19
+ {topic.label}
20
+ {topic.badge && (
21
+ <Badge class="starlight-sidebar-topics-badge" text={topic.badge.text} variant={topic.badge.variant} />
22
+ )}
23
+ </div>
24
+ </a>
25
+ </li>
26
+ ))}
27
+ </ul>
28
+ )}
29
+ <style>
30
+ ul {
31
+ list-style: none;
32
+ padding: 0;
33
+ }
34
+
35
+ ul::after {
36
+ content: '';
37
+ display: block;
38
+ margin-top: 1rem;
39
+ height: 1px;
40
+ border-top: 1px solid var(--sl-color-hairline-light);
41
+ }
42
+
43
+ li {
44
+ overflow-wrap: anywhere;
45
+ }
46
+
47
+ li + li {
48
+ margin-top: 0.25rem;
49
+ }
50
+
51
+ a {
52
+ align-items: center;
53
+ color: var(--sl-color-white);
54
+ display: flex;
55
+ font-size: var(--sl-text-base);
56
+ font-weight: 600;
57
+ gap: 0.5rem;
58
+ line-height: 1.5;
59
+ padding: 0.3em 0.5rem;
60
+ text-decoration: none;
61
+ }
62
+
63
+ a:is(.starlight-sidebar-topics-current, :hover, :focus-visible) {
64
+ color: var(--sl-color-accent-high);
65
+ }
66
+
67
+ :global([data-theme='light']) a.starlight-sidebar-topics-current {
68
+ color: var(--sl-color-accent);
69
+ }
70
+
71
+ .starlight-sidebar-topics-icon {
72
+ align-items: center;
73
+ border-radius: 0.25rem;
74
+ border: 1px solid var(--sl-color-gray-4);
75
+ display: flex;
76
+ justify-content: center;
77
+ padding: 0.25rem;
78
+ }
79
+
80
+ a:is(.starlight-sidebar-topics-current, :hover, :focus-visible) .starlight-sidebar-topics-icon {
81
+ background-color: var(--sl-color-text-accent);
82
+ border-color: var(--sl-color-text-accent);
83
+ color: var(--sl-color-text-invert);
84
+ }
85
+
86
+ .starlight-sidebar-topics-badge {
87
+ margin-inline-start: 0.25em;
88
+ }
89
+ </style>
@@ -0,0 +1,39 @@
1
+ import type { StarlightIcon } from "@astrojs/starlight/types"
2
+
3
+ import type { SidebarTopicBadge } from "./libs/config"
4
+
5
+ export interface StarlightSidebarTopicsRouteData {
6
+ /**
7
+ * Indicates if the current page is associated with a topic or not.
8
+ */
9
+ isPageWithTopic: boolean
10
+ /**
11
+ * A list of all configured topics.
12
+ */
13
+ topics: {
14
+ /**
15
+ * The optional badge associated with the topic.
16
+ */
17
+ badge?: {
18
+ text: string
19
+ variant: SidebarTopicBadge["variant"]
20
+ }
21
+ /**
22
+ * The name of an optional icon associated with the topic set to one of Starlight’s built-in
23
+ * icons.
24
+ */
25
+ icon?: StarlightIcon
26
+ /**
27
+ * Indicates if the current page is part of the topic.
28
+ */
29
+ isCurrent: boolean
30
+ /**
31
+ * The label of the topic.
32
+ */
33
+ label: string
34
+ /**
35
+ * The link to the topic’s content.
36
+ */
37
+ link: string
38
+ }[]
39
+ }