From 989d4f1978902edd4f8e2efff8eda6f6af3d3eb7 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Mon, 27 Jul 2026 07:15:58 +0000 Subject: [PATCH 1/3] refactor: centralize dock-icon class map in shared design helpers The vite and next hub playgrounds each hand-maintained an identical copy of the dock-icon -> UnoCSS class map. Move it into the shared design/design.ts class-helper module (already the one source of truth for devframe -> @antfu/design class chains) and have both examples' icons.ts re-export it, matching the existing local design.ts re-export pattern used by every other plugin/example. --- design/design.ts | 37 ++++++++++++++++++ .../src/client/app/icons.ts | 39 ++----------------- .../src/client/icons.ts | 39 ++----------------- 3 files changed, 45 insertions(+), 70 deletions(-) diff --git a/design/design.ts b/design/design.ts index bb10822e..c6c41d2b 100644 --- a/design/design.ts +++ b/design/design.ts @@ -13,6 +13,43 @@ export function cx(...parts: Array): string { return parts.filter(Boolean).join(' ') } +// ── Dock icons ────────────────────────────────────────────────────────────── +// Map a devframe dock `icon` (e.g. `ph:git-branch-duotone`) to a UnoCSS +// `preset-icons` class. Keeping the class strings literal lets UnoCSS +// statically extract them and inline only these glyphs from `@iconify-json/ph` +// at build time — the same icon strategy vite-devtools uses. This is the one +// source of truth every hub shell (vite, next, storybook, …) shares instead of +// hand-maintaining its own copy — add a row here to support another dock icon. +const DOCK_ICON_CLASS: Record = { + 'ph:git-branch-duotone': 'i-ph-git-branch-duotone', + 'ph:terminal-window-duotone': 'i-ph-terminal-window-duotone', + 'ph:code-duotone': 'i-ph-code-duotone', + 'ph:stethoscope-duotone': 'i-ph-stethoscope-duotone', + 'ph:wheelchair-duotone': 'i-ph-wheelchair-duotone', + 'ph:rocket-duotone': 'i-ph-rocket-duotone', + 'ph:wrench-duotone': 'i-ph-wrench-duotone', + 'ph:plug-duotone': 'i-ph-plug-duotone', + 'ph:layout-duotone': 'i-ph-layout-duotone', + 'ph:note-pencil-duotone': 'i-ph-note-pencil-duotone', + 'ph:squares-four-duotone': 'i-ph-squares-four-duotone', + 'ph:house-duotone': 'i-ph-house-duotone', + 'ph:puzzle-piece-duotone': 'i-ph-puzzle-piece-duotone', + 'ph:clock-duotone': 'i-ph-clock-duotone', + 'ph:gear-duotone': 'i-ph-gear-duotone', + 'ph:sliders-horizontal-duotone': 'i-ph-sliders-horizontal-duotone', +} + +/** + * Resolve a dock icon to its UnoCSS class, or an empty string when the icon + * isn't mapped (the caller falls back to a text initial). + */ +export function iconClass(name: string | { light: string, dark: string } | undefined): string { + if (!name) + return '' + const id = typeof name === 'string' ? name : name.light + return DOCK_ICON_CLASS[id] ?? '' +} + export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link' export type ButtonSize = 'md' | 'sm' | 'lg' export interface ButtonProps { variant?: ButtonVariant, size?: ButtonSize, class?: string } diff --git a/examples/minimal-next-devframe-hub/src/client/app/icons.ts b/examples/minimal-next-devframe-hub/src/client/app/icons.ts index be6d4885..488d8111 100644 --- a/examples/minimal-next-devframe-hub/src/client/app/icons.ts +++ b/examples/minimal-next-devframe-hub/src/client/app/icons.ts @@ -1,35 +1,4 @@ -// @unocss-include -// Map a devframe dock `icon` (e.g. `ph:git-branch-duotone`) to a UnoCSS -// `preset-icons` class. Keeping the class strings literal lets UnoCSS -// statically extract them and inline only these glyphs from `@iconify-json/ph` -// at build time — the same icon strategy vite-devtools uses. Add a row here to -// support another dock icon. -const ICON_CLASS: Record = { - 'ph:git-branch-duotone': 'i-ph-git-branch-duotone', - 'ph:terminal-window-duotone': 'i-ph-terminal-window-duotone', - 'ph:code-duotone': 'i-ph-code-duotone', - 'ph:stethoscope-duotone': 'i-ph-stethoscope-duotone', - 'ph:wheelchair-duotone': 'i-ph-wheelchair-duotone', - 'ph:rocket-duotone': 'i-ph-rocket-duotone', - 'ph:wrench-duotone': 'i-ph-wrench-duotone', - 'ph:plug-duotone': 'i-ph-plug-duotone', - 'ph:layout-duotone': 'i-ph-layout-duotone', - 'ph:note-pencil-duotone': 'i-ph-note-pencil-duotone', - 'ph:squares-four-duotone': 'i-ph-squares-four-duotone', - 'ph:house-duotone': 'i-ph-house-duotone', - 'ph:puzzle-piece-duotone': 'i-ph-puzzle-piece-duotone', - 'ph:clock-duotone': 'i-ph-clock-duotone', - 'ph:gear-duotone': 'i-ph-gear-duotone', - 'ph:sliders-horizontal-duotone': 'i-ph-sliders-horizontal-duotone', -} - -/** - * Resolve a dock icon to its UnoCSS class, or an empty string when the icon - * isn't mapped (the caller falls back to a text initial). - */ -export function iconClass(name: string | { light: string, dark: string } | undefined): string { - if (!name) - return '' - const id = typeof name === 'string' ? name : name.light - return ICON_CLASS[id] ?? '' -} +// Re-exports the shared devframe dock-icon → UnoCSS class map (see +// `design/design.ts`) so this surface stays in lockstep with every other hub +// shell instead of hand-maintaining its own copy of the icon table. +export { iconClass } from '../../../../../design/design' diff --git a/examples/minimal-vite-devframe-hub/src/client/icons.ts b/examples/minimal-vite-devframe-hub/src/client/icons.ts index be6d4885..31d6408a 100644 --- a/examples/minimal-vite-devframe-hub/src/client/icons.ts +++ b/examples/minimal-vite-devframe-hub/src/client/icons.ts @@ -1,35 +1,4 @@ -// @unocss-include -// Map a devframe dock `icon` (e.g. `ph:git-branch-duotone`) to a UnoCSS -// `preset-icons` class. Keeping the class strings literal lets UnoCSS -// statically extract them and inline only these glyphs from `@iconify-json/ph` -// at build time — the same icon strategy vite-devtools uses. Add a row here to -// support another dock icon. -const ICON_CLASS: Record = { - 'ph:git-branch-duotone': 'i-ph-git-branch-duotone', - 'ph:terminal-window-duotone': 'i-ph-terminal-window-duotone', - 'ph:code-duotone': 'i-ph-code-duotone', - 'ph:stethoscope-duotone': 'i-ph-stethoscope-duotone', - 'ph:wheelchair-duotone': 'i-ph-wheelchair-duotone', - 'ph:rocket-duotone': 'i-ph-rocket-duotone', - 'ph:wrench-duotone': 'i-ph-wrench-duotone', - 'ph:plug-duotone': 'i-ph-plug-duotone', - 'ph:layout-duotone': 'i-ph-layout-duotone', - 'ph:note-pencil-duotone': 'i-ph-note-pencil-duotone', - 'ph:squares-four-duotone': 'i-ph-squares-four-duotone', - 'ph:house-duotone': 'i-ph-house-duotone', - 'ph:puzzle-piece-duotone': 'i-ph-puzzle-piece-duotone', - 'ph:clock-duotone': 'i-ph-clock-duotone', - 'ph:gear-duotone': 'i-ph-gear-duotone', - 'ph:sliders-horizontal-duotone': 'i-ph-sliders-horizontal-duotone', -} - -/** - * Resolve a dock icon to its UnoCSS class, or an empty string when the icon - * isn't mapped (the caller falls back to a text initial). - */ -export function iconClass(name: string | { light: string, dark: string } | undefined): string { - if (!name) - return '' - const id = typeof name === 'string' ? name : name.light - return ICON_CLASS[id] ?? '' -} +// Re-exports the shared devframe dock-icon → UnoCSS class map (see +// `design/design.ts`) so this surface stays in lockstep with every other hub +// shell instead of hand-maintaining its own copy of the icon table. +export { iconClass } from '../../../../design/design' From e6cb0db9e7513aa01e63ad920da1e378a4b100cd Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Mon, 27 Jul 2026 07:34:21 +0000 Subject: [PATCH 2/3] refactor: replace dock-icon class map with a runtime Iconify SVG fetch Port @antfu/design's DisplayIconifyRemoteIcon (utils/iconify.ts's getIconifySvg) instead of a hand-maintained ph:icon -> UnoCSS-class table: design/dock-icon.ts resolves a dock icon id to its sanitized SVG, fetched live from api.iconify.design, so any Iconify id renders with no @iconify-json/* collection to install and no table to keep in sync. The vite hub patches the resolved SVG into the dock button once it settles; the Next hub does the same via a small effect hook. --- design/design.ts | 37 -------------- design/dock-icon.ts | 45 +++++++++++++++++ .../minimal-next-devframe-hub/package.json | 1 + .../src/client/app/icons.ts | 9 ++-- .../src/client/app/page.tsx | 31 ++++++++++-- .../minimal-vite-devframe-hub/package.json | 1 + .../src/client/icons.ts | 9 ++-- .../src/client/main.ts | 48 ++++++++++++++++--- pnpm-lock.yaml | 6 +++ 9 files changed, 131 insertions(+), 56 deletions(-) create mode 100644 design/dock-icon.ts diff --git a/design/design.ts b/design/design.ts index c6c41d2b..bb10822e 100644 --- a/design/design.ts +++ b/design/design.ts @@ -13,43 +13,6 @@ export function cx(...parts: Array): string { return parts.filter(Boolean).join(' ') } -// ── Dock icons ────────────────────────────────────────────────────────────── -// Map a devframe dock `icon` (e.g. `ph:git-branch-duotone`) to a UnoCSS -// `preset-icons` class. Keeping the class strings literal lets UnoCSS -// statically extract them and inline only these glyphs from `@iconify-json/ph` -// at build time — the same icon strategy vite-devtools uses. This is the one -// source of truth every hub shell (vite, next, storybook, …) shares instead of -// hand-maintaining its own copy — add a row here to support another dock icon. -const DOCK_ICON_CLASS: Record = { - 'ph:git-branch-duotone': 'i-ph-git-branch-duotone', - 'ph:terminal-window-duotone': 'i-ph-terminal-window-duotone', - 'ph:code-duotone': 'i-ph-code-duotone', - 'ph:stethoscope-duotone': 'i-ph-stethoscope-duotone', - 'ph:wheelchair-duotone': 'i-ph-wheelchair-duotone', - 'ph:rocket-duotone': 'i-ph-rocket-duotone', - 'ph:wrench-duotone': 'i-ph-wrench-duotone', - 'ph:plug-duotone': 'i-ph-plug-duotone', - 'ph:layout-duotone': 'i-ph-layout-duotone', - 'ph:note-pencil-duotone': 'i-ph-note-pencil-duotone', - 'ph:squares-four-duotone': 'i-ph-squares-four-duotone', - 'ph:house-duotone': 'i-ph-house-duotone', - 'ph:puzzle-piece-duotone': 'i-ph-puzzle-piece-duotone', - 'ph:clock-duotone': 'i-ph-clock-duotone', - 'ph:gear-duotone': 'i-ph-gear-duotone', - 'ph:sliders-horizontal-duotone': 'i-ph-sliders-horizontal-duotone', -} - -/** - * Resolve a dock icon to its UnoCSS class, or an empty string when the icon - * isn't mapped (the caller falls back to a text initial). - */ -export function iconClass(name: string | { light: string, dark: string } | undefined): string { - if (!name) - return '' - const id = typeof name === 'string' ? name : name.light - return DOCK_ICON_CLASS[id] ?? '' -} - export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link' export type ButtonSize = 'md' | 'sm' | 'lg' export interface ButtonProps { variant?: ButtonVariant, size?: ButtonSize, class?: string } diff --git a/design/dock-icon.ts b/design/dock-icon.ts new file mode 100644 index 00000000..7a9836af --- /dev/null +++ b/design/dock-icon.ts @@ -0,0 +1,45 @@ +// Framework-neutral port of @antfu/design's `DisplayIconifyRemoteIcon`: +// https://github.com/antfu/design/blob/main/packages/design/components/Display/DisplayIconifyRemoteIcon.vue +// +// Resolves a devframe dock `icon` (an Iconify `collection:icon` id, e.g. +// `ph:git-branch-duotone`) to its live, sanitized SVG markup, fetched from the +// public `api.iconify.design` CDN. Unlike a UnoCSS `preset-icons` class, this +// needs no `@iconify-json/*` collection installed and no hand-maintained +// id -> class table — any Iconify id just works, at the cost of a network +// round-trip on first render. We reuse @antfu/design's own fetcher, cache and +// sanitizer (`utils/iconify.ts`) rather than reimplementing them; only the id +// parsing and light/dark selection below are devframe-specific, mirroring the +// upstream Vue component's own `icon` prop parsing. Vue surfaces should render +// `DisplayIconifyRemoteIcon` directly instead of using this port. +import { getIconifySvg } from '@antfu/design/utils/iconify' + +// Mirrors DisplayIconifyRemoteIcon.vue's own `collection:icon` parse (with an +// optional `i-` prefix tolerated so a UnoCSS-style id also works). +const ICONIFY_ID = /^(?:i-)?([\w-]+):([\w-]+)$/ + +/** + * Resolve a dock icon (a `collection:icon` string, or a `{ light, dark }` + * pair — the `light` variant is fetched) to its sanitized SVG markup. + * + * Returns `undefined` when the id doesn't parse or the fetch fails, so the + * caller can fall back to a text initial. + * + * @example + * await dockIconSvg('ph:git-branch-duotone') // → '...' + */ +export async function dockIconSvg(name: string | { light: string, dark: string } | undefined): Promise { + const id = typeof name === 'string' ? name : name?.light + if (!id) + return undefined + const match = id.match(ICONIFY_ID) + if (!match) + return undefined + try { + return await getIconifySvg(match[1]!, match[2]!) + } + catch { + // A failed fetch (offline / flaky CDN) degrades to the text-initial + // fallback, not a thrown error out of a render path. + return undefined + } +} diff --git a/examples/minimal-next-devframe-hub/package.json b/examples/minimal-next-devframe-hub/package.json index 7edf0dfd..2fb5b770 100644 --- a/examples/minimal-next-devframe-hub/package.json +++ b/examples/minimal-next-devframe-hub/package.json @@ -23,6 +23,7 @@ "@json-render/react": "catalog:frontend", "colorjs.io": "catalog:frontend", "devframe": "workspace:*", + "dompurify": "catalog:frontend", "minimal-json-render": "workspace:*", "next": "catalog:frontend", "react": "catalog:frontend", diff --git a/examples/minimal-next-devframe-hub/src/client/app/icons.ts b/examples/minimal-next-devframe-hub/src/client/app/icons.ts index 488d8111..cc572f39 100644 --- a/examples/minimal-next-devframe-hub/src/client/app/icons.ts +++ b/examples/minimal-next-devframe-hub/src/client/app/icons.ts @@ -1,4 +1,5 @@ -// Re-exports the shared devframe dock-icon → UnoCSS class map (see -// `design/design.ts`) so this surface stays in lockstep with every other hub -// shell instead of hand-maintaining its own copy of the icon table. -export { iconClass } from '../../../../../design/design' +// Re-exports the shared devframe dock-icon resolver (see +// `design/dock-icon.ts`, a port of @antfu/design's `DisplayIconifyRemoteIcon`) +// so this surface stays in lockstep with every other hub shell instead of +// hand-maintaining its own icon table. +export { dockIconSvg } from '../../../../../design/dock-icon' diff --git a/examples/minimal-next-devframe-hub/src/client/app/page.tsx b/examples/minimal-next-devframe-hub/src/client/app/page.tsx index 722c0e08..f7949c42 100644 --- a/examples/minimal-next-devframe-hub/src/client/app/page.tsx +++ b/examples/minimal-next-devframe-hub/src/client/app/page.tsx @@ -13,7 +13,7 @@ import type { DevframeJsonRenderDockEntry } from '@devframes/json-render/hub' import { connectDevframe, createDevframeClientHost, FRAME_NAV_CHANNEL } from '@devframes/hub/client' import { useEffect, useMemo, useRef, useState } from 'react' import { createReactJsonRenderDockRenderer } from '../json-render/dock-renderer' -import { iconClass } from './icons' +import { dockIconSvg } from './icons' const HUB_BASE = '/__hub/' @@ -138,11 +138,32 @@ function createClientPlaygroundSpec(clientType: string): DevframeJsonRenderSpec } } -/** Render a dock icon, falling back to the title's initial when unmapped. */ +/** Fetches (and caches, for the component's lifetime) a dock icon's sanitized SVG. */ +function useDockIconSvg(icon: DevframeDockEntry['icon']): string | undefined { + const [svg, setSvg] = useState(undefined) + const key = typeof icon === 'string' ? icon : icon?.light + + useEffect(() => { + let cancelled = false + setSvg(undefined) + void dockIconSvg(icon).then((resolved) => { + if (!cancelled) + setSvg(resolved) + }) + return () => { + cancelled = true + } + // Re-fetch only when the icon id itself changes, not on every `icon` object identity. + }, [key]) + + return svg +} + +/** Render a dock icon, falling back to the title's initial while it loads or when unmapped. */ function DockIcon({ entry }: { entry: DevframeDockEntry }) { - const cls = iconClass(entry.icon) - if (cls) - return + const svg = useDockIconSvg(entry.icon) + if (svg) + return const initial = (entry.title?.[0] ?? '?').toUpperCase() return {initial} } diff --git a/examples/minimal-vite-devframe-hub/package.json b/examples/minimal-vite-devframe-hub/package.json index ceef72c7..8e0af049 100644 --- a/examples/minimal-vite-devframe-hub/package.json +++ b/examples/minimal-vite-devframe-hub/package.json @@ -25,6 +25,7 @@ "@devframes/plugin-terminals": "workspace:*", "colorjs.io": "catalog:frontend", "devframe": "workspace:*", + "dompurify": "catalog:frontend", "minimal-json-render": "workspace:*", "vue": "catalog:frontend" }, diff --git a/examples/minimal-vite-devframe-hub/src/client/icons.ts b/examples/minimal-vite-devframe-hub/src/client/icons.ts index 31d6408a..606d0e34 100644 --- a/examples/minimal-vite-devframe-hub/src/client/icons.ts +++ b/examples/minimal-vite-devframe-hub/src/client/icons.ts @@ -1,4 +1,5 @@ -// Re-exports the shared devframe dock-icon → UnoCSS class map (see -// `design/design.ts`) so this surface stays in lockstep with every other hub -// shell instead of hand-maintaining its own copy of the icon table. -export { iconClass } from '../../../../design/design' +// Re-exports the shared devframe dock-icon resolver (see +// `design/dock-icon.ts`, a port of @antfu/design's `DisplayIconifyRemoteIcon`) +// so this surface stays in lockstep with every other hub shell instead of +// hand-maintaining its own icon table. +export { dockIconSvg } from '../../../../design/dock-icon' diff --git a/examples/minimal-vite-devframe-hub/src/client/main.ts b/examples/minimal-vite-devframe-hub/src/client/main.ts index 994df9c7..75efd012 100644 --- a/examples/minimal-vite-devframe-hub/src/client/main.ts +++ b/examples/minimal-vite-devframe-hub/src/client/main.ts @@ -9,7 +9,7 @@ import type { DevframeJsonRenderSpec } from '@devframes/json-render' import type { DevframeJsonRenderDockEntry } from '@devframes/json-render/hub' import { connectDevframe, createDevframeClientHost, FRAME_NAV_CHANNEL } from '@devframes/hub/client' import { createJsonRenderDockRenderer } from '@devframes/json-render-ui' -import { iconClass } from './icons' +import { dockIconSvg } from './icons' import 'virtual:uno.css' import '@antfu/design/styles.css' @@ -40,13 +40,48 @@ function renderList(host: HTMLElement, items: readonly T[], render: (item: T) host.innerHTML = items.map(render).join('') } -/** Render a dock icon, falling back to the title's initial when unknown. */ +// Session-lifetime cache of resolved dock-icon SVGs, keyed by the icon id +// (e.g. `ph:git-branch-duotone`) — `dockIconSvg` itself caches per fetch, this +// just lets a re-render (e.g. a badge update) paint an already-resolved icon +// synchronously instead of flashing the fallback initial again. +const dockIconCache = new Map() + +function dockIconKey(icon: DevframeDockEntry['icon']): string | undefined { + return typeof icon === 'string' ? icon : icon?.light +} + +/** Render a dock icon's placeholder markup: a resolved SVG if cached, else the title's initial. */ function dockIcon(entry: DevframeDockEntry): string { - const cls = iconClass(entry.icon) - if (cls) - return `` + const key = entry.icon ? dockIconKey(entry.icon) : undefined + const cached = key ? dockIconCache.get(key) : undefined + if (cached) + return `${cached}` const initial = (entry.title?.[0] ?? '?').toUpperCase() - return `${initial}` + return `${initial}` +} + +/** + * Resolve (and cache) each unresolved dock's icon SVG, then patch just that + * dock's `[data-dock-icon]` element in place — no full re-render, since the + * fetch is async and the dock list may already be showing by the time it + * settles. A dock with no icon or an unparsable/failed fetch keeps its + * fallback initial. + */ +async function hydrateDockIcons(list: readonly DevframeDockEntry[]): Promise { + await Promise.all(list.map(async (entry) => { + const key = entry.icon ? dockIconKey(entry.icon) : undefined + if (!key || dockIconCache.has(key)) + return + const svg = await dockIconSvg(entry.icon) ?? null + dockIconCache.set(key, svg) + if (!svg) + return + const el = docksEl.querySelector(`[data-dock-icon="${entry.id}"]`) + if (el) { + el.className = 'h-5 w-5 shrink-0 text-lg' + el.innerHTML = svg + } + })) } function isIframeDock(d: DevframeDockEntry): d is DevframeDockEntry & { type: 'iframe', url: string } { @@ -315,6 +350,7 @@ async function main() { renderList(docksEl, list, d => `
  • `) + void hydrateDockIcons(list) void showSelection(list) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a131c1fe..9a72ad63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -562,6 +562,9 @@ importers: devframe: specifier: workspace:* version: link:../../packages/devframe + dompurify: + specifier: catalog:frontend + version: 3.4.12 minimal-json-render: specifier: workspace:* version: link:../minimal-json-render @@ -647,6 +650,9 @@ importers: devframe: specifier: workspace:* version: link:../../packages/devframe + dompurify: + specifier: catalog:frontend + version: 3.4.12 minimal-json-render: specifier: workspace:* version: link:../minimal-json-render From c61f9bcd4c6279d1d1c401cd1b53fb21c268365f Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Mon, 27 Jul 2026 07:38:43 +0000 Subject: [PATCH 3/3] fix: transpile @antfu/design in the Next hub build Next/Turbopack has no loader for the bare .ts source @antfu/design ships (utils/iconify.ts, now imported by design/dock-icon.ts) and fails the production build with "Unknown module type". Add it to transpilePackages so Next runs the package through its own transform, matching how Vite already handles it. --- .../minimal-next-devframe-hub/src/client/next.config.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/minimal-next-devframe-hub/src/client/next.config.mjs b/examples/minimal-next-devframe-hub/src/client/next.config.mjs index af0d54b8..766add98 100644 --- a/examples/minimal-next-devframe-hub/src/client/next.config.mjs +++ b/examples/minimal-next-devframe-hub/src/client/next.config.mjs @@ -1,6 +1,12 @@ /** @type {import('next').NextConfig} */ const nextConfig = { images: { unoptimized: true }, + // @antfu/design ships raw, uncompiled `.ts`/`.vue` source (see its README — + // "no bundling, your build compiles it"). `dockIconSvg` (design/dock-icon.ts) + // imports its `utils/iconify.ts` directly, so Next/Turbopack — which + // otherwise treats node_modules as pre-built and has no loader for a bare + // `.ts` file there — needs to run this package through its own transform. + transpilePackages: ['@antfu/design'], // The workspace typecheck owns source-level project references. typescript: { ignoreBuildErrors: true }, // Mounted devframe SPAs are served at `/__/` and reference their assets