Compare commits

...
Author SHA1 Message Date
Eric f00986df24 upgrade to nitro 3 2026-11-06 13:05:36 +01:00
Eric 0052a4e012 Merge branch 'vitepress-2-upgrade' into vitepress-nitro-plugin 2026-11-06 12:35:09 +01:00
Eric da870c8119 Fix ts imports 2026-11-06 12:34:58 +01:00
Eric 3d4c190134 Merge branch 'vitepress-2-upgrade' into vitepress-nitro-plugin 2026-11-06 12:28:19 +01:00
Eric ce082fe4a9 Merge branch 'main' of https://github.com/lucide-icons/lucide into vitepress-nitro-plugin 2026-11-06 12:09:27 +01:00
Eric 66f607629d Fix types and imports 2026-11-06 12:00:45 +01:00
Claude 43e9f059d7 chore(docs): upgrade to VitePress 2
- Bump vitepress to 2.0.0-alpha.21
- Drop direct @docsearch/js and @docsearch/css deps; VitePress 2 ships
  DocSearch v4 and imports its styles itself
- VPNavBarSearch override: use the new VPAlgoliaSearchBox props
  (algoliaOptions + openRequest) instead of dispatching synthetic
  keydown events
- VPCarbonAds override: replace removed useAside composable with a
  media query, matching upstream
