From 0585c9c7338187beab4c8d81b34be07b1d85e98d Mon Sep 17 00:00:00 2001 From: Sam Van Campenhout Date: Mon, 18 Mar 2024 20:48:06 +0100 Subject: [PATCH 1/8] WIP generate icon components --- .gitignore | 1 + lib/generate-icon-components.mjs | 46 ++++++++++++++++++++++++++++++++ package-lock.json | 7 +++++ package.json | 2 ++ 4 files changed, 56 insertions(+) create mode 100644 lib/generate-icon-components.mjs 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/lib/generate-icon-components.mjs b/lib/generate-icon-components.mjs new file mode 100644 index 000000000..bd02f55c3 --- /dev/null +++ b/lib/generate-icon-components.mjs @@ -0,0 +1,46 @@ +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); + + const iconContent = await readFile(join(ICON_FOLDER, iconName + '.svg')); + const iconContentWithAttributes = iconContent.toString().replace('>', ' ...attributes>'); // We assume the first closing element is the svg element + + const component = `// 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: SVGElement; +} + +export const ${componentName}Icon: TOC<${componentName}IconSignature> = ; +export default ${componentName}Icon;` + + await writeFile(join(COMPONENT_ICON_FOLDER, iconName + '.gts'), component) +} + +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..9a4c4f19a 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ }, "scripts": { "build": "run-s icons build-ember build-storybook", + "build:icons": "node lib/generate-icon-components.mjs", "build:types": "glint -d", "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"", "lint:css": "stylelint --allow-empty-input \"**/*.css\"", @@ -135,6 +136,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", From eb7e480b9679380a1ee07dc5d6d20a451d426dba Mon Sep 17 00:00:00 2001 From: Sam Van Campenhout Date: Tue, 19 Mar 2024 18:05:53 +0100 Subject: [PATCH 2/8] Format the file --- lib/generate-icon-components.mjs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/generate-icon-components.mjs b/lib/generate-icon-components.mjs index bd02f55c3..a83a57689 100644 --- a/lib/generate-icon-components.mjs +++ b/lib/generate-icon-components.mjs @@ -1,20 +1,28 @@ -import { pascalCase } from 'change-case'; +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'; +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 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')); +const icons = files + .filter((file) => extname(file) === ".svg") + .map((svg) => basename(svg, ".svg")); await prepareOutputDir(); -const promises = icons.map(svg => { +const promises = icons.map((svg) => { return generateComponent(svg); }); await Promise.all(promises); @@ -22,8 +30,10 @@ await Promise.all(promises); async function generateComponent(iconName) { const componentName = pascalCase(iconName); - const iconContent = await readFile(join(ICON_FOLDER, iconName + '.svg')); - const iconContentWithAttributes = iconContent.toString().replace('>', ' ...attributes>'); // We assume the first closing element is the svg element + const iconContent = await readFile(join(ICON_FOLDER, iconName + ".svg")); + const iconContentWithAttributes = iconContent + .toString() + .replace(">", " ...attributes>"); // We assume the first closing element is the svg element const component = `// THIS FILE IS GENERATED. ANY CHANGES TO THIS FILE WILL BE LOST. import type { TOC } from '@ember/component/template-only'; @@ -33,9 +43,9 @@ export interface ${componentName}IconSignature { } export const ${componentName}Icon: TOC<${componentName}IconSignature> = ; -export default ${componentName}Icon;` +export default ${componentName}Icon;`; - await writeFile(join(COMPONENT_ICON_FOLDER, iconName + '.gts'), component) + await writeFile(join(COMPONENT_ICON_FOLDER, iconName + ".gts"), component); } async function prepareOutputDir() { From 71ee9082f09a314c645fcd21a460bad93f510b6e Mon Sep 17 00:00:00 2001 From: Sam Van Campenhout Date: Tue, 19 Mar 2024 18:08:01 +0100 Subject: [PATCH 3/8] some improvements - set `mergeAmbiguousCharacters` to true since some of the icons contain numbers in the name: circle-step-1 > CircleStep_1Icon vs CircleStep1Icon - rename some variables - shorten some code --- lib/generate-icon-components.mjs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/generate-icon-components.mjs b/lib/generate-icon-components.mjs index a83a57689..cc250c589 100644 --- a/lib/generate-icon-components.mjs +++ b/lib/generate-icon-components.mjs @@ -28,24 +28,25 @@ const promises = icons.map((svg) => { await Promise.all(promises); async function generateComponent(iconName) { - const componentName = pascalCase(iconName); + const componentName = pascalCase(iconName, { mergeAmbiguousCharacters: true }); - const iconContent = await readFile(join(ICON_FOLDER, iconName + ".svg")); - const iconContentWithAttributes = iconContent + const iconContent = (await readFile(join(ICON_FOLDER, iconName + ".svg"))) .toString() - .replace(">", " ...attributes>"); // We assume the first closing element is the svg element + .replace(">", " ...attributes>"); // We assume the first closing bracket belongs to the svg element - const component = `// THIS FILE IS GENERATED. ANY CHANGES TO THIS FILE WILL BE LOST. + 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: SVGElement; + Element: SVGSVGElement; } -export const ${componentName}Icon: TOC<${componentName}IconSignature> = ; -export default ${componentName}Icon;`; +export const ${componentName}Icon: TOC<${componentName}IconSignature> = ;`; - await writeFile(join(COMPONENT_ICON_FOLDER, iconName + ".gts"), component); + await writeFile( + join(COMPONENT_ICON_FOLDER, iconName + ".gts"), + componentContent, + ); } async function prepareOutputDir() { From 6bd341456ffac370046ae70f267a253bac0bd7cc Mon Sep 17 00:00:00 2001 From: Sam Van Campenhout Date: Tue, 19 Mar 2024 18:11:06 +0100 Subject: [PATCH 4/8] run the generate script as part of the `prepare` script. I need to double check that this runs when we want, but we were already using it for the symbolset, so I guess it does. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a4c4f19a..72f415cba 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "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", + "prepare": "concurrently npm:build:icons npm:icons", "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook", "build-ember": "ember build --environment=production", From 05b6536ed4fffdaba72203876ddd5237b90d3c68 Mon Sep 17 00:00:00 2001 From: Sam Van Campenhout Date: Tue, 19 Mar 2024 18:15:39 +0100 Subject: [PATCH 5/8] add the icon bulid script to npmignore --- .npmignore | 3 +++ 1 file changed, 3 insertions(+) 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/ From f677552d2cac1bb9917bda32570a47404fa160c9 Mon Sep 17 00:00:00 2001 From: Sam Van Campenhout Date: Tue, 19 Mar 2024 18:16:53 +0100 Subject: [PATCH 6/8] prettier --- lib/generate-icon-components.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/generate-icon-components.mjs b/lib/generate-icon-components.mjs index cc250c589..576a9c771 100644 --- a/lib/generate-icon-components.mjs +++ b/lib/generate-icon-components.mjs @@ -28,7 +28,9 @@ const promises = icons.map((svg) => { await Promise.all(promises); async function generateComponent(iconName) { - const componentName = pascalCase(iconName, { mergeAmbiguousCharacters: true }); + const componentName = pascalCase(iconName, { + mergeAmbiguousCharacters: true, + }); const iconContent = (await readFile(join(ICON_FOLDER, iconName + ".svg"))) .toString() From 21851f323f97f22e59a946815621ba567804eefa Mon Sep 17 00:00:00 2001 From: Sam Van Campenhout Date: Tue, 19 Mar 2024 18:28:10 +0100 Subject: [PATCH 7/8] Add a simple test --- .../components/icon-components-test.gts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/integration/components/icon-components-test.gts 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(); + }); +}); From 6cf23ae8deec03ac9ded1aa7b0330642abfaa318 Mon Sep 17 00:00:00 2001 From: Sam Van Campenhout Date: Thu, 21 Mar 2024 09:44:30 +0100 Subject: [PATCH 8/8] build the icons when needed It seems the prepare script has some issues and we can't always depend on it for donig the right thing. We'll just run the build script when needed. --- .github/workflows/ci.yml | 6 ++++++ package.json | 14 ++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) 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/package.json b/package.json index 72f415cba..05319076a 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "test": "tests" }, "scripts": { - "build": "run-s icons build-ember build-storybook", - "build:icons": "node lib/generate-icon-components.mjs", + "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\"", @@ -46,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": "concurrently npm:build:icons npm:icons", "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" },