From 4adde2276885b511945f478328f711c12b111dc4 Mon Sep 17 00:00:00 2001 From: Ignacio Becerra Date: Tue, 13 Feb 2024 13:02:46 -0800 Subject: [PATCH] chore(react): remove react wrappers (#11502) * chore(react): remove react wrapper * chore(deps): remove uneeded deps * fix(react): remove more react stuff --- .../carbon-web-components/.eslintrc.js | 52 -- .../packages/carbon-web-components/README.md | 54 +- .../carbon-web-components/docs/form.md | 35 - .../carbon-web-components/docs/form.mdx | 4 - .../carbon-web-components/docs/welcome.mdx | 14 +- .../gulp-tasks/build/modules.js | 6 - .../gulp-tasks/build/modules/react-defs.js | 85 --- .../gulp-tasks/build/modules/react-types.js | 59 -- .../gulp-tasks/build/modules/react.js | 86 --- .../gulp-tasks/build/modules/types.js | 2 +- .../carbon-web-components/package.json | 7 - .../src/components/button/button-set.ts | 1 - .../src/components/implementation-practice.md | 27 - .../wrappers/createReactCustomElementType.ts | 336 ---------- .../tests/integration/build/ie_steps.js | 48 -- .../integration/build/react-ssr_steps.js | 48 -- .../tests/integration/build/react_steps.js | 48 -- ...in-create-react-custom-element-type-def.js | 106 --- ...plugin-create-react-custom-element-type.js | 626 ------------------ .../carbon-web-components/tsconfig.json | 1 - 20 files changed, 8 insertions(+), 1637 deletions(-) delete mode 100644 web-components/packages/carbon-web-components/gulp-tasks/build/modules/react-defs.js delete mode 100644 web-components/packages/carbon-web-components/gulp-tasks/build/modules/react-types.js delete mode 100644 web-components/packages/carbon-web-components/gulp-tasks/build/modules/react.js delete mode 100644 web-components/packages/carbon-web-components/src/components/implementation-practice.md delete mode 100644 web-components/packages/carbon-web-components/src/globals/wrappers/createReactCustomElementType.ts delete mode 100644 web-components/packages/carbon-web-components/tests/integration/build/ie_steps.js delete mode 100644 web-components/packages/carbon-web-components/tests/integration/build/react-ssr_steps.js delete mode 100644 web-components/packages/carbon-web-components/tests/integration/build/react_steps.js delete mode 100644 web-components/packages/carbon-web-components/tools/babel-plugin-create-react-custom-element-type-def.js delete mode 100644 web-components/packages/carbon-web-components/tools/babel-plugin-create-react-custom-element-type.js diff --git a/web-components/packages/carbon-web-components/.eslintrc.js b/web-components/packages/carbon-web-components/.eslintrc.js index 3ab5cc35c399..0e667c2a58b0 100644 --- a/web-components/packages/carbon-web-components/.eslintrc.js +++ b/web-components/packages/carbon-web-components/.eslintrc.js @@ -57,56 +57,12 @@ module.exports = { 'no-undef': 0, }, }, - { - files: ['**/*.tsx', '**/components-react/**/*-container.ts'], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'react'], - rules: { - 'no-unused-vars': 0, - // TODO: See why the ESLint plugin does not work with `.tsx` - '@carbon/react-prop-type-comments/require-proptype-comment': 0, - '@typescript-eslint/no-unused-vars': 2, - // 'import/no-unresolved': [ - // 2, - // { - // ignore: ['^./'], - // }, - // ], - 'jsdoc/require-param-type': 0, - 'jsdoc/require-returns-type': 0, - 'react/jsx-uses-react': 2, - 'react/jsx-uses-vars': 2, - }, - }, { files: ['**/defs.ts'], rules: { 'import/prefer-default-export': 0, }, }, - { - files: ['**/*.stories.react.tsx'], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'react'], - rules: { - 'no-unused-vars': 0, - '@typescript-eslint/no-unused-vars': 2, - // 'import/no-extraneous-dependencies': 0, - // 'import/no-unresolved': [ - // 2, - // { - // ignore: [ - // '^carbon-web-components/es/(components-react|icons)/', - // '^@carbon/ibmdotcom-web-components/es/(components-react|icons)/', - // '/components-react/', - // ], - // }, - // ], - 'react/jsx-uses-react': 2, - 'react/jsx-uses-vars': 2, - 'react/prop-types': 0, - }, - }, { files: ['examples/codesandbox/**/*.js', 'examples/codesandbox/**/*.ts'], parserOptions: { @@ -128,14 +84,6 @@ module.exports = { // 'import/no-unresolved': 0, }, }, - { - files: ['examples/codesandbox/{react,form/redux-form}/**/*.js'], - plugins: ['react'], - rules: { - 'react/jsx-uses-react': 2, - 'react/jsx-uses-vars': 2, - }, - }, { files: [ 'examples/codesandbox/**/*.config.js', diff --git a/web-components/packages/carbon-web-components/README.md b/web-components/packages/carbon-web-components/README.md index 78f8566288f8..151b75269c8e 100644 --- a/web-components/packages/carbon-web-components/README.md +++ b/web-components/packages/carbon-web-components/README.md @@ -260,15 +260,9 @@ same manner as native HTML tags, for example: ## JavaScript framework support -In addition to the available Web Component versions of Carbon components, this -library also supports usage with JavaScript frameworks like Angular, React, and -Vue if the desire is to use instead of the pure framework versions of Carbon -components. Specifically for React, this library comes with a wrapper -implementation around the Carbon Web Components for more seamless integration -with your React application. - -This is achievable since Web Components is the modern browser standard, and -works well with other front-end frameworks that exist in the application. In +This package can also be used within other JavaScript frameworks such as Angular and +Vue. This is achievable since web components are the modern browser standard, and +work well with other front-end frameworks that exist in the application. In turn, this also comes with the benefits of encapsulation within the Shadow DOM: ### Angular @@ -306,48 +300,6 @@ application can use those `.d.ts` files: as follows: `window.__importDefault = mod => (mod?.__esModule ? mod : { default: mod })` -### React - -[![Edit carbon-web-components with React](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/carbon-design-system/carbon-for-ibm-dotcom/tree/feat/cwc-v2/packages/carbon-web-components/examples/codesandbox/react) - -You can use wrapper React components in -`@carbon/web-components/es/components-react` generated -[automatically from the custom elements](./src/globals/wrappers/createReactCustomElementType.ts) -which allows you to use our components seamlessly in your React code. Here's an -example: - -```javascript -import React from 'react'; -import { render } from 'react-dom'; -import CDSDropdown from '@carbon/web-components/es/components-react/dropdown/dropdown.js'; -import CDSDropdownItem from '@carbon/web-components/es/components-react/dropdown/dropdown-item.js'; - -const App = () => ( - - Option 1 - Option 2 - Option 3 - Option 4 - Option 5 - -); - -render(, document.getElementById('root')); -``` - -Note: Using the React wrapper requires an additional dependency, -[`prop-types`](https://www.npmjs.com/package/prop-types). - -To run the wrapper React components in SSR environment requires Node `12.16.3` -or above that supports -["conditional mapping" feature](https://github.com/jkrems/proposal-pkg-exports#2-conditional-mapping): - -[![Edit carbon-web-components with React SSR](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/carbon-design-system/carbon-for-ibm-dotcom/tree/feat/cwc-v2/packages/carbon-web-components/examples/codesandbox/react-ssr) - -Same Node version requirement applies to Next.js: - -[![Edit carbon-web-components with React SSR](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/carbon-design-system/carbon-for-ibm-dotcom/tree/feat/cwc-v2/packages/carbon-web-components/examples/codesandbox/next) - ### Vue [![Edit carbon-web-components with Vue](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/carbon-design-system/carbon-for-ibm-dotcom/tree/feat/cwc-v2/packages/carbon-web-components/examples/codesandbox/vue) diff --git a/web-components/packages/carbon-web-components/docs/form.md b/web-components/packages/carbon-web-components/docs/form.md index 20c83256458d..c016bab86e86 100644 --- a/web-components/packages/carbon-web-components/docs/form.md +++ b/web-components/packages/carbon-web-components/docs/form.md @@ -26,38 +26,3 @@ button.addEventListener('click', () => { ``` [![Edit carbon-web-components with formdata event](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/carbon-design-system/carbon-for-ibm-dotcom/tree/feat/cwc-v2/packages/carbon-web-components/examples/codesandbox/form/basic) - -## Redux Form - -You can use our form components with Redux Form by creating a React component that wraps our form components: - -```javascript -import { Field } from 'redux-form'; -import CDSFormItem from '@carbon/web-components/es/components-react/form/form-item'; -import CDSTextInput from '@carbon/web-components/es/components-react/text-input/text-input'; - -... - -// A React component that wraps form components from `@carbon/web-components` -const FieldImpl = ({ input, label, type, meta: { touched, error } }) => { - const validityMessage = !touched ? undefined : error; - return ( - - - - ); -}; - -... - - -``` - -[![Edit carbon-web-components with Redux Form](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/carbon-design-system/carbon-web-components/tree/feat/cwc-v2/examples/codesandbox/form/redux-form) diff --git a/web-components/packages/carbon-web-components/docs/form.mdx b/web-components/packages/carbon-web-components/docs/form.mdx index 350b626cbaea..900a89bd375a 100644 --- a/web-components/packages/carbon-web-components/docs/form.mdx +++ b/web-components/packages/carbon-web-components/docs/form.mdx @@ -34,7 +34,3 @@ button.addEventListener('click', () => { style={{ width: '100%', height: '500px', border: 'solid rgba(0,0,0,0.1) 1px', boxShadow: 'rgba(0,0,0,0.1) 0 1px 3px 0' }} title="carbon-web-components-getting-started" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"> - -## Framework-specific approaches of form participation - -- [Redux form](https://web-components.carbondesignsystem.com/react/?path=/story/introduction-form-paticipation--page) diff --git a/web-components/packages/carbon-web-components/docs/welcome.mdx b/web-components/packages/carbon-web-components/docs/welcome.mdx index 58b43b75fade..6168812ee7b8 100644 --- a/web-components/packages/carbon-web-components/docs/welcome.mdx +++ b/web-components/packages/carbon-web-components/docs/welcome.mdx @@ -111,16 +111,10 @@ An alternative to using a bundler are CDN artifacts which can be added client si ### JavaScript framework integration -In addition to the available Web Component versions of Carbon components, this -library also supports usage with JavaScript frameworks like Angular, React, -and Vue if the desire is to use instead of the pure framework versions of -Carbon components. Specifically for React, this library comes with a wrapper -implementation around the Carbon Web Components for more seamless integration -with your React application. - -This is achievable since Web Components is the modern browser standard, and -works well with other front-end frameworks that exist in the application. In -turn, this also comes with the benefits of encapsulation within the Shadow DOM. +This package can also be used within other JavaScript frameworks such as Angular and +Vue. This is achievable since web components are the modern browser standard, and +work well with other front-end frameworks that exist in the application. In +turn, this also comes with the benefits of encapsulation within the Shadow DOM: ### Other usage guides diff --git a/web-components/packages/carbon-web-components/gulp-tasks/build/modules.js b/web-components/packages/carbon-web-components/gulp-tasks/build/modules.js index 6ac7898d2b0e..950ae30798ad 100644 --- a/web-components/packages/carbon-web-components/gulp-tasks/build/modules.js +++ b/web-components/packages/carbon-web-components/gulp-tasks/build/modules.js @@ -11,9 +11,6 @@ const gulp = require('gulp'); require('./modules/css'); require('./modules/icon-types'); require('./modules/icons'); -require('./modules/react'); -require('./modules/react-defs'); -require('./modules/react-types'); require('./modules/scripts'); require('./modules/scripts-node'); require('./modules/types'); @@ -24,9 +21,6 @@ gulp.task( gulp.task('build:modules:css'), gulp.task('build:modules:icon-types'), gulp.task('build:modules:icons'), - gulp.task('build:modules:react'), - gulp.task('build:modules:react-defs'), - gulp.task('build:modules:react-types'), gulp.task('build:modules:scripts'), gulp.task('build:modules:scripts-node'), gulp.task('build:modules:types') diff --git a/web-components/packages/carbon-web-components/gulp-tasks/build/modules/react-defs.js b/web-components/packages/carbon-web-components/gulp-tasks/build/modules/react-defs.js deleted file mode 100644 index 8ff8dfccee21..000000000000 --- a/web-components/packages/carbon-web-components/gulp-tasks/build/modules/react-defs.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @license - * - * Copyright IBM Corp. 2020, 2022 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -const asyncDone = require('async-done'); -const babel = require('gulp-babel'); -const gulp = require('gulp'); -const header = require('gulp-header'); -const path = require('path'); -const prettier = require('gulp-prettier'); -const replaceExtension = require('replace-ext'); -const through2 = require('through2'); -const { readFile } = require('fs'); -const { promisify } = require('util'); - -const config = require('../../config'); - -const readFileAsync = promisify(readFile); -const promisifyStream = promisify(asyncDone); - -/** - * Builds enums for React. - * - * @param {object} options The build options. - * @param {string} options.banner The banner content. - * @param {string} [options.targetEnv=browser] The target environment. - * @private - */ -const buildModulesReactDefs = ({ banner, targetEnv = 'browser' }) => { - const destDir = { - browser: `${config.jsDestDir}/components-react`, - node: `${config.cjsDestDir}/components-react-node`, - }[targetEnv]; - - const componentDestDir = { - browser: `${config.jsDestDir}/components`, - node: `${config.cjsDestDir}/components`, - }[targetEnv]; - - let stream = gulp.src([`${config.srcDir}/components/**/defs.ts`]).pipe( - through2.obj((file, enc, done) => { - const importSource = replaceExtension( - path.relative( - path.dirname(path.resolve(__dirname, '..', destDir, file.relative)), - path.resolve(__dirname, '..', componentDestDir, file.relative) - ), - '.js' - ); - file.contents = Buffer.from(`export * from ${JSON.stringify(importSource)}`); - file.path = replaceExtension(file.path, '.js'); - done(null, file); - }) - ); - - if (targetEnv === 'node') { - stream = stream.pipe( - babel({ - babelrc: false, - plugins: ['@babel/plugin-transform-modules-commonjs'], - }) - ); - } - - return stream.pipe(prettier()).pipe(header(banner)).pipe(gulp.dest(destDir)); -}; - -/** - * Builds the React defs - * - * @returns {Promise} Gulp stream - */ -async function reactDefs() { - const banner = await readFileAsync(path.resolve(__dirname, '../../../tools/license.js'), 'utf8'); - await Promise.all([ - promisifyStream(() => buildModulesReactDefs({ banner })), - promisifyStream(() => buildModulesReactDefs({ banner, targetEnv: 'node' })), - ]); -} - -gulp.task('build:modules:react-defs', reactDefs); diff --git a/web-components/packages/carbon-web-components/gulp-tasks/build/modules/react-types.js b/web-components/packages/carbon-web-components/gulp-tasks/build/modules/react-types.js deleted file mode 100644 index 3d173c32ff47..000000000000 --- a/web-components/packages/carbon-web-components/gulp-tasks/build/modules/react-types.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @license - * - * Copyright IBM Corp. 2020, 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. - */ - -const asyncDone = require('async-done'); -const babel = require('gulp-babel'); -const gulp = require('gulp'); -const header = require('gulp-header'); -const path = require('path'); -const prettier = require('gulp-prettier'); -const rename = require('gulp-rename'); -const { readFile } = require('fs'); -const { promisify } = require('util'); - -const config = require('../../config'); -const babelPluginCreateReactCustomElementTypeDef = require('../../../tools/babel-plugin-create-react-custom-element-type-def'); - -const readFileAsync = promisify(readFile); -const promisifyStream = promisify(asyncDone); - -/** - * Builds the React types - * - * @returns {Promise} Gulp stream - */ -async function reactTypes() { - const banner = await readFileAsync(path.resolve(__dirname, '../../../tools/license.js'), 'utf8'); - await promisifyStream(() => - gulp - .src([`${config.srcDir}/components/**/*.ts`, `!${config.srcDir}/**/*-story*.ts*`, `!${config.srcDir}/**/stories/*.ts`]) - .pipe( - babel({ - babelrc: false, - plugins: [ - ['@babel/plugin-syntax-decorators', { decoratorsBeforeExport: true }], - '@babel/plugin-syntax-typescript', - '@babel/plugin-transform-nullish-coalescing-operator', - '@babel/plugin-transform-optional-chaining', - babelPluginCreateReactCustomElementTypeDef, - ], - }) - ) - .pipe(prettier()) - .pipe(header(banner)) - .pipe( - rename((pathObj) => { - pathObj.extname = '.d.ts'; - }) - ) - .pipe(gulp.dest(`${config.jsDestDir}/components-react`)) - ); -} - -gulp.task('build:modules:react-types', reactTypes); diff --git a/web-components/packages/carbon-web-components/gulp-tasks/build/modules/react.js b/web-components/packages/carbon-web-components/gulp-tasks/build/modules/react.js deleted file mode 100644 index 5075a54c10fd..000000000000 --- a/web-components/packages/carbon-web-components/gulp-tasks/build/modules/react.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @license - * - * Copyright IBM Corp. 2020, 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. - */ - -const asyncDone = require('async-done'); -const gulp = require('gulp'); -const babel = require('gulp-babel'); -const header = require('gulp-header'); -const path = require('path'); -const prettier = require('gulp-prettier'); -const { promisify } = require('util'); -const { readFile } = require('fs'); - -const config = require('../../config'); -const babelPluginCreateReactCustomElementType = require('../../../tools/babel-plugin-create-react-custom-element-type'); -const babelPluginResourceCJSPaths = require('../../../tools/babel-plugin-resource-cjs-paths'); - -const readFileAsync = promisify(readFile); -const promisifyStream = promisify(asyncDone); - -/** - * Builds React modules. - * - * @param {object} options The build options. - * @param {string} options.banner The banner content. - * @param {string} [options.targetEnv=browser] The target environment. - * @private - */ -const buildModulesReact = ({ banner, targetEnv = 'browser' }) => { - let stream = gulp - .src([ - `${config.srcDir}/components/**/*.ts`, - `!${config.srcDir}/**/defs.ts*`, - `!${config.srcDir}/**/*-story*.ts*`, - `!${config.srcDir}/**/stories/*.ts`, - ]) - .pipe( - babel({ - babelrc: false, - plugins: [ - ['@babel/plugin-syntax-decorators', { decoratorsBeforeExport: true }], - '@babel/plugin-syntax-typescript', - '@babel/plugin-transform-nullish-coalescing-operator', - '@babel/plugin-transform-optional-chaining', - [babelPluginCreateReactCustomElementType, { nonUpgradable: targetEnv === 'node' }], - ], - }) - ); - - if (targetEnv === 'node') { - stream = stream.pipe( - babel({ - babelrc: false, - // Ensures `babel-plugin-resource-cjs-paths` runs before `@babel/plugin-transform-modules-commonjs` - plugins: [babelPluginResourceCJSPaths, '@babel/plugin-transform-modules-commonjs'], - }) - ); - } - - const destDir = { - browser: `${config.jsDestDir}/components-react`, - node: `${config.cjsDestDir}/components-react-node`, - }[targetEnv]; - - return stream.pipe(prettier()).pipe(header(banner)).pipe(gulp.dest(destDir)); -}; - -/** - * Builds the react modules - * - * @returns {Promise} Gulp stream - */ -async function react() { - const banner = await readFileAsync(path.resolve(__dirname, '../../../tools/license.js'), 'utf8'); - await Promise.all([ - promisifyStream(() => buildModulesReact({ banner })), - promisifyStream(() => buildModulesReact({ banner, targetEnv: 'node' })), - ]); -} - -gulp.task('build:modules:react', react); diff --git a/web-components/packages/carbon-web-components/gulp-tasks/build/modules/types.js b/web-components/packages/carbon-web-components/gulp-tasks/build/modules/types.js index 6f86cf87c513..7751268ed83d 100644 --- a/web-components/packages/carbon-web-components/gulp-tasks/build/modules/types.js +++ b/web-components/packages/carbon-web-components/gulp-tasks/build/modules/types.js @@ -23,7 +23,7 @@ const config = require('../../config'); function types() { const tsProject = typescript.createProject(path.resolve(__dirname, '../../../tsconfig.json')); const { dts } = gulp - .src([`${config.srcDir}/**/*.ts`, `!${config.srcDir}/**/*-story*.ts*`, `!${config.srcDir}/**/stories/**/*.ts*`]) + .src([`${config.srcDir}/**/*.ts`, `!${config.srcDir}/**/*.stories.ts*`, `!${config.srcDir}/**/stories/**/*.ts*`]) .pipe(sourcemaps.init()) .pipe(tsProject()); return dts diff --git a/web-components/packages/carbon-web-components/package.json b/web-components/packages/carbon-web-components/package.json index c8276e0043c1..0506e03514d4 100644 --- a/web-components/packages/carbon-web-components/package.json +++ b/web-components/packages/carbon-web-components/package.json @@ -12,10 +12,6 @@ "main": "es/index.js", "module": "es/index.js", "exports": { - "./es/components-react/*": { - "node": "./lib/components-react-node/*", - "default": "./es/components-react/*" - }, "./es/components/*": { "node": "./lib/components/*", "default": "./es/components/*" @@ -108,7 +104,6 @@ "@rollup/plugin-terser": "^0.4.3", "@rollup/pluginutils": "^4.2.0", "@storybook/addon-essentials": "^7.6.4", - "@storybook/addon-knobs": "^7.0.2", "@storybook/addon-links": "^7.6.4", "@storybook/addon-mdx-gfm": "^7.6.5", "@storybook/addon-storysource": "^7.6.4", @@ -184,8 +179,6 @@ "postcss-selector-parser": "^6.0.0", "prop-types": "^15.7.2", "puppeteer": "^13.0.0", - "react": "16.14.0", - "react-dom": "16.14.0", "read-pkg-up": "^7.0.0", "replace-ext": "^2.0.0", "resize-observer-polyfill": "^1.5.0", diff --git a/web-components/packages/carbon-web-components/src/components/button/button-set.ts b/web-components/packages/carbon-web-components/src/components/button/button-set.ts index 12b7bfb9c3dc..555e08f0a134 100644 --- a/web-components/packages/carbon-web-components/src/components/button/button-set.ts +++ b/web-components/packages/carbon-web-components/src/components/button/button-set.ts @@ -71,5 +71,4 @@ class CDSButtonSet extends LitElement { static styles = styles; // `styles` here is a `CSSResult` generated by custom Vite loader } -/* @__GENERATE_REACT_CUSTOM_ELEMENT_TYPE__ */ export default CDSButtonSet; diff --git a/web-components/packages/carbon-web-components/src/components/implementation-practice.md b/web-components/packages/carbon-web-components/src/components/implementation-practice.md deleted file mode 100644 index e1114cfc1b9b..000000000000 --- a/web-components/packages/carbon-web-components/src/components/implementation-practice.md +++ /dev/null @@ -1,27 +0,0 @@ -# Component implementation practice - -Starting point: -https://github.com/carbon-design-system/carbon/blob/v10.3.2/packages/react/src/components/TextInput/TextInput.js - -- A set of React props for a component works as a public API of the component, - in some ways similar to `@Input()`. What is the corresponding feature in - Custom Elements? -- Some React props works as event listeners, similar to `@Output()`. How can we - define event listener API in Custom Elements? Will - [event retargeting](https://javascript.info/shadow-dom-events) help, or do we - need to do declarative event listener (similar to Angular - `(event)="myEventListener"`) to translate the event for the caller? If it - depends, what will make us choose one vs. another? -- There is - [code to prevent event from being fired if the component is disabled](https://github.com/carbon-design-system/carbon/blob/v10.3.2/packages/react/src/components/TextInput/TextInput.js#L43-L52). - Should we do the same? If so, how can we do that? Can we make the disable - style automatically take care of that? -- Some `carbon-components-react` codebase has - [a way to generate class list from a set of props](https://github.com/carbon-design-system/carbon/blob/v10.3.2/packages/react/src/components/TextInput/TextInput.js#L59-L65) - (similar to `ngClass`). Can we do the same with `lit-html`? -- Some `carbon-components-react` code - [composes sub-portions of a component](https://github.com/carbon-design-system/carbon/blob/v10.3.2/packages/react/src/components/TextInput/TextInput.js#L76-L81), - often conditionally (similar to `ngIf`). Can we do the same with `lit-html`? -- How can we - [render a Carbon icon](https://github.com/carbon-design-system/carbon/blob/v10.3.2/packages/react/src/components/TextInput/TextInput.js#L91) - in `lit-html`, with specific CSS class applied? diff --git a/web-components/packages/carbon-web-components/src/globals/wrappers/createReactCustomElementType.ts b/web-components/packages/carbon-web-components/src/globals/wrappers/createReactCustomElementType.ts deleted file mode 100644 index 4462ce041aac..000000000000 --- a/web-components/packages/carbon-web-components/src/globals/wrappers/createReactCustomElementType.ts +++ /dev/null @@ -1,336 +0,0 @@ -/** - * @license - * - * Copyright IBM Corp. 2019, 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 React, { Component, createElement, forwardRef } from 'react'; -import on from '../mixins/on'; -import Handle from '../internal/handle'; - -/** - * A descriptor for a React event prop of a custom element. - */ -interface CustomElementEventDescriptor { - /** - * The event name. - */ - name: string; - - /** - * A boolean to detemine usage of capture mode or the event options. - */ - options?: boolean | EventListenerOptions; -} - -/** - * A descriptor for a React prop for an attribute of a custom element. - */ -interface CustomElementPropDescriptor { - /** - * The attribute name for the prop. - */ - attribute?: string | false; - - /** - * The event name (or descriptor) for the prop. - */ - event?: string | CustomElementEventDescriptor; - - /** - * A function that takes a property value and returns the corresponding attribute value. - */ - serialize?: (value: any) => string | void; -} - -/** - * A descriptor for a set of React props for attributes of a custom element. - */ -interface CustomElementPropsDescriptor { - [propName: string]: CustomElementPropDescriptor; -} - -/** - * React props for the component `createCustomElementType()` generates. - */ -interface CustomElementTypeProps { - /** - * Ordinal prop. - */ - [propName: string]: any; - - /** - * Child nodes. - */ - // eslint-disable-next-line react/no-unused-prop-types - children?: React.ReactNode; -} - -/** - * @param refs List of React refs to merge. - * @returns Merged React ref. - */ -const mergeRefs = - (...refs: React.Ref[]) => - (el) => { - refs.forEach((ref) => { - // https://github.com/facebook/react/issues/13029#issuecomment-410002316 - if (typeof ref === 'function') { - ref(el); - } else if (Object(ref) === ref) { - // `React.Ref.current` is read-only for regular use case, but we update it here - (ref as { current: T }).current = el; - } - }); - }; - -/** - * @param prop A prop value. - * @param descriptor A React prop descriptor. - * @returns The corresponding attribute value for the given prop value. - */ -const convertProp = (prop: any, descriptor: CustomElementPropDescriptor) => { - if (!descriptor) { - return prop; - } - const { event, serialize } = descriptor; - if (event) { - // Events are not set as props, we use DOM `addEventListener()` instead - return undefined; - } - return !serialize ? prop : serialize(prop); -}; - -/** - * @param props A set of React props. - * @param descriptor A set of React prop desciptor. - * @returns The set of React props to set to a custom element, corresponding to the given React props. - */ -const convertProps = ( - props: CustomElementTypeProps, - descriptor: CustomElementPropsDescriptor -) => - Object.keys(props).reduce((acc, propName) => { - const { [propName]: descriptorItem } = descriptor; - const converted = convertProp(props[propName], descriptorItem); - const { attribute } = descriptorItem ?? {}; - return attribute === false - ? acc - : { - ...acc, - [attribute || propName]: converted, - }; - }, {}); - -/** - * Attaches listeners of custom events, to a custom element. - * - * @param elem The custom element. - * @param descriptor An object, keyed by prop name, of data that may have custom event names. - * @param callback A callback function that runs as the custom events fire. - * @returns A handle that allows to release all event listeners attached. - */ -const attachEventListeners = ( - elem: HTMLElement, - descriptor: CustomElementPropsDescriptor, - callback: (name: string, event: Event) => void -): Handle => { - const handles = new Set(); - Object.keys(descriptor).forEach((propName) => { - if (descriptor[propName]) { - const { event: eventDescriptor } = descriptor[propName]; - const name = - Object(eventDescriptor) !== eventDescriptor - ? (eventDescriptor as string) - : (eventDescriptor as CustomElementEventDescriptor).name; - const options = - Object(eventDescriptor) !== eventDescriptor - ? undefined - : (eventDescriptor as CustomElementEventDescriptor).options; - if (name) { - handles.add( - on( - elem, - name, - (event) => { - callback(propName, event); - }, - options - ) - ); - } - } - }); - return { - release() { - handles.forEach((handle) => { - handle.release(); - handles.delete(handle); - }); - return null; - }, - }; -}; - -/** - * @param name The tag name of the custom element. - * @param descriptor A descriptor for a set of React props for attributes of a custom element. - * @returns A React component working as a wrapper for the given custom element. - * @example - * import { render } from 'react-dom'; - * import createCustomElementType, { booleanSerializer } from '/path/to/createCustomElementType'; - * - * const CDSDropdown = createCustomElementType(`${prefix}-dropdown`, { - * disabled: { - * // Sets `disabled` attribute when the React prop value is truthy, unsets otherwise - * serialize: booleanSerializer, - * }, - * helperText: { - * // Maps `helperText` React prop to `helper-text` attribute - * attribute: 'helper-text', - * }, - * onBeforeSelect: { - * // Sets `onBeforeSelect` React prop value as a listener of `cds-dropdown-beingselected` custom event - * event: `${prefix}-dropdown-beingselected`, - * }, - * }); - * - * render( - * ( - * { console.log(`${prefix}-dropdown-beingselected is fired!`, event); }}> - * Option 1 - * Option 2 - * Option 3 - * - * ) - * document.body - * ); - */ -const createReactCustomElementType = ( - name: string, - descriptor: CustomElementPropsDescriptor -) => { - /** - * Array of React prop names that should be mapped to DOM properties instead of attributes. - */ - const nonAttributeProps = Object.keys(descriptor).filter((propName) => { - const { [propName]: descriptorItem } = descriptor; - const { attribute } = descriptorItem ?? {}; - return attribute === false; - }); - - /** - * A React component working as a wrapper for the custom element. - */ - class CustomElementType extends Component { - /** - * The element. - */ - private _elem: HTMLElement | null = null; - - /** - * The handle that allows to release all event listeners attached to this custom element. - */ - private _eventListenersHandle: Handle | null = null; - - /** - * The callback function that runs as the custom events fire. - * - * @param propName The React prop name associated with the event listener. - * @param event The event. - */ - private _handleEvent = (propName: string, event: Event) => { - const { [propName]: listener } = this.props; - if (listener) { - listener.call(event.currentTarget, event); - } - }; - - /** - * Handles getting/losing the React `ref` object of this custom element. - * - * @param elem The custom element. - */ - private _handleElemRef = (elem: HTMLElement) => { - this._elem = elem; - if (this._eventListenersHandle) { - this._eventListenersHandle.release(); - this._eventListenersHandle = null; - } - if (elem) { - this._eventListenersHandle = attachEventListeners( - elem, - descriptor, - this._handleEvent - ); - } - }; - - /** - * Reflects change in React props to DOM properties. - * - * @param prevProps The previous props. - */ - updateProps(prevProps: { [key: string]: any } = {}) { - const { props, _elem: elem } = this; - nonAttributeProps.forEach((propName) => { - const { [propName]: prevValue } = prevProps; - const { [propName]: value } = props; - if (prevValue !== value) { - elem![propName] = value; - } - }); - } - - componentDidMount() { - this.updateProps(); - } - - componentDidUpdate(prevProps) { - this.updateProps(prevProps); - } - - render() { - // eslint-disable-next-line react/prop-types - const { children, innerRef, ...props } = this.props; - const mergedRef = mergeRefs(innerRef, this._handleElemRef); - return createElement( - name, - { ref: mergedRef, ...convertProps(props, descriptor) }, - children - ); - } - } - - return forwardRef((props, ref) => - createElement(CustomElementType, { ...props, innerRef: ref }) - ); -}; - -/** - * @param value A React prop value. - * @returns Serialized version of React prop value, as a boolean attribute in a custom element. - */ -export const booleanSerializer = (value) => (!value ? undefined : ''); - -/** - * @param value A React prop value. - * @returns Serialized version of React prop value, as a number attribute in a custom element. - */ -export const numberSerializer = (value) => - value == null ? value : String(value); - -/** - * @param value A React prop value. - * @returns Serialized version of React prop value, as a object attribute in a custom element. - */ -export const objectSerializer = (value) => - value == null ? value : JSON.stringify(value); - -export default createReactCustomElementType; diff --git a/web-components/packages/carbon-web-components/tests/integration/build/ie_steps.js b/web-components/packages/carbon-web-components/tests/integration/build/ie_steps.js deleted file mode 100644 index 638a16fae42d..000000000000 --- a/web-components/packages/carbon-web-components/tests/integration/build/ie_steps.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * - * Copyright IBM Corp. 2020, 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. - */ - -const path = require('path'); -const fs = require('fs-extra'); -const { setup: setupDevServer, teardown: teardownDevServer } = require('jest-dev-server'); -const exec = require('../exec'); -const replaceDependencies = require('../replace-dependencies'); - -const PORT = 8081; - -describe('IE example', () => { - beforeAll(async () => { - const projectRoot = path.resolve(__dirname, '../../..'); - const src = path.resolve(projectRoot, 'examples/codesandbox/ie'); - const tmpDir = process.env.CCE_EXAMPLE_TMPDIR; - await fs.copy(src, `${tmpDir}/ie`); - await replaceDependencies([`${tmpDir}/ie/package.json`]); - await exec('yarn', ['install'], { cwd: `${tmpDir}/ie` }); - await setupDevServer({ - command: `cd ${tmpDir}/ie && node ${path.resolve(__dirname, 'webpack-server.js')} --port=${PORT}`, - launchTimeout: Number(process.env.LAUNCH_TIMEOUT), - port: PORT, - }); - await page.goto(`http://localhost:${PORT}`); - }, Number(process.env.LAUNCH_TIMEOUT)); - - it('should show a title', async () => { - await expect(page).toMatch('Hello World!'); - }); - - it('should have dropdown interactive', async () => { - await expect(page).toClick('cds-dropdown'); - await expect(page).toMatchElement('cds-dropdown[open]'); - await expect(page).toClick('cds-dropdown'); - await expect(page).toMatchElement('cds-dropdown:not([open])'); - }); - - afterAll(async () => { - await teardownDevServer(); - }); -}); diff --git a/web-components/packages/carbon-web-components/tests/integration/build/react-ssr_steps.js b/web-components/packages/carbon-web-components/tests/integration/build/react-ssr_steps.js deleted file mode 100644 index 9857fb497afd..000000000000 --- a/web-components/packages/carbon-web-components/tests/integration/build/react-ssr_steps.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * - * Copyright IBM Corp. 2020, 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. - */ - -const path = require('path'); -const fs = require('fs-extra'); -const { setup: setupDevServer, teardown: teardownDevServer } = require('jest-dev-server'); -const exec = require('../exec'); -const replaceDependencies = require('../replace-dependencies'); - -const PORT = 3001; - -describe('React SSR example', () => { - beforeAll(async () => { - const projectRoot = path.resolve(__dirname, '../../..'); - const src = path.resolve(projectRoot, 'examples/codesandbox/react-ssr'); - const tmpDir = process.env.CCE_EXAMPLE_TMPDIR; - await fs.copy(src, `${tmpDir}/react-ssr`); - await replaceDependencies([`${tmpDir}/react-ssr/package.json`]); - await exec('yarn', ['install'], { cwd: `${tmpDir}/react-ssr` }); - await setupDevServer({ - command: `cd ${tmpDir}/react-ssr && cross-env PORT=${PORT} yarn start`, - launchTimeout: Number(process.env.LAUNCH_TIMEOUT), - port: PORT, - }); - await page.goto(`http://localhost:${PORT}`); - }, Number(process.env.LAUNCH_TIMEOUT)); - - it('should show a title', async () => { - await expect(page).toMatch('Hello World!'); - }); - - it('should have dropdown interactive', async () => { - await expect(page).toClick('cds-dropdown'); - await expect(page).toMatchElement('cds-dropdown[open]'); - await expect(page).toClick('cds-dropdown'); - await expect(page).toMatchElement('cds-dropdown:not([open])'); - }); - - afterAll(async () => { - await teardownDevServer(); - }); -}); diff --git a/web-components/packages/carbon-web-components/tests/integration/build/react_steps.js b/web-components/packages/carbon-web-components/tests/integration/build/react_steps.js deleted file mode 100644 index c28bb2f56957..000000000000 --- a/web-components/packages/carbon-web-components/tests/integration/build/react_steps.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * - * Copyright IBM Corp. 2020, 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. - */ - -const path = require('path'); -const fs = require('fs-extra'); -const { setup: setupDevServer, teardown: teardownDevServer } = require('jest-dev-server'); -const exec = require('../exec'); -const replaceDependencies = require('../replace-dependencies'); - -const PORT = 3000; - -describe('React example', () => { - beforeAll(async () => { - const projectRoot = path.resolve(__dirname, '../../..'); - const src = path.resolve(projectRoot, 'examples/codesandbox/react'); - const tmpDir = process.env.CCE_EXAMPLE_TMPDIR; - await fs.copy(src, `${tmpDir}/react`); - await replaceDependencies([`${tmpDir}/react/package.json`]); - await exec('yarn', ['install'], { cwd: `${tmpDir}/react` }); - await setupDevServer({ - command: `cd ${tmpDir}/react && node ${path.resolve(__dirname, 'webpack-server.js')} --port=${PORT}`, - launchTimeout: Number(process.env.LAUNCH_TIMEOUT), - port: PORT, - }); - await page.goto(`http://localhost:${PORT}`); - }, Number(process.env.LAUNCH_TIMEOUT)); - - it('should show a title', async () => { - await expect(page).toMatch('Hello World!'); - }); - - it('should have dropdown interactive', async () => { - await expect(page).toClick('cds-dropdown'); - await expect(page).toMatchElement('cds-dropdown[open]'); - await expect(page).toClick('cds-dropdown'); - await expect(page).toMatchElement('cds-dropdown:not([open])'); - }); - - afterAll(async () => { - await teardownDevServer(); - }); -}); diff --git a/web-components/packages/carbon-web-components/tools/babel-plugin-create-react-custom-element-type-def.js b/web-components/packages/carbon-web-components/tools/babel-plugin-create-react-custom-element-type-def.js deleted file mode 100644 index 75f925449542..000000000000 --- a/web-components/packages/carbon-web-components/tools/babel-plugin-create-react-custom-element-type-def.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @license - * - * Copyright IBM Corp. 2020, 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. - */ - -const { default: template } = require('@babel/template'); -const { - createMetadataVisitor, -} = require('./babel-plugin-create-react-custom-element-type'); - -const regexEvent = /^event/; -const regexMagicComment = /The name of the custom event/g; -const magicCommentForReact = 'The event handler for the custom event'; - -module.exports = function generateCreateReactCustomElementType(api) { - const { types: t } = api; - const metadataVisitor = createMetadataVisitor(api); - - const types = { - Boolean: 'boolean', - Number: 'number', - }; - - return { - name: 'create-react-custom-element-type-def', - visitor: { - Program(path, { file }) { - const declaredProps = {}; - const customEvents = {}; - const namedExportsSources = {}; - const context = { - file, - declaredProps, - customEvents, - namedExportsSources, - }; - // Gathers metadata of custom element properties and events, into `context` - path.traverse(metadataVisitor, context); - - const { - className, - classComments = [], - parentDescriptorSource, - } = context; - const props = Object.keys(declaredProps).reduce((acc, key) => { - const { comments = [], type } = declaredProps[key]; - return [ - ...acc, - comments.map(({ value }) => `/*${value}*/`).join('\n'), - `${key}?: ${types[type] || 'string'};`, - ]; - }, []); - const events = Object.keys(customEvents).reduce((acc, key) => { - const { comments = [] } = customEvents[key]; - return [ - ...acc, - comments - .map( - ({ value }) => - `/*${value.replace( - regexMagicComment, - magicCommentForReact - )}*/` - ) - .join('\n'), - `${key.replace(regexEvent, 'on')}?: (event: CustomEvent) => void;`, - ]; - }, []); - - const build = template( - ` - import { Component } from 'react'; - ${ - !parentDescriptorSource - ? '' - : `import { ComponentProps as ParentComponentProps } from '${parentDescriptorSource}';` - } - export interface ComponentProps${ - !parentDescriptorSource ? '' : ' extends ParentComponentProps' - } { - ${props.join('\n')} - ${events.join('\n')} - ${parentDescriptorSource ? '' : '[prop: string]: unknown;'} - } - ${classComments.map((value) => `/*${value}*/`).join('\n')} - declare class ${className} extends Component {} - export default ${className}; - `, - { - plugins: ['typescript'], - preserveComments: true, - sourceType: 'module', - } - ); - - const body = build(); - path.replaceWith(t.program(body)); - path.stop(); - }, - }, - }; -}; diff --git a/web-components/packages/carbon-web-components/tools/babel-plugin-create-react-custom-element-type.js b/web-components/packages/carbon-web-components/tools/babel-plugin-create-react-custom-element-type.js deleted file mode 100644 index e4064bcbeab0..000000000000 --- a/web-components/packages/carbon-web-components/tools/babel-plugin-create-react-custom-element-type.js +++ /dev/null @@ -1,626 +0,0 @@ -/** - * @license - * - * Copyright IBM Corp. 2019, 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. - */ - -const { dirname, isAbsolute, relative, resolve } = require('path'); -const { default: template } = require('@babel/template'); -const { default: traverse } = require('@babel/traverse'); -const { - default: transformTemplateLiterals, -} = require('@babel/plugin-transform-template-literals'); -const replaceExtension = require('replace-ext'); - -const regexEvent = /^event/; - -/** - * @param {string} source The source file path. - * @param {string} extension The extension to replace source file path with. - * @returns {string} Given `source` with its extension replaced with the given one, preserving `./`. - */ -function replaceExtensionRelative(source, extension) { - return !/^\./.test(source) - ? source - : `${dirname(source) !== '.' ? '' : './'}${replaceExtension( - source, - extension - )}`; -} - -function createMetadataVisitor(api) { - const { types: t } = api; - - /** - * @param {string} path The Babel path what a `@property()` decorator call refers to. - * @returns {boolean} `true` if such decorator is imported from `lit-element`. - */ - const propertyIsFromLit = (path) => { - const { parentPath } = path; - return ( - path.isImportSpecifier() && - path.get('imported').isIdentifier({ name: 'property' }) && - parentPath.isImportDeclaration && - parentPath.get('source').isStringLiteral({ value: 'lit-element' }) - ); - }; - - const getParentClassImportSource = (path) => { - const { parentPath } = path; - if ( - path.isImportDefaultSpecifier() && - parentPath.isImportDeclaration && - parentPath.get('source').isStringLiteral() - ) { - return parentPath.get('source').node.value; - } - return undefined; - }; - - /** - * Metadata harvested from `@property` decorator. - * - * @typedef {object} PropertyMetadata - * @property {string} [type] The property type. - * @property {string|boolean} [attribute] - * The attribute name the property maps to. - * `false` means there is no corresponding attribute. - */ - - /** - * @param {string} path The Babel path for `@property()` decorator call. - * @returns {PropertyMetadata} The metadata harvested from the given `@property()` decorator call. - */ - const getPropertyMetadata = (path) => { - const metadata = {}; - const expression = path.get('expression'); - if (!t.isCallExpression(expression)) { - return undefined; - } - - if ( - !expression.get('callee').isIdentifier() || - !propertyIsFromLit( - path.scope.getBinding(expression.get('callee.name').node).path - ) - ) { - return undefined; - } - - const firstArg = expression.get('arguments.0'); - if (firstArg && firstArg.isObjectExpression()) { - // eslint-disable-next-line no-restricted-syntax - for (const property of firstArg.get('properties')) { - const key = property.get('key'); - const value = property.get('value'); - if (key.isIdentifier({ name: 'type' })) { - value.assertIdentifier(); - metadata.type = value.get('name').node; - } else if (key.isIdentifier({ name: 'attribute' })) { - if (!value.isBooleanLiteral() && !value.isStringLiteral()) { - throw value.buildCodeFrameError( - '`attribute` in `@property` must point to a boolean literal or a string literal.' - ); - } - metadata.attribute = value.get('value').node; - } - } - } - - const leadingComments = path.parentPath.get('leadingComments'); - if (leadingComments) { - metadata.comments = ( - Array.isArray(leadingComments) ? leadingComments : [leadingComments] - ) - .map((item) => item.node) - .filter(Boolean); - } - - return metadata; - }; - - /** - * @param {string} path The Babel path of the superclass. - * @returns {PropertyMetadata} - * The given Babel path itself if it's an identifier. - * The first argument if the given Babel path is a function, assuming it as a mixin call. - */ - const getTarget = (path) => { - if (path.isIdentifier()) { - return path; - } - if (path.isCallExpression()) { - return getTarget(path.get('arguments.0')); - } - return null; - }; - - /** - * A visitor to gather metadata of custom element properties and events, - * from `type` in `@property()` for the former, from `eventSomething` for the latter. - * The gathered metadata is stored in the context `declaredProps` for the former, `customEvents` for the latter. - */ - const metadataVisitor = { - ClassDeclaration(path, context) { - const { file } = context; - const superClass = getTarget(path.get('superClass')); - if (superClass) { - const parentClassImportSource = getParentClassImportSource( - superClass.scope.getBinding(superClass.node.name).path - ); - if (parentClassImportSource) { - const relativeTarget = relative( - resolve(__dirname, '../src/components'), - resolve(dirname(file.opts.filename), parentClassImportSource) - ); - if (!isAbsolute(relativeTarget) && !relativeTarget.startsWith('..')) { - context.parentDescriptorSource = parentClassImportSource; - } - } - } - const leadingComments = path.get('leadingComments'); - if (leadingComments) { - context.classComments = ( - Array.isArray(leadingComments) ? leadingComments : [leadingComments] - ) - .map((item) => item.node) - .filter(Boolean); - } - context.className = path.get('id.name').node; - }, - - ClassMethod(path, { customEvents }) { - const { static: staticMethod, kind, key } = path.node; - const { name } = key; - if (staticMethod && kind === 'get' && regexEvent.test(name)) { - const body = path.get('body'); - const firstBody = body.get('body.0'); - firstBody.assertReturnStatement(); - const argument = firstBody.get('argument'); - if (!argument.isStringLiteral() && !argument.isTemplateLiteral()) { - throw firstBody.buildCodeFrameError( - '`static get eventFoo` must have and be only with a return statement with a string literal or a template literal.' - ); - } - const metadata = { - eventName: t.cloneDeep(argument.node), - }; - const leadingComments = path.get('leadingComments'); - if (leadingComments) { - metadata.comments = ( - Array.isArray(leadingComments) ? leadingComments : [leadingComments] - ) - .map((item) => item.node) - .filter(Boolean); - } - customEvents[name] = metadata; - } - }, - - ClassProperty(path, { customEvents }) { - const { static: staticField, key } = path.node; - const value = path.get('value'); - const { name } = key; - if (staticField && regexEvent.test(name)) { - if (!value.isStringLiteral() && !value.isTemplateLiteral()) { - throw value.buildCodeFrameError( - '`static eventFoo` must refer to a string literal or a template literal.' - ); - } - const metadata = { - eventName: t.cloneDeep(value.node), - }; - const leadingComments = path.get('leadingComments'); - if (leadingComments) { - metadata.comments = ( - Array.isArray(leadingComments) ? leadingComments : [leadingComments] - ) - .map((item) => item.node) - .filter(Boolean); - } - customEvents[name] = metadata; - } - }, - - Decorator(path, context) { - const { parent, parentPath } = path; - const { declaredProps } = context; - const expression = path.get('expression'); - const customElementName = expression.get('arguments.0'); - if ( - expression.isCallExpression() && - expression.get('callee').isIdentifier({ name: 'customElement' }) - ) { - if ( - !customElementName.isStringLiteral() && - !customElementName.isTemplateLiteral() - ) { - throw customElementName.buildCodeFrameError( - '`@customElement()` must be called with the custom element name.' - ); - } - context.customElementName = customElementName.node; - } - - const metadata = getPropertyMetadata(path); - if (metadata) { - if ( - !parentPath.isClassProperty() && - (!parentPath.isClassMethod() || - (parentPath.node.kind !== 'get' && parentPath.node.kind !== 'set')) - ) { - throw parentPath.buildCodeFrameError( - '`@property()` must target class properties.' - ); - } - declaredProps[parent.key.name] = metadata; - } - }, - - ExportNamedDeclaration(path, context) { - const { source, specifiers } = path.node; - const { namedExportsSources } = context; - if (specifiers.length > 0) { - if (source) { - const { value: sourceValue } = source; - namedExportsSources[sourceValue] = - namedExportsSources[sourceValue] || {}; - // eslint-disable-next-line no-restricted-syntax - for (const { local, exported } of specifiers) { - namedExportsSources[sourceValue][exported.name] = local.name; - } - } else { - // eslint-disable-next-line no-restricted-syntax - for (const { local, exported } of specifiers) { - const { path: bindingPath } = path.scope.getBinding(local.name); - const { value: bindingSourceValue } = - bindingPath.parentPath.node.source; - namedExportsSources[bindingSourceValue] = - namedExportsSources[bindingSourceValue] || {}; - namedExportsSources[bindingSourceValue][exported.name] = - bindingPath.isImportDefaultSpecifier() - ? 'default' - : bindingPath.get('imported').node.name; - } - } - } - }, - }; - - return metadataVisitor; -} - -module.exports = function generateCreateReactCustomElementType( - api, - { nonUpgradable } = {} -) { - const { types: t } = api; - - const booleanSerializerIdentifier = t.identifier('booleanSerializer'); - const numberSerializerIdentifier = t.identifier('numberSerializer'); - const objectSerializerIdentifier = t.identifier('objectSerializer'); - - /** - * The named import specifiers associated with `type` in `@property`. - * - * @type {(boolean|number|object)} - */ - const importSpecifiers = { - Boolean: t.importSpecifier( - booleanSerializerIdentifier, - booleanSerializerIdentifier - ), - Number: t.importSpecifier( - numberSerializerIdentifier, - numberSerializerIdentifier - ), - Object: t.importSpecifier( - objectSerializerIdentifier, - objectSerializerIdentifier - ), - }; - - /** - * The serializers associated with `type` in `@property`. - * - * @type {(boolean|number|object)} - */ - const serializers = { - Boolean: booleanSerializerIdentifier, - Number: numberSerializerIdentifier, - Object: objectSerializerIdentifier, - }; - - /** - * The prop types associated with `type` in `@property`. - * - * @type {(string|boolean|number|object)} - */ - const propTypesForLitTypes = { - String: t.memberExpression( - t.identifier('PropTypes'), - t.identifier('string') - ), - Boolean: t.memberExpression( - t.identifier('PropTypes'), - t.identifier('bool') - ), - Number: t.memberExpression( - t.identifier('PropTypes'), - t.identifier('number') - ), - Object: t.memberExpression( - t.identifier('PropTypes'), - t.identifier('object') - ), - }; - - /** - * - * @param {object} declaredProps The list of metadata harvested from `@property()` decorator calls. - * @returns {string} The `import` statement for `src/globals/wrappers/createReactCustomElementType`. - */ - const buildCreateReactCustomElementTypeImport = (declaredProps) => { - const typesInUse = Object.keys(declaredProps) - .map((name) => declaredProps[name].type) - .filter((type) => importSpecifiers[type]); - - return t.importDeclaration( - [ - t.importDefaultSpecifier(t.identifier('createReactCustomElementType')), - ...Array.from(new Set(typesInUse)).map( - (type) => importSpecifiers[type] - ), - ], - t.stringLiteral('../../globals/wrappers/createReactCustomElementType.js') - ); - }; - - /** - * @param {object} declaredProps The list of metadata harvested from `@property()` decorator calls. - * @returns {object} - * The list of `{ attribute: 'attribute-name', serialize: typeSerializer }` generated from `@property()` decorators. - */ - const buildPropsDescriptor = (declaredProps) => - Object.keys(declaredProps).map((name) => { - const { type, attribute } = declaredProps[name]; - const propDesciptor = []; - if (attribute === false) { - propDesciptor.push( - t.objectProperty(t.identifier('attribute'), t.booleanLiteral(false)) - ); - } else { - if (type && type !== 'String') { - const serializer = serializers[type]; - if (!serializer) { - throw new Error(`No serializer found for type: ${type}`); - } - propDesciptor.push( - t.objectProperty(t.identifier('serialize'), serializer) - ); - } - if (attribute) { - propDesciptor.push( - t.objectProperty( - t.identifier('attribute'), - t.stringLiteral(attribute) - ) - ); - } - } - return t.objectProperty( - t.identifier(name), - t.objectExpression(propDesciptor) - ); - }); - - /** - * @param {string} customEvents - * The list of metadata harvested from `eventSomething` static properties. - * @returns {object} The list of `{ event: 'event-name' }` generated from `eventSomething` static properties. - */ - const buildEventsDescriptor = (customEvents) => - Object.keys(customEvents).map((name) => - t.objectProperty( - t.identifier(name.replace(regexEvent, 'on')), - t.objectExpression([ - t.objectProperty(t.identifier('event'), customEvents[name].eventName), - ]) - ) - ); - - /** - * @param {object} declaredProps The list of metadata harvested from `@property()` decorator calls. - * @returns {object} The list of `PropTypes.someType` generated from `@property()` decorators. - */ - const buildPropTypes = (declaredProps) => - Object.keys(declaredProps).map((name) => { - const { type } = declaredProps[name]; - const propType = propTypesForLitTypes[type || 'String']; - if (!propType) { - throw new Error(`No React prop type found for type: ${type}`); - } - return t.objectProperty(t.identifier(name), propType); - }); - - /** - * @param {string} customEvents - * The list of metadata harvested from `eventSomething` static properties. - * @returns {object} The list of `PropTypes.func` generated from `eventSomething` static properties. - */ - const buildEventsPropTypes = (customEvents) => - Object.keys(customEvents).map((name) => - t.objectProperty( - t.identifier(name.replace(regexEvent, 'on')), - t.memberExpression(t.identifier('PropTypes'), t.identifier('func')) - ) - ); - - const metadataVisitor = createMetadataVisitor(api); - - /** - * A Babel plugin that first gathers metadata of custom element properties/events from AST, - * then creates another AST of `createReactCustomElementType()` and replaces the original AST with the created one. - */ - return { - name: 'create-react-custom-element-type', - visitor: { - Program(path, { file }) { - const declaredProps = {}; - const customEvents = {}; - const namedExportsSources = {}; - const context = { - file, - declaredProps, - customEvents, - namedExportsSources, - }; - // Gathers metadata of custom element properties and events, into `context` - path.traverse(metadataVisitor, context); - - const relativePath = relative( - resolve(__dirname, '../src/components'), - file.opts.filename - ); - const retargedPath = t.stringLiteral( - `../../components/${replaceExtension(relativePath, '.js')}` - ); - - // Creates a module with `createReactCustomElementType()` - // with the gathered metadata of custom element properties and events - const descriptors = t.objectExpression([ - ...buildPropsDescriptor(declaredProps), - ...buildEventsDescriptor(customEvents), - ]); - const descriptorsWithParent = !context.parentDescriptorSource - ? descriptors - : t.callExpression( - t.memberExpression( - t.identifier('Object'), - t.identifier('assign') - ), - [ - t.objectExpression([]), - t.identifier('parentDescriptor'), - descriptors, - ] - ); - - const propTypes = t.objectExpression([ - ...buildPropTypes(declaredProps), - ...buildEventsPropTypes(customEvents), - ]); - const propTypesWithParent = !context.parentDescriptorSource - ? propTypes - : t.callExpression( - t.memberExpression( - t.identifier('Object'), - t.identifier('assign') - ), - [ - t.objectExpression([]), - t.identifier('parentPropTypes'), - propTypes, - ] - ); - - const body = []; - if (!context.customElementName) { - if (context.className) { - // Class name found but custom element name not found means that it's likely a module not for custom element - // (e.g. an abstract class like floating menu) - // If so, we just export empty `descriptor` and re-export from the original class - body.unshift( - ...template.ast` - export var descriptor = ${descriptorsWithParent}; - export var propTypes = ${propTypesWithParent}; - ` - ); - } - } else { - body.unshift( - buildCreateReactCustomElementTypeImport(declaredProps), - ...template.ast` - import PropTypes from "prop-types"; - import { prefix } from '../../globals/settings.js'; - export var descriptor = ${descriptorsWithParent}; - export var propTypes = ${propTypesWithParent}; - const Component = createReactCustomElementType(${context.customElementName}, descriptor); - Component.propTypes = propTypes; - export default Component; - ` - ); - if (!nonUpgradable) { - body.unshift( - t.exportNamedDeclaration( - null, - [ - t.exportSpecifier( - t.identifier('default'), - t.identifier('CustomElement') - ), - ], - retargedPath - ) - ); - } - } - if (context.parentDescriptorSource) { - body.unshift( - t.importDeclaration( - [ - t.importSpecifier( - t.identifier('parentDescriptor'), - t.identifier('descriptor') - ), - ], - t.stringLiteral( - replaceExtensionRelative(context.parentDescriptorSource, '.js') - ) - ), - t.importDeclaration( - [ - t.importSpecifier( - t.identifier('parentPropTypes'), - t.identifier('propTypes') - ), - ], - t.stringLiteral( - replaceExtensionRelative(context.parentDescriptorSource, '.js') - ) - ) - ); - } - // eslint-disable-next-line no-restricted-syntax - for (const [source, exports] of Object.entries(namedExportsSources)) { - body.unshift( - t.exportNamedDeclaration( - null, - Object.keys(exports).map((exportedName) => - t.exportSpecifier( - t.identifier(exports[exportedName]), - t.identifier(exportedName) - ) - ), - t.stringLiteral(replaceExtensionRelative(source, '.js')) - ) - ); - } - const program = t.program(body); - traverse( - program, - transformTemplateLiterals(api).visitor, - path.scope, - path - ); - path.replaceWith(program); - path.stop(); - }, - }, - }; -}; - -module.exports.createMetadataVisitor = createMetadataVisitor; diff --git a/web-components/packages/carbon-web-components/tsconfig.json b/web-components/packages/carbon-web-components/tsconfig.json index 0fa7829e6598..73e255537924 100644 --- a/web-components/packages/carbon-web-components/tsconfig.json +++ b/web-components/packages/carbon-web-components/tsconfig.json @@ -17,7 +17,6 @@ "experimentalDecorators": true, "strict": true, "noImplicitAny": false, - "jsx": "react", "allowSyntheticDefaultImports": true, "resolveJsonModule": true, },