Emotion upgrade for React and Next plugins (#4088)

* chore(react): update emotion to new versions and new package names on react plugin

* chore(nextjs): update emotion to latest version and the new package names

* feat(misc): have react and next plugin migrations update emotion imports

 * add renamePackageImports and renameNpmPackage rules to workspace utils
 * update migrations to update emotion imports to the new name and version
This commit is contained in:
Adam L Barrett 2020-11-27 13:32:42 -06:00 committed by GitHub
parent 81ba64ad1f
commit 889b648886
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 979 additions and 14 deletions

View File

@ -14,6 +14,16 @@
"version": "9.3.1-beta.1", "version": "9.3.1-beta.1",
"description": "Rename @nrwl/next:dev-server to @nrwl/next:server", "description": "Rename @nrwl/next:dev-server to @nrwl/next:server",
"factory": "./src/migrations/update-9-3-1/update-9-3-1" "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": { "packageJsonUpdates": {
@ -47,6 +57,23 @@
"alwaysAddToPackageJson": false "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
}
}
} }
} }
} }

View File

@ -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'`
);
});
});

View File

@ -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(),
]);
}

View File

@ -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',
},
});
});
});

View File

@ -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(),
]);
}

View File

@ -20,7 +20,7 @@ export const NEXT_SPECIFIC_STYLE_DEPENDENCIES = {
'@emotion/styled': { '@emotion/styled': {
dependencies: { dependencies: {
...CSS_IN_JS_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, devDependencies: CSS_IN_JS_DEPENDENCIES['@emotion/styled'].devDependencies,
}, },

View File

@ -1,10 +1,10 @@
export const nxVersion = '*'; export const nxVersion = '*';
export const nextVersion = '10.0.0'; export const nextVersion = '10.0.1';
export const zeitNextCss = '1.0.1'; export const zeitNextCss = '1.0.1';
export const zeitNextSass = '1.0.1'; export const zeitNextSass = '1.0.1';
export const nodeSass = '4.14.1'; export const nodeSass = '4.14.1';
export const zeitNextLess = '1.0.1'; export const zeitNextLess = '1.0.1';
export const zeitNextStylus = '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'; export const babelPluginStyledComponentsVersion = '1.10.7';

View File

@ -59,6 +59,16 @@
"version": "10.4.0-beta.1", "version": "10.4.0-beta.1",
"description": "Update libraries", "description": "Update libraries",
"factory": "./src/migrations/update-10-4-0/update-10-4-0" "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": { "packageJsonUpdates": {
@ -380,6 +390,27 @@
"alwaysAddToPackageJson": false "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
}
}
} }
} }
} }

View File

@ -5,7 +5,7 @@ function getRollupOptions(options: rollup.RollupOptions) {
react: 'React', react: 'React',
'react-dom': 'ReactDOM', 'react-dom': 'ReactDOM',
'styled-components': 'styled', 'styled-components': 'styled',
'@emotion/core': 'emotionCore', '@emotion/react': 'emotionReact',
'@emotion/styled': 'emotionStyled', '@emotion/styled': 'emotionStyled',
}; };
if (Array.isArray(options.output)) { if (Array.isArray(options.output)) {

View File

@ -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'`
);
});
});

View File

@ -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(),
]);
}

View File

@ -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',
},
});
});
});

View File

@ -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(),
]);
}

View File

@ -531,7 +531,7 @@ describe('app', () => {
); );
const packageJSON = readJsonInTree(tree, 'package.json'); const packageJSON = readJsonInTree(tree, 'package.json');
expect(packageJSON.dependencies['@emotion/core']).toBeDefined(); expect(packageJSON.dependencies['@emotion/react']).toBeDefined();
expect(packageJSON.dependencies['@emotion/styled']).toBeDefined(); expect(packageJSON.dependencies['@emotion/styled']).toBeDefined();
}); });
}); });

View File

@ -175,7 +175,7 @@ describe('component', () => {
const packageJSON = readJsonInTree(tree, 'package.json'); const packageJSON = readJsonInTree(tree, 'package.json');
expect(packageJSON.dependencies['@emotion/styled']).toBeDefined(); expect(packageJSON.dependencies['@emotion/styled']).toBeDefined();
expect(packageJSON.dependencies['@emotion/core']).toBeDefined(); expect(packageJSON.dependencies['@emotion/react']).toBeDefined();
}); });
}); });

View File

