@octaviaflow/icon-build-helpers 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +50 -0
  2. package/package.json +55 -0
  3. package/src/builders/index.js +33 -0
  4. package/src/builders/plugins/virtual.js +51 -0
  5. package/src/builders/react/builder.js +313 -0
  6. package/src/builders/react/components/CarbonIcon.d.ts +18 -0
  7. package/src/builders/react/components/Icon.tsx +87 -0
  8. package/src/builders/react/next/babel.js +37 -0
  9. package/src/builders/react/next/convert.js +107 -0
  10. package/src/builders/react/next/templates.js +71 -0
  11. package/src/builders/react/next/typescript.js +87 -0
  12. package/src/builders/react/next.js +382 -0
  13. package/src/builders/svg.js +28 -0
  14. package/src/builders/vanilla.js +115 -0
  15. package/src/index.js +21 -0
  16. package/src/metadata/README.md +37 -0
  17. package/src/metadata/adapters/index.js +34 -0
  18. package/src/metadata/adapters/memory.js +55 -0
  19. package/src/metadata/adapters/yml.js +92 -0
  20. package/src/metadata/extension.js +92 -0
  21. package/src/metadata/extensions/assets.js +45 -0
  22. package/src/metadata/extensions/categories.js +141 -0
  23. package/src/metadata/extensions/deprecated.js +73 -0
  24. package/src/metadata/extensions/icons.js +152 -0
  25. package/src/metadata/extensions/index.js +48 -0
  26. package/src/metadata/extensions/module-info.js +154 -0
  27. package/src/metadata/extensions/module-name.js +54 -0
  28. package/src/metadata/extensions/output/getModuleName.js +52 -0
  29. package/src/metadata/extensions/output/index.js +251 -0
  30. package/src/metadata/extensions/output/optimizer.js +226 -0
  31. package/src/metadata/extensions/pictograms.js +85 -0
  32. package/src/metadata/index.js +178 -0
  33. package/src/metadata/migrations/2020-01-27.js +78 -0
  34. package/src/metadata/migrations/2020-02-17-remove-usage-fields.js +45 -0
  35. package/src/metadata/migrations/2020-02-17-update-pictogram-files.js +79 -0
  36. package/src/metadata/migrations/README.md +12 -0
  37. package/src/metadata/storage.js +68 -0
  38. package/src/metadata/validate.js +54 -0
  39. package/src/registry.js +158 -0
  40. package/src/tools.js +18 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Copyright OctaviaFlow
