From 27f168e53a7800b3041291fbe75fd6aabff056c3 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Wed, 17 Mar 2021 15:25:32 -0400 Subject: [PATCH] fix(react): add migrations to fix babel setup for workspaces using react, next.js, and gatsby (#5041) --- packages/web/migrations.json | 18 +++ .../update-11-5-0/update-babel-config.spec.ts | 117 -------------- .../create-babelrc-for-workspace-libs.spec.ts | 64 ++++++++ .../create-babelrc-for-workspace-libs.ts | 29 ++++ .../update-existing-babelrc-files.spec.ts | 107 +++++++++++++ .../update-existing-babelrc-files.ts} | 20 +-- .../update-root-babel-config.spec.ts | 26 ++++ .../update-11-5-2/update-root-babel-config.ts | 15 ++ .../migrations/update-11-5-2/utils.spec.ts | 144 ++++++++++++++++++ .../web/src/migrations/update-11-5-2/utils.ts | 38 +++++ 10 files changed, 443 insertions(+), 135 deletions(-) delete mode 100644 packages/web/src/migrations/update-11-5-0/update-babel-config.spec.ts create mode 100644 packages/web/src/migrations/update-11-5-2/create-babelrc-for-workspace-libs.spec.ts create mode 100644 packages/web/src/migrations/update-11-5-2/create-babelrc-for-workspace-libs.ts create mode 100644 packages/web/src/migrations/update-11-5-2/update-existing-babelrc-files.spec.ts rename packages/web/src/migrations/{update-11-5-0/update-babel-config.ts => update-11-5-2/update-existing-babelrc-files.ts} (57%) create mode 100644 packages/web/src/migrations/update-11-5-2/update-root-babel-config.spec.ts create mode 100644 packages/web/src/migrations/update-11-5-2/update-root-babel-config.ts create mode 100644 packages/web/src/migrations/update-11-5-2/utils.spec.ts create mode 100644 packages/web/src/migrations/update-11-5-2/utils.ts diff --git a/packages/web/migrations.json b/packages/web/migrations.json index 7b672650ca6bb..4fc2d3d77dc56 100644 --- a/packages/web/migrations.json +++ b/packages/web/migrations.json @@ -14,6 +14,24 @@ "version": "9.2.0-beta.1", "description": "Set buildLibsFromSource property to true to not break existing projects.", "factory": "./src/migrations/update-9-2-0/set-build-libs-from-source" + }, + "update-root-babel-config-11-5-2": { + "cli": "nx", + "version": "11.5.2-beta.1", + "description": "Remove presets from the root babel.config.json file to prevent conflicts with babel presets used in apps.", + "factory": "./src/migrations/update-11-5-2/update-root-babel-config" + }, + "create-babelrc-for-workspace-libs-11-5-2": { + "cli": "nx", + "version": "11.5.2-beta.1", + "description": "Create .babelrc files for workspace libs to fix compatibility with the new babel.config.json setup.", + "factory": "./src/migrations/update-11-5-2/create-babelrc-for-workspace-libs" + }, + "update-existing-babelrc-files-11-5-2": { + "cli": "nx", + "version": "11.5.2-beta.1", + "description": "Update existing .babelrc files to add missing '@nrwl/web/babel' preset if necessary.", + "factory": "./src/migrations/update-11-5-2/update-existing-babelrc-files" } } } diff --git a/packages/web/src/migrations/update-11-5-0/update-babel-config.spec.ts b/packages/web/src/migrations/update-11-5-0/update-babel-config.spec.ts deleted file mode 100644 index cb39f6f5363a2..0000000000000 --- a/packages/web/src/migrations/update-11-5-0/update-babel-config.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; -import { readJson, Tree } from '@nrwl/devkit'; -import { updateBabelConfig } from './update-babel-config'; -describe('Migrate babel setup', () => { - let tree: Tree; - - beforeEach(async () => { - tree = createTreeWithEmptyWorkspace(); - }); - - it(`should add web babel preset if it does not exist`, async () => { - tree.write( - 'workspace.json', - JSON.stringify({ - projects: { - app1: { - root: 'apps/app1', - projectType: 'application', - }, - app2: { - root: 'apps/app2', - projectType: 'application', - }, - app3: { - root: 'apps/app3', - projectType: 'application', - }, - app4: { - root: 'apps/app4', - projectType: 'application', - }, - app5: { - root: 'apps/app5', - projectType: 'application', - }, - app6: { - root: 'apps/app6', - projectType: 'application', - }, - lib1: { - root: 'libs/lib1', - projectType: 'library', - }, - }, - }) - ); - tree.write( - 'nx.json', - JSON.stringify({ - projects: { - app1: {}, - app2: {}, - app3: {}, - app4: {}, - app5: {}, - app6: {}, - lib1: {}, - }, - }) - ); - tree.write( - 'babel.config.json', - JSON.stringify({ - presets: ['@nrwl/web/babel'], - }) - ); - tree.write('apps/app1/.babelrc', JSON.stringify({})); - tree.write( - 'apps/app2/.babelrc', - JSON.stringify({ presets: ['@nrwl/web/babel'] }) - ); - tree.write( - 'apps/app3/.babelrc', - JSON.stringify({ presets: ['@nrwl/react/babel'] }) - ); - tree.write( - 'apps/app4/.babelrc', - JSON.stringify({ presets: ['@nrwl/gatsby/babel'] }) - ); - tree.write( - 'apps/app6/.babelrc', - JSON.stringify({ presets: ['@nrwl/next/babel'] }) - ); - - await updateBabelConfig(tree); - - expect(readJson(tree, 'babel.config.json').presets).not.toContain( - '@nrwl/web/babel' - ); - - expect(readJson(tree, 'apps/app1/.babelrc')).toMatchObject({ - presets: ['@nrwl/web/babel'], - }); - - expect(readJson(tree, 'apps/app2/.babelrc')).toMatchObject({ - presets: ['@nrwl/web/babel'], - }); - - expect(readJson(tree, 'apps/app3/.babelrc')).toMatchObject({ - presets: ['@nrwl/react/babel'], - }); - - expect(readJson(tree, 'apps/app4/.babelrc')).toMatchObject({ - presets: ['@nrwl/gatsby/babel'], - }); - - expect(tree.exists('apps/app5/.babelrc')).not.toBeTruthy(); - - expect(readJson(tree, 'apps/app6/.babelrc')).toMatchObject({ - presets: ['@nrwl/next/babel'], - }); - - expect(readJson(tree, 'libs/lib1/.babelrc')).toMatchObject({ - presets: ['@nrwl/web/babel'], - }); - }); -}); diff --git a/packages/web/src/migrations/update-11-5-2/create-babelrc-for-workspace-libs.spec.ts b/packages/web/src/migrations/update-11-5-2/create-babelrc-for-workspace-libs.spec.ts new file mode 100644 index 0000000000000..70868dc2418de --- /dev/null +++ b/packages/web/src/migrations/update-11-5-2/create-babelrc-for-workspace-libs.spec.ts @@ -0,0 +1,64 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { readJson, Tree } from '@nrwl/devkit'; +import { createBabelrcForWorkspaceLibs } from './create-babelrc-for-workspace-libs'; + +describe('Create missing .babelrc files', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + }); + + it(`should create .babelrc files for libs that are used in '@nrwl/web:build'`, async () => { + tree.write( + 'workspace.json', + JSON.stringify({ + projects: { + webapp: { + root: 'apps/webapp', + projectType: 'application', + targets: { + build: { executor: '@nrwl/web:build' }, + }, + }, + nodeapp: { + root: 'apps/nodeapp', + projectType: 'application', + targets: { + build: { executor: '@nrwl/node:build' }, + }, + }, + weblib: { + root: 'libs/weblib', + projectType: 'library', + }, + nodelib: { + root: 'libs/nodelib', + projectType: 'library', + }, + }, + }) + ); + tree.write( + 'nx.json', + JSON.stringify({ + npmScope: 'proj', + projects: { + webapp: {}, + nodeapp: {}, + weblib: {}, + nodelib: {}, + }, + }) + ); + tree.write('apps/webapp/index.ts', `import '@proj/weblib';`); + + await createBabelrcForWorkspaceLibs(tree); + + expect(readJson(tree, 'libs/weblib/.babelrc')).toMatchObject({ + presets: ['@nrwl/web/babel'], + }); + + expect(tree.exists('libs/nodelib/.babelrc')).toBeFalsy(); + }); +}); diff --git a/packages/web/src/migrations/update-11-5-2/create-babelrc-for-workspace-libs.ts b/packages/web/src/migrations/update-11-5-2/create-babelrc-for-workspace-libs.ts new file mode 100644 index 0000000000000..6599c68e74dd3 --- /dev/null +++ b/packages/web/src/migrations/update-11-5-2/create-babelrc-for-workspace-libs.ts @@ -0,0 +1,29 @@ +import { formatFiles, getProjects, Tree } from '@nrwl/devkit'; +import { reverse } from '@nrwl/workspace/src/core/project-graph'; +import { createProjectGraphFromTree } from '@nrwl/workspace/src/utilities/create-project-graph-from-tree'; +import { hasDependentAppUsingWebBuild } from '@nrwl/web/src/migrations/update-11-5-2/utils'; + +export async function createBabelrcForWorkspaceLibs(host: Tree) { + const projects = getProjects(host); + const graph = reverse(createProjectGraphFromTree(host)); + + for (const [name, p] of projects.entries()) { + if (!hasDependentAppUsingWebBuild(name, graph, projects)) { + continue; + } + + const babelrcPath = `${p.root}/.babelrc`; + if (p.projectType === 'library' && !host.exists(babelrcPath)) { + // Library is included in applications that require .babelrc to + // exist and contain '@nrwl/web/babel' preset. + host.write( + babelrcPath, + JSON.stringify({ presets: ['@nrwl/web/babel'] }, null, 2) + ); + } + } + + await formatFiles(host); +} + +export default createBabelrcForWorkspaceLibs; diff --git a/packages/web/src/migrations/update-11-5-2/update-existing-babelrc-files.spec.ts b/packages/web/src/migrations/update-11-5-2/update-existing-babelrc-files.spec.ts new file mode 100644 index 0000000000000..efb3a20a0ed1e --- /dev/null +++ b/packages/web/src/migrations/update-11-5-2/update-existing-babelrc-files.spec.ts @@ -0,0 +1,107 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { readJson, Tree } from '@nrwl/devkit'; +import { updateExistingBabelrcFiles } from './update-existing-babelrc-files'; + +describe('Create missing .babelrc files', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + }); + + it(`should add web babel preset if it does not exist`, async () => { + tree.write( + 'workspace.json', + JSON.stringify({ + projects: { + 'missing-babel-presets': { + root: 'apps/missing-babel-presets', + projectType: 'application', + }, + 'web-app': { + root: 'apps/web-app', + projectType: 'application', + }, + 'react-app': { + root: 'apps/react-app', + projectType: 'application', + }, + 'gatsby-app': { + root: 'apps/gatsby-app', + projectType: 'application', + }, + 'not-using-babel': { + root: 'apps/not-using-babel', + projectType: 'application', + }, + 'next-app': { + root: 'apps/next-app', + projectType: 'application', + }, + }, + }) + ); + tree.write( + 'nx.json', + JSON.stringify({ + projects: { + 'missing-babel-presets': {}, + 'web-app': {}, + 'react-app': {}, + 'gatsby-app': {}, + 'not-using-babel': {}, + 'next-app': {}, + }, + }) + ); + tree.write( + 'babel.config.json', + JSON.stringify({ + presets: ['@nrwl/web/babel'], + }) + ); + tree.write('apps/missing-babel-presets/.babelrc', JSON.stringify({})); + tree.write( + 'apps/web-app/.babelrc', + JSON.stringify({ presets: ['@nrwl/web/babel'] }) + ); + tree.write( + 'apps/react-app/.babelrc', + JSON.stringify({ presets: ['@nrwl/react/babel'] }) + ); + tree.write( + 'apps/gatsby-app/.babelrc', + JSON.stringify({ presets: ['@nrwl/gatsby/babel'] }) + ); + tree.write( + 'apps/next-app/.babelrc', + JSON.stringify({ presets: ['@nrwl/next/babel'] }) + ); + + await updateExistingBabelrcFiles(tree); + + expect(readJson(tree, 'apps/missing-babel-presets/.babelrc')).toMatchObject( + { + presets: ['@nrwl/web/babel'], + } + ); + + expect(readJson(tree, 'apps/web-app/.babelrc')).toMatchObject({ + presets: ['@nrwl/web/babel'], + }); + + expect(readJson(tree, 'apps/react-app/.babelrc')).toMatchObject({ + presets: ['@nrwl/react/babel'], + }); + + expect(readJson(tree, 'apps/gatsby-app/.babelrc')).toMatchObject({ + presets: ['@nrwl/gatsby/babel'], + }); + + expect(tree.exists('apps/not-using-babel/.babelrc')).not.toBeTruthy(); + + expect(readJson(tree, 'apps/next-app/.babelrc')).toMatchObject({ + presets: ['@nrwl/next/babel'], + }); + }); +}); diff --git a/packages/web/src/migrations/update-11-5-0/update-babel-config.ts b/packages/web/src/migrations/update-11-5-2/update-existing-babelrc-files.ts similarity index 57% rename from packages/web/src/migrations/update-11-5-0/update-babel-config.ts rename to packages/web/src/migrations/update-11-5-2/update-existing-babelrc-files.ts index 1c3492c1c5e91..7e0fdea98e267 100644 --- a/packages/web/src/migrations/update-11-5-0/update-babel-config.ts +++ b/packages/web/src/migrations/update-11-5-2/update-existing-babelrc-files.ts @@ -1,17 +1,8 @@ import { formatFiles, getProjects, Tree, updateJson } from '@nrwl/devkit'; -export async function updateBabelConfig(host: Tree) { +export async function updateExistingBabelrcFiles(host: Tree) { const projects = getProjects(host); - if (host.exists('babel.config.json')) { - updateJson(host, 'babel.config.json', (json) => { - if (Array.isArray(json.presets)) { - json.presets = json.presets.filter((x) => x !== '@nrwl/web/babel'); - } - return json; - }); - } - projects.forEach((p) => { const babelrcPath = `${p.root}/.babelrc`; @@ -34,17 +25,10 @@ export async function updateBabelConfig(host: Tree) { } return json; }); - // Non-buildable libraries might be included in applications that - // require .babelrc to exist and contain '@nrwl/web/babel' preset - } else if (p.projectType === 'library') { - host.write( - babelrcPath, - JSON.stringify({ presets: ['@nrwl/web/babel'] }, null, 2) - ); } }); await formatFiles(host); } -export default updateBabelConfig; +export default updateExistingBabelrcFiles; diff --git a/packages/web/src/migrations/update-11-5-2/update-root-babel-config.spec.ts b/packages/web/src/migrations/update-11-5-2/update-root-babel-config.spec.ts new file mode 100644 index 0000000000000..c7cdb5925c101 --- /dev/null +++ b/packages/web/src/migrations/update-11-5-2/update-root-babel-config.spec.ts @@ -0,0 +1,26 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { readJson, Tree } from '@nrwl/devkit'; +import { updateRootBabelConfig } from './update-root-babel-config'; + +describe('Update root babel config', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + }); + + it(`should add web babel preset if it does not exist`, async () => { + tree.write( + 'babel.config.json', + JSON.stringify({ + presets: ['@nrwl/web/babel'], + }) + ); + + await updateRootBabelConfig(tree); + + expect(readJson(tree, 'babel.config.json').presets).not.toContain( + '@nrwl/web/babel' + ); + }); +}); diff --git a/packages/web/src/migrations/update-11-5-2/update-root-babel-config.ts b/packages/web/src/migrations/update-11-5-2/update-root-babel-config.ts new file mode 100644 index 0000000000000..1f0507ae13ab9 --- /dev/null +++ b/packages/web/src/migrations/update-11-5-2/update-root-babel-config.ts @@ -0,0 +1,15 @@ +import { formatFiles, Tree, updateJson } from '@nrwl/devkit'; + +export async function updateRootBabelConfig(host: Tree) { + if (host.exists('babel.config.json')) { + updateJson(host, 'babel.config.json', (json) => { + if (Array.isArray(json.presets)) { + json.presets = json.presets.filter((x) => x !== '@nrwl/web/babel'); + } + return json; + }); + } + await formatFiles(host); +} + +export default updateRootBabelConfig; diff --git a/packages/web/src/migrations/update-11-5-2/utils.spec.ts b/packages/web/src/migrations/update-11-5-2/utils.spec.ts new file mode 100644 index 0000000000000..388067881493d --- /dev/null +++ b/packages/web/src/migrations/update-11-5-2/utils.spec.ts @@ -0,0 +1,144 @@ +import { getProjects } from '@nrwl/devkit'; +import { ProjectGraph } from '@nrwl/workspace'; +import { + DependencyType, + reverse, +} from '@nrwl/workspace/src/core/project-graph'; +import { hasDependentAppUsingWebBuild } from '@nrwl/web/src/migrations/update-11-5-2/utils'; + +describe('hasDependentAppUsingWebBuild', () => { + const graph: ProjectGraph = reverse({ + nodes: { + webapp: { name: 'webapp', data: { files: [] }, type: 'application' }, + nodeapp: { name: 'nodeapp', data: { files: [] }, type: 'application' }, + weblib1: { name: 'weblib1', data: { files: [] }, type: 'library' }, + weblib2: { name: 'weblib2', data: { files: [] }, type: 'library' }, + weblib3: { name: 'weblib3', data: { files: [] }, type: 'library' }, + stylelib: { name: 'stylelib', data: { files: [] }, type: 'library' }, + sharedlib: { name: 'sharedlib', data: { files: [] }, type: 'library' }, + nodelib: { name: 'nodelib', data: { files: [] }, type: 'library' }, + }, + dependencies: { + webapp: [ + { source: 'webapp', target: 'weblib1', type: DependencyType.static }, + { source: 'webapp', target: 'sharedlib', type: DependencyType.static }, + { + source: 'webapp', + target: 'stylelib', + type: DependencyType.implicit, + }, + ], + nodeapp: [ + { source: 'nodeapp', target: 'nodelib', type: DependencyType.static }, + { source: 'nodeapp', target: 'sharedlib', type: DependencyType.static }, + ], + weblib1: [ + { source: 'weblib1', target: 'weblib2', type: DependencyType.static }, + ], + weblib2: [ + { source: 'weblib2', target: 'weblib3', type: DependencyType.static }, + ], + }, + }); + + const projects: ReturnType = new Map([ + [ + 'webapp', + { + projectType: 'application', + root: 'webapp', + targets: { + build: { executor: '@nrwl/web:build' }, + }, + }, + ], + [ + 'nodeapp', + { + projectType: 'application', + root: 'nodeapp', + targets: { + build: { executor: '@nrwl/node:build' }, + }, + }, + ], + [ + 'weblib1', + { + projectType: 'library', + root: 'weblib1', + targets: {}, + }, + ], + [ + 'weblib2', + { + projectType: 'library', + root: 'weblib2', + targets: {}, + }, + ], + [ + 'weblib3', + { + projectType: 'library', + root: 'weblib3', + targets: {}, + }, + ], + [ + 'stylelib', + { + projectType: 'library', + root: 'stylelib', + targets: {}, + }, + ], + [ + 'nodelib', + { + projectType: 'library', + root: 'nodelib', + targets: {}, + }, + ], + [ + 'sharedlib', + { + projectType: 'library', + root: 'sharedlib', + targets: {}, + }, + ], + ]); + + it('should return true if project is in the dependency chain of an app using `@nrwl/web:build`', () => { + expect( + hasDependentAppUsingWebBuild('weblib1', graph, projects) + ).toBeTruthy(); + + expect( + hasDependentAppUsingWebBuild('weblib2', graph, projects) + ).toBeTruthy(); + + expect( + hasDependentAppUsingWebBuild('weblib3', graph, projects) + ).toBeTruthy(); + + expect( + hasDependentAppUsingWebBuild('sharedlib', graph, projects) + ).toBeTruthy(); + }); + + it('should return false if project an implicit dependency of an app using`@nrwl/web:build`', () => { + expect( + hasDependentAppUsingWebBuild('stylelib', graph, projects) + ).toBeFalsy(); + }); + + it('should return false if project is not used by an app using `@nrwl/web:build`', () => { + expect( + hasDependentAppUsingWebBuild('nodelib', graph, projects) + ).toBeFalsy(); + }); +}); diff --git a/packages/web/src/migrations/update-11-5-2/utils.ts b/packages/web/src/migrations/update-11-5-2/utils.ts new file mode 100644 index 0000000000000..ca682d94ea032 --- /dev/null +++ b/packages/web/src/migrations/update-11-5-2/utils.ts @@ -0,0 +1,38 @@ +import { ProjectGraph } from '@nrwl/workspace'; +import { getProjects } from '@nrwl/devkit'; +import { DependencyType } from '@nrwl/workspace/src/core/project-graph'; + +const cache = new Map(); + +export function hasDependentAppUsingWebBuild( + projectName: string, + reversedProjectGraph: ProjectGraph, + projects: ReturnType +) { + function walk(currProject: string) { + if (cache.has(currProject)) { + return cache.get(currProject); + } + const project = projects.get(currProject); + + if (project?.targets?.build?.executor === '@nrwl/web:build') { + cache.set(currProject, true); + return true; + } + + const deps = reversedProjectGraph.dependencies[currProject]; + + if (deps.length === 0) { + cache.set(currProject, false); + return false; + } + + const result = deps.some( + (dep) => dep.type !== DependencyType.implicit && walk(dep.target) + ); + cache.set(currProject, result); + return result; + } + + return walk(projectName); +}