@ -455,7 +455,7 @@ describe('lib', () => {
expect(workspaceJson.projects['my-lib'].architect.build).toMatchObject({ expect(workspaceJson.projects['my-lib'].architect.build).toMatchObject({
options: { options: {
external: ['react', 'react-dom', '@emotion/styled', '@emotion/core'], external: ['react', 'react-dom', '@emotion/styled', '@emotion/react'],
}, },
}); });
}); });

View File

@ -22,7 +22,7 @@ export function updateBabelOptions(options: any): void {
const packageJson = readJsonFile(join(appRootPath, 'package.json')); const packageJson = readJsonFile(join(appRootPath, 'package.json'));
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
const hasStyledComponents = !!deps['styled-components']; const hasStyledComponents = !!deps['styled-components'];
const hasEmotion = !!deps['@emotion/core']; const hasEmotion = !!deps['@emotion/react'];
if (hasStyledComponents && !hasEmotion) { if (hasStyledComponents && !hasEmotion) {
options.plugins.splice(0, 0, [ options.plugins.splice(0, 0, [
require.resolve('babel-plugin-styled-components'), require.resolve('babel-plugin-styled-components'),

View File

@ -1,7 +1,7 @@
import { import {
babelPluginStyledComponentsVersion, babelPluginStyledComponentsVersion,
emotionBabelPresetCssPropVersion, emotionBabelPresetCssPropVersion,
emotionCoreVersion, emotionReactVersion,
emotionStyledVersion, emotionStyledVersion,
reactIsVersion, reactIsVersion,
styledComponentsVersion, styledComponentsVersion,
@ -29,7 +29,7 @@ export const CSS_IN_JS_DEPENDENCIES: {
'@emotion/styled': { '@emotion/styled': {
dependencies: { dependencies: {
'@emotion/styled': emotionStyledVersion, '@emotion/styled': emotionStyledVersion,
'@emotion/core': emotionCoreVersion, '@emotion/react': emotionReactVersion,
}, },
devDependencies: { devDependencies: {
'@emotion/babel-preset-css-prop': emotionBabelPresetCssPropVersion, '@emotion/babel-preset-css-prop': emotionBabelPresetCssPropVersion,

View File

@ -11,9 +11,9 @@ export const typesStyledComponentsVersion = '5.1.4';
export const reactIsVersion = '17.0.1'; export const reactIsVersion = '17.0.1';
export const typesReactIsVersion = '16.7.1'; export const typesReactIsVersion = '16.7.1';
export const emotionStyledVersion = '10.0.27'; export const emotionStyledVersion = '11.0.0';
export const emotionCoreVersion = '10.0.28'; export const emotionReactVersion = '11.0.0';
export const emotionBabelPresetCssPropVersion = '10.0.27'; export const emotionBabelPresetCssPropVersion = '11.0.0';
export const styledJsxVersion = '3.3.1'; export const styledJsxVersion = '3.3.1';
export const typesStyledJsxVersion = '2.2.8'; export const typesStyledJsxVersion = '2.2.8';

View File

@ -74,6 +74,8 @@ export * from './src/utils/rules/ng-add';
export { updateKarmaConf } from './src/utils/rules/update-karma-conf'; export { updateKarmaConf } from './src/utils/rules/update-karma-conf';
export { visitNotIgnoredFiles } from './src/utils/rules/visit-not-ignored-files'; export { visitNotIgnoredFiles } from './src/utils/rules/visit-not-ignored-files';
export { setDefaultCollection } from './src/utils/rules/workspace'; 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'; import * as strings from './src/utils/strings';
export { checkAndCleanWithSemver } from './src/utils/version-utils'; export { checkAndCleanWithSemver } from './src/utils/version-utils';
export { updatePackagesInPackageJson } from './src/utils/update-packages-in-package-json'; export { updatePackagesInPackageJson } from './src/utils/update-packages-in-package-json';

View File

@ -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',
},
});
});
});

View File

@ -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(),
]);
};
}

View File

@ -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`
);
});
});

View File

@ -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<void> => {
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);
}
});
});
};
}

View File

@ -3,4 +3,4 @@ export {
getFileContent, getFileContent,
MockBuilderContext, MockBuilderContext,
} from './src/utils/testing-utils'; } from './src/utils/testing-utils';
export { callRule } from './src/utils/testing'; export { callRule, runSchematic } from './src/utils/testing';