Skip to content

Commit

Permalink
refactor(icons-react): add TypeScript types to icons-react package
Browse files Browse the repository at this point in the history
  • Loading branch information
lewandom committed Sep 26, 2023
1 parent 4dadab3 commit e769119
Show file tree
Hide file tree
Showing 20 changed files with 503 additions and 95 deletions.
6 changes: 5 additions & 1 deletion packages/cli/src/commands/bundle/bundlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
'use strict';

const javascript = require('./javascript');
const typescript = require('./typescript');

const bundlers = new Map([['.js', javascript]]);
const bundlers = new Map([
['.js', javascript],
['.ts', typescript],
]);

module.exports = bundlers;
155 changes: 155 additions & 0 deletions packages/cli/src/commands/bundle/typescript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* Copyright IBM Corp. 2018, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

const { babel } = require('@rollup/plugin-babel');
const commonjs = require('@rollup/plugin-commonjs');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const typescript = require('@rollup/plugin-typescript');
const { pascalCase } = require('change-case');
const fs = require('fs-extra');
const path = require('path');
const { rollup } = require('rollup');

async function bundle(entrypoint, options) {
const globals = options.globals ? formatGlobals(options.globals) : {};
const { name } = options;
const packageFolder = await findPackageFolder(entrypoint);

const outputFolders = [
{
format: 'esm',
directory: path.join(packageFolder, 'es'),
},
{
format: 'cjs',
directory: path.join(packageFolder, 'lib'),
},
{
format: 'umd',
directory: path.join(packageFolder, 'umd'),
},
];

await Promise.all(outputFolders.map(({ directory }) => fs.remove(directory)));

const jsEntryPoints = outputFolders.map(({ directory, format }) => ({
outputDir: directory,
file: path.join(directory, 'index.js'),
format,
}));

const packageJsonPath = path.join(packageFolder, 'package.json');
const packageJson = await fs.readJson(packageJsonPath);
const { dependencies = {} } = packageJson;

await Promise.all(
jsEntryPoints.map(async ({ outputDir, file, format }) => {
const bundle = await rollup({
input: entrypoint,
external: Object.keys(dependencies),
plugins: [
typescript({
noEmitOnError: true,
noForceEmit: true,
outputToFilesystem: false,
compilerOptions: {
rootDir: 'src',
emitDeclarationOnly: true,
outDir: outputDir,
},
}),
babel({
exclude: 'node_modules/**',
babelrc: false,
presets: [
[
'@babel/preset-env',
{
modules: false,
targets: {
browsers: ['last 1 version', 'ie >= 11', 'Firefox ESR'],
},
},
],
'@babel/preset-typescript',
],
babelHelpers: 'bundled',
extensions: ['.ts', '.tsx', '.js', '.jsx'],
}),
nodeResolve(),
commonjs({
include: [/node_modules/],
extensions: ['.js'],
}),
],
});
const outputOptions = {
exports: 'auto',
file,
format,
};

if (format === 'umd') {
outputOptions.name = name;
outputOptions.globals = {
...formatDependenciesIntoGlobals(dependencies),
...globals,
};
}

return bundle.write(outputOptions);
})
);
}

function formatGlobals(string) {
const mappings = string.split(',').map((mapping) => {
return mapping.split('=');
});
return mappings.reduce(
(acc, [pkg, global]) => ({
...acc,
[pkg]: global,
}),
{}
);
}

function formatDependenciesIntoGlobals(dependencies) {
return Object.keys(dependencies).reduce((acc, key) => {
const parts = key.split('/').map((identifier, i) => {
if (i === 0) {
return identifier.replace(/@/, '');
}
return identifier;
});

return {
...acc,
[key]: pascalCase(parts.join(' ')),
};
}, {});
}

async function findPackageFolder(entrypoint) {
let packageFolder = entrypoint;

while (packageFolder !== '/' && path.dirname(packageFolder) !== '/') {
packageFolder = path.dirname(packageFolder);
const packageJsonPath = path.join(packageFolder, 'package.json');

if (await fs.pathExists(packageJsonPath)) {
break;
}
}

return packageFolder;
}

module.exports = bundle;
25 changes: 14 additions & 11 deletions packages/icon-build-helpers/src/builders/react/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ const babelConfig = {
},
],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-transform-react-constant-elements',
'babel-plugin-dev-expression',
],
babelHelpers: 'bundled',
extensions: ['.ts', '.tsx', '.js', '.jsx'],
};

