diff --git a/config/typescript-config-carbon/.gitignore b/config/typescript-config-carbon/.gitignore new file mode 100644 index 000000000000..d2f172d3206e --- /dev/null +++ b/config/typescript-config-carbon/.gitignore @@ -0,0 +1,2 @@ +/index.js +/index.d.ts diff --git a/config/typescript-config-carbon/README.md b/config/typescript-config-carbon/README.md new file mode 100644 index 000000000000..08215fe2d1c9 --- /dev/null +++ b/config/typescript-config-carbon/README.md @@ -0,0 +1,29 @@ +# typescript-config-carbon + +> TypeScript configuration and utilities for Carbon + +## Getting started + +To install `typescript-config-carbon` in your project, you will need to run the +following command using [npm](https://www.npmjs.com/): + +```bash +npm install -S typescript-config-carbon +``` + +If you prefer [Yarn](https://yarnpkg.com/en/), use the following command +instead: + +```bash +yarn add typescript-config-carbon +``` + +## 🙌 Contributing + +We're always looking for contributors to help us fix bugs, build new features, +or help us improve the project documentation. If you're interested, definitely +check out our [Contributing Guide](/.github/CONTRIBUTING.md)! 👀 + +## 📝 License + +Licensed under the [Apache 2.0 License](/LICENSE). diff --git a/config/typescript-config-carbon/index.ts b/config/typescript-config-carbon/index.ts new file mode 100644 index 000000000000..d046297137a1 --- /dev/null +++ b/config/typescript-config-carbon/index.ts @@ -0,0 +1,37 @@ +/** + * 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 path from 'path'; +import ts from 'typescript'; + +const tsConfigFile = path.join(__dirname, 'tsconfig.base.json'); + +export const diagnosticToMessage = (diagnostic: ts.Diagnostic) => { + const { file, messageText } = diagnostic; + const filePrefix = file ? `${file.fileName}: ` : ''; + const text = ts.flattenDiagnosticMessageText(messageText, '\n'); + return `${filePrefix}${text}`; +}; + +export const loadTsCompilerOpts = (path: string) => { + const { config, error } = ts.readConfigFile(path, ts.sys.readFile); + if (error) { + throw new Error(diagnosticToMessage(error)); + } + const opts = ts.convertCompilerOptionsFromJson(config.compilerOptions, ''); + const { errors } = opts; + if (errors.length > 0) { + errors.forEach((diagnostic) => { + console.log(diagnosticToMessage(diagnostic)); + }); + throw new Error('Base TypeScript config file errors found'); + } + return opts.options; +}; + +export const loadBaseTsCompilerOpts = () => { + return loadTsCompilerOpts(tsConfigFile); +}; diff --git a/config/typescript-config-carbon/package.json b/config/typescript-config-carbon/package.json new file mode 100644 index 000000000000..13266790a6bc --- /dev/null +++ b/config/typescript-config-carbon/package.json @@ -0,0 +1,35 @@ +{ + "name": "typescript-config-carbon", + "description": "TypeScript configuration for Carbon", + "version": "0.1.0", + "license": "Apache-2.0", + "main": "index.js", + "types": "index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/carbon-design-system/carbon.git", + "directory": "config/typescript-config-carbon" + }, + "bugs": "https://github.com/carbon-design-system/carbon/issues", + "keywords": [ + "carbon", + "carbon-design-system", + "components", + "react", + "typescript" + ], + "publishConfig": { + "access": "public", + "provenance": true + }, + "scripts": { + "clean": "rimraf index.js index.d.ts", + "build": "yarn clean && tsc" + }, + "dependencies": { + "typescript": "^4.8.4" + }, + "devDependencies": { + "rimraf": "^5.0.0" + } +} diff --git a/config/typescript-config-carbon/tsconfig.base.json b/config/typescript-config-carbon/tsconfig.base.json new file mode 100644 index 000000000000..dcbe6315f2bc --- /dev/null +++ b/config/typescript-config-carbon/tsconfig.base.json @@ -0,0 +1,106 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": ["dom", "dom.iterable", "esnext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "jsx": "react-jsx", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "esnext", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* + * Type Checking + * Note: some checks are set to false since linting takes care of them + */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + "noUnusedLocals": false, /* Enable error reporting when local variables aren't read. */ + "noUnusedParameters": false, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + "noFallthroughCasesInSwitch": false, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/config/typescript-config-carbon/tsconfig.json b/config/typescript-config-carbon/tsconfig.json new file mode 100644 index 000000000000..2e200d67f527 --- /dev/null +++ b/config/typescript-config-carbon/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "emitDeclarationOnly": false, + "module": "commonjs" + }, + "include": ["index.ts"] +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 6a4c2be12bbc..156bbd964a23 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -51,6 +51,7 @@ "rollup": "^2.79.1", "sass": "^1.51.0", "sassdoc": "^2.7.3", + "typescript-config-carbon": "^0.1.0", "yargs": "^17.0.1" } } diff --git a/packages/cli/src/commands/bundle/bundlers.js b/packages/cli/src/commands/bundle/bundlers.js index 26bdf6b78ad0..9bc95fd39523 100644 --- a/packages/cli/src/commands/bundle/bundlers.js +++ b/packages/cli/src/commands/bundle/bundlers.js @@ -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; diff --git a/packages/cli/src/commands/bundle/javascript.js b/packages/cli/src/commands/bundle/javascript.js index 45c314a323c1..4e5875e7c24e 100644 --- a/packages/cli/src/commands/bundle/javascript.js +++ b/packages/cli/src/commands/bundle/javascript.js @@ -10,10 +10,14 @@ const { babel } = require('@rollup/plugin-babel'); const commonjs = require('@rollup/plugin-commonjs'); const { nodeResolve } = require('@rollup/plugin-node-resolve'); -const { pascalCase } = require('change-case'); const fs = require('fs-extra'); const path = require('path'); const { rollup } = require('rollup'); +const { + formatGlobals, + findPackageFolder, + formatDependenciesIntoGlobals, +} = require('./utils'); async function bundle(entrypoint, options) { const globals = options.globals ? formatGlobals(options.globals) : {}; @@ -95,48 +99,4 @@ async function bundle(entrypoint, options) { ); } -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; diff --git a/packages/cli/src/commands/bundle/typescript.js b/packages/cli/src/commands/bundle/typescript.js new file mode 100644 index 000000000000..198c42dde046 --- /dev/null +++ b/packages/cli/src/commands/bundle/typescript.js @@ -0,0 +1,118 @@ +/** + * 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. + */ + +'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 fs = require('fs-extra'); +const path = require('path'); +const { rollup } = require('rollup'); +const { loadBaseTsCompilerOpts } = require('typescript-config-carbon'); +const { + formatGlobals, + findPackageFolder, + formatDependenciesIntoGlobals, +} = require('./utils'); + +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; + + const baseTsCompilerOpts = loadBaseTsCompilerOpts(); + + 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: { + ...baseTsCompilerOpts, + rootDir: 'src', + 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); + }) + ); +} + +module.exports = bundle; diff --git a/packages/cli/src/commands/bundle/utils.js b/packages/cli/src/commands/bundle/utils.js new file mode 100644 index 000000000000..8ad277782871 --- /dev/null +++ b/packages/cli/src/commands/bundle/utils.js @@ -0,0 +1,62 @@ +/** + * 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. + */ + +'use strict'; + +const path = require('path'); +const fs = require('fs-extra'); +const { pascalCase } = require('change-case'); + +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 = { + formatGlobals, + formatDependenciesIntoGlobals, + findPackageFolder, +}; diff --git a/packages/icon-build-helpers/package.json b/packages/icon-build-helpers/package.json index 2317c49eb954..335437da8399 100644 --- a/packages/icon-build-helpers/package.json +++ b/packages/icon-build-helpers/package.json @@ -50,6 +50,7 @@ "rollup-plugin-strip-banner": "^3.0.0", "svg-parser": "^2.0.4", "svgo": "^1.1.1", - "svgson": "^5.2.1" + "svgson": "^5.2.1", + "typescript-config-carbon": "^0.1.0" } } diff --git a/packages/icon-build-helpers/src/builders/react/builder.js b/packages/icon-build-helpers/src/builders/react/builder.js index fcad7615efd7..a538adde2184 100644 --- a/packages/icon-build-helpers/src/builders/react/builder.js +++ b/packages/icon-build-helpers/src/builders/react/builder.js @@ -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 }) { @@ -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; @@ -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, @@ -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, @@ -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}; diff --git a/packages/icon-build-helpers/src/builders/react/components/CarbonIcon.d.ts b/packages/icon-build-helpers/src/builders/react/components/CarbonIcon.d.ts new file mode 100644 index 000000000000..33af5a09c777 --- /dev/null +++ b/packages/icon-build-helpers/src/builders/react/components/CarbonIcon.d.ts @@ -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 +>; diff --git a/packages/icon-build-helpers/src/builders/react/components/Icon.js b/packages/icon-build-helpers/src/builders/react/components/Icon.tsx similarity index 62% rename from packages/icon-build-helpers/src/builders/react/components/Icon.js rename to packages/icon-build-helpers/src/builders/react/components/Icon.tsx index 87c020fe564a..cde30d0af000 100644 --- a/packages/icon-build-helpers/src/builders/react/components/Icon.js +++ b/packages/icon-build-helpers/src/builders/react/components/Icon.tsx @@ -8,6 +8,17 @@ import { getAttributes } from '@carbon/icon-helpers'; import PropTypes from 'prop-types'; import React from 'react'; +export interface IconProps + extends Omit, '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, @@ -16,20 +27,25 @@ const Icon = React.forwardRef(function Icon( xmlns = 'http://www.w3.org/2000/svg', preserveAspectRatio = 'xMidYMid meet', ...rest - }, - ref + }: IconProps, + ref: React.ForwardedRef ) { - const { tabindex, ...props } = getAttributes({ + const { tabindex, ...attrs } = getAttributes({ ...rest, tabindex: tabIndex, }); + const props: React.SVGProps = 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) { @@ -49,14 +65,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, diff --git a/packages/icon-build-helpers/src/builders/react/next.js b/packages/icon-build-helpers/src/builders/react/next.js index 3454ab4a46c2..c976cd832e4b 100644 --- a/packages/icon-build-helpers/src/builders/react/next.js +++ b/packages/icon-build-helpers/src/builders/react/next.js @@ -14,10 +14,12 @@ const { babel } = require('@rollup/plugin-babel'); const fs = require('fs-extra'); const path = require('path'); const { rollup } = require('rollup'); +const ts = require('typescript'); const virtual = require('../plugins/virtual'); const { babelConfig } = require('./next/babel'); const { svgToJSX, jsToAST } = require('./next/convert'); const templates = require('./next/templates'); +const { writeTsDefinitions } = require('./next/typescript'); // This builder outputs a collection of CommonJS modules representing our icon // components. It does not generate an `index.js` entrypoint file due to the @@ -84,13 +86,14 @@ async function builder(metadata, { output }) { // `input` object to files that we're generating for each icon component in // the `files` object const files = { - 'index.js': template.ast(` - import Icon from './Icon.js'; + 'index.ts': template.ast(` + import Icon from './Icon.tsx'; export { Icon }; `), }; const input = { - 'index.js': 'index.js', + 'index.js': 'index.ts', + 'Icon.js': './Icon.tsx', }; const BUCKET_SIZE = 125; const buckets = [ @@ -126,7 +129,7 @@ async function builder(metadata, { output }) { input[filename] = filename; files[filename] = template.ast(` import React from 'react'; - import Icon from './Icon.js'; + import Icon from './Icon.tsx'; import { iconPropTypes } from './iconPropTypes.js'; const didWarnAboutDeprecation = {}; `); @@ -137,16 +140,16 @@ async function builder(metadata, { output }) { files[filename] = t.file(t.program(files[filename])); files[filename] = generate(files[filename]).code; - files['index.js'].push(template.ast(`export * from '${filename}';`)); + files['index.ts'].push(template.ast(`export * from '${filename}';`)); } - files['index.js'] = generate(t.file(t.program(files['index.js']))).code; + files['index.ts'] = generate(t.file(t.program(files['index.ts']))).code; const defaultVirtualOptions = { - // Each of our Icon modules use the "./Icon.js" path to import this base - // componnet - './Icon.js': await fs.readFile( - path.resolve(__dirname, './components/Icon.js'), + // Each Icon module uses the "./Icon.tsx" path to import this base component + // Babel transforms the .tsx extension to .js + './Icon.tsx': await fs.readFile( + path.resolve(__dirname, './components/Icon.tsx'), 'utf8' ), './iconPropTypes.js': ` @@ -179,10 +182,12 @@ async function builder(metadata, { output }) { { directory: path.join(output, 'es'), format: 'esm', + tsModuleKind: ts.ModuleKind.ESNext, }, { directory: path.join(output, 'lib'), format: 'commonjs', + tsModuleKind: ts.ModuleKind.CommonJS, }, ]; @@ -194,6 +199,9 @@ async function builder(metadata, { output }) { banner: templates.banner, exports: 'auto', }); + + // write TypeScript definition files + writeTsDefinitions(modules, buckets, target.tsModuleKind, target.directory); } } @@ -206,7 +214,7 @@ async function builder(metadata, { output }) { * * ```jsx * import React from 'react'; - * import Icon from './Icon.js'; + * import Icon from './Icon.tsx'; * * const ComponentName = React.forwardRef( * function ComponentName({ children, size = 32, ... rest}, ref) { @@ -230,7 +238,7 @@ function createIconEntrypoint(moduleName, source, isDeprecated = false) { const statements = [ // Import statements template.ast(`import React from 'react';`), - template.ast(`import Icon from './Icon.js';`), + template.ast(`import Icon from './Icon.tsx';`), template.ast(`import { iconPropTypes } from './iconPropTypes.js';`), ]; diff --git a/packages/icon-build-helpers/src/builders/react/next/babel.js b/packages/icon-build-helpers/src/builders/react/next/babel.js index 65ec0985f888..b2b0db7173ad 100644 --- a/packages/icon-build-helpers/src/builders/react/next/babel.js +++ b/packages/icon-build-helpers/src/builders/react/next/babel.js @@ -20,12 +20,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'], }; module.exports = { diff --git a/packages/icon-build-helpers/src/builders/react/next/typescript.js b/packages/icon-build-helpers/src/builders/react/next/typescript.js new file mode 100644 index 000000000000..c7716b1112b2 --- /dev/null +++ b/packages/icon-build-helpers/src/builders/react/next/typescript.js @@ -0,0 +1,96 @@ +/** + * 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. + */ + +'use strict'; + +const fs = require('fs-extra'); +const path = require('path'); +const templates = require('./templates'); +const ts = require('typescript'); +const { + diagnosticToMessage, + loadBaseTsCompilerOpts, +} = require('typescript-config-carbon'); + +function emitIconComponent(compileOpts) { + const baseOpts = loadBaseTsCompilerOpts(); + const options = { ...baseOpts, ...compileOpts }; + const host = ts.createCompilerHost(options); + const iconComponentPath = path.resolve(__dirname, '../components/Icon.tsx'); + const program = ts.createProgram([iconComponentPath], options, host); + const emitResult = program.emit(); + const diagnostics = ts + .getPreEmitDiagnostics(program) + .concat(emitResult.diagnostics); + if (diagnostics.length > 0) { + diagnostics.forEach((diagnostic) => { + console.log(diagnosticToMessage(diagnostic)); + }); + throw new Error('Icon.tsx compilation failed'); + } +} + +async function copyCarbonIconType(outDir) { + const srcPath = path.resolve(__dirname, '../components/CarbonIcon.d.ts'); + const destPath = path.resolve(outDir, 'CarbonIcon.d.ts'); + await fs.copyFile(srcPath, destPath); +} + +async function writeModuleTypes(modules, outDir) { + for (const m of modules) { + const content = + templates.banner + + '\n' + + "import type { CarbonIconType } from './CarbonIcon';\n" + + 'export const ' + + m.name + + ': CarbonIconType;\n'; + const filename = path.resolve(outDir, m.filepath.replace(/\.js$/, '.d.ts')); + await fs.writeFile(filename, content); + } +} + +async function writeBucketTypes(buckets, outDir) { + for (const bucket of buckets) { + const iconLines = []; + for (const m of bucket.modules) { + iconLines.push('export const ' + m.name + ': CarbonIconType;'); + } + const bucketModule = `generated/${bucket.id}`; + const filepath = path.resolve(outDir, `${bucketModule}.d.ts`); + const content = + templates.banner + + '\n' + + "import type { CarbonIconType } from '../CarbonIcon';\n" + + iconLines.join('\n') + + '\n'; + await fs.writeFile(filepath, content); + } +} + +async function writeIndex(buckets, outDir) { + const bucketModules = buckets.map((bucket) => `generated/${bucket.id}`); + const indexContent = + templates.banner + + '\n' + + "export { default as Icon } from './Icon';\n" + + bucketModules.map((path) => "export * from './" + path + "';").join('\n') + + '\n'; + await fs.writeFile(path.resolve(outDir, 'index.d.ts'), indexContent); +} + +async function writeTsDefinitions(modules, buckets, moduleKind, outDir) { + emitIconComponent({ module: moduleKind, outDir }); + copyCarbonIconType(outDir); + writeModuleTypes(modules, outDir); + writeBucketTypes(buckets, outDir); + writeIndex(buckets, outDir); +} + +module.exports = { + writeTsDefinitions, +}; diff --git a/packages/icon-helpers/package.json b/packages/icon-helpers/package.json index 44ac0dd98353..bead5523b549 100644 --- a/packages/icon-helpers/package.json +++ b/packages/icon-helpers/package.json @@ -30,12 +30,13 @@ "provenance": true }, "scripts": { - "build": "yarn clean && carbon-cli bundle src/index.js --name CarbonIconHelpers", + "build": "yarn clean && carbon-cli bundle src/index.ts --name CarbonIconHelpers", "clean": "rimraf es lib umd" }, "devDependencies": { "@carbon/cli": "^11.14.0", - "rimraf": "^5.0.0" + "rimraf": "^5.0.0", + "typescript-config-carbon": "^0.1.0" }, "sideEffects": false } diff --git a/packages/icon-helpers/src/getAttributes.js b/packages/icon-helpers/src/getAttributes.ts similarity index 80% rename from packages/icon-helpers/src/getAttributes.js rename to packages/icon-helpers/src/getAttributes.ts index 3bb9a389ae86..c35c2fb4fc10 100644 --- a/packages/icon-helpers/src/getAttributes.js +++ b/packages/icon-helpers/src/getAttributes.ts @@ -4,8 +4,16 @@ * 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 React from 'react'; -export const defaultAttributes = { +interface IconAttributes + extends Omit, 'tabIndex'> { + tabindex?: string | number | undefined; + + title?: string | undefined; +} + +export const defaultAttributes: IconAttributes = { // Reference: // https://github.com/IBM/carbon-components-react/issues/1392 // https://github.com/PolymerElements/iron-iconset-svg/pull/47 @@ -23,9 +31,9 @@ export default function getAttributes({ height, viewBox = `0 0 ${width} ${height}`, ...attributes -} = {}) { +}: IconAttributes = {}): IconAttributes { const { tabindex, ...rest } = attributes; - const iconAttributes = { + const iconAttributes: IconAttributes = { ...defaultAttributes, ...rest, width, diff --git a/packages/icon-helpers/src/index.js b/packages/icon-helpers/src/index.ts similarity index 100% rename from packages/icon-helpers/src/index.js rename to packages/icon-helpers/src/index.ts diff --git a/packages/icon-helpers/tsconfig.json b/packages/icon-helpers/tsconfig.json new file mode 100644 index 000000000000..289e22a627be --- /dev/null +++ b/packages/icon-helpers/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "typescript-config-carbon/tsconfig.base.json", + "include": ["src/**/*"] +} diff --git a/packages/pictograms-react/package.json b/packages/pictograms-react/package.json index db895d6fae37..5a797226ef87 100644 --- a/packages/pictograms-react/package.json +++ b/packages/pictograms-react/package.json @@ -44,7 +44,8 @@ }, "devDependencies": { "@carbon/icon-build-helpers": "^1.18.0", - "@carbon/pictograms": "^12.24.0" + "@carbon/pictograms": "^12.24.0", + "rimraf": "^5.0.0" }, "sideEffects": false } diff --git a/packages/react/icons/.gitignore b/packages/react/icons/.gitignore index 2acb30a937bf..8bb6d436611b 100644 --- a/packages/react/icons/.gitignore +++ b/packages/react/icons/.gitignore @@ -1,2 +1,3 @@ /index.esm.js /index.js +/index.d.ts diff --git a/packages/react/icons/src/index.js b/packages/react/icons/src/index.ts similarity index 100% rename from packages/react/icons/src/index.js rename to packages/react/icons/src/index.ts diff --git a/packages/react/package.json b/packages/react/package.json index b1a157bee5db..34852bed3e37 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -34,7 +34,7 @@ }, "scripts": { "build": "yarn clean && node tasks/build-styles.js && node tasks/build.js", - "clean": "rimraf es lib icons/index.js icons/index.esm.js storybook-static", + "clean": "rimraf es lib icons/index.js icons/index.d.ts icons/index.esm.js storybook-static", "postinstall": "carbon-telemetry collect --install", "storybook": "storybook dev -p 3000", "storybook:build": "storybook build" @@ -124,7 +124,7 @@ "storybook-readme": "^5.0.9", "stream-browserify": "^3.0.0", "style-loader": "^3.3.1", - "typescript": "^4.8.4", + "typescript-config-carbon": "^0.1.0", "webpack": "^5.65.0", "webpack-dev-server": "^4.7.4" }, diff --git a/packages/react/src/components/FileUploader/Filename.tsx b/packages/react/src/components/FileUploader/Filename.tsx index 855944da93f3..1dddf7f0fa62 100644 --- a/packages/react/src/components/FileUploader/Filename.tsx +++ b/packages/react/src/components/FileUploader/Filename.tsx @@ -11,11 +11,12 @@ import React from 'react'; import Loading from '../Loading'; import { usePrefix } from '../../internal/usePrefix'; import { ReactAttr } from '../../types/common'; - export type FilenameStatus = 'edit' | 'complete' | 'uploading'; +type SVGAttr = React.SVGAttributes; + export interface FilenameProps - extends Omit, 'tabIndex'> { + extends Omit { /** * Specify an id that describes the error to be read by screen readers when the filename is invalid */ @@ -88,7 +89,7 @@ function Filename({ aria-label={iconDescription} className={`${prefix}--file-complete`} {...rest} - tabIndex={null}> + tabIndex={-1}> {iconDescription && {iconDescription}} ); diff --git a/packages/react/tasks/build.js b/packages/react/tasks/build.js index 4168b07a73cb..2e2a187cd7b6 100644 --- a/packages/react/tasks/build.js +++ b/packages/react/tasks/build.js @@ -14,15 +14,21 @@ const typescript = require('@rollup/plugin-typescript'); const path = require('path'); const { rollup } = require('rollup'); const stripBanner = require('rollup-plugin-strip-banner'); +const { + loadBaseTsCompilerOpts, + loadTsCompilerOpts, +} = require('typescript-config-carbon'); const packageJson = require('../package.json'); async function build() { const reactEntrypoint = { filepath: path.resolve(__dirname, '..', 'src', 'index.ts'), + rootDir: 'src', outputDirectory: path.resolve(__dirname, '..'), }; const iconsEntrypoint = { - filepath: path.resolve(__dirname, '..', 'icons', 'src', 'index.js'), + filepath: path.resolve(__dirname, '..', 'icons', 'src', 'index.ts'), + rootDir: path.join('icons', 'src'), outputDirectory: path.resolve(__dirname, '..', 'icons'), }; const formats = [ @@ -45,8 +51,8 @@ async function build() { const reactInputConfig = getRollupConfig( reactEntrypoint.filepath, - outputDirectory, - true + reactEntrypoint.rootDir, + outputDirectory ); const reactBundle = await rollup(reactInputConfig); @@ -58,20 +64,24 @@ async function build() { banner, exports: 'named', }); - } - const iconsInputConfig = getRollupConfig(iconsEntrypoint.filepath); - const iconsBundle = await rollup(iconsInputConfig); + const iconsInputConfig = getRollupConfig( + iconsEntrypoint.filepath, + iconsEntrypoint.rootDir, + outputDirectory + ); + const iconsBundle = await rollup(iconsInputConfig); - // Build @carbon/react icons - for (const format of formats) { - await iconsBundle.write({ - file: - format.type === 'commonjs' ? 'icons/index.js' : 'icons/index.esm.js', - format: format.type, - banner, - exports: 'named', - }); + // Build @carbon/react icons + for (const format of formats) { + await iconsBundle.write({ + file: + format.type === 'commonjs' ? 'icons/index.js' : 'icons/index.esm.js', + format: format.type, + banner, + exports: 'named', + }); + } } } @@ -98,6 +108,7 @@ const babelConfig = { }, ], '@babel/preset-react', + '@babel/preset-typescript', ], plugins: [ 'dev-expression', @@ -107,9 +118,17 @@ const babelConfig = { '@babel/plugin-transform-react-constant-elements', ], babelHelpers: 'bundled', + extensions: ['.ts', '.tsx', '.js', '.jsx'], }; -function getRollupConfig(input, outDir, useTS) { +function getTsCompilerOptions() { + const baseOpts = loadBaseTsCompilerOpts(); + const projectTsConfigPath = path.resolve(__dirname, '../tsconfig.json'); + const overrideOpts = loadTsCompilerOpts(projectTsConfigPath); + return { ...baseOpts, ...overrideOpts }; +} + +function getRollupConfig(input, rootDir, outDir) { return { input, // Mark dependencies listed in `package.json` as external so that they are @@ -132,8 +151,17 @@ function getRollupConfig(input, outDir, useTS) { commonjs({ include: /node_modules/, }), - // Modify plugins for builds that require typescript - ...(useTS ? getTSPlugins(outDir) : getPlugins()), + typescript({ + noEmitOnError: true, + noForceEmit: true, + outputToFilesystem: false, + compilerOptions: { + ...getTsCompilerOptions(), + rootDir, + outDir, + }, + }), + babel(babelConfig), stripBanner(), { transform(_code, id) { @@ -150,39 +178,6 @@ function getRollupConfig(input, outDir, useTS) { }; } -/** - * Rollup plugins to support typescript compilation/transpilation - * @param {*} outDir - * @returns - */ -function getTSPlugins(outDir) { - return [ - typescript({ - noEmitOnError: true, - noForceEmit: true, - outputToFilesystem: false, - compilerOptions: { - rootDir: 'src', - emitDeclarationOnly: true, - outDir, - }, - }), - babel({ - ...babelConfig, - presets: [...babelConfig.presets, '@babel/preset-typescript'], - extensions: ['.ts', '.tsx', '.js', '.jsx'], - }), - ]; -} - -/** - * Rollup plugins to support pure JS compilation - * @returns - */ -function getPlugins() { - return [babel(babelConfig)]; -} - build().catch((error) => { console.log(error); process.exit(1); diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 819211ef4edb..88b0679a84e9 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -1,26 +1,8 @@ { + "extends": "typescript-config-carbon/tsconfig.base.json", "compilerOptions": { - "target": "ES6", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "node", - "isolatedModules": true, - "resolveJsonModule": true, - "jsx": "react-jsx", - "emitDeclarationOnly": true, - "declaration": true, - // use linting instead - "noUnusedLocals": false, - "noUnusedParameters": false, - "noFallthroughCasesInSwitch": true, // TODO: Turn back on once stricter typings for internal utitlies are complete "noImplicitAny": false }, - "include": ["src/**/*", "icons/**/*"], - "exclude": ["node_modules", "build"] + "include": ["src/**/*", "icons/src/index.ts"] } diff --git a/yarn.lock b/yarn.lock index 87b4699421e0..e07cefc9ea47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1876,6 +1876,7 @@ __metadata: rollup: ^2.79.1 sass: ^1.51.0 sassdoc: ^2.7.3 + typescript-config-carbon: ^0.1.0 yargs: ^17.0.1 bin: carbon-cli: ./bin/carbon-cli.js @@ -1991,6 +1992,7 @@ __metadata: svg-parser: ^2.0.4 svgo: ^1.1.1 svgson: ^5.2.1 + typescript-config-carbon: ^0.1.0 languageName: unknown linkType: soft @@ -2000,6 +2002,7 @@ __metadata: dependencies: "@carbon/cli": ^11.14.0 rimraf: ^5.0.0 + typescript-config-carbon: ^0.1.0 languageName: unknown linkType: soft @@ -2094,6 +2097,7 @@ __metadata: "@carbon/pictograms": ^12.24.0 "@carbon/telemetry": 0.1.0 prop-types: ^15.7.2 + rimraf: ^5.0.0 peerDependencies: react: ">=16" languageName: unknown @@ -2185,7 +2189,7 @@ __metadata: storybook-readme: ^5.0.9 stream-browserify: ^3.0.0 style-loader: ^3.3.1 - typescript: ^4.8.4 + typescript-config-carbon: ^0.1.0 use-resize-observer: ^6.0.0 webpack: ^5.65.0 webpack-dev-server: ^4.7.4 @@ -30661,6 +30665,15 @@ __metadata: languageName: node linkType: hard +"typescript-config-carbon@^0.1.0, typescript-config-carbon@workspace:config/typescript-config-carbon": + version: 0.0.0-use.local + resolution: "typescript-config-carbon@workspace:config/typescript-config-carbon" + dependencies: + rimraf: ^5.0.0 + typescript: ^4.8.4 + languageName: unknown + linkType: soft + "typescript@npm:>=3 < 6": version: 5.1.3 resolution: "typescript@npm:5.1.3"