From dfea8cc850c798847a5bac8c48129ee8de297ec2 Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Thu, 30 Jan 2025 10:09:42 -0600 Subject: [PATCH 1/9] docs: add new JSX Transform guide Signed-off-by: Paul Schultz --- docs/tutorials/jsx-transform-migration.md | 477 ++++++++++++++++++++++ microsite/sidebars.ts | 1 + 2 files changed, 478 insertions(+) create mode 100644 docs/tutorials/jsx-transform-migration.md diff --git a/docs/tutorials/jsx-transform-migration.md b/docs/tutorials/jsx-transform-migration.md new file mode 100644 index 0000000000..a5c8a74ee5 --- /dev/null +++ b/docs/tutorials/jsx-transform-migration.md @@ -0,0 +1,477 @@ +--- +id: jsx-transform-migration +title: Transitioning to the New JSX Transform +description: A guide to migrating your project to the New JSX Transform +--- + +The Backstage core libraries are in the process of deprecating React 16 and evaluating the adoption of React 19. In React 19, the [New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html), introduced in September 2020, is now [mandatory](https://react.dev/blog/2024/04/25/react-19-upgrade-guide#installing). This requires a modification in how React is imported into components to ensure compatibility with React 19. + +## Migration Process + +### Using the Codemod + +The codemod referenced in the [Introducing the New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) article is no longer functional. However, we have identified a working solution detailed below. + +1. **Create a transformation file** + + Create a file named `transform.js` in the root directory of your Backstage project. + + ```js + /** + * (c) Facebook, Inc. and its affiliates. Confidential and proprietary. + * + * @format + */ + + module.exports = function (file, api, options) { + const j = api.jscodeshift; + const printOptions = options.printOptions || {}; + const root = j(file.source); + const destructureNamespaceImports = options.destructureNamespaceImports; + + // + function getFirstNode() { + return root.find(j.Program).get('body', 0).node; + } + + // Save the comments attached to the first node + const firstNode = getFirstNode(); + const { comments } = firstNode; + + function isVariableDeclared(variable) { + return ( + root + .find(j.Identifier, { + name: variable, + }) + .filter( + path => + path.parent.value.type !== 'MemberExpression' && + path.parent.value.type !== 'QualifiedTypeIdentifier' && + // Added this + path.parent.value.type !== 'TSQualifiedName' && + path.parent.value.type !== 'JSXMemberExpression', + ) + .size() > 0 + ); + } + + // Get all paths that import from React + const reactImportPaths = root + .find(j.ImportDeclaration, { + type: 'ImportDeclaration', + }) + .filter(path => { + return ( + (path.value.source.type === 'Literal' || + path.value.source.type === 'StringLiteral') && + (path.value.source.value === 'React' || + path.value.source.value === 'react') + ); + }); + + // get all namespace/default React imports + const reactPaths = reactImportPaths.filter(path => { + return ( + path.value.specifiers.length > 0 && + path.value.importKind === 'value' && + path.value.specifiers.some( + specifier => specifier.local.name === 'React', + ) + ); + }); + + if (reactPaths.size() > 1) { + throw Error( + 'There should only be one React import. Please remove the duplicate import and try again.', + ); + } + + if (reactPaths.size() === 0) { + return null; + } + + const reactPath = reactPaths.paths()[0]; + // Reuse the node so that we can preserve quoting style. + const reactLiteral = reactPath.value.source; + + const isDefaultImport = reactPath.value.specifiers.some( + specifier => + specifier.type === 'ImportDefaultSpecifier' && + specifier.local.name === 'React', + ); + + // Check to see if we should keep the React import + const isReactImportUsed = + root + .find(j.Identifier, { + name: 'React', + }) + .filter(path => { + return path.parent.parent.value.type !== 'ImportDeclaration'; + }) + .size() > 0; + + // local: imported + const reactIdentifiers = {}; + const reactTypeIdentifiers = {}; + let canDestructureReactVariable = false; + if ( + isReactImportUsed && + (isDefaultImport || destructureNamespaceImports) + ) { + // Checks to see if the react variable is used itself (rather than used to access its properties) + canDestructureReactVariable = + root + .find(j.Identifier, { + name: 'React', + }) + .filter(path => { + return path.parent.parent.value.type !== 'ImportDeclaration'; + }) + .filter( + path => + !( + path.parent.value.type === 'MemberExpression' && + path.parent.value.object.name === 'React' + ) && + !( + path.parent.value.type === 'QualifiedTypeIdentifier' && + path.parent.value.qualification.name === 'React' + ) && + !( + // Added this + ( + path.parent.value.type === 'TSQualifiedName' && + path.parent.value.left.name === 'React' + ) + ) && + !( + path.parent.value.type === 'JSXMemberExpression' && + path.parent.value.object.name === 'React' + ), + ) + .size() === 0; + + if (canDestructureReactVariable) { + // Add React identifiers to separate object so we can destructure the imports + // later if we can. If a type variable that we are trying to import has already + // been declared, do not try to destructure imports + // (ex. Element is declared and we are using React.Element) + root + .find(j.QualifiedTypeIdentifier, { + qualification: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const id = path.value.id.name; + if (path.parent.parent.value.type === 'TypeofTypeAnnotation') { + // This is a typeof import so it isn't actually a type + reactIdentifiers[id] = id; + + if (reactTypeIdentifiers[id]) { + canDestructureReactVariable = false; + } + } else { + reactTypeIdentifiers[id] = id; + + if (reactIdentifiers[id]) { + canDestructureReactVariable = false; + } + } + + if (isVariableDeclared(id)) { + canDestructureReactVariable = false; + } + }); + + // Added this + root + .find(j.TSQualifiedName, { + left: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const id = path.value.right.name; + reactIdentifiers[id] = id; + // We don't tend to use type imports + // Comment line above out and uncomment this to use type imports + // Also ignoring typeof imports? + // reactTypeIdentifiers[id] = id + + // if (reactIdentifiers[id]) { + // canDestructureReactVariable = false + // } + + if (isVariableDeclared(id)) { + canDestructureReactVariable = false; + } + }); + + // Add React identifiers to separate object so we can destructure the imports + // later if we can. If a variable that we are trying to import has already + // been declared, do not try to destructure imports + // (ex. createElement is declared and we are using React.createElement) + root + .find(j.MemberExpression, { + object: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const property = path.value.property.name; + reactIdentifiers[property] = property; + + if ( + isVariableDeclared(property) || + reactTypeIdentifiers[property] + ) { + canDestructureReactVariable = false; + } + }); + + // Add React identifiers to separate object so we can destructure the imports + // later if we can. If a JSX variable that we are trying to import has already + // been declared, do not try to destructure imports + // (ex. Fragment is declared and we are using React.Fragment) + root + .find(j.JSXMemberExpression, { + object: { + type: 'JSXIdentifier', + name: 'React', + }, + }) + .forEach(path => { + const property = path.value.property.name; + reactIdentifiers[property] = property; + + if ( + isVariableDeclared(property) || + reactTypeIdentifiers[property] + ) { + canDestructureReactVariable = false; + } + }); + } + } + + if (canDestructureReactVariable) { + // replace react identifiers + root + .find(j.QualifiedTypeIdentifier, { + qualification: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const id = path.value.id.name; + + j(path).replaceWith(j.identifier(id)); + }); + + // Added this + root + .find(j.TSQualifiedName, { + left: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const id = path.value.right.name; + + j(path).replaceWith(j.identifier(id)); + }); + + root + .find(j.MemberExpression, { + object: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const property = path.value.property.name; + + j(path).replaceWith(j.identifier(property)); + }); + + root + .find(j.JSXMemberExpression, { + object: { + type: 'JSXIdentifier', + name: 'React', + }, + }) + .forEach(path => { + const property = path.value.property.name; + + j(path).replaceWith(j.jsxIdentifier(property)); + }); + + // Add exisiting React imports to map + reactImportPaths.forEach(path => { + const specifiers = path.value.specifiers; + for (let i = 0; i < specifiers.length; i++) { + const specifier = specifiers[i]; + // get all type and regular imports that are imported + // from React + if (specifier.type === 'ImportSpecifier') { + if ( + path.value.importKind === 'type' || + specifier.importKind === 'type' + ) { + reactTypeIdentifiers[specifier.local.name] = + specifier.imported.name; + } else { + reactIdentifiers[specifier.local.name] = specifier.imported.name; + } + } + } + }); + + const regularImports = []; + Object.keys(reactIdentifiers).forEach(local => { + const imported = reactIdentifiers[local]; + regularImports.push( + j.importSpecifier(j.identifier(imported), j.identifier(local)), + ); + }); + + const typeImports = []; + Object.keys(reactTypeIdentifiers).forEach(local => { + const imported = reactTypeIdentifiers[local]; + typeImports.push( + j.importSpecifier(j.identifier(imported), j.identifier(local)), + ); + }); + + if (regularImports.length > 0) { + j(reactPath).insertAfter( + j.importDeclaration(regularImports, reactLiteral), + ); + } + if (typeImports.length > 0) { + j(reactPath).insertAfter( + j.importDeclaration(typeImports, reactLiteral, 'type'), + ); + } + + // remove all old react imports + reactImportPaths.forEach(path => { + // This is for import type React from 'react' which shouldn't + // be removed + if ( + path.value.specifiers.some( + specifier => + specifier.type === 'ImportDefaultSpecifier' && + specifier.local.name === 'React' && + (specifier.importKind === 'type' || + path.value.importKind === 'type'), + ) + ) { + j(path).insertAfter( + j.importDeclaration( + [j.importDefaultSpecifier(j.identifier('React'))], + reactLiteral, + 'type', + ), + ); + } + j(path).remove(); + }); + } else { + // Remove the import because it's not being used + // If we should keep the React import, just convert + // default imports to named imports + let isImportRemoved = false; + const specifiers = reactPath.value.specifiers; + for (let i = 0; i < specifiers.length; i++) { + const specifier = specifiers[i]; + if (specifier.type === 'ImportNamespaceSpecifier') { + if (!isReactImportUsed) { + isImportRemoved = true; + j(reactPath).remove(); + } + } else if (specifier.type === 'ImportDefaultSpecifier') { + if (isReactImportUsed) { + j(reactPath).insertAfter( + j.importDeclaration( + [j.importNamespaceSpecifier(j.identifier('React'))], + reactLiteral, + ), + ); + } + + if (specifiers.length > 1) { + const typeImports = []; + const regularImports = []; + for (let x = 0; x < specifiers.length; x++) { + if (specifiers[x].type !== 'ImportDefaultSpecifier') { + if (specifiers[x].importKind === 'type') { + typeImports.push(specifiers[x]); + } else { + regularImports.push(specifiers[x]); + } + } + } + if (regularImports.length > 0) { + j(reactPath).insertAfter( + j.importDeclaration(regularImports, reactLiteral), + ); + } + if (typeImports.length > 0) { + j(reactPath).insertAfter( + j.importDeclaration(typeImports, reactLiteral, 'type'), + ); + } + } + + isImportRemoved = true; + j(reactPath).remove(); + } + } + + if (!isImportRemoved) { + return null; + } + } + + // If the first node has been modified or deleted, reattach the comments + const firstNode2 = getFirstNode(); + if (firstNode2 !== firstNode) { + firstNode2.comments = comments; + } + + return root.toSource(printOptions); + }; + ``` + +2. **Execute the transformation** + + To apply the necessary changes, execute the following command twice from your root Backstage directory. First, run it for your packages, and then again for your plugins and any additional directories. Remember to adjust the paths to the transform script and parser source accordingly. + + ```console + npx jscodeshift --verbose=2 --ignore-pattern="**/node_modules/**" --parser ts --extensions=tsx,ts,jsx,js --transform ./path/to/the/transform.js --destructureNamespaceImports=true --parser=tsx ./path/to/src/ + ``` + +3. **Verify and clean up imports** + + Search for any remaining `import * as React from 'react'` statements and replace them with named imports, such as `import { useState } from 'react'`. + + If retaining the default React import is absolutely necessary, use: + + ```tsx + import { default as React } from 'react'; + ``` + +## Additional Resources + +- [Introducing the New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) +- [React 19 Upgrade Guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index bf97009772..7558a0fdca 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -563,6 +563,7 @@ export default { 'tutorials/yarn-migration', 'tutorials/migrate-to-mui5', 'tutorials/auth-service-migration', + 'tutorials/jsx-transform-migration' ], }, ], From b45d558d09ab67dfe0a5e4991c1115f282dd4195 Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Thu, 30 Jan 2025 10:40:35 -0600 Subject: [PATCH 2/9] update react 18 guide Signed-off-by: Paul Schultz --- docs/tutorials/react18-migration.md | 146 +++++++++++++++++++--------- 1 file changed, 99 insertions(+), 47 deletions(-) diff --git a/docs/tutorials/react18-migration.md b/docs/tutorials/react18-migration.md index 67f3dabb5d..e5da336870 100644 --- a/docs/tutorials/react18-migration.md +++ b/docs/tutorials/react18-migration.md @@ -1,74 +1,126 @@ --- id: react18-migration title: Migrating to React 18 -description: Additional resources for the Material UI v5 migration guide specifically for Backstage +description: A guide to migrating your project to React 18 --- -The Backstage core libraries and plugins are compatible with all versions of React from v16.8 to v18. This means that you can migrate projects at your own pace. We do however encourage you to do so sooner rather than later, both to keep up with the evolving ecosystem, but also because React 18 brings performance improvements, in particular in tests. +:::info + +This guide has been updated to include steps for removing support for React 16, as it is now deprecated. + +::: + +The Backstage core libraries and plugins are compatible with all versions of React from v17 to v18. This means that you can migrate projects at your own pace. We do however encourage you to do so sooner rather than later, both to keep up with the evolving ecosystem, but also because React 18 brings performance improvements, in particular in tests. ## Migration _Before diving in, this is a heads-up that for large projects this can be a tricky migration due to the fact that it is hard to break down into a gradual migration. In practice the difficult part of this migration is switching to the new version of the `@testing-library/react` package for tests, since there is no overlapping support across major React versions, more on that later._ -### Switching to React 18 +### Upgrading to React 18 -To switch a project to React 18, there are generally three changes that need to be made. +#### Backstage Instance -1. Update the resolutions in your root `package.json` to the new versions of `@types/react` and `@types/react-dom`: +To migrate a Backstage instance to React 18, follow these three steps: -```json title="package.json" - "resolutions": { - // highlight-remove-next-line - "@types/react": "^17", - // highlight-remove-next-line - "@types/react-dom": "^17", - // highlight-add-next-line - "@types/react": "^18", - // highlight-add-next-line - "@types/react-dom": "^18", - }, -``` +1. Modify the `resolutions` section in your root `package.json` to reference the latest versions of `@types/react` and `@types/react-dom`: -2. Update the `react` and `react-dom` dependencies in your `packages/app/package.json` to the new versions: + ```json title="package.json" + "resolutions": { + // highlight-remove-start + "@types/react": "^17", + "@types/react-dom": "^17", + // highlight-remove-end + // highlight-add-start + "@types/react": "^18", + "@types/react-dom": "^18", + // highlight-add-end + }, + ``` -```json title="packages/app/package.json" - "dependencies": { - ... - // highlight-remove-next-line - "react": "^17.0.2", - // highlight-remove-next-line - "react-dom": "^17.0.2", - // highlight-add-next-line - "react": "^18.0.2", - // highlight-add-next-line - "react-dom": "^18.0.2", - ... - }, -``` +2. Update the `react` and `react-dom` dependencies in `packages/app/package.json`: -3. Update `packages/app/src/index.tsx` to use the new `react-dom/client` API to render the app: + ```json title="packages/app/package.json" + "dependencies": { + ... + // highlight-remove-start + "react": "^17.0.2", + "react-dom": "^17.0.2", + // highlight-remove-end + // highlight-add-start + "react": "^18.0.2", + "react-dom": "^18.0.2", + // highlight-add-end + ... + }, + ``` -```tsx title="packages/app/src/index.tsx" -import '@backstage/cli/asset-types'; -// highlight-remove-next-line -import ReactDOM from 'react-dom'; -// highlight-add-next-line -import ReactDOM from 'react-dom/client'; -import App from './App'; +3. Adjust `packages/app/src/index.tsx` to use the `react-dom/client` API for rendering: -// highlight-remove-next-line -ReactDOM.render(, document.getElementById('root')); -// highlight-add-next-line -ReactDOM.createRoot(document.getElementById('root')!).render(); -``` + ```tsx title="packages/app/src/index.tsx" + import '@backstage/cli/asset-types'; + // highlight-remove-next-line + import ReactDOM from 'react-dom'; + // highlight-add-next-line + import ReactDOM from 'react-dom/client'; + import App from './App'; -Once these steps are done you should be able to run your app and see it working as before, except now using React 18. + // highlight-remove-next-line + ReactDOM.render(, document.getElementById('root')); + // highlight-add-next-line + ReactDOM.createRoot(document.getElementById('root')!).render(); + ``` + +#### Backstage Frontend Plugin + +1. Update the `devDependencies` and `peerDependencies` for `react`, `react-dom`, and `@types/react` in your plugin's `package.json`: + + ```json title="plugins//package.json" + "devDependencies": { + ... + // highlight-remove-start + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0", + "react-dom": "^16.13.1 || ^17.0.0", + // highlight-remove-end + // highlight-add-start + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0", + // highlight-add-end + ... + }, + ``` + + ```json title="plugins//package.json" + "peerDependencies": { + ... + // highlight-remove-start + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0", + "react-dom": "^16.13.1 || ^17.0.0", + // highlight-remove-end + // highlight-add-start + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0", + // highlight-add-end + ... + }, + ``` + +After completing these updates, your application and plugins should function as before, now utilizing React 18. + +:::note + +Be sure to update your lockfile after modifying your `package.json` files. + +::: ### TypeScript Errors When upgrading to React 18 you are likely to see a fair number of TypeScript type errors. A summary of the breaking changes can be found in the [Pull Request that introduced them](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/56210). A codemod is also provided to help with the migration. -Run `yarn tsc:full` to asses the damage. +Run `yarn tsc:full` to access the damage. The good news is that these errors can be fixed while still staying on React 17. If you have a large number of errors to fix you can address as few or many is you like at a time and merge them into your main branch **without** the version bumps from step 1. This lets you gradually migrate the types in your project while not yet fully moving over to React 18. Once all type breakages are fixed you can move on to the next step of migrating tests. From d92abd3b32592bcc87faaf77b459765533cceba7 Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Thu, 30 Jan 2025 17:06:06 -0600 Subject: [PATCH 3/9] apply requested changes Signed-off-by: Paul Schultz --- docs/tutorials/jsx-transform-migration.md | 19 ++++++++++++++++--- docs/tutorials/react18-migration.md | 4 ++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/jsx-transform-migration.md b/docs/tutorials/jsx-transform-migration.md index a5c8a74ee5..4dacf48097 100644 --- a/docs/tutorials/jsx-transform-migration.md +++ b/docs/tutorials/jsx-transform-migration.md @@ -4,15 +4,28 @@ title: Transitioning to the New JSX Transform description: A guide to migrating your project to the New JSX Transform --- -The Backstage core libraries are in the process of deprecating React 16 and evaluating the adoption of React 19. In React 19, the [New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html), introduced in September 2020, is now [mandatory](https://react.dev/blog/2024/04/25/react-19-upgrade-guide#installing). This requires a modification in how React is imported into components to ensure compatibility with React 19. +Backstage core libraries currently support React 18. We are actively evaluating the upgrade to React 19, which introduces significant changes, including making the [New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) mandatory. + +## What this means for you + +- **If you are already using the New JSX Transform:** You are not impacted by this change, regardless of your React version (17, 18, or when 19 becomes available for Backstage). +- **If you are NOT using the New JSX Transform (likely if you're importing React like `import React from 'react'`):** You will need to adopt it before upgrading to React 19. This typically involves changing your imports, as the New JSX Transform doesn't require importing the entire React namespace to use JSX. This is recommended even on React 17 or 18, as it was a change introduced with React 17. Although a best practice since React 17, Backstage did not adopt this transform when it upgraded. + +## Action Required + +While upgrading to React 19 within Backstage is not yet officially supported, it's recommended to proactively adopt the New JSX Transform if you haven't already. This will ensure a smoother transition when React 19 support is introduced and improve compatibility with the current React ecosystem. + +## Timeline + +We are currently evaluating React 19 and will provide further guidance on the upgrade path and timelines soon. For now, you can prepare by adopting the New JSX Transform. ## Migration Process ### Using the Codemod -The codemod referenced in the [Introducing the New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) article is no longer functional. However, we have identified a working solution detailed below. +While a codemod for the New JSX Transform was originally introduced in the [Introducing the New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) article, it is no longer functional. A working solution, inspired by the original, is detailed below: -1. **Create a transformation file** +1. **Create the transform file** Create a file named `transform.js` in the root directory of your Backstage project. diff --git a/docs/tutorials/react18-migration.md b/docs/tutorials/react18-migration.md index e5da336870..ee19a07703 100644 --- a/docs/tutorials/react18-migration.md +++ b/docs/tutorials/react18-migration.md @@ -20,7 +20,7 @@ _Before diving in, this is a heads-up that for large projects this can be a tric #### Backstage Instance -To migrate a Backstage instance to React 18, follow these three steps: +To migrate a Backstage instance to React 18, follow these steps: 1. Modify the `resolutions` section in your root `package.json` to reference the latest versions of `@types/react` and `@types/react-dom`: @@ -120,7 +120,7 @@ Be sure to update your lockfile after modifying your `package.json` files. When upgrading to React 18 you are likely to see a fair number of TypeScript type errors. A summary of the breaking changes can be found in the [Pull Request that introduced them](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/56210). A codemod is also provided to help with the migration. -Run `yarn tsc:full` to access the damage. +Run `yarn tsc:full` to assess the damage. The good news is that these errors can be fixed while still staying on React 17. If you have a large number of errors to fix you can address as few or many is you like at a time and merge them into your main branch **without** the version bumps from step 1. This lets you gradually migrate the types in your project while not yet fully moving over to React 18. Once all type breakages are fixed you can move on to the next step of migrating tests. From ccf4379e1cba350a7d138f66d1b06068e100bec1 Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Fri, 31 Jan 2025 09:43:23 -0600 Subject: [PATCH 4/9] add config file changes Signed-off-by: Paul Schultz --- docs/tutorials/jsx-transform-migration.md | 81 +++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/docs/tutorials/jsx-transform-migration.md b/docs/tutorials/jsx-transform-migration.md index 4dacf48097..e054d114b8 100644 --- a/docs/tutorials/jsx-transform-migration.md +++ b/docs/tutorials/jsx-transform-migration.md @@ -484,6 +484,87 @@ While a codemod for the New JSX Transform was originally introduced in the [Intr import { default as React } from 'react'; ``` +### Updating Configuration Files + +To ensure compatibility when using the `backstage-cli`, you must modify both the `tsconfig.json` and `eslintrc.js` files. Failure to do so may result in errors. + +#### TypeScript Configuration (TSConfig) + +Update the `compilerOptions.jsx` setting in `tsconfig.json` to `react-jsx`. This file is typically located in the root directory. + +```json filename="tsconfig.json" +{ + "extends": "@backstage/cli/config/tsconfig.json", + ... + "compilerOptions": { + // highlight-remove-next-line + "jsx": "react", + // highlight-add-next-line + "jsx": "react-jsx" + } +} +``` + +In the future, this setting will change to `preserve` once React 17 is fully deprecated. + +##### Explanation of `compilerOptions.jsx` Values + +- The `react` mode: This mode converts JSX into `React.createElement` calls, making it directly usable by React. The code doesn't require a separate JSX transformation step, and the output files will use the `.js` extension. + +- The `react-jsx` mode: Introduced with React 17, this mode automatically handles the JSX transformation, allowing you to use JSX without needing to import `React` in each file. + +- The `preserve` mode: This option leaves the JSX code untouched, embedding it directly into the output files. This is useful when you're planning to process the JSX with another tool like Babel later. The resulting files will use the `.jsx` extension to indicate the presence of JSX. + +#### ESLint Configuration + +:::note + +Only update `eslintrc.js` if the package or plugin contains React code, such as `app` or a frontend plugin. + +::: + +Modify `eslintrc.json` in frontend workspaces, e.g., `./packages/app/eslintrc.js`. + +##### Required Rule + +This disables the requirement for a global React import: + +```js filename="eslintrc.js" +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + 'react/react-in-jsx-scope': 'off', + }, +}); +``` + +##### Recommended Rule + +This prevents default imports of React, encouraging cleaner code: + +```js filename="eslintrc.js" +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + 'react/react-in-jsx-scope': 'off', + // highlight-add-start + 'no-restricted-syntax': [ + 'error', + { + message: 'Default React import not allowed.', + selector: + "ImportDeclaration[source.value='react'][specifiers.0.type='ImportDefaultSpecifier']", + }, + { + message: + 'Default React import not allowed. If you need a global type that conflicts with a React named export (e.g., `MouseEvent`), use `globalThis.MouseHandler`.', + selector: + "ImportDeclaration[source.value='react'] :matches(ImportDefaultSpecifier, ImportNamespaceSpecifier)", + }, + ], + // highlight-add-end + }, +}); +``` + ## Additional Resources - [Introducing the New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) From d5264894d39926ee4e608063f29556cc82a6b566 Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Tue, 4 Feb 2025 08:15:17 -0600 Subject: [PATCH 5/9] update vocab list Signed-off-by: Paul Schultz --- .github/vale/config/vocabularies/Backstage/accept.txt | 1 + microsite/sidebars.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index e8c9751ba2..f5defc22f2 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -535,3 +535,4 @@ zsh scrollable severities intellij +tsconfig diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 7558a0fdca..eb61c32daa 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -563,7 +563,7 @@ export default { 'tutorials/yarn-migration', 'tutorials/migrate-to-mui5', 'tutorials/auth-service-migration', - 'tutorials/jsx-transform-migration' + 'tutorials/jsx-transform-migration', ], }, ], From edebe418a90f56fb0af736136456d3b53577a2fb Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Tue, 8 Apr 2025 15:04:20 -0500 Subject: [PATCH 6/9] separate codemod into a different file Signed-off-by: Paul Schultz --- .../docs/tutorials/jsx-migration-codemod.md | 468 ++++++++++++++++++ docs/tutorials/jsx-transform-migration.md | 467 +---------------- 2 files changed, 478 insertions(+), 457 deletions(-) create mode 100644 contrib/docs/tutorials/jsx-migration-codemod.md diff --git a/contrib/docs/tutorials/jsx-migration-codemod.md b/contrib/docs/tutorials/jsx-migration-codemod.md new file mode 100644 index 0000000000..82d4143572 --- /dev/null +++ b/contrib/docs/tutorials/jsx-migration-codemod.md @@ -0,0 +1,468 @@ +# Migrating to the New JSX Transform using a Codemod + +## Using the Codemod + +While a codemod for the New JSX Transform was originally introduced in the [Introducing the New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) article, it is no longer functional. A working solution, inspired by the original, is detailed below: + +1. **Create the transform file** + + Create a file named `transform.js` in the root directory of your Backstage project. + + ```js + /** + * (c) Facebook, Inc. and its affiliates. Confidential and proprietary. + * + * @format + */ + + module.exports = function (file, api, options) { + const j = api.jscodeshift; + const printOptions = options.printOptions || {}; + const root = j(file.source); + const destructureNamespaceImports = options.destructureNamespaceImports; + + // + function getFirstNode() { + return root.find(j.Program).get('body', 0).node; + } + + // Save the comments attached to the first node + const firstNode = getFirstNode(); + const { comments } = firstNode; + + function isVariableDeclared(variable) { + return ( + root + .find(j.Identifier, { + name: variable, + }) + .filter( + path => + path.parent.value.type !== 'MemberExpression' && + path.parent.value.type !== 'QualifiedTypeIdentifier' && + // Added this + path.parent.value.type !== 'TSQualifiedName' && + path.parent.value.type !== 'JSXMemberExpression', + ) + .size() > 0 + ); + } + + // Get all paths that import from React + const reactImportPaths = root + .find(j.ImportDeclaration, { + type: 'ImportDeclaration', + }) + .filter(path => { + return ( + (path.value.source.type === 'Literal' || + path.value.source.type === 'StringLiteral') && + (path.value.source.value === 'React' || + path.value.source.value === 'react') + ); + }); + + // get all namespace/default React imports + const reactPaths = reactImportPaths.filter(path => { + return ( + path.value.specifiers.length > 0 && + path.value.importKind === 'value' && + path.value.specifiers.some( + specifier => specifier.local.name === 'React', + ) + ); + }); + + if (reactPaths.size() > 1) { + throw Error( + 'There should only be one React import. Please remove the duplicate import and try again.', + ); + } + + if (reactPaths.size() === 0) { + return null; + } + + const reactPath = reactPaths.paths()[0]; + // Reuse the node so that we can preserve quoting style. + const reactLiteral = reactPath.value.source; + + const isDefaultImport = reactPath.value.specifiers.some( + specifier => + specifier.type === 'ImportDefaultSpecifier' && + specifier.local.name === 'React', + ); + + // Check to see if we should keep the React import + const isReactImportUsed = + root + .find(j.Identifier, { + name: 'React', + }) + .filter(path => { + return path.parent.parent.value.type !== 'ImportDeclaration'; + }) + .size() > 0; + + // local: imported + const reactIdentifiers = {}; + const reactTypeIdentifiers = {}; + let canDestructureReactVariable = false; + if ( + isReactImportUsed && + (isDefaultImport || destructureNamespaceImports) + ) { + // Checks to see if the react variable is used itself (rather than used to access its properties) + canDestructureReactVariable = + root + .find(j.Identifier, { + name: 'React', + }) + .filter(path => { + return path.parent.parent.value.type !== 'ImportDeclaration'; + }) + .filter( + path => + !( + path.parent.value.type === 'MemberExpression' && + path.parent.value.object.name === 'React' + ) && + !( + path.parent.value.type === 'QualifiedTypeIdentifier' && + path.parent.value.qualification.name === 'React' + ) && + !( + // Added this + ( + path.parent.value.type === 'TSQualifiedName' && + path.parent.value.left.name === 'React' + ) + ) && + !( + path.parent.value.type === 'JSXMemberExpression' && + path.parent.value.object.name === 'React' + ), + ) + .size() === 0; + + if (canDestructureReactVariable) { + // Add React identifiers to separate object so we can destructure the imports + // later if we can. If a type variable that we are trying to import has already + // been declared, do not try to destructure imports + // (ex. Element is declared and we are using React.Element) + root + .find(j.QualifiedTypeIdentifier, { + qualification: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const id = path.value.id.name; + if (path.parent.parent.value.type === 'TypeofTypeAnnotation') { + // This is a typeof import so it isn't actually a type + reactIdentifiers[id] = id; + + if (reactTypeIdentifiers[id]) { + canDestructureReactVariable = false; + } + } else { + reactTypeIdentifiers[id] = id; + + if (reactIdentifiers[id]) { + canDestructureReactVariable = false; + } + } + + if (isVariableDeclared(id)) { + canDestructureReactVariable = false; + } + }); + + // Added this + root + .find(j.TSQualifiedName, { + left: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const id = path.value.right.name; + reactIdentifiers[id] = id; + // We don't tend to use type imports + // Comment line above out and uncomment this to use type imports + // Also ignoring typeof imports? + // reactTypeIdentifiers[id] = id + + // if (reactIdentifiers[id]) { + // canDestructureReactVariable = false + // } + + if (isVariableDeclared(id)) { + canDestructureReactVariable = false; + } + }); + + // Add React identifiers to separate object so we can destructure the imports + // later if we can. If a variable that we are trying to import has already + // been declared, do not try to destructure imports + // (ex. createElement is declared and we are using React.createElement) + root + .find(j.MemberExpression, { + object: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const property = path.value.property.name; + reactIdentifiers[property] = property; + + if ( + isVariableDeclared(property) || + reactTypeIdentifiers[property] + ) { + canDestructureReactVariable = false; + } + }); + + // Add React identifiers to separate object so we can destructure the imports + // later if we can. If a JSX variable that we are trying to import has already + // been declared, do not try to destructure imports + // (ex. Fragment is declared and we are using React.Fragment) + root + .find(j.JSXMemberExpression, { + object: { + type: 'JSXIdentifier', + name: 'React', + }, + }) + .forEach(path => { + const property = path.value.property.name; + reactIdentifiers[property] = property; + + if ( + isVariableDeclared(property) || + reactTypeIdentifiers[property] + ) { + canDestructureReactVariable = false; + } + }); + } + } + + if (canDestructureReactVariable) { + // replace react identifiers + root + .find(j.QualifiedTypeIdentifier, { + qualification: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const id = path.value.id.name; + + j(path).replaceWith(j.identifier(id)); + }); + + // Added this + root + .find(j.TSQualifiedName, { + left: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const id = path.value.right.name; + + j(path).replaceWith(j.identifier(id)); + }); + + root + .find(j.MemberExpression, { + object: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const property = path.value.property.name; + + j(path).replaceWith(j.identifier(property)); + }); + + root + .find(j.JSXMemberExpression, { + object: { + type: 'JSXIdentifier', + name: 'React', + }, + }) + .forEach(path => { + const property = path.value.property.name; + + j(path).replaceWith(j.jsxIdentifier(property)); + }); + + // Add exisiting React imports to map + reactImportPaths.forEach(path => { + const specifiers = path.value.specifiers; + for (let i = 0; i < specifiers.length; i++) { + const specifier = specifiers[i]; + // get all type and regular imports that are imported + // from React + if (specifier.type === 'ImportSpecifier') { + if ( + path.value.importKind === 'type' || + specifier.importKind === 'type' + ) { + reactTypeIdentifiers[specifier.local.name] = + specifier.imported.name; + } else { + reactIdentifiers[specifier.local.name] = specifier.imported.name; + } + } + } + }); + + const regularImports = []; + Object.keys(reactIdentifiers).forEach(local => { + const imported = reactIdentifiers[local]; + regularImports.push( + j.importSpecifier(j.identifier(imported), j.identifier(local)), + ); + }); + + const typeImports = []; + Object.keys(reactTypeIdentifiers).forEach(local => { + const imported = reactTypeIdentifiers[local]; + typeImports.push( + j.importSpecifier(j.identifier(imported), j.identifier(local)), + ); + }); + + if (regularImports.length > 0) { + j(reactPath).insertAfter( + j.importDeclaration(regularImports, reactLiteral), + ); + } + if (typeImports.length > 0) { + j(reactPath).insertAfter( + j.importDeclaration(typeImports, reactLiteral, 'type'), + ); + } + + // remove all old react imports + reactImportPaths.forEach(path => { + // This is for import type React from 'react' which shouldn't + // be removed + if ( + path.value.specifiers.some( + specifier => + specifier.type === 'ImportDefaultSpecifier' && + specifier.local.name === 'React' && + (specifier.importKind === 'type' || + path.value.importKind === 'type'), + ) + ) { + j(path).insertAfter( + j.importDeclaration( + [j.importDefaultSpecifier(j.identifier('React'))], + reactLiteral, + 'type', + ), + ); + } + j(path).remove(); + }); + } else { + // Remove the import because it's not being used + // If we should keep the React import, just convert + // default imports to named imports + let isImportRemoved = false; + const specifiers = reactPath.value.specifiers; + for (let i = 0; i < specifiers.length; i++) { + const specifier = specifiers[i]; + if (specifier.type === 'ImportNamespaceSpecifier') { + if (!isReactImportUsed) { + isImportRemoved = true; + j(reactPath).remove(); + } + } else if (specifier.type === 'ImportDefaultSpecifier') { + if (isReactImportUsed) { + j(reactPath).insertAfter( + j.importDeclaration( + [j.importNamespaceSpecifier(j.identifier('React'))], + reactLiteral, + ), + ); + } + + if (specifiers.length > 1) { + const typeImports = []; + const regularImports = []; + for (let x = 0; x < specifiers.length; x++) { + if (specifiers[x].type !== 'ImportDefaultSpecifier') { + if (specifiers[x].importKind === 'type') { + typeImports.push(specifiers[x]); + } else { + regularImports.push(specifiers[x]); + } + } + } + if (regularImports.length > 0) { + j(reactPath).insertAfter( + j.importDeclaration(regularImports, reactLiteral), + ); + } + if (typeImports.length > 0) { + j(reactPath).insertAfter( + j.importDeclaration(typeImports, reactLiteral, 'type'), + ); + } + } + + isImportRemoved = true; + j(reactPath).remove(); + } + } + + if (!isImportRemoved) { + return null; + } + } + + // If the first node has been modified or deleted, reattach the comments + const firstNode2 = getFirstNode(); + if (firstNode2 !== firstNode) { + firstNode2.comments = comments; + } + + return root.toSource(printOptions); + }; + ``` + +2. **Execute the transformation** + + To apply the necessary changes, execute the following command twice from your root Backstage directory. First, run it for your packages, and then again for your plugins and any additional directories. Remember to adjust the paths to the transform script and parser source accordingly. + + ```console + npx jscodeshift --verbose=2 --ignore-pattern="**/node_modules/**" --parser ts --extensions=tsx,ts,jsx,js --transform ./path/to/the/transform.js --destructureNamespaceImports=true --parser=tsx ./path/to/src/ + ``` + +3. **Verify and clean up imports** + + Review the codebase for any remaining instances of `import * as React from 'react'` or `import React from 'react'`. Replace these with named imports where possible, such as: + + ```tsx + import { useState, useEffect } from 'react'; + ``` + + If retaining the default React import is absolutely necessary, use the following syntax instead: + + ```tsx + import { default as React } from 'react'; + ``` diff --git a/docs/tutorials/jsx-transform-migration.md b/docs/tutorials/jsx-transform-migration.md index e054d114b8..19c1b693ae 100644 --- a/docs/tutorials/jsx-transform-migration.md +++ b/docs/tutorials/jsx-transform-migration.md @@ -21,468 +21,21 @@ We are currently evaluating React 19 and will provide further guidance on the up ## Migration Process -### Using the Codemod +### Updating React Imports -While a codemod for the New JSX Transform was originally introduced in the [Introducing the New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) article, it is no longer functional. A working solution, inspired by the original, is detailed below: +Find and replace all occurrences of `import * as React from 'react'` and `import React from 'react'` with named imports like: -1. **Create the transform file** +```tsx +import { useState, useEffect } from 'react'; +``` - Create a file named `transform.js` in the root directory of your Backstage project. +If you must preserve the default React import for compatibility reasons, you can use: - ```js - /** - * (c) Facebook, Inc. and its affiliates. Confidential and proprietary. - * - * @format - */ +```tsx +import { default as React } from 'react'; +``` - module.exports = function (file, api, options) { - const j = api.jscodeshift; - const printOptions = options.printOptions || {}; - const root = j(file.source); - const destructureNamespaceImports = options.destructureNamespaceImports; - - // - function getFirstNode() { - return root.find(j.Program).get('body', 0).node; - } - - // Save the comments attached to the first node - const firstNode = getFirstNode(); - const { comments } = firstNode; - - function isVariableDeclared(variable) { - return ( - root - .find(j.Identifier, { - name: variable, - }) - .filter( - path => - path.parent.value.type !== 'MemberExpression' && - path.parent.value.type !== 'QualifiedTypeIdentifier' && - // Added this - path.parent.value.type !== 'TSQualifiedName' && - path.parent.value.type !== 'JSXMemberExpression', - ) - .size() > 0 - ); - } - - // Get all paths that import from React - const reactImportPaths = root - .find(j.ImportDeclaration, { - type: 'ImportDeclaration', - }) - .filter(path => { - return ( - (path.value.source.type === 'Literal' || - path.value.source.type === 'StringLiteral') && - (path.value.source.value === 'React' || - path.value.source.value === 'react') - ); - }); - - // get all namespace/default React imports - const reactPaths = reactImportPaths.filter(path => { - return ( - path.value.specifiers.length > 0 && - path.value.importKind === 'value' && - path.value.specifiers.some( - specifier => specifier.local.name === 'React', - ) - ); - }); - - if (reactPaths.size() > 1) { - throw Error( - 'There should only be one React import. Please remove the duplicate import and try again.', - ); - } - - if (reactPaths.size() === 0) { - return null; - } - - const reactPath = reactPaths.paths()[0]; - // Reuse the node so that we can preserve quoting style. - const reactLiteral = reactPath.value.source; - - const isDefaultImport = reactPath.value.specifiers.some( - specifier => - specifier.type === 'ImportDefaultSpecifier' && - specifier.local.name === 'React', - ); - - // Check to see if we should keep the React import - const isReactImportUsed = - root - .find(j.Identifier, { - name: 'React', - }) - .filter(path => { - return path.parent.parent.value.type !== 'ImportDeclaration'; - }) - .size() > 0; - - // local: imported - const reactIdentifiers = {}; - const reactTypeIdentifiers = {}; - let canDestructureReactVariable = false; - if ( - isReactImportUsed && - (isDefaultImport || destructureNamespaceImports) - ) { - // Checks to see if the react variable is used itself (rather than used to access its properties) - canDestructureReactVariable = - root - .find(j.Identifier, { - name: 'React', - }) - .filter(path => { - return path.parent.parent.value.type !== 'ImportDeclaration'; - }) - .filter( - path => - !( - path.parent.value.type === 'MemberExpression' && - path.parent.value.object.name === 'React' - ) && - !( - path.parent.value.type === 'QualifiedTypeIdentifier' && - path.parent.value.qualification.name === 'React' - ) && - !( - // Added this - ( - path.parent.value.type === 'TSQualifiedName' && - path.parent.value.left.name === 'React' - ) - ) && - !( - path.parent.value.type === 'JSXMemberExpression' && - path.parent.value.object.name === 'React' - ), - ) - .size() === 0; - - if (canDestructureReactVariable) { - // Add React identifiers to separate object so we can destructure the imports - // later if we can. If a type variable that we are trying to import has already - // been declared, do not try to destructure imports - // (ex. Element is declared and we are using React.Element) - root - .find(j.QualifiedTypeIdentifier, { - qualification: { - type: 'Identifier', - name: 'React', - }, - }) - .forEach(path => { - const id = path.value.id.name; - if (path.parent.parent.value.type === 'TypeofTypeAnnotation') { - // This is a typeof import so it isn't actually a type - reactIdentifiers[id] = id; - - if (reactTypeIdentifiers[id]) { - canDestructureReactVariable = false; - } - } else { - reactTypeIdentifiers[id] = id; - - if (reactIdentifiers[id]) { - canDestructureReactVariable = false; - } - } - - if (isVariableDeclared(id)) { - canDestructureReactVariable = false; - } - }); - - // Added this - root - .find(j.TSQualifiedName, { - left: { - type: 'Identifier', - name: 'React', - }, - }) - .forEach(path => { - const id = path.value.right.name; - reactIdentifiers[id] = id; - // We don't tend to use type imports - // Comment line above out and uncomment this to use type imports - // Also ignoring typeof imports? - // reactTypeIdentifiers[id] = id - - // if (reactIdentifiers[id]) { - // canDestructureReactVariable = false - // } - - if (isVariableDeclared(id)) { - canDestructureReactVariable = false; - } - }); - - // Add React identifiers to separate object so we can destructure the imports - // later if we can. If a variable that we are trying to import has already - // been declared, do not try to destructure imports - // (ex. createElement is declared and we are using React.createElement) - root - .find(j.MemberExpression, { - object: { - type: 'Identifier', - name: 'React', - }, - }) - .forEach(path => { - const property = path.value.property.name; - reactIdentifiers[property] = property; - - if ( - isVariableDeclared(property) || - reactTypeIdentifiers[property] - ) { - canDestructureReactVariable = false; - } - }); - - // Add React identifiers to separate object so we can destructure the imports - // later if we can. If a JSX variable that we are trying to import has already - // been declared, do not try to destructure imports - // (ex. Fragment is declared and we are using React.Fragment) - root - .find(j.JSXMemberExpression, { - object: { - type: 'JSXIdentifier', - name: 'React', - }, - }) - .forEach(path => { - const property = path.value.property.name; - reactIdentifiers[property] = property; - - if ( - isVariableDeclared(property) || - reactTypeIdentifiers[property] - ) { - canDestructureReactVariable = false; - } - }); - } - } - - if (canDestructureReactVariable) { - // replace react identifiers - root - .find(j.QualifiedTypeIdentifier, { - qualification: { - type: 'Identifier', - name: 'React', - }, - }) - .forEach(path => { - const id = path.value.id.name; - - j(path).replaceWith(j.identifier(id)); - }); - - // Added this - root - .find(j.TSQualifiedName, { - left: { - type: 'Identifier', - name: 'React', - }, - }) - .forEach(path => { - const id = path.value.right.name; - - j(path).replaceWith(j.identifier(id)); - }); - - root - .find(j.MemberExpression, { - object: { - type: 'Identifier', - name: 'React', - }, - }) - .forEach(path => { - const property = path.value.property.name; - - j(path).replaceWith(j.identifier(property)); - }); - - root - .find(j.JSXMemberExpression, { - object: { - type: 'JSXIdentifier', - name: 'React', - }, - }) - .forEach(path => { - const property = path.value.property.name; - - j(path).replaceWith(j.jsxIdentifier(property)); - }); - - // Add exisiting React imports to map - reactImportPaths.forEach(path => { - const specifiers = path.value.specifiers; - for (let i = 0; i < specifiers.length; i++) { - const specifier = specifiers[i]; - // get all type and regular imports that are imported - // from React - if (specifier.type === 'ImportSpecifier') { - if ( - path.value.importKind === 'type' || - specifier.importKind === 'type' - ) { - reactTypeIdentifiers[specifier.local.name] = - specifier.imported.name; - } else { - reactIdentifiers[specifier.local.name] = specifier.imported.name; - } - } - } - }); - - const regularImports = []; - Object.keys(reactIdentifiers).forEach(local => { - const imported = reactIdentifiers[local]; - regularImports.push( - j.importSpecifier(j.identifier(imported), j.identifier(local)), - ); - }); - - const typeImports = []; - Object.keys(reactTypeIdentifiers).forEach(local => { - const imported = reactTypeIdentifiers[local]; - typeImports.push( - j.importSpecifier(j.identifier(imported), j.identifier(local)), - ); - }); - - if (regularImports.length > 0) { - j(reactPath).insertAfter( - j.importDeclaration(regularImports, reactLiteral), - ); - } - if (typeImports.length > 0) { - j(reactPath).insertAfter( - j.importDeclaration(typeImports, reactLiteral, 'type'), - ); - } - - // remove all old react imports - reactImportPaths.forEach(path => { - // This is for import type React from 'react' which shouldn't - // be removed - if ( - path.value.specifiers.some( - specifier => - specifier.type === 'ImportDefaultSpecifier' && - specifier.local.name === 'React' && - (specifier.importKind === 'type' || - path.value.importKind === 'type'), - ) - ) { - j(path).insertAfter( - j.importDeclaration( - [j.importDefaultSpecifier(j.identifier('React'))], - reactLiteral, - 'type', - ), - ); - } - j(path).remove(); - }); - } else { - // Remove the import because it's not being used - // If we should keep the React import, just convert - // default imports to named imports - let isImportRemoved = false; - const specifiers = reactPath.value.specifiers; - for (let i = 0; i < specifiers.length; i++) { - const specifier = specifiers[i]; - if (specifier.type === 'ImportNamespaceSpecifier') { - if (!isReactImportUsed) { - isImportRemoved = true; - j(reactPath).remove(); - } - } else if (specifier.type === 'ImportDefaultSpecifier') { - if (isReactImportUsed) { - j(reactPath).insertAfter( - j.importDeclaration( - [j.importNamespaceSpecifier(j.identifier('React'))], - reactLiteral, - ), - ); - } - - if (specifiers.length > 1) { - const typeImports = []; - const regularImports = []; - for (let x = 0; x < specifiers.length; x++) { - if (specifiers[x].type !== 'ImportDefaultSpecifier') { - if (specifiers[x].importKind === 'type') { - typeImports.push(specifiers[x]); - } else { - regularImports.push(specifiers[x]); - } - } - } - if (regularImports.length > 0) { - j(reactPath).insertAfter( - j.importDeclaration(regularImports, reactLiteral), - ); - } - if (typeImports.length > 0) { - j(reactPath).insertAfter( - j.importDeclaration(typeImports, reactLiteral, 'type'), - ); - } - } - - isImportRemoved = true; - j(reactPath).remove(); - } - } - - if (!isImportRemoved) { - return null; - } - } - - // If the first node has been modified or deleted, reattach the comments - const firstNode2 = getFirstNode(); - if (firstNode2 !== firstNode) { - firstNode2.comments = comments; - } - - return root.toSource(printOptions); - }; - ``` - -2. **Execute the transformation** - - To apply the necessary changes, execute the following command twice from your root Backstage directory. First, run it for your packages, and then again for your plugins and any additional directories. Remember to adjust the paths to the transform script and parser source accordingly. - - ```console - npx jscodeshift --verbose=2 --ignore-pattern="**/node_modules/**" --parser ts --extensions=tsx,ts,jsx,js --transform ./path/to/the/transform.js --destructureNamespaceImports=true --parser=tsx ./path/to/src/ - ``` - -3. **Verify and clean up imports** - - Search for any remaining `import * as React from 'react'` statements and replace them with named imports, such as `import { useState } from 'react'`. - - If retaining the default React import is absolutely necessary, use: - - ```tsx - import { default as React } from 'react'; - ``` +To streamline this process, consider using an automated codemod. Instructions are available in this [migration guide](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/jsx-migration-codemod.md). ### Updating Configuration Files From 918c8831c23f88be07d09db85aac1f94dd6920a2 Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Tue, 8 Apr 2025 15:09:20 -0500 Subject: [PATCH 7/9] update default react import eslint warning Signed-off-by: Paul Schultz --- .changeset/twelve-wolves-run.md | 5 +++++ packages/cli/config/eslint-factory.js | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/twelve-wolves-run.md diff --git a/.changeset/twelve-wolves-run.md b/.changeset/twelve-wolves-run.md new file mode 100644 index 0000000000..8d17bba533 --- /dev/null +++ b/.changeset/twelve-wolves-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Included a reference to the JSX transform guide in the warning about using the default React import. diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js index e51fe88db5..1e398fc250 100644 --- a/packages/cli/config/eslint-factory.js +++ b/packages/cli/config/eslint-factory.js @@ -249,13 +249,13 @@ function createConfigForRole(dir, role, extraConfig = {}) { 'warn', { message: - 'React default imports are deprecated. Follow the x migration guide for details.', + 'React default imports are deprecated. Follow the https://backstage.io/docs/tutorials/jsx-transform-migration migration guide for details.', selector: "ImportDeclaration[source.value='react'][specifiers.0.type='ImportDefaultSpecifier']", }, { message: - 'React default imports are deprecated. Follow the x migration guide for details. If you need a global type that collides with a React named export (such as `MouseEvent`), try using `globalThis.MouseHandler`.', + 'React default imports are deprecated. Follow the https://backstage.io/docs/tutorials/jsx-transform-migration migration guide for details. If you need a global type that collides with a React named export (such as `MouseEvent`), try using `globalThis.MouseHandler`.', selector: "ImportDeclaration[source.value='react'] :matches(ImportDefaultSpecifier, ImportNamespaceSpecifier)", }, From fa24be548e63279c4792d37e5841ebc65fe22add Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Tue, 15 Apr 2025 07:24:31 -0500 Subject: [PATCH 8/9] Update docs/tutorials/jsx-transform-migration.md Co-authored-by: Patrik Oldsberg Signed-off-by: Paul Schultz --- docs/tutorials/jsx-transform-migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/jsx-transform-migration.md b/docs/tutorials/jsx-transform-migration.md index 19c1b693ae..c49d80f637 100644 --- a/docs/tutorials/jsx-transform-migration.md +++ b/docs/tutorials/jsx-transform-migration.md @@ -39,7 +39,7 @@ To streamline this process, consider using an automated codemod. Instructions ar ### Updating Configuration Files -To ensure compatibility when using the `backstage-cli`, you must modify both the `tsconfig.json` and `eslintrc.js` files. Failure to do so may result in errors. +To ensure compatibility when using `@backstage/cli`, you must update your `tsconfig.json` to use the new JSX transforms. #### TypeScript Configuration (TSConfig) From e775870cc2375a4417139fa246dcf9c1278161ee Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Tue, 15 Apr 2025 07:38:45 -0500 Subject: [PATCH 9/9] apply requested changes Signed-off-by: Paul Schultz --- docs/tutorials/jsx-transform-migration.md | 50 ----------------------- 1 file changed, 50 deletions(-) diff --git a/docs/tutorials/jsx-transform-migration.md b/docs/tutorials/jsx-transform-migration.md index c49d80f637..d82ba5e8aa 100644 --- a/docs/tutorials/jsx-transform-migration.md +++ b/docs/tutorials/jsx-transform-migration.md @@ -68,56 +68,6 @@ In the future, this setting will change to `preserve` once React 17 is fully dep - The `preserve` mode: This option leaves the JSX code untouched, embedding it directly into the output files. This is useful when you're planning to process the JSX with another tool like Babel later. The resulting files will use the `.jsx` extension to indicate the presence of JSX. -#### ESLint Configuration - -:::note - -Only update `eslintrc.js` if the package or plugin contains React code, such as `app` or a frontend plugin. - -::: - -Modify `eslintrc.json` in frontend workspaces, e.g., `./packages/app/eslintrc.js`. - -##### Required Rule - -This disables the requirement for a global React import: - -```js filename="eslintrc.js" -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - rules: { - 'react/react-in-jsx-scope': 'off', - }, -}); -``` - -##### Recommended Rule - -This prevents default imports of React, encouraging cleaner code: - -```js filename="eslintrc.js" -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - rules: { - 'react/react-in-jsx-scope': 'off', - // highlight-add-start - 'no-restricted-syntax': [ - 'error', - { - message: 'Default React import not allowed.', - selector: - "ImportDeclaration[source.value='react'][specifiers.0.type='ImportDefaultSpecifier']", - }, - { - message: - 'Default React import not allowed. If you need a global type that conflicts with a React named export (e.g., `MouseEvent`), use `globalThis.MouseHandler`.', - selector: - "ImportDeclaration[source.value='react'] :matches(ImportDefaultSpecifier, ImportNamespaceSpecifier)", - }, - ], - // highlight-add-end - }, -}); -``` - ## Additional Resources - [Introducing the New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html)