diff --git a/packages/next/migrations.json b/packages/next/migrations.json index 9a3fc04761..114c67687e 100644 --- a/packages/next/migrations.json +++ b/packages/next/migrations.json @@ -14,6 +14,16 @@ "version": "9.3.1-beta.1", "description": "Rename @nrwl/next:dev-server to @nrwl/next:server", "factory": "./src/migrations/update-9-3-1/update-9-3-1" + }, + "rename-emotion-packages-11.0.0": { + "version": "11.0.0-beta.0", + "description": "Rename emotion packages to match new 11.0.0 package names", + "factory": "./src/migrations/update-11-0-0/rename-emotion-packages-11-0-0" + }, + "update-11.0.0": { + "version": "11.0.0-beta.0", + "description": "Update libraries", + "factory": "./src/migrations/update-11-0-0/update-11-0-0" } }, "packageJsonUpdates": { @@ -47,6 +57,23 @@ "alwaysAddToPackageJson": false } } + }, + "11.0.0": { + "version": "11.0.0-beta.0", + "packages": { + "next": { + "version": "10.0.1", + "alwaysAddToPackageJson": false + }, + "@emotion/server": { + "version": "11.0.0", + "alwaysAddToPackageJson": false + }, + "@emotion/styled": { + "version": "11.0.0", + "alwaysAddToPackageJson": false + } + } } } } diff --git a/packages/next/src/migrations/update-11-0-0/rename-emotion-packages-11-0-0.spec.ts b/packages/next/src/migrations/update-11-0-0/rename-emotion-packages-11-0-0.spec.ts new file mode 100644 index 0000000000..1f19671b9b --- /dev/null +++ b/packages/next/src/migrations/update-11-0-0/rename-emotion-packages-11-0-0.spec.ts @@ -0,0 +1,62 @@ +import { Tree } from '@angular-devkit/schematics'; +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { readJsonInTree } from '@nrwl/workspace'; +import * as path from 'path'; +import { createEmptyWorkspace, runSchematic } from '@nrwl/workspace/testing'; +import { emotionServerVersion } from '../../utils/versions'; + +describe('Rename Emotion Packages 11.0.0', () => { + let tree: Tree; + let schematicRunner: SchematicTestRunner; + + beforeEach(async () => { + tree = Tree.empty(); + tree = createEmptyWorkspace(tree); + schematicRunner = new SchematicTestRunner( + '@nrwl/next', + path.join(__dirname, '../../../migrations.json') + ); + tree.overwrite( + 'package.json', + JSON.stringify({ + devDependencies: { + 'emotion-server': '10.0.27', + }, + }) + ); + }); + + it(`should update emotion, if used, to the new package names`, async () => { + tree = await schematicRunner + .runSchematicAsync('rename-emotion-packages-11.0.0', {}, tree) + .toPromise(); + + const packageJson = readJsonInTree(tree, '/package.json'); + expect(packageJson).toMatchObject({ + devDependencies: { + '@emotion/server': emotionServerVersion, + }, + }); + }); + + it(`should update emotion, if used, to the new package names where imported`, async () => { + tree = await runSchematic('lib', { name: 'library-1' }, tree); + + const moduleThatImports = 'libs/library-1/src/importer.ts'; + tree.create( + moduleThatImports, + `import serve from 'emotion-server'; + + export const doSomething = (...args) => serve(...args); + ` + ); + + tree = await schematicRunner + .runSchematicAsync('rename-emotion-packages-11.0.0', {}, tree) + .toPromise(); + + expect(tree.read(moduleThatImports).toString()).toContain( + `import serve from '@emotion/server'` + ); + }); +}); diff --git a/packages/next/src/migrations/update-11-0-0/rename-emotion-packages-11-0-0.ts b/packages/next/src/migrations/update-11-0-0/rename-emotion-packages-11-0-0.ts new file mode 100644 index 0000000000..4a8d51affb --- /dev/null +++ b/packages/next/src/migrations/update-11-0-0/rename-emotion-packages-11-0-0.ts @@ -0,0 +1,12 @@ +import { chain } from '@angular-devkit/schematics'; +import { formatFiles, renameNpmPackages } from '@nrwl/workspace'; +import { emotionServerVersion } from '../../utils/versions'; + +export default function update() { + return chain([ + renameNpmPackages({ + 'emotion-server': ['@emotion/server', emotionServerVersion], + }), + formatFiles(), + ]); +} diff --git a/packages/next/src/migrations/update-11-0-0/update-11-0-0.spec.ts b/packages/next/src/migrations/update-11-0-0/update-11-0-0.spec.ts new file mode 100644 index 0000000000..9d0d6a5128 --- /dev/null +++ b/packages/next/src/migrations/update-11-0-0/update-11-0-0.spec.ts @@ -0,0 +1,45 @@ +import { Tree } from '@angular-devkit/schematics'; +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { readJsonInTree } from '@nrwl/workspace'; +import * as path from 'path'; +import { createEmptyWorkspace } from '@nrwl/workspace/testing'; + +describe('Update 11.0.0', () => { + let tree: Tree; + let schematicRunner: SchematicTestRunner; + + beforeEach(async () => { + tree = Tree.empty(); + tree = createEmptyWorkspace(tree); + schematicRunner = new SchematicTestRunner( + '@nrwl/react', + path.join(__dirname, '../../../migrations.json') + ); + }); + + it(`should update libs`, async () => { + tree.overwrite( + 'package.json', + JSON.stringify({ + dependencies: {}, + devDependencies: { + next: '9.5.2', + '@emotion/server': '10.0.27', + }, + }) + ); + + tree = await schematicRunner + .runSchematicAsync('update-11.0.0', {}, tree) + .toPromise(); + + const packageJson = readJsonInTree(tree, '/package.json'); + expect(packageJson).toMatchObject({ + dependencies: {}, + devDependencies: { + next: '10.0.1', + '@emotion/server': '11.0.0', + }, + }); + }); +}); diff --git a/packages/next/src/migrations/update-11-0-0/update-11-0-0.ts b/packages/next/src/migrations/update-11-0-0/update-11-0-0.ts new file mode 100644 index 0000000000..f1e4543f83 --- /dev/null +++ b/packages/next/src/migrations/update-11-0-0/update-11-0-0.ts @@ -0,0 +1,13 @@ +import { chain, Rule } from '@angular-devkit/schematics'; +import { formatFiles, updatePackagesInPackageJson } from '@nrwl/workspace'; +import * as path from 'path'; + +export default function update(): Rule { + return chain([ + updatePackagesInPackageJson( + path.join(__dirname, '../../../', 'migrations.json'), + '11.0.0' + ), + formatFiles(), + ]); +} diff --git a/packages/next/src/utils/styles.ts b/packages/next/src/utils/styles.ts index d9e7a66dfd..eb80043f47 100644 --- a/packages/next/src/utils/styles.ts +++ b/packages/next/src/utils/styles.ts @@ -20,7 +20,7 @@ export const NEXT_SPECIFIC_STYLE_DEPENDENCIES = { '@emotion/styled': { dependencies: { ...CSS_IN_JS_DEPENDENCIES['@emotion/styled'].dependencies, - 'emotion-server': emotionServerVersion, + '@emotion/server': emotionServerVersion, }, devDependencies: CSS_IN_JS_DEPENDENCIES['@emotion/styled'].devDependencies, }, diff --git a/packages/next/src/utils/versions.ts b/packages/next/src/utils/versions.ts index 1d84184766..47fb83367d 100644 --- a/packages/next/src/utils/versions.ts +++ b/packages/next/src/utils/versions.ts @@ -1,10 +1,10 @@ export const nxVersion = '*'; -export const nextVersion = '10.0.0'; +export const nextVersion = '10.0.1'; export const zeitNextCss = '1.0.1'; export const zeitNextSass = '1.0.1'; export const nodeSass = '4.14.1'; export const zeitNextLess = '1.0.1'; export const zeitNextStylus = '1.0.1'; -export const emotionServerVersion = '10.0.27'; +export const emotionServerVersion = '11.0.0'; export const babelPluginStyledComponentsVersion = '1.10.7'; diff --git a/packages/react/migrations.json b/packages/react/migrations.json index 761692c418..5393e3720d 100644 --- a/packages/react/migrations.json +++ b/packages/react/migrations.json @@ -59,6 +59,16 @@ "version": "10.4.0-beta.1", "description": "Update libraries", "factory": "./src/migrations/update-10-4-0/update-10-4-0" + }, + "rename-emotion-packages-11.0.0": { + "version": "11.0.0-beta.0", + "description": "Rename emotion packages to match new 11.0.0 package names", + "factory": "./src/migrations/update-11-0-0/rename-emotion-packages-11-0-0" + }, + "update-11.0.0": { + "version": "11.0.0-beta.0", + "description": "Update libraries", + "factory": "./src/migrations/update-11-0-0/update-11-0-0" } }, "packageJsonUpdates": { @@ -380,6 +390,27 @@ "alwaysAddToPackageJson": false } } + }, + "11.0.0": { + "version": "11.0.0-beta.0", + "packages": { + "@emotion/react": { + "version": "11.0.0", + "alwaysAddToPackageJson": false + }, + "@emotion/styled": { + "version": "11.0.0", + "alwaysAddToPackageJson": false + }, + "@emotion/babel-preset-css-prop": { + "version": "11.0.0", + "alwaysAddToPackageJson": false + }, + "@emotion/server": { + "version": "11.0.0", + "alwaysAddToPackageJson": false + } + } } } } diff --git a/packages/react/plugins/bundle-rollup.ts b/packages/react/plugins/bundle-rollup.ts index 74d2248cf9..76907729e1 100644 --- a/packages/react/plugins/bundle-rollup.ts +++ b/packages/react/plugins/bundle-rollup.ts @@ -5,7 +5,7 @@ function getRollupOptions(options: rollup.RollupOptions) { react: 'React', 'react-dom': 'ReactDOM', 'styled-components': 'styled', - '@emotion/core': 'emotionCore', + '@emotion/react': 'emotionReact', '@emotion/styled': 'emotionStyled', }; if (Array.isArray(options.output)) { diff --git a/packages/react/src/migrations/update-11-0-0/rename-emotion-packages-11-0-0.spec.ts b/packages/react/src/migrations/update-11-0-0/rename-emotion-packages-11-0-0.spec.ts new file mode 100644 index 0000000000..af78e1a8f1 --- /dev/null +++ b/packages/react/src/migrations/update-11-0-0/rename-emotion-packages-11-0-0.spec.ts @@ -0,0 +1,62 @@ +import { Tree } from '@angular-devkit/schematics'; +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { readJsonInTree } from '@nrwl/workspace'; +import * as path from 'path'; +import { createEmptyWorkspace, runSchematic } from '@nrwl/workspace/testing'; +import { emotionReactVersion } from '../../utils/versions'; + +describe('Rename Emotion Packages 11.0.0', () => { + let tree: Tree; + let schematicRunner: SchematicTestRunner; + + beforeEach(async () => { + tree = Tree.empty(); + tree = createEmptyWorkspace(tree); + schematicRunner = new SchematicTestRunner( + '@nrwl/react', + path.join(__dirname, '../../../migrations.json') + ); + tree.overwrite( + 'package.json', + JSON.stringify({ + dependencies: { + '@emotion/core': '10.1.1', + }, + }) + ); + }); + + it(`should update emotion, if used, to the new package names`, async () => { + tree = await schematicRunner + .runSchematicAsync('rename-emotion-packages-11.0.0', {}, tree) + .toPromise(); + + const packageJson = readJsonInTree(tree, '/package.json'); + expect(packageJson).toMatchObject({ + dependencies: { + '@emotion/react': emotionReactVersion, + }, + }); + }); + + it(`should update emotion, if used, to the new package names where imported`, async () => { + tree = await runSchematic('lib', { name: 'library-1' }, tree); + + const moduleThatImports = 'libs/library-1/src/importer.ts'; + tree.create( + moduleThatImports, + `import emotion from '@emotion/core'; + + export const doSomething = (...args) => something(...args); + ` + ); + + tree = await schematicRunner + .runSchematicAsync('rename-emotion-packages-11.0.0', {}, tree) + .toPromise(); + + expect(tree.read(moduleThatImports).toString()).toContain( + `import emotion from '@emotion/react'` + ); + }); +}); diff --git a/packages/react/src/migrations/update-11-0-0/rename-emotion-packages-11-0-0.ts b/packages/react/src/migrations/update-11-0-0/rename-emotion-packages-11-0-0.ts new file mode 100644 index 0000000000..8845c6dd9c --- /dev/null +++ b/packages/react/src/migrations/update-11-0-0/rename-emotion-packages-11-0-0.ts @@ -0,0 +1,12 @@ +import { chain } from '@angular-devkit/schematics'; +import { formatFiles, renameNpmPackages } from '@nrwl/workspace'; +import { emotionReactVersion } from '../../utils/versions'; + +export default function update() { + return chain([ + renameNpmPackages({ + '@emotion/core': ['@emotion/react', emotionReactVersion], + }), + formatFiles(), + ]); +} diff --git a/packages/react/src/migrations/update-11-0-0/update-11-0-0.spec.ts b/packages/react/src/migrations/update-11-0-0/update-11-0-0.spec.ts new file mode 100644 index 0000000000..d56fe83d74 --- /dev/null +++ b/packages/react/src/migrations/update-11-0-0/update-11-0-0.spec.ts @@ -0,0 +1,45 @@ +import { Tree } from '@angular-devkit/schematics'; +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { readJsonInTree } from '@nrwl/workspace'; +import * as path from 'path'; +import { createEmptyWorkspace } from '@nrwl/workspace/testing'; + +describe('Update 11.0.0', () => { + let tree: Tree; + let schematicRunner: SchematicTestRunner; + + beforeEach(async () => { + tree = Tree.empty(); + tree = createEmptyWorkspace(tree); + schematicRunner = new SchematicTestRunner( + '@nrwl/react', + path.join(__dirname, '../../../migrations.json') + ); + }); + + it(`should update libs`, async () => { + tree.overwrite( + 'package.json', + JSON.stringify({ + dependencies: {}, + devDependencies: { + '@emotion/styled': '10.0.27', + '@emotion/babel-preset-css-prop': '10.0.27', + }, + }) + ); + + tree = await schematicRunner + .runSchematicAsync('update-11.0.0', {}, tree) + .toPromise(); + + const packageJson = readJsonInTree(tree, '/package.json'); + expect(packageJson).toMatchObject({ + dependencies: {}, + devDependencies: { + '@emotion/styled': '11.0.0', + '@emotion/babel-preset-css-prop': '11.0.0', + }, + }); + }); +}); diff --git a/packages/react/src/migrations/update-11-0-0/update-11-0-0.ts b/packages/react/src/migrations/update-11-0-0/update-11-0-0.ts new file mode 100644 index 0000000000..f1e4543f83 --- /dev/null +++ b/packages/react/src/migrations/update-11-0-0/update-11-0-0.ts @@ -0,0 +1,13 @@ +import { chain, Rule } from '@angular-devkit/schematics'; +import { formatFiles, updatePackagesInPackageJson } from '@nrwl/workspace'; +import * as path from 'path'; + +export default function update(): Rule { + return chain([ + updatePackagesInPackageJson( + path.join(__dirname, '../../../', 'migrations.json'), + '11.0.0' + ), + formatFiles(), + ]); +} diff --git a/packages/react/src/schematics/application/application.spec.ts b/packages/react/src/schematics/application/application.spec.ts index 100438084a..d85169dc87 100644 --- a/packages/react/src/schematics/application/application.spec.ts +++ b/packages/react/src/schematics/application/application.spec.ts @@ -531,7 +531,7 @@ describe('app', () => { ); const packageJSON = readJsonInTree(tree, 'package.json'); - expect(packageJSON.dependencies['@emotion/core']).toBeDefined(); + expect(packageJSON.dependencies['@emotion/react']).toBeDefined(); expect(packageJSON.dependencies['@emotion/styled']).toBeDefined(); }); }); diff --git a/packages/react/src/schematics/component/component.spec.ts b/packages/react/src/schematics/component/component.spec.ts index 834889e9c2..068660b995 100644 --- a/packages/react/src/schematics/component/component.spec.ts +++ b/packages/react/src/schematics/component/component.spec.ts @@ -175,7 +175,7 @@ describe('component', () => { const packageJSON = readJsonInTree(tree, 'package.json'); expect(packageJSON.dependencies['@emotion/styled']).toBeDefined(); - expect(packageJSON.dependencies['@emotion/core']).toBeDefined(); + expect(packageJSON.dependencies['@emotion/react']).toBeDefined(); }); }); diff --git a/packages/react/src/schematics/library/library.spec.ts b/packages/react/src/schematics/library/library.spec.ts index 58465aa7e5..789fc477e5 100644 --- a/packages/react/src/schematics/library/library.spec.ts +++ b/packages/react/src/schematics/library/library.spec.ts @@ -455,7 +455,7 @@ describe('lib', () => { expect(workspaceJson.projects['my-lib'].architect.build).toMatchObject({ options: { - external: ['react', 'react-dom', '@emotion/styled', '@emotion/core'], + external: ['react', 'react-dom', '@emotion/styled', '@emotion/react'], }, }); }); diff --git a/packages/react/src/utils/babel-utils.ts b/packages/react/src/utils/babel-utils.ts index 0a54bb7a6f..e29dc83c87 100644 --- a/packages/react/src/utils/babel-utils.ts +++ b/packages/react/src/utils/babel-utils.ts @@ -22,7 +22,7 @@ export function updateBabelOptions(options: any): void { const packageJson = readJsonFile(join(appRootPath, 'package.json')); const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; const hasStyledComponents = !!deps['styled-components']; - const hasEmotion = !!deps['@emotion/core']; + const hasEmotion = !!deps['@emotion/react']; if (hasStyledComponents && !hasEmotion) { options.plugins.splice(0, 0, [ require.resolve('babel-plugin-styled-components'), diff --git a/packages/react/src/utils/styled.ts b/packages/react/src/utils/styled.ts index 1cc4b7b154..1e8800ac5a 100644 --- a/packages/react/src/utils/styled.ts +++ b/packages/react/src/utils/styled.ts @@ -1,7 +1,7 @@ import { babelPluginStyledComponentsVersion, emotionBabelPresetCssPropVersion, - emotionCoreVersion, + emotionReactVersion, emotionStyledVersion, reactIsVersion, styledComponentsVersion, @@ -29,7 +29,7 @@ export const CSS_IN_JS_DEPENDENCIES: { '@emotion/styled': { dependencies: { '@emotion/styled': emotionStyledVersion, - '@emotion/core': emotionCoreVersion, + '@emotion/react': emotionReactVersion, }, devDependencies: { '@emotion/babel-preset-css-prop': emotionBabelPresetCssPropVersion, diff --git a/packages/react/src/utils/versions.ts b/packages/react/src/utils/versions.ts index 4681fc81d7..1d07be190f 100644 --- a/packages/react/src/utils/versions.ts +++ b/packages/react/src/utils/versions.ts @@ -11,9 +11,9 @@ export const typesStyledComponentsVersion = '5.1.4'; export const reactIsVersion = '17.0.1'; export const typesReactIsVersion = '16.7.1'; -export const emotionStyledVersion = '10.0.27'; -export const emotionCoreVersion = '10.0.28'; -export const emotionBabelPresetCssPropVersion = '10.0.27'; +export const emotionStyledVersion = '11.0.0'; +export const emotionReactVersion = '11.0.0'; +export const emotionBabelPresetCssPropVersion = '11.0.0'; export const styledJsxVersion = '3.3.1'; export const typesStyledJsxVersion = '2.2.8'; diff --git a/packages/workspace/index.ts b/packages/workspace/index.ts index 536ce48f08..41cc7385e8 100644 --- a/packages/workspace/index.ts +++ b/packages/workspace/index.ts @@ -74,6 +74,8 @@ export * from './src/utils/rules/ng-add'; export { updateKarmaConf } from './src/utils/rules/update-karma-conf'; export { visitNotIgnoredFiles } from './src/utils/rules/visit-not-ignored-files'; export { setDefaultCollection } from './src/utils/rules/workspace'; +export { renamePackageImports } from './src/utils/rules/rename-package-imports'; +export { renameNpmPackages } from './src/utils/rules/rename-npm-packages'; import * as strings from './src/utils/strings'; export { checkAndCleanWithSemver } from './src/utils/version-utils'; export { updatePackagesInPackageJson } from './src/utils/update-packages-in-package-json'; diff --git a/packages/workspace/src/utils/rules/rename-npm-packages.spec.ts b/packages/workspace/src/utils/rules/rename-npm-packages.spec.ts new file mode 100644 index 0000000000..8237754dbd --- /dev/null +++ b/packages/workspace/src/utils/rules/rename-npm-packages.spec.ts @@ -0,0 +1,245 @@ +import { Tree } from '@angular-devkit/schematics'; +import { UnitTestTree } from '@angular-devkit/schematics/testing'; +import { readJsonInTree } from '../ast-utils'; +import { callRule, runSchematic, createEmptyWorkspace } from '../../../testing'; +import { renameNpmPackages, PackageRenameMapping } from './rename-npm-packages'; + +describe('renameNpmPackages Rule', () => { + let tree: UnitTestTree; + + beforeEach(async () => { + tree = new UnitTestTree(Tree.empty()); + tree = createEmptyWorkspace(tree) as UnitTestTree; + }); + + it('should rename an npm package in both package.json and any file that imports it', async () => { + tree.overwrite( + 'package.json', + JSON.stringify({ + dependencies: { + 'package-to-rename': '1.2.3', + }, + }) + ); + tree = await runSchematic('lib', { name: 'library-1' }, tree); + + const moduleThatImports = 'libs/library-1/src/importer.ts'; + tree.create( + moduleThatImports, + `import { something } from 'package-to-rename'; + + export const doSomething = (...args) => something(...args); + ` + ); + + tree = (await callRule( + renameNpmPackages({ 'package-to-rename': '@package/renamed' }), + tree + )) as UnitTestTree; + + const packageJson = readJsonInTree(tree, '/package.json'); + expect(packageJson).toMatchObject({ + dependencies: { + '@package/renamed': '1.2.3', + }, + }); + + expect(tree.read(moduleThatImports).toString()).toContain( + `import { something } from '@package/renamed'` + ); + }); + + it('should accept a new version that will also be updated in the package.json when renamed', async () => { + tree.overwrite( + 'package.json', + JSON.stringify({ + dependencies: { + 'package-to-rename': '1.2.3', + }, + }) + ); + + tree = (await callRule( + renameNpmPackages({ 'package-to-rename': ['@package/renamed', '9.9.9'] }), + tree + )) as UnitTestTree; + + const packageJson = readJsonInTree(tree, '/package.json'); + expect(packageJson).toMatchObject({ + dependencies: { + '@package/renamed': '9.9.9', + }, + }); + }); + + it('should rename multiple npm packages if more are passed in the PackageRenameMapping', async () => { + tree.overwrite( + 'package.json', + JSON.stringify({ + dependencies: { + 'package-to-rename': '1.2.3', + }, + devDependencies: { + '@old/packageName': '0.0.1', + }, + }) + ); + + tree = await runSchematic('lib', { name: 'library-1' }, tree); + tree = await runSchematic('lib', { name: 'library-2' }, tree); + tree = await runSchematic( + 'preset', + { name: 'app-one', preset: 'angular' }, + tree + ); + + const lib1ImportFile = 'libs/library-1/src/importer.ts'; + tree.create( + lib1ImportFile, + `import { something } from 'package-to-rename'; + import { anotherThing } from '@old/packageName'; + + export const doSomething = (...args) => something(...args); + export const doSomethingElse = (...args) => anotherThing(...args); + ` + ); + const lib2ImportFile = 'libs/library-2/src/importer.ts'; + tree.create( + lib2ImportFile, + `import { something } from 'package-to-rename'; + + export const doSomething = (...args) => something(...args); + ` + ); + const lib2ImportFile2 = 'libs/library-2/src/lib/second-importer.ts'; + tree.create( + lib2ImportFile2, + `import { something } from '@old/packageName'; + + export const doSomething = (...args) => something(...args); + ` + ); + + const appImportFile = 'apps/app-one/src/importer.ts'; + tree.create( + appImportFile, + `import { something } from 'package-to-rename'; + + export const doSomething = (...args) => something(...args); + ` + ); + + tree = (await callRule( + renameNpmPackages({ + 'package-to-rename': '@package/renamed', + '@old/packageName': 'new-improved-pacakge', + }), + tree + )) as UnitTestTree; + + const packageJson = readJsonInTree(tree, '/package.json'); + expect(packageJson).toMatchObject({ + dependencies: { + '@package/renamed': '1.2.3', + }, + devDependencies: { + 'new-improved-pacakge': '0.0.1', + }, + }); + + // Lib1 (one file with multiple import name changes) + expect(tree.read(lib1ImportFile).toString()).toContain( + `import { anotherThing } from 'new-improved-pacakge'` + ); + expect(tree.read(lib1ImportFile).toString()).toContain( + `import { something } from '@package/renamed'` + ); + + // Lib2 (one lib with multiple files with import changes) + expect(tree.read(lib2ImportFile).toString()).toContain( + `import { something } from '@package/renamed'` + ); + expect(tree.read(lib2ImportFile2).toString()).toContain( + `import { something } from 'new-improved-pacakge'` + ); + + // App (make sure it's changed in apps too) + expect(tree.read(appImportFile).toString()).toContain( + `import { something } from '@package/renamed'` + ); + }); + + it('should only update libs / apps that import the npm package as a dep', async () => { + tree.overwrite( + 'package.json', + JSON.stringify({ + dependencies: { + 'package-to-rename': '1.2.3', + }, + }) + ); + + tree = await runSchematic('lib', { name: 'library-1' }, tree); + tree = await runSchematic('lib', { name: 'library-2' }, tree); + + const lib1ImportFile = 'libs/library-1/src/importer.ts'; + tree.create( + lib1ImportFile, + `import { something } from 'package-to-rename'; + + export const doSomething = (...args) => something(...args); + ` + ); + const lib2ImportFile = 'libs/library-2/src/non-importer.ts'; + tree.create( + lib2ImportFile, + `// just a comment about import { something } from 'package-to-rename'` + ); + + tree = (await callRule( + renameNpmPackages({ + 'package-to-rename': '@package/renamed', + }), + tree + )) as UnitTestTree; + + expect(tree.read(lib1ImportFile).toString()).toContain( + `import { something } from '@package/renamed'` + ); + + expect(tree.read(lib2ImportFile).toString()).toContain( + `// just a comment about import { something } from 'package-to-rename'` + ); + }); + + it('should do nothing if the packages are not found in the package.json', async () => { + tree.overwrite( + 'package.json', + JSON.stringify({ + dependencies: { + 'not-me': '1.0.0', + }, + devDependencies: { + 'nor-me': '0.0.2', + }, + }) + ); + + tree = (await callRule( + renameNpmPackages({ + 'package-to-rename': '@package/renamed', + }), + tree + )) as UnitTestTree; + + const packageJson = readJsonInTree(tree, '/package.json'); + expect(packageJson).toMatchObject({ + dependencies: { + 'not-me': '1.0.0', + }, + devDependencies: { + 'nor-me': '0.0.2', + }, + }); + }); +}); diff --git a/packages/workspace/src/utils/rules/rename-npm-packages.ts b/packages/workspace/src/utils/rules/rename-npm-packages.ts new file mode 100644 index 0000000000..9b812c0a99 --- /dev/null +++ b/packages/workspace/src/utils/rules/rename-npm-packages.ts @@ -0,0 +1,127 @@ +import { + chain, + Rule, + noop, + Tree, + SchematicContext, +} from '@angular-devkit/schematics'; +import { + addDepsToPackageJson, + updateJsonInTree, + readJsonInTree, +} from '../ast-utils'; +import { + renamePackageImports, + PackageNameMapping, +} from './rename-package-imports'; +import { formatFiles } from './format-files'; + +export interface PackageRenameMapping { + [packageName: string]: string | [newPackageName: string, version: string]; +} + +interface NormalizedRenameDescriptors { + packageName: string; + newPackageName: string; + version: string; + isDevDep: boolean; + inPackageJson: boolean; +} + +const normalizeToDescriptors = (packageJson: any) => ([ + packageName, + newPackageNameConfig, +]): NormalizedRenameDescriptors => { + const isDevDep = + !!packageJson.devDependencies && packageName in packageJson.devDependencies; + const inPackageJson = + (packageJson.dependencies && packageName in packageJson.dependencies) || + isDevDep; + const newPackageName = Array.isArray(newPackageNameConfig) + ? newPackageNameConfig[0] + : newPackageNameConfig; + const version = + Array.isArray(newPackageNameConfig) && newPackageNameConfig[1] + ? newPackageNameConfig[1] + : isDevDep + ? packageJson.devDependencies[packageName] + : packageJson.dependencies[packageName]; + return { + packageName, + newPackageName, + version, + isDevDep, + inPackageJson, + }; +}; + +/** + * Updates all the imports in the workspace, and adjust the package.json appropriately. + * + * @param packageNameMapping The packageNameMapping provided to the schematic + */ +export function renameNpmPackages(packageRenameMapping: PackageRenameMapping) { + return (tree: Tree, context: SchematicContext): Rule => { + const pkg = readJsonInTree(tree, 'package.json'); + + const renameDescriptors = Object.entries(packageRenameMapping).map( + normalizeToDescriptors(pkg) + ); + + // if you don't find the packageName in package.json abort + if ( + renameDescriptors.filter(({ inPackageJson }) => inPackageJson).length === + 0 + ) { + return noop(); + } + + const packageNameMapping: PackageNameMapping = renameDescriptors.reduce( + (mapping, { packageName, newPackageName }) => { + mapping[packageName] = newPackageName; + return mapping; + }, + {} + ); + + const depAdditions = renameDescriptors.reduce( + (mapping, { newPackageName, version, isDevDep }) => { + if (!isDevDep) { + mapping[newPackageName] = version; + } + return mapping; + }, + {} + ); + + const devDepAdditions = renameDescriptors.reduce( + (mapping, { newPackageName, version, isDevDep }) => { + if (isDevDep) { + mapping[newPackageName] = version; + } + return mapping; + }, + {} + ); + + return chain([ + // rename all the imports before the package.json changes and we can't find the imports + renamePackageImports(packageNameMapping), + // add the new name at either the old version or a new version + addDepsToPackageJson(depAdditions, devDepAdditions), + // delete the old entry from the package.json + updateJsonInTree('package.json', (json) => { + renameDescriptors.forEach(({ packageName, isDevDep }) => { + if (isDevDep) { + delete json.devDependencies[packageName]; + } else { + delete json.dependencies[packageName]; + } + }); + + return json; + }), + formatFiles(), + ]); + }; +} diff --git a/packages/workspace/src/utils/rules/rename-package-imports.spec.ts b/packages/workspace/src/utils/rules/rename-package-imports.spec.ts new file mode 100644 index 0000000000..3e6ff2150c --- /dev/null +++ b/packages/workspace/src/utils/rules/rename-package-imports.spec.ts @@ -0,0 +1,156 @@ +import { Tree } from '@angular-devkit/schematics'; +import { UnitTestTree } from '@angular-devkit/schematics/testing'; +import { createEmptyWorkspace } from '@nrwl/workspace/testing'; +import { callRule, runSchematic } from '../testing'; +import { renamePackageImports } from './rename-package-imports'; + +describe('renamePackageImports Rule', () => { + let tree: UnitTestTree; + + beforeEach(async () => { + tree = new UnitTestTree(Tree.empty()); + tree = createEmptyWorkspace(tree) as UnitTestTree; + tree.overwrite( + 'package.json', + JSON.stringify({ + dependencies: { + 'package-to-rename': '1.2.3', + }, + }) + ); + }); + + it('should rename package imports', async () => { + tree = await runSchematic('lib', { name: 'library-1' }, tree); + + const moduleThatImports = 'libs/library-1/src/importer.ts'; + tree.create( + moduleThatImports, + `import { something } from 'package-to-rename'; + + export const doSomething = (...args) => something(...args); + ` + ); + + tree = (await callRule( + renamePackageImports({ 'package-to-rename': '@package/renamed' }), + tree + )) as UnitTestTree; + + expect(tree.read(moduleThatImports).toString()).toContain( + `import { something } from '@package/renamed'` + ); + }); + + it('should be able to rename multiple package imports to the new packageName', async () => { + tree = await runSchematic('lib', { name: 'library-1' }, tree); + tree = await runSchematic('lib', { name: 'library-2' }, tree); + tree = await runSchematic('lib', { name: 'dont-include-me' }, tree); + tree = await runSchematic( + 'preset', + { name: 'app-one', preset: 'angular' }, + tree + ); + + const lib1ImportFile = 'libs/library-1/src/importer.ts'; + tree.create( + lib1ImportFile, + `import { something } from 'package-to-rename'; + import { anotherThing } from '@old/packageName'; + + export const doSomething = (...args) => something(...args); + export const doSomethingElse = (...args) => anotherThing(...args); + ` + ); + const lib2ImportFile = 'libs/library-2/src/importer.ts'; + tree.create( + lib2ImportFile, + `import { something } from 'package-to-rename'; + + export const doSomething = (...args) => something(...args); + ` + ); + const lib2ImportFile2 = 'libs/library-2/src/lib/second-importer.ts'; + tree.create( + lib2ImportFile2, + `import { something } from '@old/packageName'; + + export const doSomething = (...args) => something(...args); + ` + ); + + const appImportFile = 'apps/app-one/src/importer.ts'; + tree.create( + appImportFile, + `import { something } from 'package-to-rename'; + + export const doSomething = (...args) => something(...args); + ` + ); + + tree = (await callRule( + renamePackageImports({ + 'package-to-rename': '@package/renamed', + '@old/packageName': 'new-improved-pacakge', + }), + tree + )) as UnitTestTree; + + // Lib1 (one file with multiple import name changes) + expect(tree.read(lib1ImportFile).toString()).toContain( + `import { anotherThing } from 'new-improved-pacakge'` + ); + expect(tree.read(lib1ImportFile).toString()).toContain( + `import { something } from '@package/renamed'` + ); + + // Lib2 (one lib with multiple files with import changes) + expect(tree.read(lib2ImportFile).toString()).toContain( + `import { something } from '@package/renamed'` + ); + expect(tree.read(lib2ImportFile2).toString()).toContain( + `import { something } from 'new-improved-pacakge'` + ); + + // App (make sure it's changed in apps too) + expect(tree.read(appImportFile).toString()).toContain( + `import { something } from '@package/renamed'` + ); + }); + + it('should NOT modify anything BUT the module import', async () => { + tree = await runSchematic('lib', { name: 'library-1' }, tree); + + const moduleThatImports = 'libs/library-1/src/importer.ts'; + tree.create( + moduleThatImports, + `// a comment about package-to-rename + import { something } from 'package-to-rename'; + + // a comment about package-to-rename + export const objectThingy = { + 'package-to-rename': something + }; + ` + ); + + tree = (await callRule( + renamePackageImports({ 'package-to-rename': '@package/renamed' }), + tree + )) as UnitTestTree; + + const fileContents = tree.read(moduleThatImports).toString(); + + expect(fileContents).toContain( + `import { something } from '@package/renamed'` + ); + // Leave comment alone + expect(tree.read(moduleThatImports).toString()).toContain( + `// a comment about package-to-rename` + ); + // Leave object key alone + expect(tree.read(moduleThatImports).toString()).toContain( + `'package-to-rename': something` + ); + }); +}); diff --git a/packages/workspace/src/utils/rules/rename-package-imports.ts b/packages/workspace/src/utils/rules/rename-package-imports.ts new file mode 100644 index 0000000000..f9246a82e0 --- /dev/null +++ b/packages/workspace/src/utils/rules/rename-package-imports.ts @@ -0,0 +1,113 @@ +import * as ts from 'typescript'; +import { SchematicContext, Tree } from '@angular-devkit/schematics'; +import { getWorkspace } from '@nrwl/workspace'; +import { + getFullProjectGraphFromHost, + findNodes, + insert, + ReplaceChange, +} from '@nrwl/workspace/src/utils/ast-utils'; + +export interface PackageNameMapping { + [packageName: string]: string; +} + +const getProjectNamesWithDepsToRename = ( + packageNameMapping: PackageNameMapping, + tree: Tree +) => { + const packagesToRename = Object.entries(packageNameMapping); + const projectGraph = getFullProjectGraphFromHost(tree); + + return Object.entries(projectGraph.dependencies) + .filter(([, deps]) => + deps.some( + (dep) => + dep.type === 'static' && + packagesToRename.some( + ([packageName]) => packageName === dep.target.replace('npm:', '') + ) + ) + ) + .map(([projectName]) => projectName); +}; + +/** + * Updates all the imports found in the workspace + * + * @param packageNameMapping The packageNameMapping provided to the schematic + */ +export function renamePackageImports(packageNameMapping: PackageNameMapping) { + return async (tree: Tree, _context: SchematicContext): Promise => { + const workspace = await getWorkspace(tree); + + const projectNamesThatImportAPackageToRename = getProjectNamesWithDepsToRename( + packageNameMapping, + tree + ); + + const projectsThatImportPackage = [...workspace.projects].filter(([name]) => + projectNamesThatImportAPackageToRename.includes(name) + ); + + projectsThatImportPackage + .map(([, definition]) => tree.getDir(definition.root)) + .forEach((projectDir) => { + projectDir.visit((file) => { + // only look at .(j|t)s(x) files + if (!/(j|t)sx?$/.test(file)) { + return; + } + // if it doesn't contain at least 1 reference to the packages to be renamed bail out + const contents = tree.read(file).toString('utf-8'); + if ( + !Object.keys(packageNameMapping).some((packageName) => + contents.includes(packageName) + ) + ) { + return; + } + + const astSource = ts.createSourceFile( + file, + contents, + ts.ScriptTarget.Latest, + true + ); + const changes = Object.entries(packageNameMapping) + .map(([packageName, newPackageName]) => { + const nodes = findNodes( + astSource, + ts.SyntaxKind.ImportDeclaration + ) as ts.ImportDeclaration[]; + + return nodes + .filter((node) => { + return ( + // remove quotes from module name + node.moduleSpecifier.getText().slice(1).slice(0, -1) === + packageName + ); + }) + .map( + (node) => + new ReplaceChange( + file, + node.moduleSpecifier.getStart(), + node.moduleSpecifier.getText(), + `'${newPackageName}'` + ) + ); + }) + // .flatMap()/.flat() is not available? So, here's a flat poly + .reduce((acc, val) => acc.concat(val), []); + + // if the reference to packageName was in fact an import statement + if (changes.length > 0) { + // update the file in the tree + insert(tree, file, changes); + } + }); + }); + }; +} diff --git a/packages/workspace/testing.ts b/packages/workspace/testing.ts index ab52b2dbf7..295759d6b8 100644 --- a/packages/workspace/testing.ts +++ b/packages/workspace/testing.ts @@ -3,4 +3,4 @@ export { getFileContent, MockBuilderContext, } from './src/utils/testing-utils'; -export { callRule } from './src/utils/testing'; +export { callRule, runSchematic } from './src/utils/testing';