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> = ${iconContent};`;
+
+ 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();
+ });
+});