diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3b90ec100..a180e565c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,10 +31,7 @@ jobs: uses: bahmutov/npm-install@v1 - name: 🏄 Copy test env vars - run: cp .env.example .env - - - name: 🖼 Build icons - run: npm run build:icons + run: cp .env.example .env - name: 🛠 Setup Database run: npx prisma migrate deploy && npx prisma generate --sql @@ -57,11 +54,11 @@ jobs: - name: 📥 Download deps uses: bahmutov/npm-install@v1 - - name: 🏄 Copy test env vars - run: cp .env.example .env + - name: 🏗 Build + run: npm run build - - name: 🖼 Build icons - run: npm run build:icons + - name: 🏄 Copy test env vars + run: cp .env.example .env - name: 🛠 Setup Database run: npx prisma migrate deploy && npx prisma generate --sql @@ -85,10 +82,7 @@ jobs: uses: bahmutov/npm-install@v1 - name: 🏄 Copy test env vars - run: cp .env.example .env - - - name: 🖼 Build icons - run: npm run build:icons + run: cp .env.example .env - name: 🛠 Setup Database run: npx prisma migrate deploy && npx prisma generate --sql diff --git a/other/build-icons.ts b/other/build-icons.ts deleted file mode 100644 index ccaaf1a44..000000000 --- a/other/build-icons.ts +++ /dev/null @@ -1,153 +0,0 @@ -import * as path from 'node:path' -import { $ } from 'execa' -import fsExtra from 'fs-extra' -import { glob } from 'glob' -import { parse } from 'node-html-parser' - -const cwd = process.cwd() -const inputDir = path.join(cwd, 'other', 'svg-icons') -const inputDirRelative = path.relative(cwd, inputDir) -const outputDir = path.join(cwd, 'app', 'components', 'ui', 'icons') -await fsExtra.ensureDir(outputDir) - -const files = glob - .sync('**/*.svg', { - cwd: inputDir, - }) - .sort((a, b) => a.localeCompare(b)) - -const shouldVerboseLog = process.argv.includes('--log=verbose') -const logVerbose = shouldVerboseLog ? console.log : () => {} - -if (files.length === 0) { - console.log(`No SVG files found in ${inputDirRelative}`) -} else { - await generateIconFiles() -} - -async function generateIconFiles() { - const spriteFilepath = path.join(outputDir, 'sprite.svg') - const typeOutputFilepath = path.join(outputDir, 'name.d.ts') - const currentSprite = await fsExtra - .readFile(spriteFilepath, 'utf8') - .catch(() => '') - const currentTypes = await fsExtra - .readFile(typeOutputFilepath, 'utf8') - .catch(() => '') - - const iconNames = files.map((file) => iconName(file)) - - const spriteUpToDate = iconNames.every((name) => - currentSprite.includes(`id=${name}`), - ) - const typesUpToDate = iconNames.every((name) => - currentTypes.includes(`"${name}"`), - ) - - if (spriteUpToDate && typesUpToDate) { - logVerbose(`Icons are up to date`) - return - } - - logVerbose(`Generating sprite for ${inputDirRelative}`) - - const spriteChanged = await generateSvgSprite({ - files, - inputDir, - outputPath: spriteFilepath, - }) - - for (const file of files) { - logVerbose('✅', file) - } - logVerbose(`Saved to ${path.relative(cwd, spriteFilepath)}`) - - const stringifiedIconNames = iconNames.map((name) => JSON.stringify(name)) - - const typeOutputContent = `// This file is generated by npm run build:icons - -export type IconName = -\t| ${stringifiedIconNames.join('\n\t| ')}; -` - const typesChanged = await writeIfChanged( - typeOutputFilepath, - typeOutputContent, - ) - - logVerbose(`Manifest saved to ${path.relative(cwd, typeOutputFilepath)}`) - - const readmeChanged = await writeIfChanged( - path.join(outputDir, 'README.md'), - `# Icons - -This directory contains SVG icons that are used by the app. - -Everything in this directory is generated by \`npm run build:icons\`. -`, - ) - - if (spriteChanged || typesChanged || readmeChanged) { - console.log(`Generated ${files.length} icons`) - } -} - -function iconName(file: string) { - return file.replace(/\.svg$/, '') -} - -/** - * Creates a single SVG file that contains all the icons - */ -async function generateSvgSprite({ - files, - inputDir, - outputPath, -}: { - files: string[] - inputDir: string - outputPath: string -}) { - // Each SVG becomes a symbol and we wrap them all in a single SVG - const symbols = await Promise.all( - files.map(async (file) => { - const input = await fsExtra.readFile(path.join(inputDir, file), 'utf8') - const root = parse(input) - - const svg = root.querySelector('svg') - if (!svg) throw new Error('No SVG element found') - - svg.tagName = 'symbol' - svg.setAttribute('id', iconName(file)) - svg.removeAttribute('xmlns') - svg.removeAttribute('xmlns:xlink') - svg.removeAttribute('version') - svg.removeAttribute('width') - svg.removeAttribute('height') - - return svg.toString().trim() - }), - ) - - const output = [ - ``, - ``, - ``, - ``, // for semantics: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs - ...symbols, - ``, - ``, - '', // trailing newline - ].join('\n') - - return writeIfChanged(outputPath, output) -} - -async function writeIfChanged(filepath: string, newContent: string) { - const currentContent = await fsExtra - .readFile(filepath, 'utf8') - .catch(() => '') - if (currentContent === newContent) return false - await fsExtra.writeFile(filepath, newContent, 'utf8') - await $`prettier --write ${filepath} --ignore-unknown` - return true -} diff --git a/package-lock.json b/package-lock.json index 327a79f38..4052816ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -124,6 +124,7 @@ "tw-animate-css": "^1.2.4", "typescript": "^5.8.2", "vite": "^6.2.2", + "vite-plugin-icons-spritesheet": "^3.0.1", "vitest": "^3.0.9" }, "engines": { @@ -16407,6 +16408,26 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-plugin-icons-spritesheet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vite-plugin-icons-spritesheet/-/vite-plugin-icons-spritesheet-3.0.1.tgz", + "integrity": "sha512-Cr0+Z6wRMwSwKisWW9PHeTjqmQFv0jwRQQMc3YgAhAgZEe03j21el0P/CA31KN/L5eiL1LhR14VTXl96LetonA==", + "dev": true, + "license": "MIT", + "workspaces": [ + ".", + "test-apps/*" + ], + "dependencies": { + "chalk": "^5.4.1", + "glob": "^11.0.1", + "node-html-parser": "^7.0.1", + "tinyexec": "^0.3.2" + }, + "peerDependencies": { + "vite": ">=5.2.0" + } + }, "node_modules/vite/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", diff --git a/package.json b/package.json index 0dea38816..d043c6064 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,8 @@ }, "scripts": { "build": "run-s build:*", - "build:icons": "tsx ./other/build-icons.ts", "build:remix": "react-router build", "build:server": "tsx ./other/build-server.ts", - "predev": "npm run build:icons --silent", "dev": "cross-env NODE_ENV=development MOCKS=true node ./server/dev-server.js", "dev:no-mocks": "cross-env NODE_ENV=development node ./server/dev-server.js", "format": "prettier --write .", @@ -147,7 +145,6 @@ "fs-extra": "^11.3.0", "jsdom": "^25.0.1", "msw": "^2.7.3", - "node-html-parser": "^7.0.1", "npm-run-all": "^4.1.5", "prettier": "^3.5.3", "prettier-plugin-sql": "^0.18.1", @@ -157,6 +154,7 @@ "tw-animate-css": "^1.2.4", "typescript": "^5.8.2", "vite": "^6.2.2", + "vite-plugin-icons-spritesheet": "^3.0.1", "vitest": "^3.0.9" }, "engines": { @@ -166,4 +164,4 @@ "seed": "tsx prisma/seed.ts" }, "prettier": "@epic-web/config/prettier" -} +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 60aec3ad8..d7eaf2d3a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,10 +8,7 @@ "paths": { "#app/*": ["./app/*"], "#tests/*": ["./tests/*"], - "@/icon-name": [ - "./app/components/ui/icons/name.d.ts", - "./types/icon-name.d.ts" - ] + "@/icon-name": ["./app/components/ui/icons/types.ts", "./types/types.ts"] } } } diff --git a/vite.config.ts b/vite.config.ts index 05a01bc4a..81a0fe61f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,11 +1,12 @@ import { reactRouter } from '@react-router/dev/vite' import { - sentryReactRouter, type SentryReactRouterBuildOptions, + sentryReactRouter, } from '@sentry/react-router' import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' import { envOnlyMacros } from 'vite-env-only' +import { iconsSpritesheet } from 'vite-plugin-icons-spritesheet' const MODE = process.env.NODE_ENV @@ -20,7 +21,6 @@ export default defineConfig((config) => ({ assetsInlineLimit: (source: string) => { if ( - source.endsWith('sprite.svg') || source.endsWith('favicon.svg') || source.endsWith('apple-touch-icon.png') ) { @@ -39,6 +39,13 @@ export default defineConfig((config) => ({ plugins: [ envOnlyMacros(), tailwindcss(), + iconsSpritesheet({ + inputDir: './other/svg-icons', + outputDir: './app/components/ui/icons', + fileName: 'sprite.svg', + withTypes: true, + iconNameTransformer: (name) => name, + }), // it would be really nice to have this enabled in tests, but we'll have to // wait until https://github.com/remix-run/remix/issues/9871 is fixed MODE === 'test' ? null : reactRouter(),