3
+ * Author: Vishal Kumar
4
+ * Created: 11/November/2025
5
+ *
6
+ * This source code is licensed under the Apache-2.0 license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+ const { babel } = require("@rollup/plugin-babel");
11
+ const path = require("path");
12
+ const { rollup } = require("rollup");
13
+ const virtual = require("./plugins/virtual");
14
+
15
+ const BANNER = `/**
16
+ * Copyright IBM Corp. 2016, 2023
17
+ *
18
+ * This source code is licensed under the Apache-2.0 license found in the
19
+ * LICENSE file in the root directory of this source tree.
20
+ *
21
+ * Code generated by @octaviaflow/icon-build-helpers. DO NOT EDIT.
22
+ */`;
23
+
24
+ const babelConfig = {
25
+ babelrc: false,
26
+ exclude: /node_modules/,
27
+ presets: [
28
+ [
29
+ "@babel/preset-env",
30
+ {
31
+ targets: {
32
+ browsers: ["extends browserslist-config-octaviaflow"],
33
+ },
34
+ },
35
+ ],
36
+ ],
37
+ babelHelpers: "bundled",
38
+ };
39
+
40
+ async function builder(metadata, { output }) {
41
+ const modules = metadata.icons.flatMap((icon) => {
42
+ return icon.output.map((size) => {
43
+ const source = `export default ${JSON.stringify(size.descriptor)};`;
44
+ return {
45
+ source,
46
+ filepath: size.filepath,
47
+ moduleName: size.moduleName,
48
+ };
49
+ });
50
+ });
51
+
52
+ const files = {
53
+ "index.js": `${BANNER}\n\n`,
54
+ };
55
+ const input = {
56
+ "index.js": "index.js",
57
+ };
58
+
59
+ for (const m of modules) {
60
+ files[m.filepath] = m.source;
61
+ input[m.filepath] = m.filepath;
62
+ files["index.js"] +=
63
+ `\nexport { default as ${m.moduleName} } from '${m.filepath}';`;
64
+ }
65
+
66
+ const bundle = await rollup({
67
+ input,
68
+ plugins: [virtual(files), babel(babelConfig)],
69
+ onwarn: (warning, warn) => {
70
+ // Suppress warnings for large number of files
71
+ if (warning.code === "CIRCULAR_DEPENDENCY") return;
72
+ warn(warning);
73
+ },
74
+ });
75
+
76
+ const bundles = [
77
+ {
78
+ directory: path.join(output, "es"),
79
+ format: "esm",
80
+ },
81
+ {
82
+ directory: path.join(output, "lib"),
83
+ format: "commonjs",
84
+ },
85
+ ];
86
+
87
+ for (const { directory, format } of bundles) {
88
+ const outputOptions = {
89
+ dir: directory,
90
+ format,
91
+ entryFileNames: "[name]",
92
+ banner: BANNER,
93
+ exports: "auto",
94
+ };
95
+
96
+ await bundle.write(outputOptions);
97
+ }
98
+
99
+ const umd = await rollup({
100
+ input: "index.js",
101
+ plugins: [virtual(files), babel(babelConfig)],
102
+ onwarn: (warning, warn) => {
103
+ if (warning.code === "CIRCULAR_DEPENDENCY") return;
104
+ warn(warning);
105
+ },
106
+ });
107
+
108
+ await umd.write({
109
+ file: path.join(output, "umd/index.js"),
110
+ format: "umd",
111
+ name: "OctaviaFlowIcons",
112
+ });
113
+ }
114
+
115
+ module.exports = builder;
package/src/index.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Copyright OctaviaFlow
3
+ * Author: Vishal Kumar
4
+ * Created: 11/November/2025
5
+ *
6
+ * This source code is licensed under the Apache-2.0 license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+
11
+ 'use strict';
12
+
13
+ const builders = require('./builders');
14
+ const Metadata = require('./metadata');
15
+ const Registry = require('./registry');
16
+
17
+ module.exports = {
18
+ builders,
19
+ Metadata,
20
+ Registry,
21
+ };
@@ -0,0 +1,37 @@
1
+ # Metadata
2
+
3
+ ## About
4
+
5
+ We store metadata for a collection of SVG assets to help with searching and
6
+ using icons in the Carbon Design System. However, due to the large number of
7
+ assets we maintain, we needed to build a system for organizing information about
8
+ these icons and creating an output file to be consumed by tooling.
9
+
10
+ As a result, the Metadata module provides support for building, scaffolding, and
11
+ verifying metadata information about a collection of SVG assets. Under the hood,
12
+ this module will build up a registry of all available assets and compare them
13
+ against metadata files called "extensions". These files are typically written in
14
+ YAML and are separated to make authoring of specific types of data easier.
15
+
16
+ In general, we support the following extension types:
17
+
18
+ - Icons: provides information for icons available from the source directory of
19
+ SVG assets
20
+ - Categories: provides category and subcategory information for a collection of
21
+ icons
22
+ - Module name: provides computed names used in code for an icon
23
+ - Deprecations: provides a listing of icons that have been deprecated and
24
+ details how to update usage to the preferred format
25
+
26
+ To support the above use-cases, Metadata makes use of the following concepts:
27
+
28
+ - [Registry](./registry.js): build up a source of truth for all available SVG
29
+ assets in a source tree
30
+ - [Extensions](./extensions/index.js): distinct sources of metadata captured for
31
+ icons. These modules provide support for verifying file structure, extending
32
+ the resulting output metadata, and logical checks to verify information
33
+ present (or not present) in these files
34
+ - [Adapters](./adapters.js): provide support for authoring metadata in a variety
35
+ of formats, we currently author in YAML
36
+ - [Storage](./storage.js): ability to read and write extension files for a given
37
+ adapter
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Copyright OctaviaFlow
3
+ * Author: Vishal Kumar
4
+ * Created: 11/November/2025
5
+ *
6
+ * This source code is licensed under the Apache-2.0 license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+ /**
11
+ * Copyright IBM Corp. 2020, 2023
12
+ *
13
+ * This source code is licensed under the Apache-2.0 license found in the
14
+ * LICENSE file in the root directory of this source tree.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const memory = require('./memory');
20
+ const yml = require('./yml');
21
+
22
+ /**
23
+ * An adapter defines how we work with a specific file format and defines the
24
+ * operations for reading and writing files of that type for a particular
25
+ * location
26
+ * @typedef {object} Adapter
27
+ * @property {Function} read
28
+ * @property {Function} write
29
+ */
30
+
31
+ module.exports = {
32
+ memory,
33
+ yml,
34
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Copyright OctaviaFlow
3
+ * Author: Vishal Kumar
4
+ * Created: 11/November/2025
5
+ *
6
+ * This source code is licensed under the Apache-2.0 license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+ /**
11
+ * Copyright IBM Corp. 2020, 2023
12
+ *
13
+ * This source code is licensed under the Apache-2.0 license found in the
14
+ * LICENSE file in the root directory of this source tree.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const path = require('path');
20
+
21
+ const filesystem = new Map();
22
+
23
+ /**
24
+ * @param {string} directory
25
+ * @param {string} name
26
+ * @returns {Promise<any>}
27
+ */
28
+ async function read(directory, name) {
29
+ const filepath = path.join(directory, path.format({ name }));
30
+
31
+ if (filesystem.has(filepath)) {
32
+ return filesystem.get(filepath);
33
+ }
34
+
35
+ throw new Error(
36
+ `Unable to find extension \`${name}\` at filepath: ${filepath}`
37
+ );
38
+ }
39
+
40
+ /**
41
+ * @param {string} directory
42
+ * @param {string} name
43
+ * @param {any} data
44
+ * @returns {Promise<void>}
45
+ */
46
+ async function write(directory, name, data) {
47
+ const filepath = path.join(directory, path.format({ name }));
48
+ filesystem.set(filepath, data);
49
+ }
50
+
51
+ module.exports = {
52
+ read,
53
+ write,
54
+ filesystem,
55
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Copyright OctaviaFlow
3
+ * Author: Vishal Kumar
4
+ * Created: 11/November/2025
5
+ *
6
+ * This source code is licensed under the Apache-2.0 license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+ /**
11
+ * Copyright IBM Corp. 2020, 2023
12
+ *
13
+ * This source code is licensed under the Apache-2.0 license found in the
14
+ * LICENSE file in the root directory of this source tree.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs-extra');
20
+ const yaml = require('js-yaml');
21
+ const path = require('path');
22
+
23
+ /**
24
+ * Get the filename for a given path basename. This is helpful to
25
+ * figure out which file we need to load in from the filesystem for the given
26
+ * adapter.
27
+ * @param {string} name
28
+ * @returns {string}
29
+ */
30
+ function getFilenameFor(name) {
31
+ return path.format({
32
+ name,
33
+ ext: '.yml',
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Serialize the given data to a YML format
39
+ * @param {object} data
40
+ * @returns {string}
41
+ */
42
+ function serialize(data) {
43
+ return yaml.safeDump(data);
44
+ }
45
+
46
+ /**
47
+ * Deserialize the given YML data to JavaScript
48
+ * @param {object} data
49
+ * @returns {string}
50
+ */
51
+ function deserialize(data) {
52
+ return yaml.safeLoad(data);
53
+ }
54
+
55
+ /**
56
+ * @param {string} directory
57
+ * @param {string} name
58
+ * @returns {Promise<any>}
59
+ */
60
+ async function read(directory, name) {
61
+ const filepath = path.join(directory, getFilenameFor(name));
62
+
63
+ if (!(await fs.pathExists(filepath))) {
64
+ throw new Error(
65
+ `Unable to find extension \`${name}\` at filepath: ` +
66
+ `${filepath}. Either create the file or update the extension ` +
67
+ `to be computed.`
68
+ );
69
+ }
70
+
71
+ return deserialize(await fs.readFile(filepath, 'utf8'));
72
+ }
73
+
74
+ /**
75
+ * @param {string} directory
76
+ * @param {string} name
77
+ * @param {any} data
78
+ * @returns {Promise<void>}
79
+ */
80
+ async function write(directory, name, data) {
81
+ const filepath = path.join(directory, getFilenameFor(name));
82
+
83
+ await fs.ensureFile(filepath);
84
+ await fs.writeFile(filepath, serialize(data), 'utf8');
85
+ }
86
+
87
+ module.exports = {
88
+ serialize,
89
+ deserialize,
90
+ read,
91
+ write,
92
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Copyright OctaviaFlow
3
+ * Author: Vishal Kumar
4
+ * Created: 11/November/2025
5
+ *
6
+ * This source code is licensed under the Apache-2.0 license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+ /**
11
+ * Copyright IBM Corp. 2020, 2023
12
+ *
13
+ * This source code is licensed under the Apache-2.0 license found in the
14
+ * LICENSE file in the root directory of this source tree.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ /**
20
+ * @typedef {object} Extension
21
+ * @property {string} name - the name of the extension
22
+ * @property {JoiSchema} [schema] - a schema that validates the structure of
23
+ * the file for an extension
24
+ * @property {Function} [extend] - add information for the extension to the
25
+ * output metadata structure
26
+ * @property {Function} [validate] - validate that the data available in the
27
+ * registry matches the data received for the extension
28
+ */
29
+
30
+ /**
31
+ * @typedef {Function} ExtensionBuilder
32
+ * @param {object} config
33
+ * @returns {Extension}
34
+ */
35
+
36
+ /**
37
+ * @param {Extension} extension
38
+ * @returns {void}
39
+ */
40
+ function validate(extension) {
41
+ if (!extension.name) {
42
+ throw new Error(
43
+ `Expected extension to have a name, instead received: \`${extension.name}\``
44
+ );
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @param {(ExtensionBuilder | [Extension, object])} builderOrOptions
50
+ * @returns {Extension}
51
+ */
52
+ function loadExtension(builderOrOptions) {
53
+ if (Array.isArray(builderOrOptions)) {
54
+ const [builder, options] = builderOrOptions;
55
+ const extension = builder(options);
56
+ validate(extension);
57
+ return extension;
58
+ }
59
+ const extension = builderOrOptions();
60
+ validate(extension);
61
+ return extension;
62
+ }
63
+
64
+ /**
65
+ * @param {Array<ExtensionBuilder | [Extension, object]>} [builderOrOptions]
66
+ * @returns {Array<Extension>}
67
+ */
68
+ function load(builderOrOptions = []) {
69
+ const extensions = builderOrOptions.map(loadExtension);
70
+ const runOrder = [];
71
+
72
+ function order(extension) {
73
+ const match = runOrder.find((entry) => entry.name === extension.name);
74
+ if (match) {
75
+ return;
76
+ }
77
+
78
+ if (Array.isArray(extension.before)) {
79
+ extension.before.map(loadExtension).forEach(order);
80
+ }
81
+
82
+ runOrder.push(extension);
83
+ }
84
+
85
+ extensions.forEach(order);
86
+
87
+ return runOrder;
88
+ }
89
+
90
+ module.exports = {
91
+ load,
92
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Copyright OctaviaFlow
3
+ * Author: Vishal Kumar
4
+ * Created: 11/November/2025
5
+ *
6
+ * This source code is licensed under the Apache-2.0 license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+ /**
11
+ * Copyright IBM Corp. 2020, 2023
12
+ *
13
+ * This assets code is licensed under the Apache-2.0 license found in the
14
+ * LICENSE file in the root directory of this assets tree.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs-extra');
20
+ const path = require('path');
21
+
22
+ /**
23
+ * Provide source and filepath asset information for a given icon
24
+ * @type {Extension}
25
+ */
26
+ const assets = () => {
27
+ return {
28
+ name: 'assets',
29
+ computed: true,
30
+ extend(metadata, _data, registry, { input }) {
31
+ for (const entry of metadata.icons) {
32
+ const icon = registry.get(entry.name);
33
+ entry.assets = icon.assets.map(({ size, filepath }) => {
34
+ return {
35
+ size,
36
+ filepath: path.relative(input.svg, filepath),
37
+ source: fs.readFileSync(filepath, 'utf8'),
38
+ };
39
+ });
40
+ }
41
+ },
42
+ };
43
+ };
44
+
45
+ module.exports = assets;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Copyright OctaviaFlow
3
+ * Author: Vishal Kumar
4
+ * Created: 11/November/2025
5
+ *
6
+ * This source code is licensed under the Apache-2.0 license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+ /**
11
+ * Copyright IBM Corp. 2020, 2023
12
+ *
13
+ * This source code is licensed under the Apache-2.0 license found in the
14
+ * LICENSE file in the root directory of this source tree.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const Joi = require('@hapi/joi');
20
+
21
+ // Supports both top-level categories and subcategories
22
+ //
23
+ // categories:
24
+ // - name: Category name
25
+ // members:
26
+ // - Member 1
27
+ // - Member 2
28
+ //
29
+ // categories:
30
+ // - name: Category name
31
+ // subcategories:
32
+ // - name: Subcategory name
33
+ // members:
34
+ // - Member 1
35
+ // - Member 2
36
+ const categories = () => {
37
+ return {
38
+ name: 'categories',
39
+
40
+ schema: Joi.object().keys({
41
+ categories: Joi.array()
42
+ .items(
43
+ Joi.object().keys({
44
+ name: Joi.string().required(),
45
+ members: Joi.array().items(Joi.string()),
46
+ subcategories: Joi.array().items(
47
+ Joi.object().keys({
48
+ name: Joi.string().required(),
49
+ members: Joi.array().items(Joi.string()).required(),
50
+ })
51
+ ),
52
+ })
53
+ )
54
+ .required(),
55
+ }),
56
+
57
+ extend(metadata, data) {
58
+ const { categories } = data;
59
+
60
+ metadata.categories = categories;
61
+
62
+ for (const category of categories) {
63
+ const { name, members, subcategories } = category;
64
+
65
+ if (Array.isArray(subcategories)) {
66
+ for (const subcategory of subcategories) {
67
+ for (const member of subcategory.members) {
68
+ const icon = metadata.icons.find(({ name }) => name === member);
69
+ icon.category = name;
70
+ icon.subcategory = subcategory.name;
71
+ }
72
+ }
73
+ continue;
74
+ }
75
+
76
+ for (const member of members) {
77
+ const icon = metadata.icons.find(({ name }) => name === member);
78
+ icon.category = name;
79
+ }
80
+ }
81
+ },
82
+
83
+ validate(registry, data) {
84
+ // Flatten the category data into a flat list of members with corresponding
85
+ // name and category data
86
+ const members = [];
87
+
88
+ for (const category of data.categories) {
89
+ if (Array.isArray(category.subcategories)) {
90
+ for (const subcategory of category.subcategories) {
91
+ for (const member of subcategory.members) {
92
+ members.push({
93
+ name: member,
94
+ category: category.name,
95
+ subcategory: subcategory.name,
96
+ });
97
+ }
98
+ }
99
+ continue;
100
+ }
101
+
102
+ for (const member of category.members) {
103
+ members.push({
104
+ name: member,
105
+ category: category.name,
106
+ });
107
+ }
108
+ }
109
+
110
+ // Verify that every asset in the registry has category information
111
+ for (const icon of registry.values()) {
112
+ const match = members.find((member) => member.name === icon.id);
113
+ if (!match) {
114
+ const filepaths = icon.assets.map((asset) => asset.filepath);
115
+ throw new Error(
116
+ `Expected the following icon to have category information: ` +
117
+ `\`${icon.id}\`. This icon has assets in the following locations:\n` +
118
+ filepaths.join('\n')
119
+ );
120
+ }
121
+ }
122
+
123
+ // Verify that every asset with a category exists in the registry
124
+ for (const member of members) {
125
+ if (!registry.has(member.name)) {
126
+ let categoryPath = `category \`${member.category}\``;
127
+ if (member.subcategory) {
128
+ categoryPath += `, subcategory \`${member.subcategory}\``;
129
+ }
130
+ throw new Error(
131
+ `Found the entry \`${member.name}\` in ${categoryPath} that does ` +
132
+ `not have a corresponding icon or asset. Either this icon does ` +
133
+ `not exist, or is not available in the current directory.`
134
+ );
135
+ }
136
+ }
137
+ },
138
+ };
139
+ };
140
+
141
+ module.exports = categories;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Copyright OctaviaFlow
3
+ * Author: Vishal Kumar
4
+ * Created: 11/November/2025
5
+ *
6
+ * This source code is licensed under the Apache-2.0 license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+ /**
11
+ * Copyright IBM Corp. 2020, 2023
12
+ *
13
+ * This source code is licensed under the Apache-2.0 license found in the
14
+ * LICENSE file in the root directory of this source tree.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const Joi = require('@hapi/joi');
20
+
21
+ // Supports a list of deprecated assets
22
+ //
23
+ // deprecated:
24
+ // - name: asset-name-1
25
+ // - name: asset-name-2
26
+ //
27
+ // In the future, we may want to include a reason for the deprecation, or a
28
+ // notice for what icon to use instead.
29
+ const deprecated = () => {
30
+ return {
31
+ name: 'deprecated',
32
+
33
+ schema: Joi.object().keys({
34
+ deprecated: Joi.array()
35
+ .items(
36
+ Joi.object().keys({
37
+ name: Joi.string().required(),
38
+ reason: Joi.string(),
39
+ })
40
+ )
41
+ .required(),
42
+ }),
43
+
44
+ extend(metadata, data) {
45
+ const { deprecated } = data;
46
+
47
+ for (const icon of metadata.icons) {
48
+ const entry = deprecated.find(({ name }) => name === icon.name);
49
+ if (entry) {
50
+ icon.deprecated = true;
51
+ if (entry.reason) {
52
+ icon.reason = entry.reason;
53
+ }
54
+ }
55
+ }
56
+ },
57
+
58
+ validate(registry, data) {
59
+ for (const icon of data.deprecated) {
60
+ const entry = registry.has(icon.name);
61
+ if (!entry) {
62
+ throw new Error(
63
+ `Expected the deprecated icon \`${icon.name}\` to exist. Either ` +
64
+ `this icon does not exist, or is not available in the given SVG ` +
65
+ `directory`
66
+ );
67
+ }
68
+ }
69
+ },
70
+ };
71
+ };
72
+
73
+ module.exports = deprecated;