From 0791d7afed218c71e08e7b02ce6faf2c84a16aaa Mon Sep 17 00:00:00 2001 From: Sam Van Campenhout Date: Mon, 25 Mar 2024 12:55:34 +0100 Subject: [PATCH] Add icon components (#487) This adds a script that can generate .gts components from the .svg icons in the public folder. This setup saves us a lot of manual work and it easily allows us to manage the icons we bundle. --- .github/workflows/ci.yml | 6 ++ .gitignore | 1 + .npmignore | 3 + lib/generate-icon-components.mjs | 59 +++++++++++++++++++ package-lock.json | 7 +++ package.json | 14 ++--- .../components/icon-components-test.gts | 14 +++++ 7 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 lib/generate-icon-components.mjs create mode 100644 tests/integration/components/icon-components-test.gts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1e4ffe57..95f6ecb87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: cache: npm - name: Install Dependencies run: npm ci + - name: Build icons + run: npm run build:icons - name: Lint run: npm run lint - name: Run Tests @@ -44,6 +46,8 @@ jobs: cache: npm - name: Install Dependencies run: npm install --no-shrinkwrap + - name: Build icons + run: npm run build:icons - name: Run Tests run: npm run test:ember @@ -73,5 +77,7 @@ jobs: cache: npm - name: Install Dependencies run: npm ci + - name: Build icons + run: npm run build:icons - name: Run Tests run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} diff --git a/.gitignore b/.gitignore index e7c6f8e9b..8cec3813c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /build-storybook.log /.storybook/preview-head.html /declarations/ +/addon/components/icons/ # dependencies /node_modules/ diff --git a/.npmignore b/.npmignore index af8978530..f71207a10 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,6 @@ +# build scripts +/lib/generate-icon-components.mjs + # compiled output /dist/ /tmp/ diff --git a/lib/generate-icon-components.mjs b/lib/generate-icon-components.mjs new file mode 100644 index 000000000..576a9c771 --- /dev/null +++ b/lib/generate-icon-components.mjs @@ -0,0 +1,59 @@ +import { pascalCase } from "change-case"; +import { existsSync } from "node:fs"; +import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import { basename, dirname, extname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const ICON_FOLDER = join(__dirname, "..", "public", "icons"); +const COMPONENT_ICON_FOLDER = join( + __dirname, + "..", + "addon", + "components", + "icons", +); + +const files = await readdir(ICON_FOLDER); +const icons = files + .filter((file) => extname(file) === ".svg") + .map((svg) => basename(svg, ".svg")); + +await prepareOutputDir(); + +const promises = icons.map((svg) => { + return generateComponent(svg); +}); +await Promise.all(promises); + +async function generateComponent(iconName) { + const componentName = pascalCase(iconName, { + mergeAmbiguousCharacters: true, + }); + + const iconContent = (await readFile(join(ICON_FOLDER, iconName + ".svg"))) + .toString() + .replace(">", " ...attributes>"); // We assume the first closing bracket belongs to the svg element + + const componentContent = `// THIS FILE IS GENERATED. ANY CHANGES TO THIS FILE WILL BE LOST. +import type { TOC } from '@ember/component/template-only'; + +export interface ${componentName}IconSignature { + Element: SVGSVGElement; +} + +export const ${componentName}Icon: TOC<${componentName}IconSignature> = ;`; + + await writeFile( + join(COMPONENT_ICON_FOLDER, iconName + ".gts"), + componentContent, + ); +} + +async function prepareOutputDir() { + if (existsSync(COMPONENT_ICON_FOLDER)) { + await rm(COMPONENT_ICON_FOLDER, { recursive: true }); + } + await mkdir(COMPONENT_ICON_FOLDER); +} diff --git a/package-lock.json b/package-lock.json index 6d3428589..257986422 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", "broccoli-asset-rev": "^3.0.0", + "change-case": "^5.4.3", "chromatic": "^6.5.4", "concurrently": "^8.2.2", "ember-auto-import": "^2.7.0", @@ -16889,6 +16890,12 @@ "node": ">=4" } }, + "node_modules/change-case": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.3.tgz", + "integrity": "sha512-4cdyvorTy/lViZlVzw2O8/hHCLUuHqp4KpSSP3DlauhFCf3LdnfF+p5s0EAhjKsU7bqrMzu7iQArYfoPiHO2nw==", + "dev": true + }, "node_modules/character-entities": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", diff --git a/package.json b/package.json index 9821c75d9..05319076a 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "test": "tests" }, "scripts": { - "build": "run-s icons build-ember build-storybook", + "build": "run-s build-ember build-storybook", + "build:icons": "concurrently \"node lib/generate-icon-components.mjs\" \"svg-symbols ./public/icons > ./public/appuniversum-symbolset.svg\"", "build:types": "glint -d", "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"", "lint:css": "stylelint --allow-empty-input \"**/*.css\"", @@ -45,20 +46,18 @@ "lint:js": "eslint . --cache", "lint:js:fix": "eslint . --fix", "lint:types": "glint", - "prepack": "glint -d", + "prepack": "run-s build:icons build:types", "postpack": "rimraf declarations", "start": "run-p ember storybook", - "ember": "ember serve", + "ember": "npm run build:icons && ember serve", "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"", "test:ember": "ember test", "test:ember-compatibility": "ember try:each", - "icons": "svg-symbols ./public/icons > ./public/appuniversum-symbolset.svg", - "prepare": "svg-symbols ./public/icons > ./public/appuniversum-symbolset.svg", "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook", - "build-ember": "ember build --environment=production", + "build-ember": "npm run build:icons && ember build --environment=production", "build-ember-chromatic": "ember build --environment=chromatic", - "prep-chromatic": "run-s icons build-ember-chromatic", + "prep-chromatic": "run-s build:icons build-ember-chromatic", "release": "release-it", "chromatic": "chromatic --exit-zero-on-changes" }, @@ -135,6 +134,7 @@ "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", "broccoli-asset-rev": "^3.0.0", + "change-case": "^5.4.3", "chromatic": "^6.5.4", "concurrently": "^8.2.2", "ember-auto-import": "^2.7.0", diff --git a/tests/integration/components/icon-components-test.gts b/tests/integration/components/icon-components-test.gts new file mode 100644 index 000000000..c813b70b8 --- /dev/null +++ b/tests/integration/components/icon-components-test.gts @@ -0,0 +1,14 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { AddIcon } from '@appuniversum/ember-appuniversum/components/icons/add'; + +module('Integration | Icon components', function (hooks) { + setupRenderingTest(hooks); + + test('the icon components accept attributes', async function (assert) { + await render(); + + assert.dom('[data-test-icon]').exists(); + }); +});