Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
042bad9f53 | ||
|
|
88e52a976a |
+36
-69
@@ -6,26 +6,23 @@ import { createResolver } from '@nuxt/kit'
|
|||||||
|
|
||||||
const { resolve } = createResolver(import.meta.url)
|
const { resolve } = createResolver(import.meta.url)
|
||||||
|
|
||||||
// Every `i-<collection>-<name>` icon referenced from content/*.md or
|
// Every `i-<collection>-<name>` icon referenced from content/*.md needs its
|
||||||
// content/**/.navigation.yml needs both its collection (for serverBundle,
|
// collection listed below, but Nuxt Icon's own static scanner only looks at
|
||||||
// used during SSR/prerender) and its exact name (for clientBundle, used
|
// .vue/.ts source, not markdown content, so it can't find these on its own.
|
||||||
// client-side) listed below, but Nuxt Icon's own static scanner only looks
|
// Scanning content here instead of hand-maintaining the list means a new
|
||||||
// at .vue/.ts source, not content, so it can't find any of these on its
|
// icon collection used in an article gets bundled automatically on the next
|
||||||
// own. Scanning content here instead of hand-maintaining both lists means a
|
// build; the only manual step left is `npm install @iconify-json/<name>`
|
||||||
// newly used icon gets bundled automatically on the next build; the only
|
// for a genuinely new collection, and a missing one now fails the build
|
||||||
// manual step left is `npm install @iconify-json/<name>` for a genuinely
|
// loudly (unresolved import) instead of silently breaking at runtime behind
|
||||||
// new collection, and a missing one now fails the build loudly (unresolved
|
// the CSP (icons falling back to a live, blocked api.iconify.design call).
|
||||||
// import) instead of silently breaking at runtime behind the CSP (icons
|
function scanContentIconCollections(): string[] {
|
||||||
// falling back to a live, blocked api.iconify.design call).
|
|
||||||
function scanContentIcons(): { collections: string[], icons: string[] } {
|
|
||||||
// Collection prefixes can contain hyphens themselves (simple-icons,
|
// Collection prefixes can contain hyphens themselves (simple-icons,
|
||||||
// fluent-color), same as the separator before the icon name, so a plain
|
// fluent-color), same as the separator before the icon name, so a plain
|
||||||
// "first segment" split is ambiguous. Matching against the actual
|
// "first segment" split is ambiguous. Matching against the actual
|
||||||
// installed @iconify-json package names, longest first, resolves it.
|
// installed @iconify-json package names, longest first, resolves it.
|
||||||
const installed = readdirSync(resolve('./node_modules/@iconify-json'))
|
const installed = readdirSync(resolve('./node_modules/@iconify-json'))
|
||||||
.sort((a, b) => b.length - a.length)
|
.sort((a, b) => b.length - a.length)
|
||||||
const collections = new Set<string>()
|
const found = new Set<string>()
|
||||||
const icons = new Set<string>()
|
|
||||||
const pattern = /icon=["']i-([a-z0-9-]+)["']|icon:\s*["']?i-([a-z0-9-]+)/g
|
const pattern = /icon=["']i-([a-z0-9-]+)["']|icon:\s*["']?i-([a-z0-9-]+)/g
|
||||||
function walk(dir: string) {
|
function walk(dir: string) {
|
||||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||||
@@ -33,12 +30,7 @@ function scanContentIcons(): { collections: string[], icons: string[] } {
|
|||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
walk(full)
|
walk(full)
|
||||||
}
|
}
|
||||||
// .navigation.yml files set a section's nav icon (e.g. `icon:
|
else if (entry.name.endsWith('.md')) {
|
||||||
// i-lucide-chart-no-axes-column`) and are just as invisible to this
|
|
||||||
// scan as markdown content is to Nuxt Icon's own .vue/.ts scanner if
|
|
||||||
// only .md files are walked here, which is exactly how the first
|
|
||||||
// version of this function missed them.
|
|
||||||
else if (entry.name.endsWith('.md') || entry.name.endsWith('.yml')) {
|
|
||||||
const text = readFileSync(full, 'utf8')
|
const text = readFileSync(full, 'utf8')
|
||||||
for (const match of text.matchAll(pattern)) {
|
for (const match of text.matchAll(pattern)) {
|
||||||
const iconRef = match[1] ?? match[2]
|
const iconRef = match[1] ?? match[2]
|
||||||
@@ -47,24 +39,17 @@ function scanContentIcons(): { collections: string[], icons: string[] } {
|
|||||||
// installed yet: still wrong, but it now surfaces as a clear
|
// installed yet: still wrong, but it now surfaces as a clear
|
||||||
// "cannot resolve @iconify-json/<name>" build error to fix,
|
// "cannot resolve @iconify-json/<name>" build error to fix,
|
||||||
// rather than a silent runtime CSP block.
|
// rather than a silent runtime CSP block.
|
||||||
const resolved = collection ?? iconRef.split('-')[0]
|
found.add(collection ?? iconRef.split('-')[0])
|
||||||
collections.add(resolved)
|
|
||||||
icons.add(`${resolved}:${iconRef.slice(resolved.length + 1)}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
walk(resolve('./content'))
|
walk(resolve('./content'))
|
||||||
// 'brand' is the local customCollections prefix (app/assets/brand-icons),
|
// 'brand' is the local customCollections prefix (app/assets/brand-icons),
|
||||||
// not an installable Iconify package, and never needs live/API resolution.
|
// not an installable Iconify package.
|
||||||
collections.delete('brand')
|
found.delete('brand')
|
||||||
const brandPrefix = /^brand:/
|
return [...found]
|
||||||
return {
|
|
||||||
collections: [...collections],
|
|
||||||
icons: [...icons].filter(icon => !brandPrefix.test(icon)),
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const contentIcons = scanContentIcons()
|
|
||||||
|
|
||||||
// Contributors per page: read straight from git history rather than an
|
// 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
|
// API, so it needs no token and no network call, but the CI checkout must
|
||||||
@@ -133,17 +118,6 @@ export default defineNuxtConfig({
|
|||||||
dir: fileURLToPath(new URL('./public', import.meta.url)),
|
dir: fileURLToPath(new URL('./public', import.meta.url)),
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
// This site builds to a fully static export (nginx serving prerendered
|
|
||||||
// files, no Nitro server at runtime), so the `/api/_nuxt_icon` route
|
|
||||||
// Nuxt Icon's client falls back to for anything not in clientBundle
|
|
||||||
// doesn't exist in production: that fallback goes straight to the live
|
|
||||||
// Iconify API instead, which the CSP's connect-src blocks. `provider:
|
|
||||||
// 'none'` plus `fallbackToApi: false` removes that live-API fallback
|
|
||||||
// path entirely (both server- and client-side) rather than relying on
|
|
||||||
// clientBundle/serverBundle being perfectly exhaustive to avoid ever
|
|
||||||
// triggering it, per Nuxt Icon's own recommended static-site config.
|
|
||||||
provider: 'none',
|
|
||||||
fallbackToApi: false,
|
|
||||||
customCollections: [
|
customCollections: [
|
||||||
{
|
{
|
||||||
prefix: 'brand',
|
prefix: 'brand',
|
||||||
@@ -154,41 +128,34 @@ export default defineNuxtConfig({
|
|||||||
// dynamically (not as a literal `i-xxx` string anywhere), and
|
// dynamically (not as a literal `i-xxx` string anywhere), and
|
||||||
// FileTreeNode.vue resolves vscode-icons file-type icons the same way,
|
// FileTreeNode.vue resolves vscode-icons file-type icons the same way,
|
||||||
// so Nuxt Icon's static scanner can't pick any of them up for the local
|
// so Nuxt Icon's static scanner can't pick any of them up for the local
|
||||||
// bundle. Without this, they'd need the live-API fallback above just
|
// bundle. Without this, they fall back to a live api.iconify.design
|
||||||
// disabled, which is now blocked instead of avoided. Bundling all
|
// request at prerender time, which times out wherever outbound access
|
||||||
// three collections in full sidesteps that.
|
// is restricted. Bundling all three collections in full avoids that.
|
||||||
// Everything else used as a literal icon in content/*.md or
|
// Everything else used as a literal icon in content/*.md is added by
|
||||||
// .navigation.yml is added by scanContentIcons() above, so a new
|
// scanContentIconCollections() above, so a new collection introduced in
|
||||||
// collection introduced in an article gets bundled automatically
|
// an article gets bundled automatically instead of needing a manual
|
||||||
// instead of needing a manual addition here.
|
// addition here.
|
||||||
serverBundle: {
|
serverBundle: {
|
||||||
collections: [...new Set(['simple-icons', 'lucide', 'vscode-icons', ...contentIcons.collections])],
|
collections: [...new Set(['simple-icons', 'lucide', 'vscode-icons', ...scanContentIconCollections()])],
|
||||||
},
|
},
|
||||||
// The codeIcon values above, forced into the content-hashed client
|
// The fixed, small set of codeIcon values above. Forces them into the
|
||||||
// bundle instead of relying on the (now-disabled) runtime route: that
|
// content-hashed client bundle instead of Nuxt Icon's runtime
|
||||||
// route's URL doesn't change between builds, so a browser or CDN
|
// /api/_nuxt_icon route: that route's URL doesn't change between
|
||||||
// caching an old (or, before the serverBundle fix above, broken)
|
// builds, so a browser or CDN caching an old (or, before the
|
||||||
// response for it kept serving that stale result until the cache
|
// serverBundle fix above, broken) response for it keeps serving that
|
||||||
// expired or was purged, which is what "icon disappears until a hard
|
// stale result until the cache expires or is purged, which is what
|
||||||
// refresh" actually was. `scan: true` catches any other icon used
|
// "icon disappears until a hard refresh" actually was. vscode-icons'
|
||||||
// literally in .vue/.ts source; scanContentIcons() above covers content
|
// per-file-extension icons aren't listed here: there are too many to
|
||||||
// the same way it does for serverBundle. vscode-icons' per-file-
|
// enumerate and new file types keep appearing in content, so they
|
||||||
// extension icons aren't listed here: there are too many to enumerate
|
// still rely on the runtime route plus cache purging on deploy.
|
||||||
// and new file types keep appearing in content, so with the API
|
|
||||||
// fallback disabled, an unbundled one now renders as a missing icon
|
|
||||||
// instead of a broken network request; still preferable to a CSP
|
|
||||||
// violation, and serverBundle above still renders it correctly in the
|
|
||||||
// page's own initial prerendered HTML.
|
|
||||||
clientBundle: {
|
clientBundle: {
|
||||||
scan: true,
|
icons: [
|
||||||
icons: [...new Set([
|
|
||||||
'lucide:folder-tree',
|
'lucide:folder-tree',
|
||||||
'lucide:settings',
|
'lucide:settings',
|
||||||
'simple-icons:apple',
|
'simple-icons:apple',
|
||||||
'simple-icons:linux',
|
'simple-icons:linux',
|
||||||
'simple-icons:windows',
|
'simple-icons:windows',
|
||||||
...contentIcons.icons,
|
],
|
||||||
])],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
|
|||||||
Reference in New Issue
Block a user