From d7d27282bae1650b7c152a21e51543a0a996a977 Mon Sep 17 00:00:00 2001 From: Pascal Garber Date: Wed, 15 Apr 2026 15:50:25 +0200 Subject: [PATCH 1/2] feat(iconset): resolve via package dependency, drop per-demo copies Elevate @ribajs/iconset to a proper package dependency that Vite resolves automatically. Demos no longer need to carry their own public/iconset copy. - packages/iconset: expose built assets through exports (./svg/*, ./png/*, ./svg.json, ./png.json) pointing into dist/, drop src/* export, make clean real, narrow files to /dist - infra/vite-config: export ribaIconsetPlugin with configurable baseUrl / outputDir, resolve dist/svg, throw when iconset is unbuilt - infra/doc: drop duplicate iconset plugin and alias, reuse shared plugin with /iconset mount; add @ribajs/vite-config devDependency - packages/bs5: migrate bs5-theme-button hardcoded icon paths to ?url imports from @ribajs/iconset/svg/*; add iconset dependency - root: enable topological workspace build ordering so iconset builds before consumers --- infra/doc/package.json | 1 + infra/doc/vite-plugin-doc-pages.js | 2 +- infra/doc/vite.config.js | 32 ++---------------- infra/vite-config/index.js | 33 ++++++++++++++----- package.json | 4 +-- packages/bs5/package.json | 1 + .../bs5-theme-button.component.ts | 6 ++-- packages/iconset/package.json | 9 ++--- yarn.lock | 2 ++ 9 files changed, 42 insertions(+), 48 deletions(-) diff --git a/infra/doc/package.json b/infra/doc/package.json index 0001362c8..6361a6d99 100644 --- a/infra/doc/package.json +++ b/infra/doc/package.json @@ -43,6 +43,7 @@ "devDependencies": { "@ribajs/eslint-config": "workspace:^", "@ribajs/tsconfig": "workspace:^", + "@ribajs/vite-config": "workspace:^", "@types/node": "^24.12.2", "@types/prismjs": "^1.26.6", "concurrently": "^9.2.1", diff --git a/infra/doc/vite-plugin-doc-pages.js b/infra/doc/vite-plugin-doc-pages.js index bd5cfab12..b15602e6b 100644 --- a/infra/doc/vite-plugin-doc-pages.js +++ b/infra/doc/vite-plugin-doc-pages.js @@ -72,7 +72,7 @@ function loadLocals(contentDir, projectRoot) { let icons = []; try { const esmRequire = createRequire(import.meta.url); - const iconsetPath = esmRequire.resolve('@ribajs/iconset/dist/svg.json'); + const iconsetPath = esmRequire.resolve('@ribajs/iconset/svg.json'); icons = JSON.parse(readFileSync(iconsetPath, 'utf8')); } catch { // Fallback: direct monorepo path diff --git a/infra/doc/vite.config.js b/infra/doc/vite.config.js index 55e7540c8..9998148fb 100644 --- a/infra/doc/vite.config.js +++ b/infra/doc/vite.config.js @@ -1,35 +1,12 @@ import { defineConfig } from 'vite' import dns from 'dns' import { resolve } from 'path' -import { readFileSync, existsSync, readdirSync } from 'fs' +import { ribaIconsetPlugin } from '@ribajs/vite-config' import { docPagesPlugin } from './vite-plugin-doc-pages.js' const __dirname = new URL('.', import.meta.url).pathname; dns.setDefaultResultOrder('verbatim') -/** - * Vite plugin to copy iconset SVGs into the production build output. - * In dev mode, the resolve.alias handles serving them. - */ -function iconsetAssetsPlugin() { - const svgDir = resolve(__dirname, '../../packages/iconset/src/svg'); - return { - name: 'vite-plugin-iconset-assets', - generateBundle() { - if (!existsSync(svgDir)) return; - for (const file of readdirSync(svgDir)) { - if (file.endsWith('.svg')) { - this.emitFile({ - type: 'asset', - fileName: `iconset/${file}`, - source: readFileSync(resolve(svgDir, file)), - }); - } - } - }, - }; -} - export default defineConfig(({ command, mode }) => { const basedir = resolve(__dirname, 'src'); const base = process.env.VITE_BASE_PATH ?? './'; @@ -46,11 +23,6 @@ export default defineConfig(({ command, mode }) => { }, }, }, - resolve: { - alias: { - '/iconset': resolve(__dirname, '../../packages/iconset/src/svg'), - }, - }, server: { fs: { allow: [resolve(__dirname, '../..')], @@ -66,7 +38,7 @@ export default defineConfig(({ command, mode }) => { basedir: resolve(basedir, 'views'), contentDir: resolve(basedir, 'content'), }), - iconsetAssetsPlugin(), + ribaIconsetPlugin({ baseUrl: '/iconset', outputDir: 'iconset' }), ], } }) diff --git a/infra/vite-config/index.js b/infra/vite-config/index.js index 2e12f5ef2..377cf7410 100644 --- a/infra/vite-config/index.js +++ b/infra/vite-config/index.js @@ -7,31 +7,46 @@ import { createRequire } from "module"; const require = createRequire(import.meta.url); /** - * Resolves the path to the @ribajs/iconset SVG directory. + * Resolves the path to the built @ribajs/iconset SVG directory (dist/svg). + * Requires the iconset package to have been built first. */ function resolveIconsetPath() { try { const iconsetPkg = require.resolve("@ribajs/iconset/package.json"); - return resolve(dirname(iconsetPkg), "src", "svg"); + return resolve(dirname(iconsetPkg), "dist", "svg"); } catch { return null; } } /** - * Vite plugin that serves @ribajs/iconset SVGs at /iconset/svg/ during dev - * and copies them to dist/iconset/svg/ during build. + * Vite plugin that serves @ribajs/iconset SVGs during dev and copies them + * into the build output. + * + * @param {Object} [options] + * @param {string} [options.baseUrl] - URL prefix for dev + build output (default: "/iconset/svg") + * @param {string} [options.outputDir] - Output directory inside the build relative to assets root (default: "iconset/svg") */ -function ribaIconsetPlugin() { +export function ribaIconsetPlugin(options = {}) { + const { baseUrl = "/iconset/svg", outputDir = "iconset/svg" } = options; const iconsetSvgPath = resolveIconsetPath(); + function assertIconsetBuilt() { + if (!iconsetSvgPath || !existsSync(iconsetSvgPath)) { + throw new Error( + "[@ribajs/vite-config] @ribajs/iconset dist/svg not found. " + + "Run `yarn workspace @ribajs/iconset build` first.", + ); + } + } + return { name: "riba-iconset", configureServer(server) { - if (!iconsetSvgPath || !existsSync(iconsetSvgPath)) return; + assertIconsetBuilt(); - server.middlewares.use("/iconset/svg", (req, res, next) => { + server.middlewares.use(baseUrl, (req, res, next) => { const filePath = resolve(iconsetSvgPath, req.url.replace(/^\//, "")); if (existsSync(filePath)) { res.setHeader("Content-Type", "image/svg+xml"); @@ -46,7 +61,7 @@ function ribaIconsetPlugin() { }, async generateBundle() { - if (!iconsetSvgPath || !existsSync(iconsetSvgPath)) return; + assertIconsetBuilt(); const { readdirSync, readFileSync } = await import("fs"); const files = readdirSync(iconsetSvgPath); @@ -54,7 +69,7 @@ function ribaIconsetPlugin() { if (file.endsWith(".svg")) { this.emitFile({ type: "asset", - fileName: `iconset/svg/${file}`, + fileName: `${outputDir}/${file}`, source: readFileSync(resolve(iconsetSvgPath, file)), }); } diff --git a/package.json b/package.json index 0e5a3abb1..d45a96663 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,8 @@ "test:watch": "vitest", "format": "yarn run lint:ext && yarn workspaces foreach -v -p -W run lint", "lint:ext": "node lint-file-extension.js", - "build": "yarn workspaces foreach -v -W --jobs 5 run build", - "build:libs": "yarn workspaces foreach -v -p -W --no-private --jobs 5 run build", + "build": "yarn workspaces foreach -v -t -W --jobs 5 run build", + "build:libs": "yarn workspaces foreach -v -p -t -W --no-private --jobs 5 run build", "clean": "yarn workspaces foreach -v -p -W run clean", "release": "GITHUB_TOKEN=${GITHUB_TOKEN:-$(gh auth token)} release-it", "release:patch": "GITHUB_TOKEN=${GITHUB_TOKEN:-$(gh auth token)} release-it patch", diff --git a/packages/bs5/package.json b/packages/bs5/package.json index caf95f39d..4403b4cf3 100644 --- a/packages/bs5/package.json +++ b/packages/bs5/package.json @@ -72,6 +72,7 @@ "@ribajs/core": "workspace:^", "@ribajs/events": "workspace:^", "@ribajs/extras": "workspace:^", + "@ribajs/iconset": "workspace:^", "@ribajs/jsx": "workspace:^", "@ribajs/utils": "workspace:^", "@sphinxxxx/color-conversion": "^2.2.2", diff --git a/packages/bs5/src/components/bs5-theme-button/bs5-theme-button.component.ts b/packages/bs5/src/components/bs5-theme-button/bs5-theme-button.component.ts index 40c093195..062d67c56 100644 --- a/packages/bs5/src/components/bs5-theme-button/bs5-theme-button.component.ts +++ b/packages/bs5/src/components/bs5-theme-button/bs5-theme-button.component.ts @@ -2,6 +2,8 @@ import { Component, TemplateFunction } from "@ribajs/core"; import { ThemeService } from "../../services/theme.js"; import { hasChildNodesTrim } from "@ribajs/utils"; import template from "./bs5-theme-button.component.html?raw"; +import iconSunUrl from "@ribajs/iconset/svg/icon_sun.svg?url"; +import iconMoonUrl from "@ribajs/iconset/svg/icon_moon.svg?url"; import { themeChoices } from "../../constants/index.js"; import type { @@ -31,8 +33,8 @@ export class Bs5ThemeButtonComponent extends Component { light: "Light", dark: "Dark", }, - lightIconSrc: "/iconset/svg/icon_sun.svg", - darkIconSrc: "/iconset/svg/icon_moon.svg", + lightIconSrc: iconSunUrl, + darkIconSrc: iconMoonUrl, iconSize: 32, // Methods / Properties setTheme: this.setTheme.bind(this), diff --git a/packages/iconset/package.json b/packages/iconset/package.json index b3dbb68cc..5a69a1781 100644 --- a/packages/iconset/package.json +++ b/packages/iconset/package.json @@ -14,8 +14,10 @@ "module": "dist/svg.json", "exports": { ".": "./dist/svg.json", - "./src/*": "./src/*", - "./dist/*": "./dist/*", + "./svg.json": "./dist/svg.json", + "./png.json": "./dist/png.json", + "./svg/*": "./dist/svg/*", + "./png/*": "./dist/png/*", "./package.json": "./package.json" }, "license": "MIT", @@ -42,11 +44,10 @@ ], "scripts": { "build": "node build.mjs", - "clean": "echo 'Do not clean the iconset by default because we want to hold the build image files in the repo'", + "clean": "rm -rf ./dist", "check": "echo 'Config-only package, nothing to check'" }, "files": [ - "/src", "/dist" ], "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 706cbaa15..010f29133 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2792,6 +2792,7 @@ __metadata: "@ribajs/eslint-config": "workspace:^" "@ribajs/events": "workspace:^" "@ribajs/extras": "workspace:^" + "@ribajs/iconset": "workspace:^" "@ribajs/jsx": "workspace:^" "@ribajs/tsconfig": "workspace:^" "@ribajs/types": "workspace:^" @@ -4182,6 +4183,7 @@ __metadata: "@ribajs/router": "workspace:^" "@ribajs/tsconfig": "workspace:^" "@ribajs/utils": "workspace:^" + "@ribajs/vite-config": "workspace:^" "@types/node": "npm:^24.12.2" "@types/prismjs": "npm:^1.26.6" bootstrap: "npm:^5.3.8" From 2b1d9903eebba94a70c61871ec20d3d02dd6e8dd Mon Sep 17 00:00:00 2001 From: Pascal Garber Date: Wed, 15 Apr 2026 16:07:41 +0200 Subject: [PATCH 2/2] fix(iconset): auto-build via postinstall; migrate remaining src/svg usages - root: add `postinstall` that builds @ribajs/iconset, so CI (which always starts from a fresh install) has dist/svg/ available before build/test runs - infra/vite-config: revert ribaIconsetPlugin to silent skip + one-time warn when dist/svg is missing; the previous throw broke demos that do not use iconset themselves (the Vite plugin is wired into all demos by default) - demos/bs5-slider, demos/iconset: migrate `@ribajs/iconset/src/svg/...` imports and glob to `@ribajs/iconset/svg/...` to match the new exports map --- .../slider-example.component.ts | 6 ++--- demos/iconset/src/ts/main.ts | 2 +- infra/vite-config/index.js | 24 +++++++++++++------ package.json | 1 + 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/demos/bs5-slider/src/ts/components/slider-example/slider-example.component.ts b/demos/bs5-slider/src/ts/components/slider-example/slider-example.component.ts index c8f0d034c..b12eadefe 100644 --- a/demos/bs5-slider/src/ts/components/slider-example/slider-example.component.ts +++ b/demos/bs5-slider/src/ts/components/slider-example/slider-example.component.ts @@ -1,9 +1,9 @@ import { Component } from "@ribajs/core"; import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js"; -import iconArrowCarrot from "@ribajs/iconset/src/svg/arrow_carrot.svg?url"; -import iconCircleEmpty from "@ribajs/iconset/src/svg/icon_circle-empty.svg?url"; -import iconCircleSelected from "@ribajs/iconset/src/svg/icon_circle-selected.svg?url"; +import iconArrowCarrot from "@ribajs/iconset/svg/arrow_carrot.svg?url"; +import iconCircleEmpty from "@ribajs/iconset/svg/icon_circle-empty.svg?url"; +import iconCircleSelected from "@ribajs/iconset/svg/icon_circle-selected.svg?url"; export class SliderExampleComponent extends Component { public static tagName = "rv-slider-example"; diff --git a/demos/iconset/src/ts/main.ts b/demos/iconset/src/ts/main.ts index 8d837f99f..f5b975937 100644 --- a/demos/iconset/src/ts/main.ts +++ b/demos/iconset/src/ts/main.ts @@ -4,7 +4,7 @@ import { bs5Module } from "@ribajs/bs5"; import { coreModule, Riba } from "@ribajs/core"; import { IconsetModule } from "./iconset.module.js"; -const iconsetModules = import.meta.glob("@ribajs/iconset/src/svg/*.svg"); +const iconsetModules = import.meta.glob("@ribajs/iconset/svg/*.svg"); const ICONSET = Object.keys(iconsetModules) .map((modulePath) => modulePath.split("/").pop()?.replace(".svg", "")) .filter((iconName): iconName is string => Boolean(iconName)); diff --git a/infra/vite-config/index.js b/infra/vite-config/index.js index 377cf7410..552b72673 100644 --- a/infra/vite-config/index.js +++ b/infra/vite-config/index.js @@ -30,12 +30,16 @@ function resolveIconsetPath() { export function ribaIconsetPlugin(options = {}) { const { baseUrl = "/iconset/svg", outputDir = "iconset/svg" } = options; const iconsetSvgPath = resolveIconsetPath(); + const iconsetAvailable = iconsetSvgPath && existsSync(iconsetSvgPath); + let warned = false; - function assertIconsetBuilt() { - if (!iconsetSvgPath || !existsSync(iconsetSvgPath)) { - throw new Error( - "[@ribajs/vite-config] @ribajs/iconset dist/svg not found. " + - "Run `yarn workspace @ribajs/iconset build` first.", + function warnMissingOnce() { + if (!warned) { + warned = true; + console.warn( + "[@ribajs/vite-config] @ribajs/iconset dist/svg not found — " + + "runtime icon URLs (/iconset/svg/*) will not be served. " + + "Run `yarn workspace @ribajs/iconset build` to enable them.", ); } } @@ -44,7 +48,10 @@ export function ribaIconsetPlugin(options = {}) { name: "riba-iconset", configureServer(server) { - assertIconsetBuilt(); + if (!iconsetAvailable) { + warnMissingOnce(); + return; + } server.middlewares.use(baseUrl, (req, res, next) => { const filePath = resolve(iconsetSvgPath, req.url.replace(/^\//, "")); @@ -61,7 +68,10 @@ export function ribaIconsetPlugin(options = {}) { }, async generateBundle() { - assertIconsetBuilt(); + if (!iconsetAvailable) { + warnMissingOnce(); + return; + } const { readdirSync, readFileSync } = await import("fs"); const files = readdirSync(iconsetSvgPath); diff --git a/package.json b/package.json index d45a96663..ec733410a 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "Browser" ], "scripts": { + "postinstall": "yarn workspace @ribajs/iconset build", "reinstall": "YARN_CHECKSUM_BEHAVIOR=update yarn install && yarn dlx @yarnpkg/sdks vscode", "test": "vitest run", "test:watch": "vitest",