From 9f9296c3fdc476c4a3cde6337dd0fc4cd5943b60 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 7 Oct 2026 03:20:04 +1100 Subject: [PATCH 1/6] test(helpers): load each migrated sqlite database from an image Most suites build a fresh database per test and replayed every migration each time, about 100 ms per database. Replay each migration list once per file and deserialize the image after that. Node before 24.16 has no serialize, so it keeps the replay. --- test/unit/helpers/d1-sqlite.ts | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/test/unit/helpers/d1-sqlite.ts b/test/unit/helpers/d1-sqlite.ts index 7a437787..43084ed6 100644 --- a/test/unit/helpers/d1-sqlite.ts +++ b/test/unit/helpers/d1-sqlite.ts @@ -34,14 +34,43 @@ export function allMigrations(): string[] { .map(name => `migrations/${name}`) } +/** + * Replaying all migrations costs about 100 ms per database, and most suites + * build a fresh database for every test. Each distinct migration list is + * replayed once per test file, then every later database loads that image. + * + * `serialize` arrived in Node 24.16. On older versions every database replays + * the list. + */ +const migratedImages = new Map() +const canLoadImage = typeof DatabaseSync.prototype.serialize === 'function' + +function migrate(raw: DatabaseSync, migrationPaths: string[]): void { + if (!canLoadImage || migrationPaths.length === 0) { + for (const path of migrationPaths) + raw.exec(readFileSync(path, 'utf8')) + return + } + const key = migrationPaths.join('\n') + let image = migratedImages.get(key) + if (!image) { + const template = new DatabaseSync(':memory:') + for (const path of migrationPaths) + template.exec(readFileSync(path, 'utf8')) + image = template.serialize() + template.close() + migratedImages.set(key, image) + } + raw.deserialize(image) +} + export function createSqliteD1( migrationPaths: string[], options: { maximumQueries?: number } = {}, ): SqliteD1 { const raw = new DatabaseSync(':memory:') let queryCount = 0 - for (const path of migrationPaths) - raw.exec(readFileSync(path, 'utf8')) + migrate(raw, migrationPaths) /** * D1 rejects a statement with more than 100 bound parameters. SQLite itself From 2749cb6000e6f25d604a792160398154d5c30e50 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 7 Oct 2026 03:20:20 +1100 Subject: [PATCH 2/6] test: delete tests that restate source, copy, or config These files read source or docs and grep the text, assert copy and markup, restate constant tables, or repeat another file's coverage. None of them can catch a behaviour bug that typecheck, lint, or a remaining test would miss. --- test/unit/agents.test.ts | 16 - test/unit/author-profile-badge.test.ts | 99 ---- test/unit/community-creator-ui.test.ts | 156 ------- test/unit/compact-page-header-ui.test.ts | 43 -- test/unit/comparison-navigation.test.ts | 26 -- test/unit/copy-contract.test.ts | 440 ------------------ test/unit/digest-template.test.ts | 26 -- test/unit/docs-fold-contract.test.ts | 31 -- test/unit/generated-content-labels.test.ts | 26 -- test/unit/github-badge-brand-kit.test.ts | 18 - test/unit/identity-query-contracts.test.ts | 66 --- test/unit/marketing-content-config.test.ts | 21 - test/unit/me-subroute-routing.test.ts | 46 -- test/unit/no-private-page-components.test.ts | 8 - test/unit/og-image-curator.test.ts | 60 --- test/unit/og-image-skill.test.ts | 30 -- test/unit/operational-event.test.ts | 34 -- test/unit/protocol-fixtures.test.ts | 51 -- test/unit/repo-skill-dependencies.test.ts | 59 --- test/unit/repo-source-fallback.test.ts | 23 - .../scripts-beacon-offline-fallback.test.ts | 52 --- test/unit/setup-snippet.test.ts | 22 - test/unit/skill-receipts-panel.test.ts | 34 -- test/unit/skills-collision.test.ts | 108 ----- .../skills-registry-loading-state.test.ts | 79 ---- test/unit/skills-sitemap-outcomes.test.ts | 23 - test/unit/tag-link-path.test.ts | 14 - test/unit/title-template-suffix.test.ts | 58 --- test/unit/weekly-email-preview-inert.test.ts | 23 - 29 files changed, 1692 deletions(-) delete mode 100644 test/unit/agents.test.ts delete mode 100644 test/unit/author-profile-badge.test.ts delete mode 100644 test/unit/community-creator-ui.test.ts delete mode 100644 test/unit/compact-page-header-ui.test.ts delete mode 100644 test/unit/comparison-navigation.test.ts delete mode 100644 test/unit/copy-contract.test.ts delete mode 100644 test/unit/digest-template.test.ts delete mode 100644 test/unit/docs-fold-contract.test.ts delete mode 100644 test/unit/generated-content-labels.test.ts delete mode 100644 test/unit/github-badge-brand-kit.test.ts delete mode 100644 test/unit/identity-query-contracts.test.ts delete mode 100644 test/unit/marketing-content-config.test.ts delete mode 100644 test/unit/me-subroute-routing.test.ts delete mode 100644 test/unit/no-private-page-components.test.ts delete mode 100644 test/unit/og-image-curator.test.ts delete mode 100644 test/unit/og-image-skill.test.ts delete mode 100644 test/unit/operational-event.test.ts delete mode 100644 test/unit/protocol-fixtures.test.ts delete mode 100644 test/unit/repo-skill-dependencies.test.ts delete mode 100644 test/unit/repo-source-fallback.test.ts delete mode 100644 test/unit/scripts-beacon-offline-fallback.test.ts delete mode 100644 test/unit/setup-snippet.test.ts delete mode 100644 test/unit/skill-receipts-panel.test.ts delete mode 100644 test/unit/skills-collision.test.ts delete mode 100644 test/unit/skills-registry-loading-state.test.ts delete mode 100644 test/unit/skills-sitemap-outcomes.test.ts delete mode 100644 test/unit/tag-link-path.test.ts delete mode 100644 test/unit/title-template-suffix.test.ts delete mode 100644 test/unit/weekly-email-preview-inert.test.ts diff --git a/test/unit/agents.test.ts b/test/unit/agents.test.ts deleted file mode 100644 index 0a8ea504..00000000 --- a/test/unit/agents.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { AGENT_TARGETS } from '../../app/utils/agents' - -describe('agent registry', () => { - it('tells every agent how to confirm the install landed', () => { - for (const agent of AGENT_TARGETS) { - expect(agent.label.length, agent.id).toBeGreaterThan(0) - expect(agent.verify.length, agent.id).toBeGreaterThan(0) - } - }) - - it('keeps agent ids unique so the rendered list has stable keys', () => { - const ids = AGENT_TARGETS.map(agent => agent.id) - expect(new Set(ids).size).toBe(ids.length) - }) -}) diff --git a/test/unit/author-profile-badge.test.ts b/test/unit/author-profile-badge.test.ts deleted file mode 100644 index 6830e9e6..00000000 --- a/test/unit/author-profile-badge.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { mockNuxtImport, mountSuspended } from '@nuxt/test-utils/runtime' -import { describe, expect, it, vi } from 'vitest' -import { reactive, ref, toValue } from 'vue' - -const route = reactive({ - path: '/@jd-solanki', - query: {}, - params: { login: 'jd-solanki' }, -}) - -const skills = [ - { - owner: 'jd-solanki', - repo: 'skills', - name: 'setup-jd-solanki-skills', - display_name: 'Setup JD Solanki skills', - slug: 'jd-solanki/skills/setup-jd-solanki-skills', - description: null, - likeCount: 0, - modified_at: null, - last_synced_at: null, - skill_path: 'skills/scaffolding/setup-jd-solanki-skills/SKILL.md', - source_owner: null, - source_repo: null, - default_branch: 'main', - }, - { - owner: 'jd-solanki', - repo: 'skills', - name: 'bruno', - display_name: 'Bruno', - slug: 'jd-solanki/skills/bruno', - description: null, - likeCount: 0, - modified_at: null, - last_synced_at: null, - skill_path: 'skills/bruno/SKILL.md', - source_owner: null, - source_repo: null, - default_branch: 'main', - }, -] - -mockNuxtImport('useRoute', () => () => route) -mockNuxtImport('useUserSession', () => () => ({ user: ref(null), loggedIn: ref(false), ready: ref(true) })) -mockNuxtImport('useFetch', () => (url: unknown) => { - const key = String(toValue(url)) - const value = key.includes('/skills') - ? { items: skills } - : key.includes('/collections/') - ? { items: [] } - : { - displayName: 'JD Solanki', - description: '', - avatar: 'https://github.com/jd-solanki.png', - github: 'https://github.com/jd-solanki', - blog: null, - location: null, - } - return { - data: ref(value), - error: ref(null), - refresh: vi.fn(), - } -}) - -vi.stubGlobal('defineOgImage', () => {}) - -async function mountPage() { - return await mountSuspended( - await import('../../app/pages/@[login]/index.vue').then(module => module.default), - { - global: { - stubs: { - LikeButton: true, - NuxtTime: true, - }, - }, - }, - ) -} - -describe('public author profile badges', () => { - it('shows a minimal badge for the repository page', async () => { - const wrapper = await mountPage() - - const sources = wrapper.findAll('[data-testid="minimal-badge-light"]').map(image => image.attributes('src')) - expect(sources).toContain('/b/jd-solanki/skills?theme=light&label=0') - wrapper.unmount() - }) - - it('shows a minimal badge for each individual skill page', async () => { - const wrapper = await mountPage() - - const sources = wrapper.findAll('[data-testid="minimal-badge-light"]').map(image => image.attributes('src')) - expect(sources).toContain('/b/jd-solanki/skills/setup-jd-solanki-skills?theme=light&label=0') - wrapper.unmount() - }) -}) diff --git a/test/unit/community-creator-ui.test.ts b/test/unit/community-creator-ui.test.ts deleted file mode 100644 index dd2f1d5d..00000000 --- a/test/unit/community-creator-ui.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { CommunityDirectoryItem } from '../../server/utils/community' -import { mountSuspended } from '@nuxt/test-utils/runtime' -import { describe, expect, it } from 'vitest' -import CommunityCreator from '../../app/components/community/_CommunityCreator.vue' - -function creatorFixture(overrides: Partial = {}): CommunityDirectoryItem { - return { - id: 1, - login: 'harlan-zw', - name: 'Harlan Wilton', - avatar: 'https://github.com/harlan-zw.png', - collectionCount: 3, - skillCount: 12, - featured: false, - activityAt: 1_700_000_000, - topCollection: { - authorLogin: 'harlan-zw', - slug: 'design-engineering-essentials', - name: 'Design Engineering Essentials', - preamble: 'The skills a design engineer reaches for daily.', - skillCount: 8, - skills: [ - { owner: 'antfu', repo: 'skills', name: 'nuxt', displayName: 'Nuxt' }, - { owner: 'antfu', repo: 'skills', name: 'unocss', displayName: null }, - ], - updatedAt: 1_700_000_000, - }, - topSkill: { - owner: 'antfu', - repo: 'skills', - name: 'nuxt', - displayName: 'Nuxt', - description: 'Build full-stack Vue applications.', - stars: 1200, - modifiedAt: 1_700_000_000, - registryPath: '/gh/antfu/skills/nuxt', - }, - ...overrides, - } as CommunityDirectoryItem -} - -function mountCreator(overrides: Partial = {}) { - return mountSuspended(CommunityCreator, { props: { creator: creatorFixture(overrides) } }) -} - -describe('community creator identity', () => { - it('links the display name to the profile and shows the login alongside it', async () => { - const wrapper = await mountCreator() - - const profile = wrapper.get('a[href="/@harlan-zw"]') - expect(profile.attributes('aria-label')).toBe('View Harlan Wilton\'s profile') - expect(profile.text()).toContain('Harlan Wilton') - expect(profile.text()).toContain('@harlan-zw') - - wrapper.unmount() - }) - - it('falls back to the login as the display name without repeating it underneath', async () => { - const wrapper = await mountCreator({ name: null }) - - const profile = wrapper.get('a[href="/@harlan-zw"]') - expect(profile.text().match(/@harlan-zw/g)).toHaveLength(1) - - wrapper.unmount() - }) - - it('summarises contributions with singular and plural units', async () => { - const plural = await mountCreator() - expect(plural.text()).toContain('3 collections · 12 skills') - plural.unmount() - - const singular = await mountCreator({ collectionCount: 1, skillCount: 1 }) - expect(singular.text()).toContain('1 collection · 1 skill') - singular.unmount() - - const skillsOnly = await mountCreator({ collectionCount: 0, topCollection: null }) - expect(skillsOnly.text()).toContain('12 skills') - expect(skillsOnly.text()).not.toContain('·') - skillsOnly.unmount() - }) -}) - -describe('community creator collection preview', () => { - it('lists the previewed collection skills in order as a real list', async () => { - const wrapper = await mountCreator() - - const items = wrapper.findAll('.community-contribution__skill-list li') - expect(items.map(item => item.text())).toEqual(['Nuxt', 'unocss']) - expect(wrapper.get('.community-contribution__skill-list').element.tagName).toBe('UL') - expect(wrapper.html()).not.toMatch(/role="list(item)?"/) - - wrapper.unmount() - }) - - it('reports how many skills the preview left out', async () => { - const wrapper = await mountCreator() - - expect(wrapper.text()).toContain('+6 more') - - wrapper.unmount() - }) - - it('drops the overflow note when the preview shows the whole collection', async () => { - const wrapper = await mountCreator({ - topCollection: { ...creatorFixture().topCollection!, skillCount: 2 }, - }) - - expect(wrapper.text()).not.toContain('more') - expect(wrapper.text()).toContain('2 skills') - - wrapper.unmount() - }) - - it('omits the preview entirely when the collection has no skills to show', async () => { - const wrapper = await mountCreator({ - topCollection: { ...creatorFixture().topCollection!, skills: [], skillCount: 0 }, - }) - - expect(wrapper.find('.community-contribution__skill-preview').exists()).toBe(false) - expect(wrapper.text()).toContain('0 skills') - - wrapper.unmount() - }) - - it('labels a featured creator\'s collection differently from a latest one', async () => { - const featured = await mountCreator({ featured: true }) - expect(featured.text()).toContain('Featured collection') - featured.unmount() - - const latest = await mountCreator({ featured: false }) - expect(latest.text()).toContain('Latest collection') - latest.unmount() - }) -}) - -describe('community creator top skill', () => { - it('links the most popular skill to its canonical page, with its Repository and stars', async () => { - const wrapper = await mountCreator() - - const section = wrapper.get('section[aria-label="Most starred skill by Harlan Wilton"]') - expect(section.get('a[href="/gh/antfu/skills/nuxt"]').text()).toBe('/nuxt') - expect(section.text()).toContain('skills') - expect(section.text()).toContain('1.2k GitHub stars') - - wrapper.unmount() - }) - - it('renders neither contribution card when the creator has neither', async () => { - const wrapper = await mountCreator({ topCollection: null, topSkill: null }) - - expect(wrapper.findAll('.community-contribution')).toHaveLength(0) - expect(wrapper.get('a[href="/@harlan-zw"]').exists()).toBe(true) - - wrapper.unmount() - }) -}) diff --git a/test/unit/compact-page-header-ui.test.ts b/test/unit/compact-page-header-ui.test.ts deleted file mode 100644 index 0f4de45c..00000000 --- a/test/unit/compact-page-header-ui.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { mountSuspended } from '@nuxt/test-utils/runtime' -import { describe, expect, it } from 'vitest' -import { h } from 'vue' -import CompactPageHeader from '../../app/components/CompactPageHeader.vue' - -const baseProps = { - title: 'Community', - description: 'Curators you can watch.', - headingId: 'community-heading', -} - -describe('compactPageHeader', () => { - it('renders the title as the page h1 under the id the page labels with', async () => { - const wrapper = await mountSuspended(CompactPageHeader, { props: baseProps }) - - const heading = wrapper.get('h1') - expect(heading.text()).toBe('Community') - expect(heading.attributes('id')).toBe('community-heading') - expect(wrapper.get('header').text()).toContain('Curators you can watch.') - - wrapper.unmount() - }) - - it('renders aside and default slot content, and drops the containers when unused', async () => { - const bare = await mountSuspended(CompactPageHeader, { props: baseProps }) - expect(bare.find('aside').exists()).toBe(false) - expect(bare.find('.compact-page-header__controls').exists()).toBe(false) - bare.unmount() - - const filled = await mountSuspended(CompactPageHeader, { - props: baseProps, - slots: { - aside: () => h('p', 'aside content'), - default: () => h('button', 'Publish a collection'), - }, - }) - - expect(filled.get('aside').text()).toBe('aside content') - expect(filled.get('.compact-page-header__controls').text()).toBe('Publish a collection') - - filled.unmount() - }) -}) diff --git a/test/unit/comparison-navigation.test.ts b/test/unit/comparison-navigation.test.ts deleted file mode 100644 index a67b9479..00000000 --- a/test/unit/comparison-navigation.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @vitest-environment node -import { describe, expect, it } from 'vitest' -import { comparisonLinkForSkill, comparisonLinkForTrack } from '../../shared/comparison-navigation' - -describe('comparison navigation', () => { - it('links the canonical prose editing track', () => { - expect(comparisonLinkForTrack('anti-slop')?.to).toBe('/compare/humanize-writing-skills') - expect(comparisonLinkForTrack('code-quality')).toBeUndefined() - }) - it.each([ - { owner: 'blader', repo: 'humanizer', name: 'humanizer' }, - { owner: 'hardikpandya', repo: 'stop-slop', name: 'stop-slop' }, - { owner: 'PeterGYang', repo: 'No-AI-Slop', name: 'No-AI-Slop' }, - ])('links a featured Skill to its comparison: $owner/$repo/$name', (skill) => { - expect(comparisonLinkForSkill(skill)?.to).toBe('/compare/humanize-writing-skills') - }) - - it.each([ - { owner: 'another-owner', repo: 'humanizer', name: 'humanizer' }, - { owner: 'blader', repo: 'another-repository', name: 'humanizer' }, - { owner: 'blader', repo: 'humanizer', name: 'another-skill' }, - { owner: 'brianlovin', repo: 'deslop', name: 'deslop' }, - ])('leaves an unrelated Skill without a comparison: $owner/$repo/$name', (skill) => { - expect(comparisonLinkForSkill(skill)).toBeUndefined() - }) -}) diff --git a/test/unit/copy-contract.test.ts b/test/unit/copy-contract.test.ts deleted file mode 100644 index 61612906..00000000 --- a/test/unit/copy-contract.test.ts +++ /dev/null @@ -1,440 +0,0 @@ -// @vitest-environment node - -import { existsSync, readdirSync, readFileSync } from 'node:fs' -import { join } from 'node:path' -import { fileURLToPath } from 'node:url' - -const repoRoot = fileURLToPath(new URL('../..', import.meta.url)) - -function readRootFile(name: string) { - return readFileSync(join(repoRoot, name), 'utf8') -} - -function scopeTerms() { - const frontmatter = readRootFile('COPY.md').match(/^---\r?\n([\s\S]*?)\r?\n---/) - const scopeLine = frontmatter?.[1] - .split('\n') - .find(line => line.startsWith('scope:')) - if (!scopeLine) - throw new Error('COPY.md frontmatter has no scope line') - return scopeLine - .slice('scope:'.length) - .split(':') - .at(-1)! - .split(',') - .map(term => term.trim()) - .filter(Boolean) -} - -function rootMarkdownNames() { - return readdirSync(repoRoot).filter(name => name.endsWith('.md')) -} - -function leadingWords(text: string, count: number) { - return text - .toLowerCase() - .replace(/[`*_"'.,:;!?()[\]]/g, '') - .split(/\s+/) - .filter(Boolean) - .slice(0, count) - .join(' ') -} - -function proseParagraphs(markdown: string) { - return markdown - .replace(/^---\n[\s\S]*?\n---/, '') - .split(/\n\s*\n/) - .map(block => block.split(/\n/).map(line => line.trim()).filter(Boolean).join(' ')) - .filter(Boolean) - .filter(block => !block.startsWith('#') && !block.startsWith('|') && !block.startsWith('-') && !block.startsWith('>') && !/^\d+\./.test(block)) -} - -function normalizeCopy(value: string) { - return value - .toLowerCase() - .replace(/[`*_"'.,:;!?()[\]·—–-]/g, ' ') - .replace(/\s+/g, ' ') - .trim() -} - -function tableRows(section: string) { - return section - .split('\n') - .filter(line => line.startsWith('|')) - .slice(2) - .map(row => row.split('|').map(cell => cell.trim())) -} - -function canonicalAssets() { - const section = readRootFile('COPY.md').split('## Canonical assets')[1]?.split('\n## ')[0] - if (!section) - throw new Error('COPY.md has no Canonical assets section') - return tableRows(section).map(cells => ({ asset: cells[1]!, value: cells[2]!, placement: cells[3]! })) -} - -function indexSource() { - return readFileSync(join(repoRoot, 'app/pages/index.vue'), 'utf8') -} - -function heroH1Text() { - const h1 = indexSource().match(//)?.[0] - if (!h1) - throw new Error('the index page has no hero H1') - return h1 - .replace(//g, ' ') - .replace(/\{\{[\s\S]*?\}\}/g, ' ') - .replace(/<[^>]*>/g, ' ') - .replace(/\s+/g, ' ') - .trim() -} - -function indexOgAltText() { - const alt = indexSource().match(/defineOgImage\([\s\S]*?alt:\s*'([^']*)'/)?.[1] - if (!alt) - throw new Error('the index page defines no OG image alt') - return alt -} - -function placementProbes() { - return [ - { match: 'og image alt', text: indexOgAltText() }, - { match: 'h1', text: heroH1Text() }, - { match: 'site description', text: readRootFile('nuxt.config.ts') }, - ] -} - -function walkSurfaces(dir: string, extensions: string[], found: string[] = []): string[] { - for (const entry of readdirSync(dir, { withFileTypes: true })) { - if (entry.name === 'node_modules' || entry.name === '.nuxt') - continue - const path = join(dir, entry.name) - if (entry.isDirectory()) - walkSurfaces(path, extensions, found) - else if (extensions.some(extension => entry.name.endsWith(extension))) - found.push(path) - } - return found -} - -function mdSurfaceText(source: string) { - return source - .replace(/^---\n[\s\S]*?\n---/, ' ') - .replace(//g, ' ') - .replace(/```[\s\S]*?```/g, ' ') - .replace(/`[^`]*`/g, ' ') - .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1') - .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') -} - -const bracketPairs: Record = { '(': ')', '{': '}', '[': ']' } - -function skipStringLiteral(code: string, start: number) { - const quote = code[start] - let index = start + 1 - while (index < code.length && code[index] !== quote) { - if (code[index] === '\\') { - index += 2 - continue - } - if (quote === '`' && code[index] === '$' && code[index + 1] === '{') { - index = spanEnd(code, index + 1) + 1 - continue - } - index += 1 - } - return Math.min(index + 1, code.length) -} - -function spanEnd(code: string, openIndex: number) { - const open = code[openIndex] - const close = bracketPairs[open] - if (!close) - return openIndex - const expected: string[] = [close] - let index = openIndex + 1 - while (index < code.length) { - const char = code[index]! - if (char === '\'' || char === '"' || char === '`') { - index = skipStringLiteral(code, index) - continue - } - if (char === '/' && code[index + 1] === '/') { - const newline = code.indexOf('\n', index) - if (newline < 0) - return code.length - index = newline - continue - } - if (char === '/' && code[index + 1] === '*') { - const end = code.indexOf('*/', index + 2) - if (end < 0) - return code.length - index = end + 2 - continue - } - if (char in bracketPairs) { - expected.push(bracketPairs[char]!) - } - else if (char === expected[expected.length - 1]) { - expected.pop() - if (expected.length === 0) - return index - } - index += 1 - } - return code.length -} - -function statementEnd(code: string, startIndex: number) { - const expected: string[] = [] - let index = startIndex - while (index < code.length) { - const char = code[index]! - if (char === '\'' || char === '"' || char === '`') { - index = skipStringLiteral(code, index) - continue - } - if (char === '/' && code[index + 1] === '/') { - const newline = code.indexOf('\n', index) - if (newline < 0) - return code.length - index = newline - continue - } - if (char === '/' && code[index + 1] === '*') { - const end = code.indexOf('*/', index + 2) - if (end < 0) - return code.length - index = end + 2 - continue - } - if (char in bracketPairs) - expected.push(bracketPairs[char]!) - else if (expected.length > 0 && char === expected[expected.length - 1]) - expected.pop() - else if (expected.length === 0 && (char === '\n' || char === ';')) - return index - index += 1 - } - return code.length -} - -function collectStrings(code: string) { - const strings: string[] = [] - let index = 0 - while (index < code.length) { - const char = code[index]! - if (char === '\'' || char === '"' || char === '`') { - const end = skipStringLiteral(code, index) - const value = code.slice(index + 1, end - 1) - // Routes, URLs and file paths are identifiers, not copy. - if (!value.includes('/')) - strings.push(value) - index = end - continue - } - if (char === '/' && code[index + 1] === '/') { - const newline = code.indexOf('\n', index) - if (newline < 0) - break - index = newline - continue - } - if (char === '/' && code[index + 1] === '*') { - const end = code.indexOf('*/', index + 2) - if (end < 0) - break - index = end + 2 - continue - } - index += 1 - } - return strings -} - -const metaCallNames = ['useSeoMeta', 'useHead', 'defineOgImage'] -const metaValueNames = ['title', 'description', 'ogTitle', 'ogDescription', 'twitterTitle', 'twitterDescription'] - -function scriptMetaStrings(script: string) { - const spans: string[] = [] - for (const name of metaCallNames) { - for (const match of script.matchAll(new RegExp(`\\b${name}\\s*\\(`, 'g'))) { - const open = match.index + match[0].length - 1 - const end = spanEnd(script, open) - if (end > open) - spans.push(script.slice(open + 1, end)) - } - } - for (const name of metaValueNames) { - for (const match of script.matchAll(new RegExp(`\\bconst\\s+${name}\\s*=`, 'g'))) { - const start = match.index + match[0].length - const end = statementEnd(script, start) - if (end > start) - spans.push(script.slice(start, end)) - } - } - return spans.flatMap(collectStrings).join(' ') -} - -const rootConfigMetaBlocks = ['site', 'mcpServerCard', 'agentSkills', 'mcp'] - -function rootConfigMetaText() { - const config = readRootFile('nuxt.config.ts') - return rootConfigMetaBlocks.flatMap(name => - [...config.matchAll(new RegExp(`\\b${name}:\\s*\\{`, 'g'))].flatMap((match) => { - const open = match.index + match[0].length - 1 - const end = spanEnd(config, open) - return end > open ? collectStrings(config.slice(open + 1, end)) : [] - }), - ).join(' ') -} - -function vueSurfaceText(source: string) { - const template = source.match(/