- VPFooter override: use useLayout from vitepress/theme instead of the
  removed useSidebar().hasSidebar

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G9u8FhZukDbCKe1ZFByn43
2026-11-02 18:08:55 +00:00
Eric Fennis 0a3b505812 Make it work 2026-07-17 18:17:59 +02:00
Eric Fennis f7adb61d78 update lockfile 2026-07-17 16:48:12 +02:00
Eric Fennis ca9d8d693a Merge branch 'main' of https://github.com/lucide-icons/lucide into vitepress-nitro-plugin 2026-07-17 16:48:06 +02:00
Eric Fennis c46ecc3230 Merge branch 'main' of https://github.com/lucide-icons/lucide into vitepress-nitro-plugin 2026-03-05 18:45:17 +01:00
Eric Fennis 3c7c64b583 uh 2026-03-05 13:32:53 +01:00
Eric Fennis 9beea31e3f Fix types 2026-03-05 13:07:02 +01:00
Eric Fennis ee8b855533 Update lockfile 2026-03-05 12:41:46 +01:00
Eric Fennis 9667df9f59 Merge branch 'update-nitro' of https://github.com/lucide-icons/lucide into vitepress-nitro-plugin 2026-03-05 12:39:33 +01:00
Eric Fennis 82f13daeb2 Merge branch 'main' of https://github.com/lucide-icons/lucide into update-nitro 2026-03-05 12:39:07 +01:00
Eric Fennis 7d58a7a6cd Disable public directory in nitro build 2026-03-05 12:36:44 +01:00
Eric Fennis f59954da43 update docs 2026-03-05 12:29:23 +01:00
Eric Fennis 11e50e10cc Move api directory 2026-03-05 12:27:58 +01:00
Eric Fennis 8249335371 make it work 2026-03-05 11:12:22 +01:00
Eric Fennis a0ea6a6550 Merge branch 'update-nitro' of https://github.com/lucide-icons/lucide into vitepress-nitro-plugin 2026-03-05 10:17:54 +01:00
Eric Fennis ac707c99f2 Merge branch 'main' of https://github.com/lucide-icons/lucide into update-nitro 2026-03-02 07:25:16 +01:00
Eric Fennis 839cc915af Format code 2026-02-26 17:34:02 +01:00
Eric Fennis 5f2ccade97 Try @analogjs/vite-plugin-nitro 2026-02-26 17:32:31 +01:00
Eric Fennis 2bdcec6c0a Remove @resvg/resvg-wasm 2026-02-26 17:21:11 +01:00
Eric Fennis 526aacb192 Fix types 2026-02-26 17:08:02 +01:00
Eric Fennis 68a7c22cbd Adjust react in api 2026-02-26 16:57:10 +01:00
Eric Fennis 0020c21fd6 Fix output 2026-02-26 15:46:46 +01:00
Eric Fennis 40dd406524 Adjust 404 catch all 2026-02-26 15:31:29 +01:00
Eric Fennis 68afe65eef Fix endpoints 2026-02-26 15:17:44 +01:00
Eric Fennis 896e43c732 Add await 2026-02-26 14:59:48 +01:00
Eric Fennis 2203a715cd update vercel output 2026-02-26 14:54:30 +01:00
Eric Fennis e6d4ac6b26 Try this 2026-02-26 14:45:55 +01:00
Eric Fennis 9bf36567f9 try this? 2026-02-26 12:33:53 +01:00
Eric Fennis 8c60563eea update nitro 2026-02-26 12:08:05 +01:00
50 changed files with 503 additions and 859 deletions
+1 -1
View File
@@ -175,7 +175,7 @@ cd docs
2. **Start the Local Development Server**
```sh
pnpm run docs:dev
pnpm dev
```
3. **Open the Website Locally**
@@ -1,11 +0,0 @@
import { eventHandler, setResponseHeader } from 'h3';
import iconMetaData from '../../data/iconMetaData';
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
return Object.fromEntries(
Object.entries(iconMetaData).map(([name, { categories }]) => [name, categories]),
);
});
@@ -1,14 +0,0 @@
import { eventHandler, setResponseHeader } from 'h3';
import iconMetaData from '../../../data/lab/iconMetaData';
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
for (const iconDetail in iconMetaData) {
// Remove $schema from the response
delete iconMetaData[iconDetail]['$schema'];
}
return iconMetaData;
});
-9
View File
@@ -1,9 +0,0 @@
import { eventHandler, setResponseHeader } from 'h3';
import iconMetaData from '../../data/iconMetaData';
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
return Object.fromEntries(Object.entries(iconMetaData).map(([name, { tags }]) => [name, tags]));
});
+33 -7
View File
@@ -1,15 +1,17 @@
import { fileURLToPath, URL } from 'node:url';
import type { Plugin } from 'vite';
import { defineConfig, UserConfig } from 'vitepress';
import { groupIconMdPlugin, groupIconVitePlugin } from 'vitepress-plugin-group-icons';
import sidebar from './sidebar';
import snackPlayer from './markdown/snackPlayer';
import sandpackPlugin from './markdown/sandpack';
import examplePlugin from './markdown/example';
import sidebar from './sidebar/index.ts';
import { nitro } from 'nitro/vite';
import snackPlayer from './markdown/snackPlayer.ts';
import sandpackPlugin from './markdown/sandpack.ts';
import examplePlugin from './markdown/example.ts';
import { readFile } from 'node:fs/promises';
import { resourcesSidebar } from './sidebar/resources';
import { resourcesSidebar } from './sidebar/resources.ts';
import llmstxt from 'vitepress-plugin-llms';
import { transformPageData } from './transformPageData';
import getHeadConfig from './getHeadConfig';
import { transformPageData } from './transformPageData.ts';
import getHeadConfig from './getHeadConfig.ts';
const defaultSandpackCSS = await readFile(
fileURLToPath(new URL('./theme/sandpack-default.css', import.meta.url)),
@@ -24,6 +26,26 @@ const title = 'Lucide';
const socialTitle = 'Lucide Icons';
const description = 'Beautiful & consistent icon toolkit made by the community.';
const isDev = process.env.NODE_ENV !== 'production';
/**
* Nitro's dev middleware assumes it owns the app: it answers every extensionless request and
* never falls through, so VitePress pages would 404. `_nitroHandled` is the flag it checks to
* skip a request. It is private, so re-check this when upgrading Nitro.
*/
const nitroApiOnly: Plugin = {
name: 'lucide:nitro-api-only',
apply: 'serve',
configureServer(server) {
server.middlewares.use((req, _res, next) => {
if (!req.url?.startsWith('/api/')) {
(req as typeof req & { _nitroHandled?: boolean })._nitroHandled = true;
}
next();
});
},
};
// https://vitepress.dev/reference/site-config
export default defineConfig({
title,
@@ -48,6 +70,8 @@ export default defineConfig({
},
},
vite: {
// Nitro defaults this to 'custom', which drops the HTML fallback VitePress relies on in dev.
appType: 'spa',
resolve: {
alias: [
{
@@ -81,6 +105,8 @@ export default defineConfig({
],
},
plugins: [
isDev && nitroApiOnly,
isDev && nitro(),
groupIconVitePlugin(),
llmstxt({
ignoreFiles: [
+5 -3
View File
@@ -16,8 +16,10 @@ export type Path = {
export type PathProps<
RequiredProps extends keyof React.SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
NeverProps extends keyof React.SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
> = Required<Pick<React.SVGProps<SVGElement & SVGRectElement & SVGCircleElement>, RequiredProps>> &
> = Required<
Pick<React.SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>, RequiredProps>
> &
Omit<
React.SVGProps<SVGPathElement & SVGRectElement & SVGCircleElement>,
RequiredProps & NeverProps
React.SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
RequiredProps | NeverProps
>;
+1 -1
View File
@@ -1,6 +1,6 @@
import fs from 'fs';
import path from 'path';
import { Category, IconEntity } from '../theme/types';
import type { Category, IconEntity } from '../theme/types.ts';
const directory = path.join(process.cwd(), '../categories');
@@ -1,4 +1,4 @@
import { highLightCode } from './createCodeExamples';
import { highLightCode } from './createCodeExamples.ts';
type CodeExampleType = {
title: string;
+1 -1
View File
@@ -1,6 +1,6 @@
import { promises as fs, constants } from 'fs';
import path from 'path';
import { PackageItem } from '../theme/types';
import { type PackageItem } from '../theme/types.ts';
const fileExist = (filePath) =>
fs
+1 -1
View File
@@ -1,6 +1,6 @@
import { readFileSync } from 'node:fs';
import { dirname, isAbsolute, resolve } from 'node:path';
import type { MarkdownIt, RenderRule } from './types';
import type { MarkdownIt, RenderRule } from './types.ts';
import container from 'markdown-it-container';
type ContainerArgs = [typeof container, string, { render: RenderRule }];
+2 -2
View File
@@ -1,6 +1,6 @@
import type { MarkdownIt, RenderRule, Token } from './types';
import type { MarkdownIt, RenderRule, Token } from './types.ts';
import container from 'markdown-it-container';
import sandpackTheme from '../theme/sandpackTheme.json';
import sandpackTheme from '../theme/sandpackTheme.json' with { type: 'json' };
type SnackParams = {
defaultFiles?: Record<
+1 -1
View File
@@ -16,7 +16,7 @@
* />
*/
import type { MarkdownIt } from './types';
import type { MarkdownIt } from './types.ts';
type SnackParams = Record<string, string>;
+11 -11
View File
@@ -1,15 +1,15 @@
import { DefaultTheme, UserConfig } from 'vitepress';
import { reactSidebar } from './react';
import { vueSidebar } from './vue';
import { svelteSidebar } from './svelte';
import { lucideSidebar } from './lucide';
import { solidSidebar } from './solid';
import { preactSidebar } from './preact';
import { reactNativeSidebar } from './react-native';
import { astroSidebar } from './astro';
import { resourcesSidebar } from './resources';
import { angularSidebar } from './angular';
import { lucideStaticSidebar } from './static';
import { reactSidebar } from './react.ts';
import { vueSidebar } from './vue.ts';
import { svelteSidebar } from './svelte.ts';
import { lucideSidebar } from './lucide.ts';
import { solidSidebar } from './solid.ts';
import { preactSidebar } from './preact.ts';
import { reactNativeSidebar } from './react-native.ts';
import { astroSidebar } from './astro.ts';
import { resourcesSidebar } from './resources.ts';
import { angularSidebar } from './angular.ts';
import { lucideStaticSidebar } from './static.ts';
type Sidebar = UserConfig<DefaultTheme.Config>['themeConfig']['sidebar'];
@@ -1,4 +1,4 @@
import iconNodes from '../../../data/iconNodes';
import iconNodes from '../../../data/iconNodes/index.ts';
const pickIcons = [
'bot',
@@ -1,5 +1,5 @@
import { getAllCategoryFiles, mapCategoryIconCount } from '../../../lib/categories';
import iconsMetaData from '../../../data/iconMetaData';
import { getAllCategoryFiles, mapCategoryIconCount } from '../../../lib/categories.ts';
import iconsMetaData from '../../../data/iconMetaData.ts';
const loader = {
async load() {
@@ -17,11 +17,11 @@ function onCategoriesClick() {
}
const categoriesIsActive = computed(() => {
return isActive(page.value.relativePath, '/icons/categories');
return isActive(page.value.relativePath, '', '/icons/categories');
});
const overviewIsActive = computed(() => {
return isActive(page.value.relativePath, '/icons/');
return isActive(page.value.relativePath, '', '/icons/');
});
const headers = computed(() => {
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { ref, watch, onMounted } from 'vue'
import { useAside } from 'vitepress/dist/client/theme-default/composables/aside.js'
import { useMediaQuery } from '@vueuse/core'
import { useData } from 'vitepress/dist/client/theme-default/composables/data.js'
const { page } = useData()
@@ -12,7 +12,7 @@ const props = defineProps<{
const carbonOptions = props.carbonAds
const { isAsideEnabled } = useAside()
const isAsideEnabled = useMediaQuery('(min-width: 80rem)')
const container = ref()
let isInitialized = false
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { useData } from 'vitepress';
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar.js';
import { useLayout } from 'vitepress/theme';
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue';
import { computed } from 'vue';
import SurveyAskOverlay from '../SurveyAskOverlay.vue';
const { theme } = useData();
const { hasSidebar } = useSidebar();
const { hasSidebar } = useLayout();
const githubLink = computed(
() => theme.value.socialLinks.find(({ icon }) => icon === 'github').link,
@@ -2,31 +2,33 @@
import { ref } from 'vue'
// This component can be rendered more than once on a page (e.g. a hidden
// copy inside `.content-body`). DocSearch mounts into the first `#docsearch`
// in the document, so only a single instance may ever render the search box.
// copy inside `.content-body`). DocSearch mounts into `#vp-docsearch`, so only
// a single instance may ever render the search box.
const loaded = ref(false)
// Shared so that any instance (e.g. the visible input) can open the modal
// rendered by the owning instance.
const openRequest = ref<{ target: 'search'; nonce: number } | null>(null)
let openNonce = 0
// Every instance registers the hotkeys; only handle each event once.
let lastHandledEvent: KeyboardEvent | null = null
</script>
<script lang="ts" setup>
import '@docsearch/css/dist/style.css'
import {
computed,
defineAsyncComponent,
onMounted,
onUnmounted,
} from 'vue'
import { computed, defineAsyncComponent, onMounted } from 'vue'
import { onKeyStroke } from '@vueuse/core'
import type { DefaultTheme } from 'vitepress/theme'
import { useData } from 'vitepress'
import FakeInput from '../base/FakeInput.vue'
const VPAlgoliaSearchBox = defineAsyncComponent(() => import('vitepress/dist/client/theme-default/components/VPAlgoliaSearchBox.vue'));
const VPAlgoliaSearchBox = defineAsyncComponent(
() => import('vitepress/dist/client/theme-default/components/VPAlgoliaSearchBox.vue'),
)
const { theme } = useData<DefaultTheme.Config>()
const algolia = computed(
() =>
(theme.value.search?.options ?? theme.value.algolia) as DefaultTheme.AlgoliaSearchOptions,
const algoliaOptions = computed(
() => (theme.value.search?.options ?? {}) as DefaultTheme.AlgoliaSearchOptions,
)
// whether this instance is the one rendering the (hidden) search box
@@ -36,47 +38,39 @@ const shortcutText = ref('')
// to avoid loading the docsearch js upfront (which is more than 1/3 of the
// payload), we delay initializing it until the user has actually clicked or
// hit the hotkey to invoke it.
const preconnect = () => {
onMounted(() => {
shortcutText.value = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘K' : 'Ctrl+K'
const id = 'VPAlgoliaPreconnect'
if (document.getElementById(id)) return
if (document.getElementById(id) || !algoliaOptions.value.appId) return
const rIC = window.requestIdleCallback || setTimeout
rIC(() => {
const preconnect = document.createElement('link')
preconnect.id = id
preconnect.rel = 'preconnect'
preconnect.href = `https://${algolia.value.appId}-dsn.algolia.net`
preconnect.href = `https://${algoliaOptions.value.appId}-dsn.algolia.net`
preconnect.crossOrigin = ''
document.head.appendChild(preconnect)
})
}
})
onMounted(() => {
preconnect()
shortcutText.value = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘K' : 'Ctrl+K'
// once docsearch is initialized it handles the hotkey itself
const handleSearchHotKey = (event: KeyboardEvent) => {
if (loaded.value) return remove()
if (
(event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) ||
(!isEditingContent(event) && event.key === '/')
) {
event.preventDefault()
open()
remove()
}
onKeyStroke('k', (event) => {
if (event === lastHandledEvent) return
if (event.ctrlKey || event.metaKey) {
lastHandledEvent = event
event.preventDefault()
open()
}
})
const remove = () => {
window.removeEventListener('keydown', handleSearchHotKey)
onKeyStroke('/', (event) => {
if (event === lastHandledEvent) return
if (!isEditingContent(event)) {
lastHandledEvent = event
event.preventDefault()
open()
}
window.addEventListener('keydown', handleSearchHotKey)
onUnmounted(remove)
})
function open() {
@@ -84,23 +78,8 @@ function open() {
loaded.value = true
isOwner.value = true
}
setTimeout(poll, 16)
}
function poll() {
// programmatically open the search box after initialize
const e = new Event('keydown') as any
e.key = 'k'
e.metaKey = true
window.dispatchEvent(e)
setTimeout(() => {
if (!document.querySelector('.DocSearch-Modal')) {
poll()
}
}, 16)
// The search box queues the request until DocSearch is ready.
openRequest.value = { target: 'search', nonce: ++openNonce }
}
function isEditingContent(event: KeyboardEvent): boolean {
@@ -129,7 +108,7 @@ function isEditingContent(event: KeyboardEvent): boolean {
<!-- docsearch renders its own button here; we only need its modal -->
<div v-if="isOwner" class="docsearch-host">
<VPAlgoliaSearchBox :algolia="algolia" />
<VPAlgoliaSearchBox :algolia-options="algoliaOptions" :open-request="openRequest" />
</div>
</div>
</template>
@@ -1,6 +1,6 @@
import packageDataList from '../../../data/packageData.json';
import thirdPartyPackages from '../../../data/packageData.thirdParty.json';
import fetchPackages from '../../../lib/fetchPackages';
import packageDataList from '../../../data/packageData.json' with { type: 'json' };
import thirdPartyPackages from '../../../data/packageData.thirdParty.json' with { type: 'json' };
import fetchPackages from '../../../lib/fetchPackages.ts';
const loader = {
async load() {
@@ -1,5 +1,5 @@
import iconNodes from '../../../data/iconNodes';
import fetchPackages from '../../../lib/fetchPackages';
import iconNodes from '../../../data/iconNodes/index.ts';
import fetchPackages from '../../../lib/fetchPackages.ts';
const NPM_DOWNLOADS_API = 'https://api.npmjs.org/downloads/point/last-week';
@@ -1,12 +1,9 @@
import { useFetch } from '@vueuse/core';
const useFetchCategories = () =>
useFetch<Record<string, string[]>>(
`${import.meta.env.DEV ? 'http://localhost:3000' : ''}/api/categories`,
{
immediate:
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('search'),
},
).json();
useFetch<Record<string, string[]>>('/api/categories', {
immediate:
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('search'),
}).json();
export default useFetchCategories;
@@ -1,12 +1,9 @@
import { useFetch } from '@vueuse/core';
const useFetchTags = () =>
useFetch<Record<string, string[]>>(
`${import.meta.env.DEV ? 'http://localhost:3000' : ''}/api/tags`,
{
immediate:
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('search'),
},
).json();
useFetch<Record<string, string[]>>('/api/tags', {
immediate:
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('search'),
}).json();
export default useFetchTags;
+2 -2
View File
@@ -1,6 +1,6 @@
import { type PageData } from 'vitepress';
import getStructuredData, { getFaqStructuredData } from './getStructuredData';
import { createGeneralOGImage, createIconOGImage } from './createOGImage';
import getStructuredData, { getFaqStructuredData } from './getStructuredData.ts';
import { createGeneralOGImage, createIconOGImage } from './createOGImage.ts';
export async function transformPageData(pageData: PageData) {
pageData.frontmatter.head ??= [];
+3 -10
View File
@@ -2,6 +2,7 @@
The Lucide docs website is built with Vitepress: https://vitepress.dev/
This is Markdown-based documentation powered by Vue.
For API endpoints, we use Nitro: https://nitro.unjs.io/ to create a custom API server.
## Development
@@ -11,10 +12,7 @@ pnpm install
```
```sh
# Start docs dev server
pnpm docs:dev
# Start api dev server
# Start dev server
pnpm dev
```
@@ -22,12 +20,7 @@ pnpm dev
```sh
# Build docs
pnpm docs:build
```
```sh
# Build api
pnpm build:api
pnpm build
```
## Components
+11
View File
@@ -0,0 +1,11 @@
import { defineHandler } from 'nitro/h3';
import iconMetaData from '~/.vitepress/data/iconMetaData';
export default defineHandler((event) => {
event.res.headers.set('Cache-Control', 'public, max-age=86400');
event.res.headers.set('Access-Control-Allow-Origin', '*');
return Object.fromEntries(
Object.entries(iconMetaData).map(([name, { categories }]) => [name, categories]),
);
});
@@ -1,8 +1,8 @@
import { eventHandler, setResponseHeader } from 'h3';
import iconNodes from '../../data/iconNodes/index.ts';
import iconMetaData from '../../data/iconMetaData';
import releaseMeta from '../../data/releaseMetaData.json';
import categories from '../../data/categoriesData.json';
import { defineHandler } from 'nitro/h3';
import iconNodes from '~/.vitepress/data/iconNodes/index.ts';
import iconMetaData from '~/.vitepress/data/iconMetaData';
import releaseMeta from '~/.vitepress/data/releaseMetaData.json';
import categories from '~/.vitepress/data/categoriesData.json';
const dataResponse = {
icons: Object.entries(iconNodes).reduce((acc, [name, iconNode]) => {
@@ -32,9 +32,9 @@ const dataResponse = {
categories,
};
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
export default defineHandler((event) => {
event.res.headers.set('Cache-Control', 'public, max-age=86400');
event.res.headers.set('Access-Control-Allow-Origin', '*');
return dataResponse;
});
@@ -1,12 +1,12 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import { defineHandler } from 'nitro/h3';
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
import React from 'react';
import SvgPreview from '../../lib/SvgPreview/index.tsx';
import iconNodes from '../../data/iconNodes';
import SvgPreview from '~/.vitepress/lib/SvgPreview/index.tsx';
import iconNodes from '~/.vitepress/data/iconNodes';
import createLucideIcon from 'lucide-react/src/createLucideIcon';
import Backdrop from '../../lib/SvgPreview/Backdrop.tsx';
import Backdrop from '~/.vitepress/lib/SvgPreview/Backdrop.tsx';
export default eventHandler((event) => {
export default defineHandler((event) => {
const { params } = event.context;
const pathData = params.data.split('/');
@@ -46,8 +46,8 @@ export default eventHandler((event) => {
);
}
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
event.res.headers.set('Content-Type', 'image/svg+xml');
event.res.headers.set('Cache-Control', 'public,max-age=31536000');
return renderToString(
<SvgPreview
@@ -1,11 +1,11 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import { defineHandler } from 'nitro/h3';
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
import React from 'react';
import Diff from '../../../lib/SvgPreview/Diff.tsx';
import iconNodes from '../../../data/iconNodes';
import Diff from '~/.vitepress/lib/SvgPreview/Diff.tsx';
import iconNodes from '~/.vitepress/data/iconNodes';
import createLucideIcon from 'lucide-react/src/createLucideIcon';
export default eventHandler((event) => {
export default defineHandler((event) => {
const { params } = event.context;
const pathData = params.data.split('/');
@@ -34,8 +34,8 @@ export default eventHandler((event) => {
.replace(/<svg[^>]*>|<\/svg>/g, '')
: '';
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
event.res.headers.set('Content-Type', 'image/svg+xml');
event.res.headers.set('Cache-Control', 'public,max-age=31536000');
return renderToString(
<Diff
@@ -1,11 +1,11 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import iconNodes from '../../../data/iconNodes';
import { defineHandler } from 'nitro/h3';
import iconNodes from '~/.vitepress/data/iconNodes';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import createLucideIcon from 'lucide-react/src/createLucideIcon';
import sharp from 'sharp';
export default eventHandler(async (event) => {
export default defineHandler(async (event) => {
const { params = {} } = event.context;
const imageSize = 96;
const name = params.data.split('/').at(-3);
@@ -42,8 +42,8 @@ export default eventHandler(async (event) => {
const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer();
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
event.res.headers.set('Content-Type', 'image/svg+xml');
event.res.headers.set('Cache-Control', 'public,max-age=31536000');
return `
<svg xmlns="http://www.w3.org/2000/svg" width="${imageSize}" height="${prevSvg ? imageSize * 2 : imageSize}" viewBox="0 0 ${imageSize} ${prevSvg ? imageSize * 2 : imageSize}">
@@ -1,11 +1,11 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import { defineHandler } from 'nitro/h3';
import { renderToString } from 'react-dom/server';
import React from 'react';
import Icon from 'lucide-react/src/Icon';
import type { IconNode } from 'lucide-react/src/types';
import { parseSync } from 'svgson';
export default eventHandler((event) => {
export default defineHandler((event) => {
const { params } = event.context;
const [strokeWidth, svgData] = params.data.split('/');
@@ -16,8 +16,8 @@ export default eventHandler((event) => {
({ name, attributes }) => [name, attributes],
) as IconNode;
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
event.res.headers.set('Content-Type', 'image/svg+xml');
event.res.headers.set('Cache-Control', 'public,max-age=31536000');
return renderToString(
<Icon
@@ -1,9 +1,9 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import { defineHandler } from 'nitro/h3';
import { renderToString } from 'react-dom/server';
import React from 'react';
import Diff from '../../../lib/SvgPreview/Diff.tsx';
import Diff from '~/.vitepress/lib/SvgPreview/Diff.tsx';
export default eventHandler((event) => {
export default defineHandler((event) => {
const { params } = event.context;
const pathData = params.data.split('/');
@@ -41,8 +41,8 @@ export default eventHandler((event) => {
return '';
}
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
event.res.headers.set('Content-Type', 'image/svg+xml');
event.res.headers.set('Cache-Control', 'public,max-age=31536000');
return renderToString(
<Diff
@@ -1,13 +1,13 @@
import { eventHandler, getQuery, setResponseHeader } from 'h3';
import iconNodes from '../../data/iconNodes';
import { defineHandler, getQuery } from 'nitro/h3';
import iconNodes from '~/.vitepress/data/iconNodes';
export default eventHandler((event) => {
export default defineHandler((event) => {
const query = getQuery(event);
const withUniqueKeys = query.withUniqueKeys === 'true';
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
event.res.headers.set('Cache-Control', 'public, max-age=86400');
event.res.headers.set('Access-Control-Allow-Origin', '*');
if (withUniqueKeys) {
return iconNodes;
@@ -1,21 +1,19 @@
import { eventHandler, getQuery, setResponseHeader, createError } from 'h3';
import iconNodes from '../../data/iconNodes';
import { defineHandler, getQuery, HTTPError } from 'nitro/h3';
import iconNodes from '~/.vitepress/data/iconNodes';
import createLucideIcon from 'lucide-react/src/createLucideIcon';
import { renderToString } from 'react-dom/server';
import { createElement } from 'react';
export default eventHandler((event) => {
export default defineHandler((event) => {
const { params } = event.context;
const iconNode = iconNodes[params.iconName];
if (iconNode == null) {
const error = createError({
statusCode: 404,
throw new HTTPError({
status: 404,
message: `Icon "${params.iconName}" not found`,
});
return sendError(event, error);
}
const query = getQuery<Record<string, string | undefined>>(event);
@@ -39,9 +37,9 @@ export default eventHandler((event) => {
),
).toString('utf8');
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
event.res.headers.set('Content-Type', 'image/svg+xml');
event.res.headers.set('Cache-Control', 'public,max-age=31536000');
event.res.headers.set('Access-Control-Allow-Origin', '*');
return svg;
});
@@ -1,10 +1,10 @@
import { eventHandler, setResponseHeader } from 'h3';
import iconMetaData from '../../../data/lab/iconMetaData';
import iconPopularity from '../../../data/lab/iconPopularity';
import iconNodes from '../../../data/lab/iconNodes';
import { defineHandler } from 'nitro/h3';
import iconMetaData from '~/.vitepress/data/lab/iconMetaData';
import iconPopularity from '~/.vitepress/data/lab/iconPopularity';
import iconNodes from '~/.vitepress/data/lab/iconNodes';
export default eventHandler((event) => {
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
export default defineHandler((event) => {
event.res.headers.set('Access-Control-Allow-Origin', '*');
return Object.fromEntries(
Object.entries(iconMetaData).map(([iconName, iconData]) => {
+14
View File
@@ -0,0 +1,14 @@
import { defineHandler } from 'nitro/h3';
import iconMetaData from '~/.vitepress/data/lab/iconMetaData';
export default defineHandler((event) => {
event.res.headers.set('Cache-Control', 'public, max-age=86400');
event.res.headers.set('Access-Control-Allow-Origin', '*');
for (const iconDetail in iconMetaData) {
// Remove $schema from the response
delete iconMetaData[iconDetail]['$schema'];
}
return iconMetaData;
});
@@ -1,14 +1,14 @@
import { eventHandler, getQuery, setResponseHeader } from 'h3';
import iconNodes from '../../../data/lab/iconNodes';
import { defineHandler, getQuery } from 'nitro/h3';
import iconNodes from '~/.vitepress/data/lab/iconNodes';
type IconNodeWithKeys = [elementName: string, attrs: Record<string, string> & { key?: string }][];
export default eventHandler((event) => {
export default defineHandler((event) => {
const query = getQuery(event);
const withUniqueKeys = query.withUniqueKeys === 'true';
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
event.res.headers.set('Cache-Control', 'public, max-age=86400');
event.res.headers.set('Access-Control-Allow-Origin', '*');
if (withUniqueKeys) {
return iconNodes;
+3
View File
@@ -0,0 +1,3 @@
export default async function render() {
return '';
}
+9
View File
@@ -0,0 +1,9 @@
import { defineHandler } from 'nitro/h3';
import iconMetaData from '~/.vitepress/data/iconMetaData';
export default defineHandler((event) => {
event.res.headers.set('Cache-Control', 'public, max-age=86400');
event.res.headers.set('Access-Control-Allow-Origin', '*');
return Object.fromEntries(Object.entries(iconMetaData).map(([name, { tags }]) => [name, tags]));
});
+2 -2
View File
@@ -1,6 +1,6 @@
<script setup>
import OverviewLink from '../../.vitepress/theme/components/base/OverviewLink.vue'
import OverviewLinkGrid from '../../.vitepress/theme/components/base/OverviewLinkGrid.vue'
import OverviewLink from '~/.vitepress/theme/components/base/OverviewLink.vue'
import OverviewLinkGrid from '~/.vitepress/theme/components/base/OverviewLinkGrid.vue'
</script>
# Getting started
+2 -2
View File
@@ -1,5 +1,5 @@
import { getAllCategoryFiles } from '../.vitepress/lib/categories';
import iconMetaData from '../.vitepress/data/iconMetaData';
import { getAllCategoryFiles } from '../.vitepress/lib/categories.ts';
import iconMetaData from '../.vitepress/data/iconMetaData.ts';
export default {
async load() {
+1 -1
View File
@@ -1,4 +1,4 @@
import createCodeExamples from '../.vitepress/lib/codeExamples/createCodeExamples';
import createCodeExamples from '../.vitepress/lib/codeExamples/createCodeExamples.ts';
export default {
async load() {
+1 -1
View File
@@ -1,4 +1,4 @@
import * as iconDetails from '../.vitepress/data/iconDetails';
import * as iconDetails from '../.vitepress/data/iconDetails/index.ts';
export default {
async load() {
+1 -1
View File
@@ -1,4 +1,4 @@
import createCodeExamples from '../../.vitepress/lib/codeExamples/createLabCodeExamples';
import createCodeExamples from '../../.vitepress/lib/codeExamples/createLabCodeExamples.ts';
export default {
async load() {
+4 -11
View File
@@ -1,15 +1,8 @@
export default defineNitroConfig({
import { defineConfig } from 'nitro';
export default defineConfig({
compatibilityDate: '2026-02-26',
preset: 'vercel',
srcDir: '.vitepress',
serverDir: './',
ignore: ['.vitepress/plugins'],
routeRules: {
'/api/**': { cors: false },
},
esbuild: {
options: {
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
},
},
});
+5 -9
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"docs:dev": "pnpm run /^prebuild:.*/ && vitepress dev",
"dev": "pnpm run /^prebuild:.*/ && vitepress dev",
"docs:build": "pnpm run /^prebuild:.*/ && vitepress build",
"docs:preview": "vitepress preview",
"build:docs": "NODE_OPTIONS=--max-old-space-size=8192 vitepress build",
@@ -21,9 +21,8 @@
"prebuild:writeLabIconNodes": "node ./scripts/writeLabIconNodes.mts",
"prebuild:writeLabIconMetaIndex": "node ./scripts/writeLabIconMetaIndex.mts",
"postbuild:vercelJson": "node ./scripts/writeVercelOutput.mts",
"dev": "nitropack dev",
"prebuild:api": "nitropack prepare",
"build:api": "NODE_OPTIONS=--max-old-space-size=8192 nitropack build",
"dev:api": "nitro dev",
"build:api": "NODE_OPTIONS=--max-old-space-size=8192 nitro build",
"build": "pnpm run /^prebuild:.*/ && pnpm run build:api && pnpm run build:docs && pnpm postbuild:vercelJson",
"preview": "node .output/server/index.mjs",
"typecheck": "vue-tsc --noEmit"
@@ -31,7 +30,6 @@
"author": "Eric Fennis",
"license": "ISC",
"devDependencies": {
"@docsearch/js": "3.8.2",
"@lucide/build-icons": "workspace:*",
"@lucide/helpers": "workspace:*",
"@lucide/shared": "workspace:*",
@@ -39,10 +37,9 @@
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@types/semver": "^7.8.0",
"h3": "^1.15.11",
"markdown-it": "^15.0.3",
"markdown-it-container": "^4.0.0",
"nitropack": "^2.13.5",
"nitro": "3.0.261103-rc",
"rollup-plugin-copy": "^3.5.0",
"semver": "^7.8.5",
"sharp": "^0.35.5",
@@ -53,13 +50,12 @@
"svg-pathdata": "^9.0.0",
"svgson": "^5.3.1",
"typescript": "^6.0.3",
"vitepress": "^1.6.4",
"vitepress": "2.0.0-alpha.21",
"vitepress-plugin-group-icons": "^1.7.6",
"vitepress-plugin-llms": "^1.13.5",
"vue-tsc": "^3.3.11"
},
"dependencies": {
"@docsearch/css": "3.8.2",
"@floating-ui/vue": "^2.0.1",
"@headlessui/vue": "^1.7.23",
"@lucide/vue": "workspace:*",
+2 -2
View File
@@ -35,7 +35,7 @@ const writeIconFiles = Object.entries(iconMetaData).map(async ([iconName, metaDa
import iconNode from '../iconNodes/${iconName}.node.json' with { type: 'json' };
import releaseData from '../releaseMetadata/${iconName}.json' with { type: 'json' };
import popularity from '../iconPopularity/${iconName}.json' with { type: 'json' };
import { IconMetaData, IconNode } from '~/.vitepress/theme/types';
import type { IconMetaData, IconNode } from '~/.vitepress/theme/types.ts';
const metaData: IconMetaData = ${JSON.stringify({ tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion })};
@@ -61,7 +61,7 @@ export default iconDetails;
await fs.promises.writeFile(location, contents, 'utf-8');
await fs.promises.appendFile(
indexFile,
`export { default as ${toCamelCase(iconName)} } from './${iconName}';\n`,
`export { default as ${toCamelCase(iconName)} } from './${iconName}.ts';\n`,
'utf-8',
);
});
+1 -1
View File
@@ -47,7 +47,7 @@ try {
await fs.promises.writeFile(
iconIndexFile,
`\
import type { IconNode } from '../../theme/types';
import type { IconNode } from '../../theme/types.ts';
${iconIndexFileImports.join('\n')}
${iconIndexFileNodes.join('\n')}
+7 -1
View File
@@ -1,6 +1,12 @@
{
"extends": "./.nitro/types/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": false,
"allowJs": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment",
+260 -606
View File
File diff suppressed because it is too large Load Diff