From 9d00394b571b443692ee51894ef761cf61428f9b Mon Sep 17 00:00:00 2001 From: Tobias Faust Date: Fri, 30 Aug 2024 21:41:28 +0200 Subject: [PATCH 1/2] feat: unplugin-icons adder --- .changeset/tiny-dryers-hope.md | 7 ++ adders/unplugin-icons/collections.ts | 27 +++++ adders/unplugin-icons/config/adder.ts | 117 +++++++++++++++++++ adders/unplugin-icons/config/checks.ts | 6 + adders/unplugin-icons/config/options.ts | 15 +++ adders/unplugin-icons/config/tests.ts | 45 +++++++ adders/unplugin-icons/index.ts | 8 ++ adders/unplugin-icons/unplugin-icons.svg | 1 + packages/config/adders/official.ts | 2 +- scripts/update-unplugin-icons-collections.js | 72 ++++++++++++ 10 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 .changeset/tiny-dryers-hope.md create mode 100644 adders/unplugin-icons/collections.ts create mode 100644 adders/unplugin-icons/config/adder.ts create mode 100644 adders/unplugin-icons/config/checks.ts create mode 100644 adders/unplugin-icons/config/options.ts create mode 100644 adders/unplugin-icons/config/tests.ts create mode 100644 adders/unplugin-icons/index.ts create mode 100644 adders/unplugin-icons/unplugin-icons.svg create mode 100644 scripts/update-unplugin-icons-collections.js diff --git a/.changeset/tiny-dryers-hope.md b/.changeset/tiny-dryers-hope.md new file mode 100644 index 00000000..e7f0e34d --- /dev/null +++ b/.changeset/tiny-dryers-hope.md @@ -0,0 +1,7 @@ +--- +'@svelte-add/config': minor +'@svelte-add/adders': minor +'svelte-add': minor +--- + +feat: unplugin-icons adder diff --git a/adders/unplugin-icons/collections.ts b/adders/unplugin-icons/collections.ts new file mode 100644 index 00000000..59e50cbd --- /dev/null +++ b/adders/unplugin-icons/collections.ts @@ -0,0 +1,27 @@ +/** + * This file is auto generated and can be updated by running: + * $ node scripts/update-unplugin-icons-collections + */ + +export const collections = [ + { name: 'none', label: 'None' }, + { name: '@iconify/json', version: '^2.2.242', label: 'Full Collection (~120MB)' }, + { name: '@iconify-json/carbon', version: '^1.1.37', label: 'Carbon' }, + { name: '@iconify-json/mdi', version: '^1.1.68', label: 'Material Design Icons' }, + { name: '@iconify-json/tabler', version: '^1.1.121', label: 'Tabler Icons' }, + { name: '@iconify-json/heroicons', version: '^1.1.24', label: 'HeroIcons' }, + { name: '@iconify-json/logos', version: '^1.1.44', label: 'SVG Logos' }, + { name: '@iconify-json/ri', version: '^1.1.22', label: 'Remix Icon' }, + { name: '@iconify-json/ph', version: '^1.1.14', label: 'Phosphor' }, + { name: '@iconify-json/simple-icons', version: '^1.1.115', label: 'Simple Icons' }, + { name: '@iconify-json/ic', version: '^1.1.18', label: 'Google Material Icons' }, + { name: '@iconify-json/lucide', version: '^1.1.209', label: 'Lucide' }, + { name: '@iconify-json/svg-spinners', version: '^1.1.3', label: 'SVG Spinners' }, + { name: '@iconify-json/bi', version: '^1.1.24', label: 'Bootstrap Icons' }, + { name: '@iconify-json/material-symbols', version: '^1.1.89', label: 'Material Symbols' }, + { name: '@iconify-json/fluent', version: '^1.1.63', label: 'Fluent UI System Icons' }, + { name: '@iconify-json/fa6-solid', version: '^1.1.24', label: 'Font Awesome Solid' }, + { name: '@iconify-json/vscode-icons', version: '^1.1.37', label: 'VSCode Icons' }, + { name: '@iconify-json/bx', version: '^1.1.11', label: 'BoxIcons' }, + { name: '@iconify-json/twemoji', version: '^1.1.16', label: 'Twitter Emoji' }, +]; diff --git a/adders/unplugin-icons/config/adder.ts b/adders/unplugin-icons/config/adder.ts new file mode 100644 index 00000000..78e2811a --- /dev/null +++ b/adders/unplugin-icons/config/adder.ts @@ -0,0 +1,117 @@ +import { defineAdderConfig } from '@svelte-add/core'; +import { options } from './options'; +import { collections } from '../collections'; +import type { PackageDefinition } from '@svelte-add/core/adder/config'; + +export const adder = defineAdderConfig({ + metadata: { + id: 'unplugin-icons', + name: 'unplugin-icons ', + description: 'Access thousands of icons as components on-demand universally.', + environments: { svelte: true, kit: true }, + website: { + logo: './unplugin-icons.svg', + keywords: ['unplugin-icons', 'svg', 'icons', 'iconify', 'iconify-json'], + documentation: 'https://www.npmjs.com/package/unplugin-icons', + }, + }, + options, + integrationType: 'inline', + packages: [ + { name: 'unplugin-icons', version: '^0.19.2', dev: true }, + ...collections + .filter((collection) => collection.name && collection.version) + .map( + ({ name, version }) => + ({ + name, + version, + dev: true, + condition: ({ options }) => options.collection === name, + }) as PackageDefinition, + ), + ], + files: [ + { + name: ({ typescript }) => `vite.config.${typescript.installed ? 'ts' : 'js'}`, + contentType: 'script', + content: ({ ast, imports, exports, functions, array, object, common }) => { + const vitePluginName = 'Icons'; + imports.addDefault(ast, 'unplugin-icons/vite', vitePluginName); + + const { value: rootObject } = exports.defaultExport( + ast, + functions.call('defineConfig', []), + ); + const param1 = functions.argumentByIndex(rootObject, 0, object.createEmpty()); + + const pluginsArray = object.property(param1, 'plugins', array.createEmpty()); + const pluginFunctionCall = functions.call(vitePluginName, []); + const pluginConfig = object.create({ + compiler: common.createLiteral('svelte'), + }); + functions.argumentByIndex(pluginFunctionCall, 0, pluginConfig); + + array.push(pluginsArray, pluginFunctionCall); + }, + }, + { + name: () => 'src/app.d.ts', + contentType: 'text', + content: ({ content, dependencies }) => { + return addImport(content, getIconTypes(dependencies)); + }, + condition: ({ typescript, kit }) => kit.installed && typescript.installed, + }, + { + name: () => 'src/vite-env.d.ts', + contentType: 'text', + content: ({ content, dependencies }) => { + return addTypeReferenceComment(content, getIconTypes(dependencies)); + }, + condition: ({ typescript, kit }) => !kit.installed && typescript.installed, + }, + ], +}); + +const addTypeReferenceComment = (content: string, types: string) => { + if (!hasTypeReferenceComment(content, types)) { + const contentTrimmed = content.trimEnd(); + const trailingTrivia = content.slice(contentTrimmed.length); + + content = `${contentTrimmed}\n/// ${trailingTrivia}`; + } + + return content; +}; + +const hasTypeReferenceComment = (content: string, types: string) => { + const regex = new RegExp(`///\\s*<\\s*reference\\s*types\\s*=\\s*(['"])${types}\\1\\s*/>`); + return regex.test(content); +}; + +const addImport = (content: string, types: string) => { + if (!hasImport(content, types)) { + content = content.trimStart(); + if (content.startsWith('//') || content.startsWith('/*')) { + content = `\n${content}`; + } + + content = `import '${types}'\n${content};`; + } + + return content; +}; + +const hasImport = (content: string, types: string) => { + const regex = new RegExp(`import\\s+(['"])${types}\\1`); + return regex.test(content); +}; + +const getIconTypes = (dependencies: Record) => { + if ((dependencies['svelte'] ?? '').startsWith('^3.')) { + return 'unplugin-icons/types/svelte3'; + } + + return 'unplugin-icons/types/svelte'; +}; diff --git a/adders/unplugin-icons/config/checks.ts b/adders/unplugin-icons/config/checks.ts new file mode 100644 index 00000000..fc08d6f3 --- /dev/null +++ b/adders/unplugin-icons/config/checks.ts @@ -0,0 +1,6 @@ +import { defineAdderChecks } from '@svelte-add/core'; +import { options } from './options'; + +export const checks = defineAdderChecks({ + options, +}); diff --git a/adders/unplugin-icons/config/options.ts b/adders/unplugin-icons/config/options.ts new file mode 100644 index 00000000..916885c5 --- /dev/null +++ b/adders/unplugin-icons/config/options.ts @@ -0,0 +1,15 @@ +import { defineAdderOptions } from '@svelte-add/core'; +import { collections } from '../collections'; + +export const options = defineAdderOptions({ + collection: { + question: 'Do you want to install an icon collection?', + type: 'select', + default: 'none', + options: collections.map((collection) => ({ + value: collection.name, + label: collection.label, + hint: collection.name, + })), + }, +}); diff --git a/adders/unplugin-icons/config/tests.ts b/adders/unplugin-icons/config/tests.ts new file mode 100644 index 00000000..362700b2 --- /dev/null +++ b/adders/unplugin-icons/config/tests.ts @@ -0,0 +1,45 @@ +import { defineAdderTests } from '@svelte-add/core'; +import { options } from './options'; + +const defaultOptionValues = { + collection: options.collection.default, +}; + +export const tests = defineAdderTests({ + options, + optionValues: [ + { ...defaultOptionValues }, + { ...defaultOptionValues, collection: '@iconify-json/mdi' }, + ], + files: [ + { + name: ({ kit }) => (kit.installed ? `${kit.routesDirectory}/+page.svelte` : `src/App.svelte`), + contentType: 'svelte', + condition: ({ options }) => options.collection !== 'none', + content: ({ js, html }) => { + js.imports.addDefault(js.ast, 'virtual:icons/mdi/add', 'IconAdd'); + js.imports.addDefault(js.ast, '~icons/mdi/minus', 'IconMinus'); + + html.addFromRawHtml( + html.ast.childNodes, + ` +
+ + +
+ `, + ); + }, + }, + ], + tests: [ + { + name: 'icons exist', + condition: ({ collection }) => collection !== 'none', + run: async ({ elementExists }) => { + await elementExists('.unplugin-icons .mdi-icon-1'); + await elementExists('.unplugin-icons .mdi-icon-2'); + }, + }, + ], +}); diff --git a/adders/unplugin-icons/index.ts b/adders/unplugin-icons/index.ts new file mode 100644 index 00000000..0ae44fcc --- /dev/null +++ b/adders/unplugin-icons/index.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +import { defineAdder } from '@svelte-add/core'; +import { adder } from './config/adder.js'; +import { checks } from './config/checks.js'; +import { tests } from './config/tests.js'; + +export default defineAdder(adder, checks, tests); diff --git a/adders/unplugin-icons/unplugin-icons.svg b/adders/unplugin-icons/unplugin-icons.svg new file mode 100644 index 00000000..b723b520 --- /dev/null +++ b/adders/unplugin-icons/unplugin-icons.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/config/adders/official.ts b/packages/config/adders/official.ts index 740c83e2..718116ce 100644 --- a/packages/config/adders/official.ts +++ b/packages/config/adders/official.ts @@ -5,7 +5,7 @@ export const adderCategories: AdderCategories = { testing: ['vitest', 'playwright'], css: ['tailwindcss'], db: ['drizzle'], - additional: ['storybook', 'mdsvex', 'routify'], + additional: ['storybook', 'mdsvex', 'routify', 'unplugin-icons'], }; export const adderIds = Object.values(adderCategories).flatMap((x) => x); diff --git a/scripts/update-unplugin-icons-collections.js b/scripts/update-unplugin-icons-collections.js new file mode 100644 index 00000000..d3d13795 --- /dev/null +++ b/scripts/update-unplugin-icons-collections.js @@ -0,0 +1,72 @@ +/** + * This script updates all collections supported by the unplugin-icons adder. + * It does this by searching all "@iconify-json/*" packages using pnpm. + */ + +// @ts-check +import { execSync } from 'node:child_process'; +import { writeFileSync } from 'node:fs'; +import { format, resolveConfig } from 'prettier'; + +(async () => { + const json = execSync('pnpm search --json "@iconify-json/"').toString('utf-8'); + /** @type {Array<{ name: string, scope: string, version: string, description: string }>} */ + const packages = JSON.parse(json); + + const allowedScopes = ['iconify', 'iconify-json']; + const stripDescription = ' icon set in Iconify JSON format'; + /** @type {Record} */ + const replacedDescriptions = { + '@iconify/json': 'Full Collection (~120MB)', + }; + + const collections = [ + { + name: 'none', + version: undefined, + label: 'None', + }, + ]; + + for (const pkg of packages) { + if (!allowedScopes.includes(pkg.scope)) { + continue; + } + + const description = + replacedDescriptions[pkg.name] ?? pkg.description.replace(stripDescription, ''); + + collections.push({ + name: pkg.name, + version: `^${pkg.version}`, + label: description, + }); + } + + const codePath = 'adders/unplugin-icons/collections.ts'; + const config = await resolveConfig(codePath, { editorconfig: true }); + + const code = await format( + ` + /** + * This file is auto generated and can be updated by running: + * $ node scripts/update-unplugin-icons-collections + */ + + export const collections = ${JSON.stringify(collections)}; + `, + { + ...(config ?? {}), + filepath: codePath, + }, + ); + writeFileSync(codePath, code); + + console.log(`Wrote ${codePath}`); +})().catch( + /** @param {unknown} e} */ + (e) => { + console.error(e); + process.exit(1); + }, +); From 9beb83ed48aa2c26294476b3be09e7ea82fd8a5f Mon Sep 17 00:00:00 2001 From: Tobias Faust Date: Sat, 31 Aug 2024 16:30:34 +0200 Subject: [PATCH 2/2] chore: Fix type checking in script --- scripts/update-unplugin-icons-collections.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/update-unplugin-icons-collections.js b/scripts/update-unplugin-icons-collections.js index d3d13795..e5db9ef6 100644 --- a/scripts/update-unplugin-icons-collections.js +++ b/scripts/update-unplugin-icons-collections.js @@ -20,6 +20,7 @@ import { format, resolveConfig } from 'prettier'; '@iconify/json': 'Full Collection (~120MB)', }; + /** @type {Array<{ name: string, version?: string, label?: string }>} */ const collections = [ { name: 'none',