async function builder(metadata, { output }) {
Expand Down Expand Up @@ -97,10 +99,11 @@ async function builder(metadata, { output }) {
}

const files = {
'index.js': `${BANNER}\n\nexport { default as Icon } from './Icon.js';`,
'index.ts': `${BANNER}\n\nexport { default as Icon } from './Icon.tsx';`,
};
const input = {
'index.js': 'index.js',
'index.js': 'index.ts',
'Icon.js': './Icon.tsx',
};
for (const m of modules) {
files[m.filepath] = m.entrypoint;
Expand All @@ -115,23 +118,23 @@ async function builder(metadata, { output }) {
files[filename] = `${BANNER}
import React from 'react';
import Icon from './Icon.js';
import Icon from './Icon.tsx';
const didWarnAboutDeprecation = {};`;

for (const m of bucket) {
files[filename] += `export ${m.source}`;
files['index.js'] += `export { ${m.moduleName} } from '${filename}';`;
files['index.ts'] += `export { ${m.moduleName} } from '${filename}';`;
}
}

const bundle = await rollup({
input: input,
input,
external,
plugins: [
virtual({
'./Icon.js': await fs.readFile(
path.resolve(__dirname, './components/Icon.js'),
'./Icon.tsx': await fs.readFile(
path.resolve(__dirname, './components/Icon.tsx'),
'utf8'
),
...files,
Expand Down Expand Up @@ -164,12 +167,12 @@ const didWarnAboutDeprecation = {};`;
}

const umd = await rollup({
input: 'index.js',
input: 'index.ts',
external,
plugins: [
virtual({
'./Icon.js': await fs.readFile(
path.resolve(__dirname, './components/Icon.js'),
'./Icon.tsx': await fs.readFile(
path.resolve(__dirname, './components/Icon.tsx'),
'utf8'
),
...files,
Expand Down Expand Up @@ -267,7 +270,7 @@ function createIconEntrypoint(moduleName, descriptor, isDeprecated = false) {
const source = createIconSource(moduleName, descriptor, deprecatedBlock);
return `${BANNER}
import React from 'react';
import Icon from './Icon.js';
import Icon from './Icon.tsx';
${deprecatedPreamble}
${source}
export default ${moduleName};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Copyright IBM Corp. 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { IconProps } from './Icon';

export interface CarbonIconProps extends IconProps {
size?: number | string;
}

export type CarbonIconType = React.ForwardRefExoticComponent<
CarbonIconProps & React.RefAttributes<React.ReactSVGElement>
>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,37 @@ import { getAttributes } from '@carbon/icon-helpers';
import PropTypes from 'prop-types';
import React from 'react';

export interface IconProps
extends Omit<React.SVGProps<React.ReactSVGElement>, 'ref' | 'tabIndex'> {
/**
* @see React.SVGAttributes.tabIndex
* @todo remove support for string in v12
*/
tabIndex?: string | number | undefined;

title?: string | undefined;
}

const Icon = React.forwardRef(function Icon(
{ className, children, tabIndex, ...rest },
ref
{ className, children, tabIndex, ...rest }: IconProps,
ref: React.ForwardedRef<React.ReactSVGElement>
) {
const { tabindex, ...props } = getAttributes({
const { tabindex, ...attrs } = getAttributes({
...rest,
tabindex: tabIndex,
});
const props: React.SVGProps<React.ReactSVGElement> = attrs;

if (className) {
props.className = className;
}

if (tabindex !== undefined && tabindex !== null) {
props.tabIndex = tabindex;
if (typeof tabindex === 'number') {
props.tabIndex = tabindex;
} else {
props.tabIndex = Number(tabIndex);
}
}

if (ref) {
Expand All @@ -34,14 +50,17 @@ const Icon = React.forwardRef(function Icon(

Icon.displayName = 'Icon';
Icon.propTypes = {
'aria-hidden': PropTypes.string,
'aria-hidden': PropTypes.oneOfType([
PropTypes.bool,
PropTypes.oneOf<'true' | 'false'>(['true', 'false']),
]),
'aria-label': PropTypes.string,
'aria-labelledby': PropTypes.string,
children: PropTypes.node,
className: PropTypes.string,
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
preserveAspectRatio: PropTypes.string,
tabIndex: PropTypes.string,
tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
viewBox: PropTypes.string,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
xmlns: PropTypes.string,
Expand Down
Loading

0 comments on commit e769119

Please sign in to comment.