diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..c79e679 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# Custom Domains affect DNS on deploy. A lead reviews this surface and its ownership. +/apps/website/wrangler.jsonc @dorelljames +/.github/CODEOWNERS @dorelljames diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d5292f..c2e11b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,10 +9,10 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 # The pnpm version comes from "packageManager" in package.json. - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: pnpm/action-setup@v6.1.0 + - uses: actions/setup-node@v7.1.0 with: node-version: 24 cache: pnpm diff --git a/apps/website/package.json b/apps/website/package.json index e052f09..dccc33d 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -8,11 +8,15 @@ "dev": "pnpm sync && astro dev", "build": "pnpm sync && astro build && tsx scripts/check-dist.ts", "preview": "astro preview", - "test": "vitest run" + "test": "pnpm check && vitest run", + "check": "astro sync && astro check && tsc --noEmit" }, "devDependencies": { + "@astrojs/check": "^0.9.12", + "jsonc-parser": "^3.3.1", "linkedom": "~0.18.14", "tsx": "^4.23.15", + "typescript": "^6.0.3", "vitest": "^5.0.5", "wrangler": "^4.153.0", "yaml": "^2.9.1" diff --git a/apps/website/public/_headers b/apps/website/public/_headers index ae3577d..8fc3d0d 100644 --- a/apps/website/public/_headers +++ b/apps/website/public/_headers @@ -6,3 +6,5 @@ Access-Control-Allow-Origin: * /brand/* Access-Control-Allow-Origin: * +/brand/tokens.css + Content-Type: text/css; charset=utf-8 diff --git a/apps/website/public/_redirects b/apps/website/public/_redirects index c3013be..fc2c783 100644 --- a/apps/website/public/_redirects +++ b/apps/website/public/_redirects @@ -1,13 +1,23 @@ # Old Notion-era jscebu.org URLs (spec: docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md §5). # Targets end in / because folders are served at /name/; a bare /name would redirect twice. /mission-vision /about/ 301 +/mission-vision/ /about/ 301 /members /about/#organizers 301 +/members/ /about/#organizers 301 /iniatives /about/ 301 +/iniatives/ /about/ 301 /initiatives /about/ 301 +/initiatives/ /about/ 301 /official-links /links/ 301 +/official-links/ /links/ 301 /community-projects /projects/ 301 +/community-projects/ /projects/ 301 # Temporary: /events becomes a real page once events come from Cebby. /events /#events 302 +/events/ /#events 302 /promotional-materials /brand/ 301 +/promotional-materials/ /brand/ 301 /annual-hacktoberfest https://hacktoberfest.jscebu.org/ 301 +/annual-hacktoberfest/ https://hacktoberfest.jscebu.org/ 301 /volunteers-training-workshop /about/ 301 +/volunteers-training-workshop/ /about/ 301 diff --git a/apps/website/scripts/check-dist.ts b/apps/website/scripts/check-dist.ts index 8f4d0a0..27f9df2 100644 --- a/apps/website/scripts/check-dist.ts +++ b/apps/website/scripts/check-dist.ts @@ -9,8 +9,8 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import { join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { brand, lintHtml, type Brand } from '@jscebu/brand'; -import { parseHTML } from 'linkedom'; +import { brand, lintHtml, findColorLiteral, type Brand } from '@jscebu/brand'; +import { DOMParser, parseHTML } from 'linkedom'; const SITE = 'https://www.jscebu.org'; @@ -35,12 +35,67 @@ export function findMissingLinks(text: string, fileExists: (path: string) => boo ...[...text.matchAll(/https:\/\/www\.jscebu\.org\/[^\s"'<>)\]|`]*/g)].map((m) => m[0].replace(/[.,;:]+$/, '')), ]; const paths = urls - .map((u) => (u.startsWith(SITE) ? u.slice(SITE.length) || '/' : u)) + .map((u) => { + if (!u.startsWith('http')) return u; + try { const url = new URL(u); return url.origin === SITE ? url.pathname + url.search + url.hash : ''; } + catch { return ''; } + }) .filter((u) => u.startsWith('/') && !u.startsWith('//')) .map((u) => u.replace(/[?#].*$/, '')); return [...new Set(paths)].filter((p) => !fileExists(p)); } +/** Resolve canonical public URLs to physical files, without accepting redirect hops. */ +export function resolveBuiltPath(urlPath: string, fileExists: (path: string) => boolean): string | undefined { + let path: string; + try { path = decodeURIComponent(urlPath); } catch { return undefined; } + if (!path.startsWith('/') || path.startsWith('//') || /[\\?#\0]/.test(path) || path.split('/').some((part) => part === '.' || part === '..')) return undefined; + if (/\.html?$/i.test(path)) return undefined; + const file = path.endsWith('/') ? `${path.slice(1)}index.html` : path.slice(1); + if (fileExists(file)) return file; + if (!path.endsWith('/') && !path.split('/').pop()!.includes('.') && fileExists(`${file}.html`)) return `${file}.html`; + return undefined; +} + +export function findRedirectProblems(redirects: { from: string; to: string; status: number }[], readUrl: (path: string) => string | undefined): string[] { + return redirects.flatMap(({ from, to }) => { + const error = (reason: string) => [`_redirects sends ${from} to ${to}: ${reason}`]; + let url: URL; + try { url = new URL(to, SITE); } catch { return error('invalid URL'); } + if (url.origin !== SITE) return []; + const html = readUrl(url.pathname); + if (html === undefined) return error('destination was not built at a canonical URL'); + if (!url.hash) return []; + let id: string; + try { id = decodeURIComponent(url.hash.slice(1)); } catch { return error('malformed fragment encoding'); } + return parseHTML(html).document.getElementById(id) ? [] : error(`missing fragment #${id}`); + }); +} + +export function findSitemapProblems(xml: string, fileExists: (path: string) => boolean): string[] { + const doc = new DOMParser().parseFromString(xml, 'text/xml'); + const entries = [...doc.querySelectorAll('url > loc')]; + if (!entries.length) return ['sitemap.xml has no page URLs']; + return entries.flatMap((el) => { + const text = el.textContent?.trim() ?? ''; + try { + const url = new URL(text); + if (url.origin === SITE && !url.search && !url.hash && fileExists(url.pathname)) return []; + } catch { /* Report invalid and noncanonical entries alike. */ } + return [`sitemap.xml links to ${text}, which is not a canonical built URL on ${SITE}`]; + }); +} + +export function findTemplateLinkProblems(html: string): string[] { + const { document } = parseHTML(html); + return ['event-post', 'fb-cover'].flatMap((name) => { + const path = `/brand/templates/${name}`; + const link = document.querySelector(`#downloads a[href="${path}"]`); + if (!link) return [`/brand/ downloads is missing ${path}`]; + return link.hasAttribute('download') ? [`/brand/ ${path} must open without a download attribute`] : []; + }); +} + const normalize = (s: string) => s.replaceAll('`', '').replace(/\s+/g, ' ').trim(); export function findParityGaps(pageText: string, md: string, rules: string[]): string[] { @@ -55,15 +110,13 @@ export function findParityGaps(pageText: string, md: string, rules: string[]): s }); } -const HEX = /(? text.split('\n').flatMap((line, i) => { - const hit = line.match(HEX) ?? line.match(COLOR_FN) ?? line.match(FONT_FAMILY); - return hit ? [`${path}:${i + 1}: "${hit[0].trim()}" (use a --jc-* variable)`] : []; + const hit = findColorLiteral(line) ?? line.match(FONT_FAMILY)?.[0]; + return hit ? [`${path}:${i + 1}: "${hit.trim()}" (use a --jc-* variable)`] : []; }), ); } @@ -105,17 +158,17 @@ function main(): void { const app = fileURLToPath(new URL('..', import.meta.url)); const dist = join(app, 'dist'); const read = (p: string) => readFileSync(join(dist, p), 'utf8'); - /** A URL path is built when it names a file, or ends in / over a folder with index.html. A bare folder path would cost a redirect. */ - const exists = (urlPath: string) => { - const p = join(dist, decodeURIComponent(urlPath)); - if (urlPath.endsWith('/')) return existsSync(join(p, 'index.html')); - return existsSync(p) && statSync(p).isFile(); + const resolve = (path: string) => resolveBuiltPath(path, (p) => existsSync(join(dist, p)) && statSync(join(dist, p)).isFile()); + const exists = (path: string) => resolve(path) !== undefined; + const readUrl = (path: string) => { + const file = resolve(path); + return file === undefined ? undefined : read(file); }; const required = [ 'index.html', 'projects/index.html', 'about/index.html', 'links/index.html', '404.html', 'brand/index.html', 'brand.md', 'brand/tokens.css', 'llms.txt', 'sitemap.xml', 'robots.txt', '_redirects', - 'brand/logo/sticker.svg', 'brand/templates/event-post.html', + 'brand/logo/sticker.svg', 'brand/templates/event-post.html', 'brand/templates/fb-cover.html', ]; const pages = walk(dist).filter((p) => p.endsWith('.html') && !p.startsWith(join(dist, 'brand', 'templates'))).map((p) => relative(dist, p)); const brandHtml = read('brand/index.html'); @@ -136,9 +189,9 @@ function main(): void { }), ...findMissingLinks(md, exists).map((p) => `brand.md links to ${p}, which was not built`), ...findMissingLinks(read('llms.txt'), exists).map((p) => `llms.txt links to ${p}, which was not built`), - ...redirects - .filter((r) => r.to.startsWith('/') && !exists(r.to.replace(/#.*$/, ''))) - .map((r) => `_redirects sends ${r.from} to ${r.to}, which was not built`), + ...findRedirectProblems(redirects, readUrl), + ...findSitemapProblems(read('sitemap.xml'), exists), + ...findTemplateLinkProblems(brandHtml), ...findParityGaps(parseHTML(brandHtml).document.body.textContent ?? '', md, parityRules(brand)), ...findLiterals(src), ]; diff --git a/apps/website/scripts/deployment-scope.ts b/apps/website/scripts/deployment-scope.ts new file mode 100644 index 0000000..099c1d4 --- /dev/null +++ b/apps/website/scripts/deployment-scope.ts @@ -0,0 +1,17 @@ +import { deepStrictEqual } from 'node:assert/strict'; + +/** A testable guard against expanding the Custom Domain/DNS surface. */ +export function assertDeploymentScope(config: unknown): void { + if (!config || typeof config !== 'object' || !('routes' in config)) throw new Error('Missing routes'); + deepStrictEqual(config.routes, [{ pattern: 'www.jscebu.org', custom_domain: true }]); + const visit = (value: unknown, path: string): void => { + if (!value || typeof value !== 'object') return; + for (const [key, child] of Object.entries(value)) { + if ((key === 'route' || key === 'routes') && !(path === '' && key === 'routes')) { + throw new Error(`Unexpected deployment override: ${path}${key}`); + } + visit(child, `${path}${key}.`); + } + }; + visit(config, ''); +} diff --git a/apps/website/scripts/url-matrix.sh b/apps/website/scripts/url-matrix.sh index 21f7e09..b87fe33 100755 --- a/apps/website/scripts/url-matrix.sh +++ b/apps/website/scripts/url-matrix.sh @@ -30,26 +30,39 @@ page() { # path [content-type prefix] fetch "$1" local type type=$(printf '%s\n' "$headers" | grep -i '^content-type:' | head -1 | sed -E 's/^[Cc]ontent-[Tt]ype: *//') - if [ "$status" = 200 ] && [[ "$type" == ${2:-}* ]]; then report ok "$1 200 ${type}" + if [ "$status" = 200 ] && [ -z "$location" ] && [[ "$type" == ${2:-}* ]]; then report ok "$1 200 ${type}" else report fail "$1 200 ${2:-}" "got $status $type"; fi } redirect /mission-vision /about/ 301 +redirect /mission-vision/ /about/ 301 redirect /members /about/#organizers 301 +redirect /members/ /about/#organizers 301 redirect /iniatives /about/ 301 +redirect /iniatives/ /about/ 301 redirect /initiatives /about/ 301 +redirect /initiatives/ /about/ 301 redirect /official-links /links/ 301 +redirect /official-links/ /links/ 301 redirect /community-projects /projects/ 301 +redirect /community-projects/ /projects/ 301 redirect /events /#events 302 +redirect /events/ /#events 302 redirect /promotional-materials /brand/ 301 +redirect /promotional-materials/ /brand/ 301 redirect /annual-hacktoberfest https://hacktoberfest.jscebu.org/ 301 +redirect /annual-hacktoberfest/ https://hacktoberfest.jscebu.org/ 301 redirect /volunteers-training-workshop /about/ 301 +redirect /volunteers-training-workshop/ /about/ 301 for p in / /projects/ /about/ /links/ /brand/; do page "$p" text/html; done page /brand.md 'text/markdown; charset=utf-8' if printf '%s\n' "$headers" | grep -qi '^access-control-allow-origin: \*'; then report ok '/brand.md CORS *'; else report fail '/brand.md CORS *' 'header missing'; fi page /llms.txt 'text/plain; charset=utf-8' -page /brand/tokens.css text/css +page /brand/tokens.css 'text/css; charset=utf-8' +if printf '%s\n' "$headers" | grep -qi '^access-control-allow-origin: \*'; then report ok '/brand/tokens.css CORS *'; else report fail '/brand/tokens.css CORS *' 'header missing'; fi +page /brand/templates/event-post text/html +page /brand/templates/fb-cover text/html page /brand/logo/sticker.svg image/svg+xml page /sitemap.xml application/xml page /robots.txt text/plain diff --git a/apps/website/src/content.config.ts b/apps/website/src/content.config.ts index 3801b7c..93ffb6f 100644 --- a/apps/website/src/content.config.ts +++ b/apps/website/src/content.config.ts @@ -3,8 +3,8 @@ import { file, glob } from 'astro/loaders'; import { linkSchema, organizerSchema, pageSchema, projectSchema } from './content/schemas'; export const collections = { - projects: defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/projects' }), schema: projectSchema }), - organizers: defineCollection({ loader: file('src/content/organizers.json'), schema: organizerSchema }), + projects: defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/projects' }), schema: (context) => projectSchema(context) }), + organizers: defineCollection({ loader: file('src/content/organizers.json'), schema: (context) => organizerSchema(context) }), links: defineCollection({ loader: file('src/content/links.json'), schema: linkSchema }), pages: defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/pages' }), schema: pageSchema }), }; diff --git a/apps/website/src/content/schemas.ts b/apps/website/src/content/schemas.ts index 21dabbc..e6eb4d1 100644 --- a/apps/website/src/content/schemas.ts +++ b/apps/website/src/content/schemas.ts @@ -1,8 +1,7 @@ import { z } from 'astro/zod'; -import type { SchemaContext } from 'astro:content'; /** Takes Astro's schema context so `image()` resolves local images; tests pass a stand-in. */ -export const projectSchema = ({ image }: Pick) => +export const projectSchema = ({ image }: { image: () => T }) => z.object({ name: z.string(), url: z.string().url(), @@ -14,7 +13,7 @@ export const projectSchema = ({ image }: Pick) => order: z.number(), }); -export const organizerSchema = ({ image }: Pick) => +export const organizerSchema = ({ image }: { image: () => T }) => z.object({ id: z.string(), name: z.string(), diff --git a/apps/website/src/pages/brand/index.astro b/apps/website/src/pages/brand/index.astro index 14e9039..cb942e4 100644 --- a/apps/website/src/pages/brand/index.astro +++ b/apps/website/src/pages/brand/index.astro @@ -191,7 +191,7 @@ const sample = 'Aa Bb Cc 0123456789'; { brand.assets.map((a) => (
  • - ↓ {a.path.replace('/brand/', '')} + ↓ {a.path.replace('/brand/', '')} {a.label} {a.use}
  • diff --git a/apps/website/test/check-dist.test.ts b/apps/website/test/check-dist.test.ts index 7586142..75f32a2 100644 --- a/apps/website/test/check-dist.test.ts +++ b/apps/website/test/check-dist.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { brand } from '@jscebu/brand'; -import { findCursorViolations, findLiterals, findMissingLinks, findParityGaps, isSourceText, parityRules } from '../scripts/check-dist.ts'; +import { resolveBuiltPath, findRedirectProblems, findSitemapProblems, findTemplateLinkProblems, findCursorViolations, findLiterals, findMissingLinks, findParityGaps, isSourceText, parityRules } from '../scripts/check-dist.ts'; const cursor = ''; @@ -83,3 +83,46 @@ describe('isSourceText', () => { expect(['g.jpg', 'h.png', 'i.webp', 'j.svg', 'k.woff2'].map(isSourceText)).toEqual([false, false, false, false, false]); }); }); + +describe('canonical built URLs', () => { + const files = new Set(['index.html', 'about/index.html', 'brand/templates/event-post.html', 'brand/tokens.css']); + it.each([ + ['/', 'index.html'], ['/about/', 'about/index.html'], + ['/brand/templates/event-post', 'brand/templates/event-post.html'], ['/brand/tokens.css', 'brand/tokens.css'], + ['/brand/templates/missing', undefined], ['/about', undefined], ['/about/index.html', undefined], + ['/brand/templates/event-post.html', undefined], ['/%ZZ', undefined], ['/../index.html', undefined], + ])('resolves %s without canonicalization hops', (path, expected) => { + expect(resolveBuiltPath(path, (p) => files.has(p))).toBe(expected); + }); +}); + +describe('redirect destinations', () => { + const read = (path: string) => path === '/about/' ? '

    Team

    Café

    ' : undefined; + it.each(['/about/#organizers', '/about/#caf%C3%A9', 'https://www.jscebu.org/about/#organizers', 'https://elsewhere.example/#missing'])('accepts %s', (to) => { + expect(findRedirectProblems([{ from: '/members', to, status: 301 }], read)).toEqual([]); + }); + it.each(['/about/#missing', '/missing/#organizers', '/about/#%ZZ'])('reports source and bad destination %s', (to) => { + const errors = findRedirectProblems([{ from: '/members', to, status: 301 }], read); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain('/members'); + expect(errors[0]).toContain(to); + }); +}); + +describe('sitemap destinations', () => { + const exists = (path: string) => path === '/about/'; + const xml = (url: string) => `${url}`; + it('accepts a canonical same-site page', () => { + expect(findSitemapProblems(xml('https://www.jscebu.org/about/'), exists)).toEqual([]); + }); + it.each(['https://www.jscebu.org/missing/', 'https://www.jscebu.org/members', 'https://www.jscebu.org/about', 'https://elsewhere.example/about/'])('rejects %s', (url) => { + expect(findSitemapProblems(xml(url), exists)).toHaveLength(1); + }); +}); + +describe('brand template navigation', () => { + const html = (attr = '') => `
    PostCover
    `; + it('accepts canonical template links without download', () => expect(findTemplateLinkProblems(html())).toEqual([])); + it('rejects even an empty download attribute', () => expect(findTemplateLinkProblems(html('download=""'))).toHaveLength(1)); + it('requires both links in downloads', () => expect(findTemplateLinkProblems('')).toHaveLength(2)); +}); diff --git a/apps/website/test/content.test.ts b/apps/website/test/content.test.ts index 9c4cf02..6721cd9 100644 --- a/apps/website/test/content.test.ts +++ b/apps/website/test/content.test.ts @@ -36,14 +36,12 @@ describe('site content', () => { it('lists the official channels from /official-links', () => { const links = z.array(linkSchema).parse(JSON.parse(read('links.json'))); - expect(links.map((l) => l.url)).toEqual( - expect.arrayContaining([ - 'https://www.facebook.com/JavaScriptCebu/', - 'https://twitter.com/javascriptcebu', - 'https://github.com/javascriptcebu/', - 'mailto:hello@jscebu.org', - ]), - ); + expect(links.map((l) => l.url)).toEqual([ + 'https://www.facebook.com/JavaScriptCebu/', + 'https://twitter.com/javascriptcebu', + 'https://github.com/javascriptcebu/', + 'mailto:hello@jscebu.org', + ]); }); it('features Cebby as the only project', () => { diff --git a/apps/website/test/deployment-config.test.ts b/apps/website/test/deployment-config.test.ts new file mode 100644 index 0000000..161bfd8 --- /dev/null +++ b/apps/website/test/deployment-config.test.ts @@ -0,0 +1,40 @@ +import { readFileSync } from 'node:fs'; +import { parse, type ParseError } from 'jsonc-parser'; +import { describe, expect, it } from 'vitest'; +import { assertDeploymentScope } from '../scripts/deployment-scope.ts'; + +const configText = readFileSync(new URL('../wrangler.jsonc', import.meta.url), 'utf8'); +const errors: ParseError[] = []; +const config = parse(configText, errors); + +it('pins the real JSONC config to the www Custom Domain', () => { + expect(errors).toEqual([]); + expect(() => assertDeploymentScope(config)).not.toThrow(); +}); + +describe('deployment guard rejects expanded DNS scope', () => { + it.each([ + [{ pattern: 'jscebu.org', custom_domain: true }], + [{ pattern: '*.jscebu.org/*', custom_domain: true }], + [{ pattern: 'www.jscebu.org', custom_domain: false }], + [...config.routes, { pattern: 'extra.jscebu.org', custom_domain: true }], + [], + ].map((routes) => [routes]))('rejects changed route array %j', (routes) => { + expect(() => assertDeploymentScope({ ...config, routes })).toThrow(); + }); + it('rejects top-level singular route', () => { + expect(() => assertDeploymentScope({ ...config, route: 'other.example/*' })).toThrow(); + }); + it.each(['route', 'routes'])('rejects %s in each override location, even empty', (key) => { + for (const value of ['', [], undefined]) { + expect(() => assertDeploymentScope({ ...config, env: { staging: { [key]: value } } })).toThrow(); + expect(() => assertDeploymentScope({ ...config, previews: { [key]: value } })).toThrow(); + } + }); +}); + +it('assigns both deployment configuration and ownership reviews to the approved owner', () => { + const owners = readFileSync(new URL('../../../.github/CODEOWNERS', import.meta.url), 'utf8'); + expect(owners).toContain('/apps/website/wrangler.jsonc @dorelljames'); + expect(owners).toContain('/.github/CODEOWNERS @dorelljames'); +}); diff --git a/apps/website/test/headers.test.ts b/apps/website/test/headers.test.ts new file mode 100644 index 0000000..345ea65 --- /dev/null +++ b/apps/website/test/headers.test.ts @@ -0,0 +1,8 @@ +import { readFileSync } from 'node:fs'; +import { expect, it } from 'vitest'; + +it('declares the tokens charset and preserves wildcard brand CORS', () => { + const headers = readFileSync(new URL('../public/_headers', import.meta.url), 'utf8'); + expect(headers).toMatch(/^\/brand\/tokens\.css\n Content-Type: text\/css; charset=utf-8$/m); + expect(headers).toMatch(/^\/brand\/\*\n Access-Control-Allow-Origin: \*$/m); +}); diff --git a/apps/website/test/redirects.test.ts b/apps/website/test/redirects.test.ts index 27b0f49..79bb5a1 100644 --- a/apps/website/test/redirects.test.ts +++ b/apps/website/test/redirects.test.ts @@ -14,7 +14,7 @@ describe('parseRedirects', () => { describe('public/_redirects', () => { it('sends every old jscebu.org URL where the v1 spec says (§5)', () => { const rules = parseRedirects(readFileSync(new URL('../public/_redirects', import.meta.url), 'utf8')); - expect(rules).toEqual([ + const originals = [ { from: '/mission-vision', to: '/about/', status: 301 }, { from: '/members', to: '/about/#organizers', status: 301 }, { from: '/iniatives', to: '/about/', status: 301 }, @@ -25,6 +25,7 @@ describe('public/_redirects', () => { { from: '/promotional-materials', to: '/brand/', status: 301 }, { from: '/annual-hacktoberfest', to: 'https://hacktoberfest.jscebu.org/', status: 301 }, { from: '/volunteers-training-workshop', to: '/about/', status: 301 }, - ]); + ]; + expect(rules).toEqual(originals.flatMap((r) => [r, { ...r, from: `${r.from}/` }])); }); }); diff --git a/apps/website/test/worker.test.ts b/apps/website/test/worker.test.ts new file mode 100644 index 0000000..687e80f --- /dev/null +++ b/apps/website/test/worker.test.ts @@ -0,0 +1,15 @@ +import { expect, it, vi } from 'vitest'; +import worker from '../worker/index.ts'; + +it('fetches the canonical 404 on the request origin and preserves its body and headers', async () => { + const fetch = vi.fn(async (input: Request | URL | string) => { + expect(String(input)).toBe('https://preview.example/404'); + return new Response('Missing page', { headers: { 'Content-Type': 'text/html; charset=utf-8', 'X-Test': 'kept' } }); + }); + const response = await worker.fetch(new Request('https://preview.example/missing'), { ASSETS: { fetch } }); + expect(fetch).toHaveBeenCalledOnce(); + expect(response.status).toBe(404); + expect(await response.text()).toBe('Missing page'); + expect(response.headers.get('x-test')).toBe('kept'); + expect(response.headers.get('content-type')).toBe('text/html; charset=utf-8'); +}); diff --git a/apps/website/worker/index.ts b/apps/website/worker/index.ts index 12f7a61..f74dc7c 100644 --- a/apps/website/worker/index.ts +++ b/apps/website/worker/index.ts @@ -5,7 +5,7 @@ interface Env { export default { async fetch(request: Request, env: Env): Promise { - const page = await env.ASSETS.fetch(new URL('/404.html', request.url)); + const page = await env.ASSETS.fetch(new URL('/404', request.url)); return new Response(page.body, { status: 404, headers: page.headers }); }, }; diff --git a/docs/superpowers/plans/2026-10-30-v1-review-fixes.md b/docs/superpowers/plans/2026-10-30-v1-review-fixes.md new file mode 100644 index 0000000..a2ec30d --- /dev/null +++ b/docs/superpowers/plans/2026-10-30-v1-review-fixes.md @@ -0,0 +1,165 @@ +# v1 review fixes: implementation plan + +## Approved decisions + +| ID | Decision | Options and recommendation | Lead answer and source | +|---|---|---|---| +| Q1 | Should the official channels include Website? | Recommend keeping the current four channels and correcting the v1 spec, since Website links back to the site displaying the list. Alternatively add `https://www.jscebu.org/` with a lead-selected label/order; this also changes the home Join section. | Approved 1A: retain four channels; MEMBER dorelljames, [issue comment](https://github.com/javascriptcebu/jscebu.org/issues/9#issuecomment-6525522776). | +| Q2 | Which GitHub user or team owns deployment configuration review? | Supply an explicit eligible owner for `/apps/website/wrangler.jsonc` and `/.github/CODEOWNERS`. Do not infer a team slug or assign an individual from authorship alone. | Approved 2A: `@dorelljames` for both files; same lead comment. | + +**Approval:** The lead answered `1A 2A` on 2026-10-30 and returned the card to Todo. This approves implementation, including the planned `0.1.1` brand patch release and changelog. Resolve canonical URLs before changing metadata to keep regression signals separate. + +**Goal:** Resolve all nine GH-9 review findings with regression coverage and no changes to the DNS surface. + +**Architecture:** Keep the existing static Astro site, asset-first Worker, brand source of truth and CI structure. Share URL resolution inside the website checker and color detection through the brand package; strengthen checks around existing outputs and configuration. + +**Tech stack:** Astro, TypeScript, Vitest, linkedom, pnpm and local Workers assets. Select compatible Astro checking and JSONC parsing dependencies during build; verify CI action runtimes from official manifests. + +**Spec:** [v1 review fixes: design](../specs/2026-10-30-v1-review-fixes-design.md). + +**Execution:** Perform these tasks directly. Use the named Superpowers workflows if available; they and subagents are not prerequisites under the session instructions. Write failing tests before implementation where code can be tested. Do not copy old plans' deploy, DNS, commit or push commands; the current session prohibits those actions. + +## Initial evidence and constraints + +- Clean sync at `1ea2203`; branch `symphony/gh-9`; no branch PR found at plan start. +- The previous workpad's missing-skills blocker is superseded by explicit session permission to perform the planning steps directly. No earlier plan exists to approve. +- Reproduced: `pnpm --filter website exec tsc --noEmit` exits 1 with five errors in `test/content.test.ts` (TS2740 at 13/23/33/50, TS2345 at 34). +- Local checker probe: zero of ten redirect sources ends in `/`; `/about/#missing-anchor` produces no missing-link report when `/about/` exists. +- Source inspection confirms `/404.html`, `.html` template metadata, absent CSS charset rule, missing sitemap checks, duplicate color regexes, omitted Website content and all three CI actions at v4. +- GitHub's official latest-release endpoints returned checkout `v7.0.1`, setup-node `v7.1.0` and pnpm/action-setup `v6.1.0` on 2026-10-30. These are discovery results, not approved pins or runtime verification; inspect manifests and migration notes before selecting compatible versions. +- Never edit `routes` in `apps/website/wrangler.jsonc`; add tests and ownership only. No DNS, deployment, repository settings or branch protection changes. +- Brand metadata changes require a version bump plus changelog: propose patch `0.1.1`, with matching `packages/brand/package.json` and brand data. No design values or Bisaya approvals change. + +## File map + +| Area | Files to modify or add after approval | +|---|---| +| Fallback | `apps/website/worker/index.ts`, new `apps/website/test/worker.test.ts` | +| Brand URLs and release | `packages/brand/src/brand.ts`, `packages/brand/package.json`, `packages/brand/test/generate.test.ts`, lockfile metadata if required | +| Brand page template behavior | `apps/website/src/pages/brand/index.astro`; built-page assertions in `apps/website/scripts/check-dist.ts` and `apps/website/test/check-dist.test.ts` | +| Shared color detection | new `packages/brand/src/color-literals.ts`, `src/index.ts`, `src/lint.ts`, brand tests | +| Built-output validation | `apps/website/scripts/check-dist.ts`, `apps/website/test/check-dist.test.ts` | +| Redirects and HTTP checks | `apps/website/public/_redirects`, `test/redirects.test.ts`, `scripts/url-matrix.sh` | +| Headers | `apps/website/public/_headers`, new focused header test | +| Type checks | `apps/website/package.json`, `src/content/schemas.ts`, `src/content.config.ts`, `test/content.test.ts`, any real Astro diagnostics exposed, `pnpm-lock.yaml` | +| Channel decision | v1 spec; `apps/website/src/content/links.json` and its content test only if Q1 selects Website | +| CI and ownership guard | `.github/workflows/ci.yml`, new `.github/CODEOWNERS`, new `apps/website/test/deployment-config.test.ts`, new `apps/website/scripts/deployment-scope.ts`, parser dependency only if needed | + +`src/...` and `test/...` entries without a package prefix in a row refer to that row's package. Keep physical template filenames and generated outputs unchanged. + +### Task 1: Finish planning and record approval + +- [x] Read sync, AGENTS.md, GitHub skill, issue/workpad, card and PR status. +- [x] Move Todo to In Progress; establish that this bundle requires planning. +- [x] Inspect source and existing spec/plan conventions; reproduce type-check and checker gaps. +- [x] Write this plan and spec with lead-only questions at the top. +- [x] Run `pnpm test` and `pnpm build`; review all changed and untracked files. +- [x] Prepare ignored host handoff files using `Plan: ` and `Refs #9`; write commit message last among local handoff writes. +Plan handoff completion is recorded in the single workpad as `Phase: plan, awaiting approval`; moving this card to Human Review is the final action. +- [x] On a later approved run, collect trusted issue and PR feedback, record Q1–Q2 answers with sources and update these documents before building. + +### Task 2: Fix the fallback and canonical template URLs + +- [x] Add a Worker handler test requiring ASSETS to receive `/404` on the request origin; assert response status 404, body and preserved headers. Run it and observe the old path fail. +- [x] Change only the fallback request path; retain the entry point and asset-first configuration. +- [x] Add explicit brand-generation assertions for both extensionless template URLs and absence of their `.html` published URLs. Run before changing the metadata to observe failure. +- [x] Update the two brand asset paths; bump brand/package versions to `0.1.1` and add a changelog entry. Keep physical `.html` templates and source design values intact. +- [x] Add a built-page checker assertion and focused fixtures requiring both canonical template links in `/brand/`'s `#downloads` section to exist without a `download` attribute. Prove a fixture with `download=""` fails and a link without the attribute passes. Wire the assertion into `check-dist`'s `main()` so the real rendered page is checked by `pnpm build`. After changing the metadata, run the build before fixing the page and observe the existing `.html` suffix condition fail this check. +- [x] In `apps/website/src/pages/brand/index.astro`, replace the `a.path.endsWith('.html')` download decision with the stable `/brand/templates/` prefix (or equivalent explicit template identity). Template links must open their HTML; retain `download` for the other downloadable assets. Verify both canonical hrefs and absent `download` attributes in the rendered output, and confirm the cover iframe and its adjacent link use the canonical cover URL. Rerun focused checker tests and the build. +- [x] Add resolver tests for extensionless template success, missing template failure, direct assets, slash-ended page folders, rejection of bare folders and `.html` canonicalization hops. +- [x] Implement a pure/testable resolver in the website checker and use it wherever URLs are validated. Keep physical required-file checks distinct. Run focused brand and checker tests, then build. + +### Task 3: Cover trailing-slash redirects and CSS headers + +- [x] Expand the redirect test to pin all twenty rows: each original source plus its slash variant, with identical target/status. Observe failure with the current ten-row file. +- [x] Add the ten slash rows without broad wildcard rules or destination changes. +- [x] Add a focused header-policy test requiring the exact CSS charset rule while retaining wildcard CORS; observe failure, then add the rule. +- [x] Extend the URL matrix to exercise all twenty redirects, extensionless templates returning 200 without Location, and tokens CSS charset plus CORS. Retain genuine missing-page 404 checks. + +### Task 4: Make type and Astro diagnostics part of tests + +- [x] Preserve the currently failing `tsc --noEmit` reproduction as the initial red signal; inspect Astro's schema context and existing content tests before editing. +- [x] Add a compatible `@astrojs/check` dependency with pnpm and update the lockfile. Add `check` = Astro sync + Astro check + `tsc --noEmit`, and run it before Vitest from website `test`. +- [x] Make the schema factories generic over their supplied image schema, preserving Astro image metadata inference in production and string inference for test stubs. Avoid assertions, `any`, diagnostic ignores and excluding test files. +- [x] Fix genuine additional diagnostics the new check exposes. If tooling rejects root TypeScript 7, select a compatible explicitly scoped tooling version after checking official requirements; do not silently remove either TS or Astro coverage. +- [x] Verify generated collection types are fresh, `pnpm --filter website check` passes and `pnpm test` actually invokes it. Retain all content assertions and on-disk photo checks. + +### Task 5: Validate redirect fragments and sitemap entries + +- [x] Add fixtures with a valid and missing redirect anchor, a missing destination file, percent-encoded IDs and malformed encodings; failures must identify source and target. Cover external redirects being skipped and same-origin absolute destinations being checked. +- [x] Add sitemap fixtures for a valid page, a missing page, a legacy redirect source, a slash-less folder URL and a foreign origin. Parse XML `loc` elements; do not rely on the HTML attribute scanner. +- [x] Run the focused tests and observe failures, then implement the checks using Task 2's resolver. Look up fragments in parsed destination HTML IDs; preserve existing cursor, parity, links and source-literal checks. +- [x] Integrate both checks into `main()` so `pnpm build` exercises the built sitemap and redirect targets. Run checker tests and the build, confirming current `#organizers` and `#events` pass. + +### Task 6: Share color detection without changing policy + +- [x] Add parameterized helper cases covering 3/4/6/8-digit hex, supported color functions, variables and `url(#id)` references; retain existing selector tests in `lintHtml` and website line-number diagnostics. +- [x] Export one color-match helper from the brand package and replace both copies of the regexes with calls to it. Keep HTML declaration-only scanning and website source scanning separate; keep font-family handling local. +- [x] Run focused brand lint/generation and website checker tests. Confirm the existing brand/template checks still pass and the new release changelog describes the public helper. + +### Task 7: Resolve the channel mismatch from Q1 + +- [x] Not applicable (1A approved); if Q1 had retained Website: add its lead-approved label/order and canonical URL to `links.json`, assert the entry and order, and verify both `/links/` and the home Join section. Keep collection-based rendering. +- [x] If Q1 removes Website: update the v1 spec §3 channel list with a decision note and source; keep the current four-entry collection unchanged. +- [x] In either case, make the approved spec and content agree; do not invent a channel or rely on an unrecorded assumption. + +### Task 8: Modernize CI actions and guard deployment scope + +- [x] Inspect official release notes and `action.yml` for compatible checkout/setup-node/pnpm action versions using a runtime newer than Node 20. Recheck the discovery versions above; record the selected references and runtime evidence. +- [x] Update the three references and any documented migration requirements; preserve pnpm 11.3.0, project Node 24, frozen install, tests and build. Do not add deployment actions. +- [x] Add a structural JSONC config test expecting exactly the current www Custom Domain array and no top-level singular `route`. Reject the presence of either `route` or `routes` under every named `env` configuration and under `previews`, including empty values. Use existing parser tooling if present, otherwise add a small explicit JSONC dev dependency. +- [x] Prove the guard fails for apex/wildcard/extra host, `custom_domain: false`, extra top-level routes, a top-level singular `route`, and separate singular `route` and plural `routes` cases in both `env.` and `previews`. Keep the current empty `previews` object passing. Mutate fixture data only, never real routes. +- [x] Add CODEOWNERS entries for the Wrangler file and CODEOWNERS itself using the explicit Q2 owner. State in the handoff that this does not change branch-protection settings. +- [x] Confirm `git diff -- apps/website/wrangler.jsonc` is empty and run focused configuration tests. CI's hosted execution is observed only after the host pushes; do not claim an unseen result. + +### Task 9: Validate and hand off the build + +- [x] Run `pnpm install --frozen-lockfile`, `pnpm test`, and `pnpm build`. All must pass; record actual output and counts rather than assuming the old baseline. +- [x] Use supported local-only Workers asset serving to run the expanded URL matrix. Load applicable local tooling guidance if available; never deploy. Record statuses, redirects, charset/CORS, templates and missing-page 404 results. Unit-test the dormant fallback separately. +- [ ] Cleanup blocked by sandbox EPERM (HTTP checks complete): separately start Astro dev (`pnpm --filter website dev`) and Astro preview (`pnpm --filter website exec astro preview`, after build), each on an available local port. Check `/brand/`, both extensionless template URLs and the cover iframe URL; record HTTP statuses and any redirects for each server, then stop both. Do not assume these servers implement Workers HTML asset resolution. If canonical URLs fail only in Astro, document the verified local-preview limitation in the handoff; the Workers URL matrix must still pass. Do not restore redirecting public `.html` URLs to mask a server difference. +- [x] Review `git diff origin/main`, untracked files and `git diff --check`. Confirm no generated outputs, secrets, unrelated changes or route edits. +- [x] Check every issue acceptance item below against evidence. Handle trusted lead feedback and every must-fix review item, or record reasoned pushback in its proper thread/workpad. +- [x] Rewrite host PR title/body around implemented behavior, remove `Plan: `, include `Closes #9` and real validation, and end with the Symphony/lead-review sentence. Write `.symphony/commit.txt` last among local handoff writes. +- [x] Set workpad `Phase: build, handed off`, then move only this card to Human Review as the final action. Do not merge, push, deploy or claim the host's future PR/CI results. + +## Acceptance criteria + +- [x] Worker fallback requests `/404` and returns the intended 404 response under a direct handler test. +- [x] Brand metadata publishes both extensionless template URLs, served by local Workers assets without a redirect; brand release version and changelog agree. Both template links in `/brand/`'s downloads section have canonical hrefs and no `download` attribute, with built-output regression coverage; other download behavior is preserved. The cover iframe and adjacent link use the canonical cover URL. +- [x] All ten old URLs work in both source forms with the existing destinations and status codes. +- [x] `pnpm test` includes passing Astro and TypeScript checks, with no weakened content validation. +- [x] `/brand/tokens.css` is served locally as `text/css; charset=utf-8` with existing CORS. +- [x] Build fails on missing redirect fragments and invalid/missing sitemap destinations; valid current outputs pass. +- [x] Brand lint and website checker share one color detector with preserved diagnostics and scan contexts. +- [x] `/links/`, home Join data and v1 spec match Q1's recorded decision. +- [x] All three CI actions use verified runtimes newer than Node 20; project commands remain on Node 24. +- [x] Config tests pin only the existing www Custom Domain, reject top-level singular `route` and both `route`/`routes` overrides in named environments and previews, and CODEOWNERS uses Q2's owner. Actual routes are unchanged. +- [x] Astro dev and preview template/iframe HTTP behavior is checked separately and recorded; any limitation is distinguished from the required passing local Workers asset checks. +- [x] Frozen install, root tests/build, focused regressions, local HTTP checks and whole-branch review pass; detached-server cleanup is separately blocked as recorded below. + +## Plan-phase validation + +Required now: `pnpm test`, `pnpm build`, and review of the diff plus untracked files. Planning prose does not need tests that assert its wording. Record current results in the workpad; leave build acceptance boxes open until implementation. Production HTTP and hosted CI are not part of this plan-phase run. + +2026-10-30 plan-phase results: frozen install passed (already up to date); root tests passed (93 brand + 21 website = 114 tests in 11 files); root build and check-dist passed (14 required files, 6 pages, 10 redirects). The separate `tsc --noEmit` reproduction remains a five-error baseline failure to resolve after approval. Whole-branch and untracked-file review found only this plan and its spec; relative documentation links and whitespace checks pass. No application, dependency, CI, ownership or route files changed. + +## Review round 1 revisions + +2026-10-30: [Claude review on PR #16](https://github.com/javascriptcebu/jscebu.org/pull/16#issuecomment-6519831600) requested changes to this plan, without approving implementation. The must-fix is covered by the brand page file-map entry, Task 2's stable template identity and built-page `download` regression checks, and the template acceptance criterion. Both suggestions are incorporated: Task 9 records Astro dev/preview behavior separately from Workers, and Task 8 covers singular and plural route keys with failure fixtures. Q1 and Q2 were pending at this review; the approved decisions above now govern the build. + +Rework validation: frozen install passed; `pnpm test` passed all 114 tests across 11 files; `pnpm build` passed, including check-dist (14 required files, 6 pages, 10 redirects). Source inspection confirmed the suffix-based download decision, and a direct expression probe with `/brand/templates/event-post` returned an empty download attribute. Reviewed the whole-branch diff against `origin/main`, untracked-file status and whitespace: only these two planning documents changed; Wrangler configuration is untouched. Dev/preview behavior and new regression tests remain build-phase work, not results claimed by this rework. + + +## Build results (2026-10-30) + +- Approval: MEMBER dorelljames answered 1A 2A and returned the card to Todo. Existing PR #16 remains open; build starts from clean sync at `4ad1ef2`. +- Red/green: fallback test caught `/404.html`; redirect fixture required 20 rows and found 10; header fixture caught absent charset; brand generation caught old template metadata/version; resolver/fragment/sitemap/color helper tests initially failed before implementation. The built template-link assertion then failed on exactly the two `download` attributes before the page fix. +- `@astrojs/check@0.9.12` declares TypeScript `^5.0.0 || ^6.0.0`, and Astro's installed CLI explicitly rejects TypeScript 7. Website now scopes `typescript@6.0.3`; root remains on 7.0.2. Generic image factories plus typed collection callbacks preserve production image metadata and string test stubs. A fresh install repaired stale local pnpm skipped-dependency state; no transitive dependency workaround was added. +- Verified selected action manifests through github_api: checkout `v7.0.1`, setup-node `v7.1.0`, pnpm/action-setup `v6.1.0` all declare `runs.using: node24`. Read their release notes and migration sections. Existing `pull_request` trigger, pnpm cache, no registry authentication and hosted ubuntu-latest need no input changes. Project Node 24 and pnpm 11.3.0 remain configured; hosted CI has not run on this build yet. +- `pnpm install --frozen-lockfile`: passed, already up to date. `pnpm test`: passed, 170 tests in 15 files (112 brand + 58 website), including Astro checking of 39 files (0 errors, 0 warnings, 3 existing URL API deprecation hints) and `tsc --noEmit`. +- `pnpm build`: passed; check-dist reports 15 required files, 6 pages, 20 redirect rows, brand parity and 30 source files without literals. Checks now run on the real redirect destinations, sitemap and brand template links. +- Workers local-only URL matrix at `http://127.0.0.1:8799`: all 36 assertions passed, including twenty redirect rows, extensionless templates returning 200 without Location, CSS `text/css; charset=utf-8` plus CORS and a genuine missing-page 404. Local log/registry paths and a larger file-descriptor limit handled sandbox defaults; no routes or deployment configuration changed. +- Astro dev at port 4391: `/brand/` 200; both extensionless templates (including the cover iframe URL) 404, no redirects. The HTML has canonical template links without download and the canonical cover iframe. This is a verified Astro dev limitation; Workers serves both correctly. +- Astro preview at port 4392: `/brand/` and both extensionless templates 200, no redirects; canonical iframe/link checks pass. Workers stopped after verification. Astro automatically detached its dev/preview processes; Ctrl-C stopped their CLI parents, but sandbox EPERM prevented terminating daemon PIDs 29334 and 29496. Astro stop reports no tracked server because its liveness probe is denied too. Both remain bound to localhost; cleanup is an environment blocker, not an HTTP failure. +- Whole-branch diff and every untracked source file reviewed; lockfile changes belong to the checker/JSONC/TypeScript tooling. No generated assets, secrets, route edits, DNS or branch-protection changes. CODEOWNERS requests @dorelljames reviews and does not enable enforcement settings. +- Claude round 1 must-fix and both suggestions implemented; round 2 ordering/version/approval-language suggestions incorporated. The add-Website suggestion is inapplicable to approved 1A. Host handoff uses the existing PR with Closes #9; no push, merge, deploy or hosted CI result is claimed. diff --git a/docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md b/docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md index f1e0801..a66d360 100644 --- a/docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md +++ b/docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md @@ -46,7 +46,7 @@ All pages use the **Terminal** personality and share one nav and footer. | `/` | **Hero:** "Build things. Ship them. Together" with the blinking cursor, the sticker, and a one-line intro. **What we do:** meetups, workshops, Hacktoberfest Cebu. **Upcoming events** (`#events`): buttons to Cebby and to JS Cebu's Facebook events. **Featured project:** Cebby. **Community photo:** real and unfiltered. **Join:** Facebook, GitHub, X, email. | site content (§4) | | `/projects` | Project cards. Cebby only in v1, as **[Decided]** "Cebby first". | `projects` collection | | `/about` | Mission and vision, plus "how we measure success" (moved verbatim from `/mission-vision`). History: GitHub org 2018, launched 2019 from Laravel Cebu and Vue.js Cebu, with the Angular, React and Vue Cebu sub-communities as history. Organizers (`#organizers`): the four leads from `/members`, as listed today. | `about.md` + `organizers` collection | -| `/links` | The official channels: Facebook, X, GitHub, website, hello@jscebu.org. | `links` collection | +| `/links` | The official channels: Facebook, X, GitHub, hello@jscebu.org. | `links` collection | | `/brand`, `/brand.md`, `/brand/*`, `/llms.txt`, `/404` | Already built. `/brand` switches to the shared nav, keeping its section links. | unchanged | | `/sitemap.xml`, `/robots.txt` | **[Proposed]** For search engines, generated by a small endpoint rather than an integration. The old sitemap returns 500. | build | @@ -169,3 +169,5 @@ Recorded so v1 doesn't paint it into a corner. This gets built in its own releas - The `_redirects` syntax. - The sitemap endpoint. - Cebby's icon file. + +GH-9 clarification (2026-10-30): keep the four existing official channels; Website is not a separate channel. Approved by dorelljames, answer 1A: https://github.com/javascriptcebu/jscebu.org/issues/9#issuecomment-6525522776. The same collection supplies the home Join section. diff --git a/docs/superpowers/specs/2026-10-30-v1-review-fixes-design.md b/docs/superpowers/specs/2026-10-30-v1-review-fixes-design.md new file mode 100644 index 0000000..b79d048 --- /dev/null +++ b/docs/superpowers/specs/2026-10-30-v1-review-fixes-design.md @@ -0,0 +1,83 @@ +# v1 review fixes: design + +- **Date:** 2026-10-30 +- **Issue:** [GH-9](https://github.com/javascriptcebu/jscebu.org/issues/9) +- **Status:** approved for implementation on 2026-10-30 (lead answers 1A 2A) +- **Scope:** the nine deferred v1 review findings; no deployment, DNS changes or route edits + +## 1. Intent and approval boundary + +Remove redirect hops from published asset links, preserve old URLs, make the existing build checks cover their blind spots, and protect the deployment configuration from accidental expansion. The original v1 design remains the content reference with Q1 now approved: keep the existing four channels and correct its `/links` row. + +The lead approved the implementation plan by returning GH-9 to Todo after answering 1A and 2A. Keep the four existing channels; assign `@dorelljames` to review Wrangler configuration and CODEOWNERS. The approval includes the planned `0.1.1` brand patch release with changelog. Source: https://github.com/javascriptcebu/jscebu.org/issues/9#issuecomment-6525522776. + +## 2. Evidence from this checkout + +At HEAD `1ea2203` on `symphony/gh-9`: + +- `apps/website/worker/index.ts` requests `/404.html`. `wrangler.jsonc` already uses asset-first serving with `not_found_handling: "404-page"`; retaining the fallback and correcting its path avoids removing the entry point or changing configuration. +- The two template asset paths in `packages/brand/src/brand.ts` end in `.html`. `brandMd()` publishes those values directly. Physical template files must retain `.html` for static asset serving and local use. +- `_redirects` has ten rows, all without trailing slashes. A local inspection through `parseRedirects()` reports `slash redirects: 0 of 10`. +- `pnpm --filter website exec tsc --noEmit` exits 1 with TS2740 at `test/content.test.ts:13, :23, :33, :50` and TS2345 at `:34`. Tests substitute `z.string()` for an image helper whose declared type only accepts Astro's image metadata schema. +- Website `test` runs only Vitest. `tsc` does not check `.astro` templates. +- `_headers` gives Markdown and plain text explicit charsets but gives `/brand/*` only CORS. There is no specific CSS content-type rule. +- `check-dist` strips fragments before existence checks, does not feed `sitemap.xml` through a link check, and repeats the color regexes from `packages/brand/src/lint.ts`. A synthetic `/about/#missing-anchor` is accepted when only `/about/` is known to exist. +- v1 spec §3 lists Website as an official channel; `links.json` has Facebook, X, GitHub and Email. Both the home Join section and `/links/` consume that collection. +- All three CI actions use `@v4`. The Node 24 version configured for project commands does not set the actions' own JavaScript runtimes. +- `wrangler.jsonc` has exactly `[{ "pattern": "www.jscebu.org", "custom_domain": true }]` in top-level routes and an empty `previews` object. No CODEOWNERS file exists in this checkout. + +The deployed HTTP symptoms in the issue are supplied findings, not fresh production measurements in this run. Implementation verification will use local Workers asset serving; production and DNS remain untouched. + +## 3. Approved behavior + +### Canonical static assets and the 404 fallback + +Keep the Worker entry point and change its asset request to `/404`. A unit test invokes the handler with an ASSETS stub, requires that path on the incoming origin, and verifies a real 404 with the returned body and headers. The stub must detect the old `/404.html` path. The assets layer's miss handling remains configured as it is today. + +Publish `/brand/templates/event-post` and `/brand/templates/fb-cover` from the brand asset data. Keep the source and copied `.html` files unchanged. Updating this public brand metadata is a patch release: align the package and brand-data versions to `0.1.1` and add a dated changelog entry describing the canonical template URLs and shared checker helper. Do not change colors, typography, logo assets or approved phrases. + +Preserve template navigation on `/brand/`: the downloads section currently distinguishes templates by their `.html` suffix, so changing only the metadata would add `download=""` to those links. Change that decision to the stable `/brand/templates/` prefix or equivalent explicit template identity. Both template links must have canonical hrefs and no `download` attribute; other assets retain their download behavior. Add built-page assertions to `check-dist` and fixture tests that reject even an empty `download` attribute, verify both links exist, and exercise the real rendered page during `pnpm build`. The cover iframe and adjacent link also use the canonical cover URL. + +Give the build checker one URL-to-built-file resolver matching the relevant static asset rules: `/folder/` resolves to `folder/index.html`, extensionless HTML paths resolve to their sibling `.html` file, and direct non-HTML assets resolve as files. Bare folder URLs and public `.html` links that incur canonicalization remain failures. Required physical output checks still refer to actual `.html` files. This does not relax the trailing-slash rule for page folders. + +### Legacy redirects and CSS headers + +Add a slash variant for each of the ten old URL sources, preserving each destination, fragment and status (including the temporary events 302). Pin all twenty rows in the regression test and exercise both forms in the local URL matrix. + +Add an exact `/brand/tokens.css` header rule with `Content-Type: text/css; charset=utf-8`. Preserve the wildcard CORS rule and verify both headers together under local asset serving. + +### Type checking + +Add a website `check` script that runs Astro sync, `astro check` and `tsc --noEmit`; invoke it from the existing website `test` script before Vitest. Install a compatible `@astrojs/check` dependency and update the lockfile without suppressing diagnostics or relying on an interactive auto-install prompt. Verify compatibility with the repository's TypeScript version during implementation. + +Make the image schema factory accept a typed generic Zod schema provider so both Astro's real image metadata and the tests' string stand-in preserve their inferred output types. Keep real image resolution in production, raw-path existence checks in tests and existing content validation. Do not hide the mismatch with `any`, blanket assertions, diagnostic ignores or excluded tests. Resolve additional real diagnostics exposed by `.astro` checking within this issue's type-check scope. + +### Build-check coverage and shared color detection + +Use the shared resolver for page links, brand documentation, sitemap entries and same-site redirect destinations. Validate each redirect's nonempty fragment against the destination HTML's IDs, including percent decoding; errors identify the source, target and missing anchor. Malformed encodings should produce a useful validation failure rather than an uncaught exception. External redirect targets remain outside local file/fragment validation. + +Parse the built sitemap as XML, read each `loc`, and verify that its canonical same-site URL resolves to a built output. Missing pages, redirect sources and slash-less folder URLs fail. Do not crawl external sites or treat arbitrary XML text as links. + +Move the existing hex/color-function matching into one small helper exported by `@jscebu/brand`. Both `lintHtml` and `findLiterals` use it while retaining their different scan contexts and diagnostics. Preserve selector and `url(#id)` handling; font-family detection remains website-specific. This is consolidation of existing rules, not a new color policy. + +### Official channels + +Q1 selects the existing four channels: Facebook, X, GitHub and Email. Amend the v1 spec with the lead decision and keep links.json unchanged; its exact four-URL assertion also covers the shared home Join data. + +### CI and configuration ownership + +Move all three CI action references to supported releases whose action manifests use a runtime newer than Node 20, retaining Node 24 and pnpm's package-manager pin for project commands. Recheck official release notes, manifests and migration requirements at build time; keep checkout, package-manager setup, node setup and frozen install in a working order. No deployment job is added. + +Add a structural test of the existing JSONC configuration. It must pin the exact top-level route array to the one www Custom Domain, reject a top-level singular `route`, and reject either `route` or `routes` under each named environment and under `previews`, even when empty. Test singular and plural keys separately in both override locations; the existing empty `previews` object passes. Parse JSONC reliably rather than stripping comments with an unsafe regex. Prefer an existing parser if available; otherwise add a small explicit development dependency and lockfile update. Never edit the routes themselves. + +Create `.github/CODEOWNERS` using only the owner selected in Q2. Cover `/apps/website/wrangler.jsonc` and the ownership file itself. CODEOWNERS requests review; it does not prove that branch protection enforces approval. Changing repository settings or branch protection is outside scope. + +## 4. Verification and boundaries + +Write failing regression tests before each tested behavior, then implement and rerun the focused checks. Cover the URL resolver's canonical success/failure cases, missing/existing fragments, sitemap targets, redirects, CSS headers, image schema inference, shared color matching and exact deployment routes. Use temporary route mutations in fixtures or pure validation inputs to prove the guard detects added/wrong hosts without editing the real configuration. + +Run frozen install, `pnpm test` and `pnpm build`. For HTTP behavior, use the existing local Workers asset workflow with supported local-only tooling, never a deploy. Extend and run `url-matrix.sh` against that local origin for all slash redirects, both extensionless templates, CSS charset/CORS and a genuine 404. A direct Worker unit test covers the fallback that asset-first misses may bypass. + +Also check `/brand/`, both extensionless templates and the cover iframe URL under Astro dev and Astro preview. Record statuses and redirects separately for each server and stop them afterward. Their mapping of extensionless URLs may differ from Workers assets; verify rather than assume that behavior. Document any confirmed Astro-only limitation in the build handoff while requiring the Workers HTTP checks to pass. These checks occur after approval; this plan does not claim observed dev/preview behavior. + +Review the full branch against `origin/main` and all untracked files. Keep generated brand copies, build output, tool state and secrets out of the change. The host handles commits, feature-branch pushes and PR updates. Handoff is Human Review; only a lead merges, and merge deploys. diff --git a/packages/brand/package.json b/packages/brand/package.json index 9a80523..9d23455 100644 --- a/packages/brand/package.json +++ b/packages/brand/package.json @@ -1,6 +1,6 @@ { "name": "@jscebu/brand", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "exports": { diff --git a/packages/brand/src/brand.ts b/packages/brand/src/brand.ts index 490cabb..708ce39 100644 --- a/packages/brand/src/brand.ts +++ b/packages/brand/src/brand.ts @@ -78,7 +78,7 @@ const tokensLink = ']/i.test(html) ? html : `${html}`); const issues: LintIssue[] = []; const styles = [...document.querySelectorAll('style')].map((s) => s.textContent ?? ''); + // Scan declaration values, preserving ID selectors and url(#id) references. const declarations = [ ...styles.flatMap(cssRules).map((r) => r.body), ...[...document.querySelectorAll('[style]')].map((el) => el.getAttribute('style') ?? ''), ]; for (const text of declarations) { - const hit = text.match(HEX) ?? text.match(COLOR_FN); + const hit = findColorLiteral(text); if (hit) { - issues.push({ rule: 'noColorLiterals', message: `Color literal "${hit[0]}". Use the --jc-* variables from tokens.css.` }); + issues.push({ rule: 'noColorLiterals', message: `Color literal "${hit}". Use the --jc-* variables from tokens.css.` }); } } diff --git a/packages/brand/test/color-literals.test.ts b/packages/brand/test/color-literals.test.ts new file mode 100644 index 0000000..e50ab77 --- /dev/null +++ b/packages/brand/test/color-literals.test.ts @@ -0,0 +1,9 @@ +import { expect, it } from 'vitest'; +import { findColorLiteral } from '../src/index.ts'; + +it.each(['#fff', '#abcd', '#AABBCC', '#AABBCCDD', 'rgb(', 'rgba(', 'hsl(', 'hsla(', 'hwb(', 'lab(', 'lch(', 'oklab(', 'oklch('])('finds %s', (value) => { + expect(findColorLiteral(`color: ${value}`)).toBe(value); +}); +it.each(['var(--jc-yellow)', 'url(#add)', 'url("#fff")', "url('#abcd')", '#abcdefghi'])('allows %s', (value) => { + expect(findColorLiteral(value)).toBeUndefined(); +}); diff --git a/packages/brand/test/generate.test.ts b/packages/brand/test/generate.test.ts index 572a4b6..e995445 100644 --- a/packages/brand/test/generate.test.ts +++ b/packages/brand/test/generate.test.ts @@ -41,7 +41,7 @@ describe('brandMd', () => { const md = brandMd(brand); it('starts with the title and version', () => { - expect(md.startsWith('# JavaScript Cebu brand · v0.1.0')).toBe(true); + expect(md.startsWith('# JavaScript Cebu brand · v0.1.1')).toBe(true); }); it('lists every MUST rule as a checkbox', () => { @@ -89,3 +89,13 @@ describe('llmsTxt', () => { expect(txt).toContain('https://www.jscebu.org/brand.md'); }); }); + +it('publishes canonical template URLs and a matching patch changelog', () => { + const md = brandMd(brand); + for (const name of ['event-post', 'fb-cover']) { + expect(md).toContain(`https://www.jscebu.org/brand/templates/${name}`); + expect(md).not.toContain(`/brand/templates/${name}.html`); + } + expect(brand.version).toBe('0.1.1'); + expect(brand.changelog[0].version).toBe(brand.version); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e56631f..73b109b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,12 +39,21 @@ importers: specifier: ~7.4.2 version: 7.4.2(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1) devDependencies: + '@astrojs/check': + specifier: ^0.9.12 + version: 0.9.12(@emnapi/runtime@1.11.3)(prettier@3.9.10)(typescript@6.0.3) + jsonc-parser: + specifier: ^3.3.1 + version: 3.3.1 linkedom: specifier: ~0.18.14 version: 0.18.14 tsx: specifier: ^4.23.15 version: 4.23.15 + typescript: + specifier: ^6.0.3 + version: 6.0.3 vitest: specifier: ^5.0.5 version: 5.0.5(@types/node@26.6.5)(vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1)) @@ -88,6 +97,16 @@ packages: '@acemir/cssom@0.9.32': resolution: {integrity: sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==} + '@astrojs/astro2tsx@0.1.3': + resolution: {integrity: sha512-GMDC7Eo0myBUA/QLBl0u+OcegJ8LiBhccmNZR34KmprQNP8gQUJlWlOOZWUsxNA2rNS6GuPYcGDyLoPnjmn4/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@astrojs/check@0.9.12': + resolution: {integrity: sha512-hazpqxIfZWj2VhyPyR0pOEQ/IhFDM/2nIApL96zMXABdD8NrUKYDmhxTy7h+drTDl9MQG/Sd1ET/4amadVJUUA==} + hasBin: true + peerDependencies: + typescript: ^5.0.0 || ^6.0.0 + '@astrojs/compiler-binding-android-arm64@0.5.2': resolution: {integrity: sha512-kckIxon6Rm/Bxzh9mGPYpRRqUbToUQoHptCmpMmE+Yt0Xumq8CqrKUidyYhrBsrHVO7bxuRzzWUUqvpf+iYceQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -162,6 +181,18 @@ packages: '@astrojs/internal-helpers@0.13.0': resolution: {integrity: sha512-20jxQqnSmpskcGyad7N5l6Xf1gz0UCa6lNde44bEBdaXGpncEYfAZJ142r2ZgzBaG4dUyzGmhqZWv1INU2eiHQ==} + '@astrojs/language-server@2.17.3': + resolution: {integrity: sha512-QIAFu1e/4quJMTPdmBOm+ZPXc5NDGZE4/YQjgR3EgjhGLl/tdidaWJDr5WmO2lesVc+BW1qN/xNBURmuePDG2A==} + hasBin: true + peerDependencies: + prettier: ^3.0.0 + prettier-plugin-astro: '>=0.11.0' + peerDependenciesMeta: + prettier: + optional: true + prettier-plugin-astro: + optional: true + '@astrojs/markdown-satteri@0.4.5': resolution: {integrity: sha512-0IeGdoFbv0f+442KJhYS44Wg8F3MuHmRsxIEYuFyqkjRubc6dPBFZ7azor5t4AeXLQoT5A5h9HyViKpd70Dl9w==} @@ -173,6 +204,9 @@ packages: resolution: {integrity: sha512-SMkvMsGOpM0FMAoT5pzhgI3LZC0JSPWf7Evwop+fZ3N6UkusPM7lI+sOJjMNI5OJrVoiei1/AGo1cY7sikTc5A==} engines: {node: '>=22.23.4'} + '@astrojs/yaml2ts@0.2.4': + resolution: {integrity: sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A==} + '@babel/helper-string-parser@7.29.11': resolution: {integrity: sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==} engines: {node: '>=6.9.0'} @@ -298,6 +332,27 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@emmetio/abbreviation@2.3.3': + resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} + + '@emmetio/css-abbreviation@2.1.8': + resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} + + '@emmetio/css-parser@0.4.1': + resolution: {integrity: sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ==} + + '@emmetio/html-matcher@1.3.0': + resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==} + + '@emmetio/scanner@1.0.4': + resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==} + + '@emmetio/stream-reader-utils@0.1.0': + resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==} + + '@emmetio/stream-reader@2.2.0': + resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} + '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} @@ -662,6 +717,13 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@napi-rs/wasm-runtime@1.2.4': + resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + '@napi-rs/wasm-runtime@1.2.6': resolution: {integrity: sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} @@ -1077,10 +1139,75 @@ packages: '@vitest/spy@5.0.5': resolution: {integrity: sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==} + '@volar/kit@2.4.30': + resolution: {integrity: sha512-VocLtumfXnqnPQEWc9Yx7uC1uOh9gyjVNlrn2+6Zx/z9aM6yTLhPK8lSqM9Ilu81XRa48I00lcYJWLi2BLxx8Q==} + peerDependencies: + typescript: <7 + + '@volar/language-core@2.4.30': + resolution: {integrity: sha512-73fjwEV8wat6aOZ6U/y/hqtE30ocrIWECg67b0txGczOMNw+MffLmvJ5fnFfN9drVUEYZzLdaNo6VAy6G6avMw==} + + '@volar/language-server@2.4.30': + resolution: {integrity: sha512-tcP9fzzUBbsLIFHhgnRU4ncQjMn9G9Vax0ffkQmdCrXVEYqRroUepmyC5AVgiju5bCiLD6a55qen5iOTQHkb3g==} + peerDependencies: + typescript: <7 + peerDependenciesMeta: + typescript: + optional: true + + '@volar/language-service@2.4.30': + resolution: {integrity: sha512-w/xDsASiqC71ggCOm7sTDXDNk/l72XEptodeRC68tgZ0e5QM4hM3zAqBGC7mBctvhrpYwKfMG+t9xcUo2hEFtA==} + peerDependencies: + typescript: <7 + peerDependenciesMeta: + typescript: + optional: true + + '@volar/source-map@2.4.30': + resolution: {integrity: sha512-zym1ob/KwDDyN+0jGVtxwbxJSRRnUvv5HspCtz3yEfVVdCG4/dzxJktMGPAgemqRuVs81O0o/y0q3qOUErR8kg==} + + '@volar/typescript@2.4.30': + resolution: {integrity: sha512-/DgA5tgYFPfrLFuSjIMXMGHk1LQv2+P8eny6+BhGNzFJELWIW7UZlc56ZLIRFhLsPpMfIFxM8/HvOPBggrjTZg==} + peerDependencies: + typescript: <7 + peerDependenciesMeta: + typescript: + optional: true + + '@vscode/emmet-helper@2.11.0': + resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==} + + '@vscode/l10n@0.0.18': + resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-i18n@4.2.0: + resolution: {integrity: sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg==} + peerDependencies: + ajv: ^8.0.0-beta.0 + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + am-i-vibing@0.5.0: resolution: {integrity: sha512-KLcsDhqf1XRrJmJr+9I/C0EX2K+sGrAGZFgkmd84c8TJO9zJBu+/kFInjazu1KfJWFhWDKdRWkpBzEs7s35DVA==} hasBin: true + ansi-regex@6.5.0: + resolution: {integrity: sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -1141,6 +1268,10 @@ packages: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1255,6 +1386,12 @@ packages: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} + emmet@2.4.11: + resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1281,6 +1418,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1294,12 +1435,18 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-string-truncated-width@3.0.3: resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.9: + resolution: {integrity: sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==} + fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -1331,6 +1478,14 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.7.0: + resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} + engines: {node: '>=18'} + get-tsconfig@5.0.0-beta.6: resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==} engines: {node: '>=20.20.0'} @@ -1371,6 +1526,12 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jsonc-parser@2.3.1: + resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} + jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -1503,6 +1664,9 @@ packages: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + nanoid@3.3.20: resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1564,6 +1728,9 @@ packages: package-manager-detector@1.9.0: resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -1592,6 +1759,11 @@ packages: resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} engines: {node: ^10 || ^12 || >=14} + prettier@3.9.10: + resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==} + engines: {node: '>=14'} + hasBin: true + prismjs@1.30.0: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} @@ -1619,6 +1791,16 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + request-light@0.5.8: + resolution: {integrity: sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==} + + request-light@0.7.0: + resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -1672,9 +1854,21 @@ packages: std-env@4.3.0: resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.3.0: + resolution: {integrity: sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==} + engines: {node: '>=20'} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -1720,6 +1914,17 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + typesafe-path@0.2.2: + resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} + + typescript-auto-import-cache@0.3.6: + resolution: {integrity: sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} @@ -1939,6 +2144,108 @@ packages: jsdom: optional: true + volar-service-css@0.0.71: + resolution: {integrity: sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-emmet@0.0.71: + resolution: {integrity: sha512-zqjzt6bN95e3CUstBm0PBFAJnrfz0ZAARka87fart46/gNCLLuP3Vujy8V/J8HEziTFLnfkgIASLFYPUhonJcA==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-html@0.0.71: + resolution: {integrity: sha512-e8tHPhgQ7ooLfudAEIku+kgd9pWkq3SSz8RbnQDI1+Eb8wbenkLGHqoirLqz5ORLV6wIMr2Iv08RWBG5eOcgpw==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-prettier@0.0.71: + resolution: {integrity: sha512-Rz7JVH3qD108UCdmIEiZvOBNljMt2nLFdbN8AXcDfn7xD9F5I2aCIsDVqBbXw21PsnxG0b7MfwtNF+zPS/NKUg==} + peerDependencies: + '@volar/language-service': ~2.4.0 + prettier: ^2.2 || ^3.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + prettier: + optional: true + + volar-service-typescript-twoslash-queries@0.0.71: + resolution: {integrity: sha512-9K2k72s4n7rV9s4bX0MyjbX9iBribvKZbBJKuEmTCZfeWJXs6Yh7bGpY4eoc7UufAjvpheBqwyZCOIPBvxCv0A==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-typescript@0.0.71: + resolution: {integrity: sha512-yTtM/BVT6hoyEYnDtaCyAtNhdNeS/mhTTABlBOdw3NNiRBUin3IznFJpgfjer4c6RYopiPjjQjc9VFhxVl1mLw==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-yaml@0.0.71: + resolution: {integrity: sha512-qYGWGuVpUTnZGu5P/CR4KLK4aIR8RrcVnmfZ2eRcj9q/I8VZCoC5yy9FtEvfNvnDp4MU17yhdJcvpQPIqhJS2Q==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + vscode-css-languageservice@6.3.10: + resolution: {integrity: sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==} + + vscode-html-languageservice@5.6.2: + resolution: {integrity: sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==} + + vscode-json-languageservice@4.1.8: + resolution: {integrity: sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==} + engines: {npm: '>=7.0.0'} + + vscode-jsonrpc@10.0.0: + resolution: {integrity: sha512-USSfxg0vDIpTQqlQBGxF3mBARyoS8cU7LHLpbYNh6dOJt32dlskxPWB0Irrh5TllTmyf7oNLAvqDqzklbwwnpA==} + engines: {node: '>=14.17.0'} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-protocol@3.18.5: + resolution: {integrity: sha512-FW4fBMzZ99N8xeob4py0l050wgCoRgTpEq5Xcsb9lM1bc29UKGR/wSV+4knaqFxssFHajy7OzNiKP/m3U1ZEuA==} + + vscode-languageserver-textdocument@1.0.16: + resolution: {integrity: sha512-qeNEuOi9a3qkqRf7Gfez2WL2VwlBTLCNekHBGO9kxPVUp9XuY15yfTJ9jwvOwKI0I2MhUUfZq95NSp+LK1CLQg==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver-types@3.18.5: + resolution: {integrity: sha512-kPd6+JQNz3HMJvjKlqUxERSlZHAntl/aGpsk7ImwIh0ewDKDcjgAEyVSYciPiOJD3ftbK9IrfiqUeTZLSbCizw==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-nls@5.2.0: + resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} + + vscode-uri@3.2.0: + resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} + why-is-node-running@3.2.1: resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} engines: {node: '>=20.11'} @@ -1959,6 +2266,10 @@ packages: '@cloudflare/workers-types': optional: true + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -1974,6 +2285,19 @@ packages: xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml-language-server@1.23.0: + resolution: {integrity: sha512-3qVyCOexLCWw06PQa5kRPwvMWMZ/eZeCRWUvgD6a0OkqL/4iCnxy2WumbWifa937Uo5xhyWJ0uxlU39ljhNh7A==} + hasBin: true + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yaml@2.9.1: resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} engines: {node: '>= 14.6'} @@ -1983,6 +2307,10 @@ packages: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@18.2.0: + resolution: {integrity: sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yocto-queue@1.2.2: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} @@ -2003,6 +2331,25 @@ snapshots: '@acemir/cssom@0.9.32': {} + '@astrojs/astro2tsx@0.1.3(@emnapi/runtime@1.11.3)': + dependencies: + '@emnapi/core': 1.11.3 + '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + transitivePeerDependencies: + - '@emnapi/runtime' + + '@astrojs/check@0.9.12(@emnapi/runtime@1.11.3)(prettier@3.9.10)(typescript@6.0.3)': + dependencies: + '@astrojs/language-server': 2.17.3(@emnapi/runtime@1.11.3)(prettier@3.9.10)(typescript@6.0.3) + chokidar: 5.0.0 + kleur: 4.1.5 + typescript: 6.0.3 + yargs: 18.2.0 + transitivePeerDependencies: + - '@emnapi/runtime' + - prettier + - prettier-plugin-astro + '@astrojs/compiler-binding-android-arm64@0.5.2': optional: true @@ -2065,6 +2412,31 @@ snapshots: unified: 11.0.5 yaml: 2.9.1 + '@astrojs/language-server@2.17.3(@emnapi/runtime@1.11.3)(prettier@3.9.10)(typescript@6.0.3)': + dependencies: + '@astrojs/astro2tsx': 0.1.3(@emnapi/runtime@1.11.3) + '@astrojs/yaml2ts': 0.2.4 + '@volar/kit': 2.4.30(typescript@6.0.3) + '@volar/language-core': 2.4.30 + '@volar/language-server': 2.4.30(typescript@6.0.3) + '@volar/language-service': 2.4.30(typescript@6.0.3) + muggle-string: 0.4.1 + tinyglobby: 0.2.17 + volar-service-css: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)) + volar-service-emmet: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)) + volar-service-html: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)) + volar-service-prettier: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3))(prettier@3.9.10) + volar-service-typescript: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)) + volar-service-typescript-twoslash-queries: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)) + volar-service-yaml: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)) + vscode-html-languageservice: 5.6.2 + vscode-uri: 3.2.0 + optionalDependencies: + prettier: 3.9.10 + transitivePeerDependencies: + - '@emnapi/runtime' + - typescript + '@astrojs/markdown-satteri@0.4.5': dependencies: '@astrojs/internal-helpers': 0.13.0 @@ -2083,6 +2455,10 @@ snapshots: is-docker: 4.0.0 package-manager-detector: 1.9.0 + '@astrojs/yaml2ts@0.2.4': + dependencies: + yaml: 2.9.1 + '@babel/helper-string-parser@7.29.11': {} '@babel/helper-validator-identifier@7.29.7': {} @@ -2170,6 +2546,29 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@emmetio/abbreviation@2.3.3': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-abbreviation@2.1.8': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-parser@0.4.1': + dependencies: + '@emmetio/stream-reader': 2.2.0 + '@emmetio/stream-reader-utils': 0.1.0 + + '@emmetio/html-matcher@1.3.0': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/scanner@1.0.4': {} + + '@emmetio/stream-reader-utils@0.1.0': {} + + '@emmetio/stream-reader@2.2.0': {} + '@emnapi/core@1.11.1': dependencies: '@emnapi/wasi-threads': 1.2.2 @@ -2180,7 +2579,6 @@ snapshots: dependencies: '@emnapi/wasi-threads': 1.2.3 tslib: 2.8.1 - optional: true '@emnapi/runtime@1.11.1': dependencies: @@ -2190,7 +2588,6 @@ snapshots: '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 - optional: true '@emnapi/wasi-threads@1.2.2': dependencies: @@ -2200,7 +2597,6 @@ snapshots: '@emnapi/wasi-threads@1.2.3': dependencies: tslib: 2.8.1 - optional: true '@esbuild/aix-ppc64@0.28.2': optional: true @@ -2410,6 +2806,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 + '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@tybys/wasm-util': 0.10.4 + '@napi-rs/wasm-runtime@1.2.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 @@ -2585,7 +2987,6 @@ snapshots: '@tybys/wasm-util@0.10.4': dependencies: tslib: 2.8.1 - optional: true '@types/chai@5.2.3': dependencies: @@ -2710,10 +3111,86 @@ snapshots: '@vitest/spy@5.0.5': {} + '@volar/kit@2.4.30(typescript@6.0.3)': + dependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + '@volar/typescript': 2.4.30(typescript@6.0.3) + typesafe-path: 0.2.2 + typescript: 6.0.3 + vscode-languageserver-textdocument: 1.0.16 + vscode-uri: 3.2.0 + + '@volar/language-core@2.4.30': + dependencies: + '@volar/source-map': 2.4.30 + + '@volar/language-server@2.4.30(typescript@6.0.3)': + dependencies: + '@volar/language-core': 2.4.30 + '@volar/language-service': 2.4.30(typescript@6.0.3) + '@volar/typescript': 2.4.30(typescript@6.0.3) + path-browserify: 1.0.1 + request-light: 0.7.0 + vscode-languageserver: 9.0.1 + vscode-languageserver-protocol: 3.18.5 + vscode-languageserver-textdocument: 1.0.16 + vscode-uri: 3.2.0 + optionalDependencies: + typescript: 6.0.3 + + '@volar/language-service@2.4.30(typescript@6.0.3)': + dependencies: + '@volar/language-core': 2.4.30 + vscode-languageserver-protocol: 3.18.5 + vscode-languageserver-textdocument: 1.0.16 + vscode-uri: 3.2.0 + optionalDependencies: + typescript: 6.0.3 + + '@volar/source-map@2.4.30': {} + + '@volar/typescript@2.4.30(typescript@6.0.3)': + dependencies: + '@volar/language-core': 2.4.30 + '@volar/language-service': 2.4.30(typescript@6.0.3) + path-browserify: 1.0.1 + vscode-uri: 3.2.0 + optionalDependencies: + typescript: 6.0.3 + + '@vscode/emmet-helper@2.11.0': + dependencies: + emmet: 2.4.11 + jsonc-parser: 2.3.1 + vscode-languageserver-textdocument: 1.0.16 + vscode-languageserver-types: 3.18.5 + vscode-uri: 3.2.0 + + '@vscode/l10n@0.0.18': {} + + ajv-draft-04@1.0.0(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-i18n@4.2.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.9 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + am-i-vibing@0.5.0: dependencies: process-ancestry: 0.1.0 + ansi-regex@6.5.0: {} + + ansi-styles@6.2.3: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -2836,6 +3313,12 @@ snapshots: ci-info@4.4.0: {} + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + clsx@2.1.1: {} comma-separated-tokens@2.0.3: {} @@ -2942,6 +3425,13 @@ snapshots: dset@3.1.4: {} + emmet@2.4.11: + dependencies: + '@emmetio/abbreviation': 2.3.3 + '@emmetio/css-abbreviation': 2.1.8 + + emoji-regex@10.6.0: {} + entities@4.5.0: {} entities@7.0.1: {} @@ -2983,6 +3473,8 @@ snapshots: '@esbuild/win32-ia32': 0.28.2 '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -2993,12 +3485,16 @@ snapshots: extend@3.0.2: {} + fast-deep-equal@3.1.3: {} + fast-string-truncated-width@3.0.3: {} fast-string-width@3.0.2: dependencies: fast-string-truncated-width: 3.0.3 + fast-uri@3.1.9: {} + fast-wrap-ansi@0.2.2: dependencies: fast-string-width: 3.0.2 @@ -3022,6 +3518,10 @@ snapshots: fsevents@2.3.3: optional: true + get-caller-file@2.0.5: {} + + get-east-asian-width@1.7.0: {} + get-tsconfig@5.0.0-beta.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -3077,6 +3577,10 @@ snapshots: is-plain-obj@4.1.0: {} + json-schema-traverse@1.0.0: {} + + jsonc-parser@2.3.1: {} + jsonc-parser@3.3.1: {} kleur@4.1.5: {} @@ -3198,6 +3702,8 @@ snapshots: mrmime@2.0.1: {} + muggle-string@0.4.1: {} + nanoid@3.3.20: {} neotraverse@1.0.1: {} @@ -3251,6 +3757,8 @@ snapshots: package-manager-detector@1.9.0: {} + path-browserify@1.0.1: {} + path-to-regexp@6.3.0: {} pathe@2.0.3: {} @@ -3271,6 +3779,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.2 + prettier@3.9.10: {} + prismjs@1.30.0: {} process-ancestry@0.1.0: {} @@ -3291,6 +3801,12 @@ snapshots: dependencies: regex-utilities: 2.3.0 + request-light@0.5.8: {} + + request-light@0.7.0: {} + + require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} retext-smartypants@6.2.0: @@ -3395,11 +3911,26 @@ snapshots: std-env@4.3.0: {} + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.7.0 + strip-ansi: 7.2.0 + + string-width@8.3.0: + dependencies: + get-east-asian-width: 1.7.0 + strip-ansi: 7.2.0 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.5.0 + supports-color@10.2.2: {} svgo@4.1.0: @@ -3431,8 +3962,7 @@ snapshots: trough@2.2.0: {} - tslib@2.8.1: - optional: true + tslib@2.8.1: {} tsx@4.23.15: dependencies: @@ -3440,6 +3970,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + typesafe-path@0.2.2: {} + + typescript-auto-import-cache@0.3.6: + dependencies: + semver: 7.8.5 + + typescript@6.0.3: {} + typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 @@ -3620,6 +4158,112 @@ snapshots: transitivePeerDependencies: - msw + volar-service-css@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)): + dependencies: + vscode-css-languageservice: 6.3.10 + vscode-languageserver-textdocument: 1.0.16 + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + + volar-service-emmet@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)): + dependencies: + '@emmetio/css-parser': 0.4.1 + '@emmetio/html-matcher': 1.3.0 + '@vscode/emmet-helper': 2.11.0 + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + + volar-service-html@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)): + dependencies: + vscode-html-languageservice: 5.6.2 + vscode-languageserver-textdocument: 1.0.16 + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + + volar-service-prettier@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3))(prettier@3.9.10): + dependencies: + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + prettier: 3.9.10 + + volar-service-typescript-twoslash-queries@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)): + dependencies: + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + + volar-service-typescript@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)): + dependencies: + path-browserify: 1.0.1 + semver: 7.8.5 + typescript-auto-import-cache: 0.3.6 + vscode-languageserver-textdocument: 1.0.16 + vscode-nls: 5.2.0 + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + + volar-service-yaml@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)): + dependencies: + vscode-uri: 3.2.0 + yaml-language-server: 1.23.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + + vscode-css-languageservice@6.3.10: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.16 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.2.0 + + vscode-html-languageservice@5.6.2: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.16 + vscode-languageserver-types: 3.18.5 + vscode-uri: 3.2.0 + + vscode-json-languageservice@4.1.8: + dependencies: + jsonc-parser: 3.3.1 + vscode-languageserver-textdocument: 1.0.16 + vscode-languageserver-types: 3.18.5 + vscode-nls: 5.2.0 + vscode-uri: 3.2.0 + + vscode-jsonrpc@10.0.0: {} + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-protocol@3.18.5: + dependencies: + vscode-jsonrpc: 10.0.0 + vscode-languageserver-types: 3.18.5 + + vscode-languageserver-textdocument@1.0.16: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver-types@3.18.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-nls@5.2.0: {} + + vscode-uri@3.2.0: {} + why-is-node-running@3.2.1: {} workerd@1.20261023.1: @@ -3647,14 +4291,48 @@ snapshots: - bufferutil - utf-8-validate + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + ws@8.21.0: {} xxhash-wasm@1.1.0: {} + y18n@5.0.8: {} + + yaml-language-server@1.23.0: + dependencies: + '@vscode/l10n': 0.0.18 + ajv: 8.20.0 + ajv-draft-04: 1.0.0(ajv@8.20.0) + ajv-i18n: 4.2.0(ajv@8.20.0) + prettier: 3.9.10 + request-light: 0.5.8 + vscode-json-languageservice: 4.1.8 + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.16 + vscode-languageserver-types: 3.18.5 + vscode-uri: 3.2.0 + yaml: 2.8.3 + + yaml@2.8.3: {} + yaml@2.9.1: {} yargs-parser@22.0.0: {} + yargs@18.2.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.3.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + yocto-queue@1.2.2: {} youch-core@0.3.3: