From 1f68655314704bfd555d250dc2e01af1ea8bfc5c Mon Sep 17 00:00:00 2001 From: Djeex Date: Mon, 7 Sep 2026 16:03:00 +0200 Subject: [PATCH] Auto-detect content icon collections for the server bundle instead of hand-maintaining the list --- nuxt.config.ts | 53 ++++++++++++++++++++++++++++++++++- package-lock.json | 70 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 7 +++++ 3 files changed, 129 insertions(+), 1 deletion(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index 98ffa5d..e85a0ef 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,9 +1,56 @@ import { fileURLToPath } from 'node:url' import { execFileSync } from 'node:child_process' +import { readdirSync, readFileSync } from 'node:fs' +import { join } from 'node:path' import { createResolver } from '@nuxt/kit' const { resolve } = createResolver(import.meta.url) +// Every `i--` icon referenced from content/*.md needs its +// collection listed below, but Nuxt Icon's own static scanner only looks at +// .vue/.ts source, not markdown content, so it can't find these on its own. +// Scanning content here instead of hand-maintaining the list means a new +// icon collection used in an article gets bundled automatically on the next +// build; the only manual step left is `npm install @iconify-json/` +// for a genuinely new collection, and a missing one now fails the build +// loudly (unresolved import) instead of silently breaking at runtime behind +// the CSP (icons falling back to a live, blocked api.iconify.design call). +function scanContentIconCollections(): string[] { + // Collection prefixes can contain hyphens themselves (simple-icons, + // fluent-color), same as the separator before the icon name, so a plain + // "first segment" split is ambiguous. Matching against the actual + // installed @iconify-json package names, longest first, resolves it. + const installed = readdirSync(resolve('./node_modules/@iconify-json')) + .sort((a, b) => b.length - a.length) + const found = new Set() + const pattern = /icon=["']i-([a-z0-9-]+)["']|icon:\s*["']?i-([a-z0-9-]+)/g + function walk(dir: string) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name) + if (entry.isDirectory()) { + walk(full) + } + else if (entry.name.endsWith('.md')) { + const text = readFileSync(full, 'utf8') + for (const match of text.matchAll(pattern)) { + const iconRef = match[1] ?? match[2] + const collection = installed.find(name => iconRef === name || iconRef.startsWith(`${name}-`)) + // Falls back to the first segment for a collection that isn't + // installed yet: still wrong, but it now surfaces as a clear + // "cannot resolve @iconify-json/" build error to fix, + // rather than a silent runtime CSP block. + found.add(collection ?? iconRef.split('-')[0]) + } + } + } + } + walk(resolve('./content')) + // 'brand' is the local customCollections prefix (app/assets/brand-icons), + // not an installable Iconify package. + found.delete('brand') + return [...found] +} + // Contributors per page: read straight from git history rather than an // API, so it needs no token and no network call, but the CI checkout must // fetch full history (not a shallow clone) or every file will only show @@ -84,8 +131,12 @@ export default defineNuxtConfig({ // bundle. Without this, they fall back to a live api.iconify.design // request at prerender time, which times out wherever outbound access // is restricted. Bundling all three collections in full avoids that. + // Everything else used as a literal icon in content/*.md is added by + // scanContentIconCollections() above, so a new collection introduced in + // an article gets bundled automatically instead of needing a manual + // addition here. serverBundle: { - collections: ['simple-icons', 'lucide', 'vscode-icons'], + collections: [...new Set(['simple-icons', 'lucide', 'vscode-icons', ...scanContentIconCollections()])], }, // The fixed, small set of codeIcon values above. Forces them into the // content-hashed client bundle instead of Nuxt Icon's runtime diff --git a/package-lock.json b/package-lock.json index 9f4ed9a..7de7cb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,13 @@ "": { "name": "docudjeex", "dependencies": { + "@iconify-json/carbon": "^1.2.26", + "@iconify-json/cbi": "^1.2.59", + "@iconify-json/cib": "^1.2.3", + "@iconify-json/fluent-color": "^1.2.21", + "@iconify-json/logos": "^1.2.14", + "@iconify-json/noto": "^1.2.7", + "@iconify-json/solar": "^1.2.10", "@nuxtjs/i18n": "^10.6.0", "better-sqlite3": "^12.11.1", "docus": "^5.13.0", @@ -1355,6 +1362,51 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify-json/carbon": { + "version": "1.2.26", + "resolved": "https://registry.npmjs.org/@iconify-json/carbon/-/carbon-1.2.26.tgz", + "integrity": "sha512-UVzlnCAzSkrNydW3GYESeggndriR1JSgU5M8DNz9S0Fu10I3NpJUYPWar+WYjMAQUIm9B7mljPpc30oal3PRSQ==", + "license": "Apache-2.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/cbi": { + "version": "1.2.59", + "resolved": "https://registry.npmjs.org/@iconify-json/cbi/-/cbi-1.2.59.tgz", + "integrity": "sha512-kvp05ix+eJk509JJ8CTEUvdNx0F5x9Xg5oBMMDnjyLzKYZTh7nEWswvGKPmAmnEQ0pVjt8R+eS65LwYmi1OvFA==", + "license": "CC-BY-NC-SA-4.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/cib": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@iconify-json/cib/-/cib-1.2.3.tgz", + "integrity": "sha512-MMvRXVTHt82z3bYN19JDAjv/X8OBNC/1B3I334SV/1nyhdPeeyvJZVoI1cGuNqYznBjMnWxKPtIhhiVgGQ1CzQ==", + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/fluent-color": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@iconify-json/fluent-color/-/fluent-color-1.2.21.tgz", + "integrity": "sha512-VETjGx7BMpVsCR3HV5g5t6oBGZ92XKzIRClS5xUvDn1YHAkQTF9OyF/aFdtxYOC8JROoI1SNO/Ql6jZeK7mroA==", + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/logos": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@iconify-json/logos/-/logos-1.2.14.tgz", + "integrity": "sha512-O36DicXkgAMT6NAsH7MTWlOql8NktHz9fBCItTjz6L/9gyRXw4vQj6qTeVgww5UZWiVugUV1MhVFA/g9HWIkBw==", + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, "node_modules/@iconify-json/lucide": { "version": "1.2.127", "resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.127.tgz", @@ -1364,6 +1416,15 @@ "@iconify/types": "*" } }, + "node_modules/@iconify-json/noto": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@iconify-json/noto/-/noto-1.2.7.tgz", + "integrity": "sha512-2pNHliXnMLpnepNTZwR9hPBSl6GmhIfXPnudFCHY5v9pJhmA7LoVb/pFY0qLDY/k+3ikET8RXRf7XFxc/pHObA==", + "license": "Apache-2.0", + "dependencies": { + "@iconify/types": "*" + } + }, "node_modules/@iconify-json/simple-icons": { "version": "1.2.94", "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.94.tgz", @@ -1373,6 +1434,15 @@ "@iconify/types": "*" } }, + "node_modules/@iconify-json/solar": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@iconify-json/solar/-/solar-1.2.10.tgz", + "integrity": "sha512-ChpZ3kNw8nDhHfUoLq5JJ3AdIzytFm57dFkjAS0fjXnL5a7WwibTINjMKfvqQDhuXBrgqejNLyGp8CCpG8UyCg==", + "license": "CC-BY-4.0", + "dependencies": { + "@iconify/types": "*" + } + }, "node_modules/@iconify-json/vscode-icons": { "version": "1.2.76", "resolved": "https://registry.npmjs.org/@iconify-json/vscode-icons/-/vscode-icons-1.2.76.tgz", diff --git a/package.json b/package.json index f40740b..8f0720b 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,13 @@ "build": "NUXT_SITE_URL=https://docu.djeex.fr nuxt build" }, "dependencies": { + "@iconify-json/carbon": "^1.2.26", + "@iconify-json/cbi": "^1.2.59", + "@iconify-json/cib": "^1.2.3", + "@iconify-json/fluent-color": "^1.2.21", + "@iconify-json/logos": "^1.2.14", + "@iconify-json/noto": "^1.2.7", + "@iconify-json/solar": "^1.2.10", "@nuxtjs/i18n": "^10.6.0", "better-sqlite3": "^12.11.1", "docus": "^5.13.0",