From a3cf6e73fd788167ba67fe84d35159ab9cc5c7fa Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Tue, 27 Jul 2021 17:01:32 +0200 Subject: [PATCH 1/2] cleanup(core): use isNpmProject and cleanup project graph utils --- ...-template-support-and-presets-to-eslint.ts | 7 +- .../migrations/update-9-4-0/babelrc-9-4-0.ts | 17 +- .../locators/npm-packages.ts | 5 +- .../src/core/file-graph/project-file-map.ts | 13 +- .../workspace/src/core/normalize-nx-json.ts | 43 ++- .../explicit-project-dependencies.ts | 2 +- .../src/utilities/buildable-libs-utils.ts | 18 +- .../src/utilities/create-package-json.ts | 5 +- .../src/utilities/project-graph-utils.ts | 8 +- .../src/utils/buildable-libs-utils.spec.ts | 86 ----- .../src/utils/buildable-libs-utils.ts | 315 ------------------ 11 files changed, 68 insertions(+), 451 deletions(-) delete mode 100644 packages/workspace/src/utils/buildable-libs-utils.spec.ts delete mode 100644 packages/workspace/src/utils/buildable-libs-utils.ts diff --git a/packages/angular/src/migrations/update-10-5-0/add-template-support-and-presets-to-eslint.ts b/packages/angular/src/migrations/update-10-5-0/add-template-support-and-presets-to-eslint.ts index 0aaa8a0a072d7..1a50314d61e1f 100644 --- a/packages/angular/src/migrations/update-10-5-0/add-template-support-and-presets-to-eslint.ts +++ b/packages/angular/src/migrations/update-10-5-0/add-template-support-and-presets-to-eslint.ts @@ -11,7 +11,10 @@ import { } from '@nrwl/workspace'; import { join } from 'path'; import { offsetFromRoot } from '@nrwl/devkit'; -import { createProjectGraphAsync } from '@nrwl/workspace/src/core/project-graph'; +import { + createProjectGraphAsync, + isNpmProject, +} from '@nrwl/workspace/src/core/project-graph'; /** * It was decided with Jason that we would do a simple replacement in this migration @@ -67,7 +70,7 @@ async function updateProjectESLintConfigsAndBuilders( !graph.dependencies[projectName].some( (dependency) => dependency.target.startsWith('npm:@angular/') && - graph.nodes[dependency.target].type === 'npm' + isNpmProject(graph.nodes[dependency.target]) ) ) { return; diff --git a/packages/react/src/migrations/update-9-4-0/babelrc-9-4-0.ts b/packages/react/src/migrations/update-9-4-0/babelrc-9-4-0.ts index 1435390c1e4a6..53124f43d86ac 100644 --- a/packages/react/src/migrations/update-9-4-0/babelrc-9-4-0.ts +++ b/packages/react/src/migrations/update-9-4-0/babelrc-9-4-0.ts @@ -10,7 +10,10 @@ import { } from '@angular-devkit/core/src/utils/literals'; import { initRootBabelConfig } from '../utils/rules'; import { addDepsToPackageJson, formatFiles } from '@nrwl/workspace'; -import { createProjectGraphAsync } from '@nrwl/workspace/src/core/project-graph'; +import { + createProjectGraphAsync, + isNpmProject, +} from '@nrwl/workspace/src/core/project-graph'; let addedEmotionPreset = false; @@ -30,7 +33,7 @@ export default function update(): Rule { context.logger.info( ` Found an existing babel.config.json file so we skipped creating it. - + You may want to update it to include the Nx preset "@nrwl/web/babel". ` ); @@ -38,7 +41,7 @@ export default function update(): Rule { context.logger.info( ` Found an existing babel.config.js file so we skipped creating it. - + You may want to update it to include the Nx preset "@nrwl/web/babel". ` ); @@ -51,7 +54,7 @@ export default function update(): Rule { const deps = projectGraph.dependencies[name]; const isReact = deps.some( (d) => - projectGraph.nodes[d.target].type === 'npm' && + isNpmProject(projectGraph.nodes[d.target]) && d.target.indexOf('react') !== -1 ); if (isReact) { @@ -69,11 +72,11 @@ export default function update(): Rule { if (conflicts.length > 0) { context.logger.info(stripIndent` The following projects already have .babelrc so we did not create them: - + ${conflicts .map(([name, babelrc]) => `${name} - ${babelrc}`) .join('\n ')} - + You may want to update them to include the Nx preset "@nrwl/react/babel". `); } @@ -109,7 +112,7 @@ function createBabelrc(host, context, babelrcPath, deps) { context.logger.warn( stripIndents`We created a babel config at ${babelrcPath} with both styled-components and emotion plugins. Only one should be used, please remove the unused plugin. - + For example, if you don't use styled-components, then remove that plugin from the .babelrc file. ` ); diff --git a/packages/workspace/src/core/affected-project-graph/locators/npm-packages.ts b/packages/workspace/src/core/affected-project-graph/locators/npm-packages.ts index 6796670353d55..1233817c40623 100644 --- a/packages/workspace/src/core/affected-project-graph/locators/npm-packages.ts +++ b/packages/workspace/src/core/affected-project-graph/locators/npm-packages.ts @@ -5,6 +5,7 @@ import { JsonChange, } from '../../../utilities/json-diff'; import { TouchedProjectLocator } from '../affected-project-graph-models'; +import { isNpmProject } from '../../project-graph/operators'; export const getTouchedNpmPackages: TouchedProjectLocator< WholeFileChange | JsonChange @@ -21,9 +22,7 @@ export const getTouchedNpmPackages: TouchedProjectLocator< let touched = []; const changes = packageJsonChange.getChanges(); - const npmPackages = Object.values(projectGraph.nodes).filter( - (node) => node.type === 'npm' - ); + const npmPackages = Object.values(projectGraph.nodes).filter(isNpmProject); for (const c of changes) { if ( diff --git a/packages/workspace/src/core/file-graph/project-file-map.ts b/packages/workspace/src/core/file-graph/project-file-map.ts index 3f6804b12f313..1a01e04fb056f 100644 --- a/packages/workspace/src/core/file-graph/project-file-map.ts +++ b/packages/workspace/src/core/file-graph/project-file-map.ts @@ -10,16 +10,15 @@ export function createProjectFileMap( // a chance to match prefix first. Object.keys(workspaceJson.projects) .sort((a, b) => { - if (!workspaceJson.projects[a].root) return -1; - if (!workspaceJson.projects[b].root) return -1; - return workspaceJson.projects[a].root.length > - workspaceJson.projects[b].root.length - ? -1 - : 1; + const projectA = workspaceJson.projects[a]; + const projectB = workspaceJson.projects[b]; + if (!projectA.root) return -1; + if (!projectB.root) return -1; + return projectA.root.length > projectB.root.length ? -1 : 1; }) .forEach((projectName) => { const p = workspaceJson.projects[projectName]; - fileMap[projectName] = fileMap[projectName] || []; + fileMap[projectName] = []; files.forEach((f) => { if (seen.has(f.file)) { return; diff --git a/packages/workspace/src/core/normalize-nx-json.ts b/packages/workspace/src/core/normalize-nx-json.ts index 0c4f2b433c588..06812f14230d8 100644 --- a/packages/workspace/src/core/normalize-nx-json.ts +++ b/packages/workspace/src/core/normalize-nx-json.ts @@ -1,5 +1,11 @@ import { NxJsonConfiguration } from '@nrwl/devkit'; +/** + * Normalize nx json by replacing wildcard `*` implicit dependencies + * to an array of all project names + * @param {NxJsonConfiguration} nxJson + * @returns {NxJsonConfiguration} + */ export function normalizeNxJson( nxJson: NxJsonConfiguration ): NxJsonConfiguration { @@ -9,22 +15,31 @@ export function normalizeNxJson( implicitDependencies: Object.entries( nxJson.implicitDependencies ).reduce((acc, [key, val]) => { - acc[key] = recur(val); + acc[key] = recur(nxJson, val); return acc; - - function recur(v: '*' | string[] | {}): string[] | {} { - if (v === '*') { - return Object.keys(nxJson.projects); - } else if (Array.isArray(v)) { - return v; - } else { - return Object.keys(v).reduce((xs, x) => { - xs[x] = recur(v[x]); - return xs; - }, {}); - } - } }, {}), } : (nxJson as NxJsonConfiguration); } + +/** + * Map recursively wildcard `*` to project names + * @param {NxJsonConfiguration} nxJson + * @param {'*' | string[] | {}} v + * @returns {string[] | {}} + */ +function recur( + nxJson: NxJsonConfiguration, + v: '*' | string[] | {} +): string[] | {} { + if (v === '*') { + return Object.keys(nxJson.projects); + } else if (Array.isArray(v)) { + return v; + } else { + return Object.keys(v).reduce((acc, key) => { + acc[key] = recur(nxJson, v[key]); + return acc; + }, {}); + } +} diff --git a/packages/workspace/src/core/project-graph/build-dependencies/explicit-project-dependencies.ts b/packages/workspace/src/core/project-graph/build-dependencies/explicit-project-dependencies.ts index f4e3bc364cfbf..562e619e34897 100644 --- a/packages/workspace/src/core/project-graph/build-dependencies/explicit-project-dependencies.ts +++ b/packages/workspace/src/core/project-graph/build-dependencies/explicit-project-dependencies.ts @@ -22,7 +22,7 @@ export function buildExplicitTypeScriptDependencies( f.file, ctx.workspace.npmScope ); - if (source && target) { + if (target) { builder.addExplicitDependency(source, f.file, target); } } diff --git a/packages/workspace/src/utilities/buildable-libs-utils.ts b/packages/workspace/src/utilities/buildable-libs-utils.ts index 2b3e1db3bbe27..1f4c4e43bec41 100644 --- a/packages/workspace/src/utilities/buildable-libs-utils.ts +++ b/packages/workspace/src/utilities/buildable-libs-utils.ts @@ -1,11 +1,6 @@ -import { ProjectType } from '../core/project-graph'; +import { isNpmProject, ProjectType } from '../core/project-graph'; import { join, resolve, dirname, relative } from 'path'; -import { - directoryExists, - fileExists, - readJsonFile, - writeJsonFile, -} from './fileutils'; +import { directoryExists, readJsonFile, writeJsonFile } from './fileutils'; import { stripIndents } from '@nrwl/devkit'; import type { ProjectGraph, ProjectGraphNode } from '@nrwl/devkit'; import { getOutputsForTargetAndConfiguration } from '../tasks-runner/utils'; @@ -66,7 +61,7 @@ export function calculateProjectDependencies( ), node: depNode, }; - } else if (depNode.type === 'npm') { + } else if (isNpmProject(depNode)) { return { name: depNode.data.packageName, outputs: [], @@ -286,8 +281,9 @@ export function updateBuildableProjectPackageJsonDependencies( let updatePackageJson = false; dependencies.forEach((entry) => { - const packageName = - entry.node.type === 'npm' ? entry.node.data.packageName : entry.name; + const packageName = isNpmProject(entry.node) + ? entry.node.data.packageName + : entry.name; if ( !hasDependency(packageJson, 'dependencies', packageName) && @@ -313,7 +309,7 @@ export function updateBuildableProjectPackageJsonDependencies( depVersion = readJsonFile(depPackageJsonPath).version; packageJson[typeOfDependency][packageName] = depVersion; - } else if (entry.node.type === 'npm') { + } else if (isNpmProject(entry.node)) { // If an npm dep is part of the workspace devDependencies, do not include it the library if ( !!workspacePackageJson.devDependencies?.[ diff --git a/packages/workspace/src/utilities/create-package-json.ts b/packages/workspace/src/utilities/create-package-json.ts index d611fa6d561d1..10374cbcaef9f 100644 --- a/packages/workspace/src/utilities/create-package-json.ts +++ b/packages/workspace/src/utilities/create-package-json.ts @@ -1,4 +1,5 @@ import type { ProjectGraph } from '@nrwl/devkit'; +import { isNpmProject } from '../core/project-graph'; import { readJsonFile } from './fileutils'; /** @@ -58,7 +59,7 @@ function findAllNpmDeps( const node = graph.nodes[projectName]; - if (node.type === 'npm') { + if (isNpmProject(node)) { list[node.data.packageName] = node.data.version; recursivelyCollectPeerDependencies(node.name, graph, list); } @@ -77,7 +78,7 @@ function recursivelyCollectPeerDependencies( ) { if ( !graph.nodes[projectName] || - graph.nodes[projectName].type !== 'npm' || + !isNpmProject(graph.nodes[projectName]) || seen.has(projectName) ) { return list; diff --git a/packages/workspace/src/utilities/project-graph-utils.ts b/packages/workspace/src/utilities/project-graph-utils.ts index 9a41ef6666092..c528403b378f9 100644 --- a/packages/workspace/src/utilities/project-graph-utils.ts +++ b/packages/workspace/src/utilities/project-graph-utils.ts @@ -67,9 +67,11 @@ export function getProjectNameFromDirPath( } /** - * Takes a filename and figures out the belonging app and from there - * collects all dependent - * @param filename name of a file in some workspace app / lib + * Find all internal project dependencies. + * All the external (npm) dependencies will be filtered out + * @param {string} parentNodeName + * @param {ProjectGraph} projectGraph + * @returns {string[]} */ function findAllProjectNodeDependencies( parentNodeName: string, diff --git a/packages/workspace/src/utils/buildable-libs-utils.spec.ts b/packages/workspace/src/utils/buildable-libs-utils.spec.ts deleted file mode 100644 index 216d3f6322f11..0000000000000 --- a/packages/workspace/src/utils/buildable-libs-utils.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - calculateProjectDependencies, - DependentBuildableProjectNode, - updatePaths, -} from './buildable-libs-utils'; -import { - DependencyType, - ProjectGraph, - ProjectType, -} from '../core/project-graph'; -import { getMockContext } from './testing'; - -describe('updatePaths', () => { - const deps: DependentBuildableProjectNode[] = [ - { name: '@proj/lib', node: {} as any, outputs: ['dist/libs/lib'] }, - ]; - - it('should add path', () => { - const paths: Record = { - '@proj/test': ['libs/test/src/index.ts'], - }; - updatePaths(deps, paths); - expect(paths).toEqual({ - '@proj/lib': ['dist/libs/lib'], - '@proj/test': ['libs/test/src/index.ts'], - }); - }); - - it('should replace paths', () => { - const paths: Record = { - '@proj/lib': ['libs/lib/src/index.ts'], - '@proj/lib/sub': ['libs/lib/sub/src/index.ts'], - }; - updatePaths(deps, paths); - expect(paths).toEqual({ - '@proj/lib': ['dist/libs/lib'], - '@proj/lib/sub': ['dist/libs/lib/sub'], - }); - }); -}); - -describe('calculateProjectDependencies', () => { - it('should include npm packages in dependency list', async () => { - const graph: ProjectGraph = { - nodes: { - example: { - type: ProjectType.lib, - name: 'example', - data: { - files: [], - root: '/root/example', - }, - }, - 'npm:formik': { - type: 'npm', - name: 'npm:formik', - data: { - files: [], - packageName: 'formik', - version: '0.0.0', - }, - }, - }, - dependencies: { - example: [ - { - source: 'example', - target: 'npm:formik', - type: DependencyType.static, - }, - ], - }, - }; - const context = await getMockContext(); - context.target.project = 'example'; - - const results = await calculateProjectDependencies(graph, context); - expect(results).toMatchObject({ - target: { - type: ProjectType.lib, - name: 'example', - }, - dependencies: [{ name: 'formik' }], - }); - }); -}); diff --git a/packages/workspace/src/utils/buildable-libs-utils.ts b/packages/workspace/src/utils/buildable-libs-utils.ts deleted file mode 100644 index 778d7b9ad035f..0000000000000 --- a/packages/workspace/src/utils/buildable-libs-utils.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { ProjectType } from '../core/project-graph'; -import type { ProjectGraph, ProjectGraphNode } from '@nrwl/devkit'; -import { BuilderContext } from '@angular-devkit/architect'; -import { join, resolve, dirname, relative } from 'path'; -import { - fileExists, - readJsonFile, - writeJsonFile, -} from '../utilities/fileutils'; -import { stripIndents } from '@angular-devkit/core/src/utils/literals'; -import { getOutputsForTargetAndConfiguration } from '../tasks-runner/utils'; -import * as ts from 'typescript'; -import { unlinkSync } from 'fs'; - -function isBuildable(target: string, node: ProjectGraphNode): boolean { - return ( - node.data.targets && - node.data.targets[target] && - node.data.targets[target].executor !== '' - ); -} - -export type DependentBuildableProjectNode = { - name: string; - outputs: string[]; - node: ProjectGraphNode; -}; - -export function calculateProjectDependencies( - projGraph: ProjectGraph, - context: BuilderContext -): { target: ProjectGraphNode; dependencies: DependentBuildableProjectNode[] } { - const target = projGraph.nodes[context.target.project]; - // gather the library dependencies - const dependencies = recursivelyCollectDependencies( - context.target.project, - projGraph, - [] - ) - .map((dep) => { - const depNode = projGraph.nodes[dep]; - if ( - depNode.type === ProjectType.lib && - isBuildable(context.target.target, depNode) - ) { - const libPackageJson = readJsonFile( - join(context.workspaceRoot, depNode.data.root, 'package.json') - ); - - return { - name: libPackageJson.name, // i.e. @workspace/mylib - outputs: getOutputsForTargetAndConfiguration( - { - overrides: {}, - target: context.target, - }, - depNode - ), - node: depNode, - }; - } else if (depNode.type === 'npm') { - return { - name: depNode.data.packageName, - outputs: [], - node: depNode, - }; - } else { - return null; - } - }) - .filter((x) => !!x); - return { target, dependencies }; -} - -function recursivelyCollectDependencies( - project: string, - projGraph: ProjectGraph, - acc: string[] -) { - (projGraph.dependencies[project] || []).forEach((dependency) => { - if (acc.indexOf(dependency.target) === -1) { - acc.push(dependency.target); - recursivelyCollectDependencies(dependency.target, projGraph, acc); - } - }); - return acc; -} - -function readTsConfigWithRemappedPaths( - tsConfig: string, - generatedTsConfigPath: string, - dependencies: DependentBuildableProjectNode[] -) { - const generatedTsConfig: any = { compilerOptions: {} }; - generatedTsConfig.extends = relative( - dirname(generatedTsConfigPath), - tsConfig - ); - generatedTsConfig.compilerOptions.paths = computeCompilerOptionsPaths( - tsConfig, - dependencies - ); - return generatedTsConfig; -} - -export function computeCompilerOptionsPaths(tsConfig, dependencies) { - const paths = readPaths(tsConfig) || {}; - updatePaths(dependencies, paths); - return paths; -} - -function readPaths(tsConfig: string) { - try { - const parsedTSConfig = ts.readConfigFile(tsConfig, ts.sys.readFile).config; - if ( - parsedTSConfig.compilerOptions && - parsedTSConfig.compilerOptions.paths - ) { - return parsedTSConfig.compilerOptions.paths; - } else if (parsedTSConfig.extends) { - return readPaths(resolve(dirname(tsConfig), parsedTSConfig.extends)); - } else { - return null; - } - } catch (e) { - return null; - } -} - -export function createTmpTsConfig( - tsconfigPath: string, - workspaceRoot: string, - projectRoot: string, - dependencies: DependentBuildableProjectNode[] -) { - const tmpTsConfigPath = join( - workspaceRoot, - 'tmp', - projectRoot, - 'tsconfig.generated.json' - ); - const parsedTSConfig = readTsConfigWithRemappedPaths( - tsconfigPath, - tmpTsConfigPath, - dependencies - ); - process.on('exit', () => { - cleanupTmpTsConfigFile(tmpTsConfigPath); - }); - writeJsonFile(tmpTsConfigPath, parsedTSConfig); - return join(tmpTsConfigPath); -} - -function cleanupTmpTsConfigFile(tmpTsConfigPath) { - try { - if (tmpTsConfigPath) { - unlinkSync(tmpTsConfigPath); - } - } catch (e) {} -} - -export function checkDependentProjectsHaveBeenBuilt( - context: BuilderContext, - projectDependencies: DependentBuildableProjectNode[] -): boolean { - const depLibsToBuildFirst: DependentBuildableProjectNode[] = []; - - // verify whether all dependent libraries have been built - projectDependencies.forEach((dep) => { - if (dep.node.type !== ProjectType.lib) { - return; - } - - const paths = dep.outputs.map((p) => - join(context.workspaceRoot, p, 'package.json') - ); - - if (!paths.some(fileExists)) { - depLibsToBuildFirst.push(dep); - } - }); - - if (depLibsToBuildFirst.length > 0) { - context.logger.error(stripIndents` - Some of the project ${ - context.target.project - }'s dependencies have not been built yet. Please build these libraries before: - ${depLibsToBuildFirst.map((x) => ` - ${x.node.name}`).join('\n')} - - Try: nx run ${context.target.project}:${context.target.target} --with-deps - `); - - return false; - } else { - return true; - } -} - -export function updatePaths( - dependencies: DependentBuildableProjectNode[], - paths: Record -) { - const pathsKeys = Object.keys(paths); - dependencies.forEach((dep) => { - if (dep.outputs && dep.outputs.length > 0) { - paths[dep.name] = dep.outputs; - // check for secondary entrypoints, only available for ng-packagr projects - for (const path of pathsKeys) { - if (path.startsWith(`${dep.name}/`)) { - const [, nestedPart] = path.split(`${dep.name}/`); - paths[path] = dep.outputs.map((o) => `${o}/${nestedPart}`); - } - } - } - }); -} - -/** - * Updates the peerDependencies section in the `dist/lib/xyz/package.json` with - * the proper dependency and version - */ -export function updateBuildableProjectPackageJsonDependencies( - context: BuilderContext, - node: ProjectGraphNode, - dependencies: DependentBuildableProjectNode[], - typeOfDependency: 'dependencies' | 'peerDependencies' = 'dependencies' -) { - const outputs = getOutputsForTargetAndConfiguration( - { - overrides: {}, - target: context.target, - }, - node - ); - - const packageJsonPath = `${outputs[0]}/package.json`; - let packageJson; - let workspacePackageJson; - try { - packageJson = readJsonFile(packageJsonPath); - workspacePackageJson = readJsonFile( - `${context.workspaceRoot}/package.json` - ); - } catch (e) { - // cannot find or invalid package.json - return; - } - - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.peerDependencies = packageJson.peerDependencies || {}; - - let updatePackageJson = false; - dependencies.forEach((entry) => { - const packageName = - entry.node.type === 'npm' ? entry.node.data.packageName : entry.name; - - if ( - !hasDependency(packageJson, 'dependencies', packageName) && - !hasDependency(packageJson, 'devDependencies', packageName) && - !hasDependency(packageJson, 'peerDependencies', packageName) - ) { - try { - let depVersion; - if (entry.node.type === ProjectType.lib) { - const outputs = getOutputsForTargetAndConfiguration( - { - overrides: {}, - target: context.target, - }, - entry.node - ); - - const depPackageJsonPath = join( - context.workspaceRoot, - outputs[0], - 'package.json' - ); - depVersion = readJsonFile(depPackageJsonPath).version; - - packageJson[typeOfDependency][packageName] = depVersion; - } else if (entry.node.type === 'npm') { - // If an npm dep is part of the workspace devDependencies, do not include it the library - if ( - !!workspacePackageJson.devDependencies?.[ - entry.node.data.packageName - ] - ) { - return; - } - - depVersion = entry.node.data.version; - - packageJson[typeOfDependency][entry.node.data.packageName] = - depVersion; - } - updatePackageJson = true; - } catch (e) { - // skip if cannot find package.json - } - } - }); - - if (updatePackageJson) { - writeJsonFile(packageJsonPath, packageJson); - } -} - -// verify whether the package.json already specifies the dep -function hasDependency(outputJson, depConfigName: string, packageName: string) { - if (outputJson[depConfigName]) { - return outputJson[depConfigName][packageName]; - } else { - return false; - } -} From 0761cf95bdff5e5e4a32c2ee1042b5234da73f93 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 28 Jul 2021 09:00:26 +0200 Subject: [PATCH 2/2] chore(core): mark utils/buildable-libs-utils as deprecated --- .../src/utils/buildable-libs-utils.spec.ts | 88 +++++ .../src/utils/buildable-libs-utils.ts | 337 ++++++++++++++++++ 2 files changed, 425 insertions(+) create mode 100644 packages/workspace/src/utils/buildable-libs-utils.spec.ts create mode 100644 packages/workspace/src/utils/buildable-libs-utils.ts diff --git a/packages/workspace/src/utils/buildable-libs-utils.spec.ts b/packages/workspace/src/utils/buildable-libs-utils.spec.ts new file mode 100644 index 0000000000000..bdfbf8b522ce5 --- /dev/null +++ b/packages/workspace/src/utils/buildable-libs-utils.spec.ts @@ -0,0 +1,88 @@ +import { + calculateProjectDependencies, + DependentBuildableProjectNode, + updatePaths, +} from './buildable-libs-utils'; +import { + DependencyType, + ProjectGraph, + ProjectType, +} from '../core/project-graph'; +import { getMockContext } from './testing'; + +// TODO(v13): remove this deprecated file + +describe('updatePaths', () => { + const deps: DependentBuildableProjectNode[] = [ + { name: '@proj/lib', node: {} as any, outputs: ['dist/libs/lib'] }, + ]; + + it('should add path', () => { + const paths: Record = { + '@proj/test': ['libs/test/src/index.ts'], + }; + updatePaths(deps, paths); + expect(paths).toEqual({ + '@proj/lib': ['dist/libs/lib'], + '@proj/test': ['libs/test/src/index.ts'], + }); + }); + + it('should replace paths', () => { + const paths: Record = { + '@proj/lib': ['libs/lib/src/index.ts'], + '@proj/lib/sub': ['libs/lib/sub/src/index.ts'], + }; + updatePaths(deps, paths); + expect(paths).toEqual({ + '@proj/lib': ['dist/libs/lib'], + '@proj/lib/sub': ['dist/libs/lib/sub'], + }); + }); +}); + +describe('calculateProjectDependencies', () => { + it('should include npm packages in dependency list', async () => { + const graph: ProjectGraph = { + nodes: { + example: { + type: ProjectType.lib, + name: 'example', + data: { + files: [], + root: '/root/example', + }, + }, + 'npm:formik': { + type: 'npm', + name: 'npm:formik', + data: { + files: [], + packageName: 'formik', + version: '0.0.0', + }, + }, + }, + dependencies: { + example: [ + { + source: 'example', + target: 'npm:formik', + type: DependencyType.static, + }, + ], + }, + }; + const context = await getMockContext(); + context.target.project = 'example'; + + const results = await calculateProjectDependencies(graph, context); + expect(results).toMatchObject({ + target: { + type: ProjectType.lib, + name: 'example', + }, + dependencies: [{ name: 'formik' }], + }); + }); +}); diff --git a/packages/workspace/src/utils/buildable-libs-utils.ts b/packages/workspace/src/utils/buildable-libs-utils.ts new file mode 100644 index 0000000000000..a119fdc54ad7d --- /dev/null +++ b/packages/workspace/src/utils/buildable-libs-utils.ts @@ -0,0 +1,337 @@ +import { ProjectType } from '../core/project-graph'; +import type { ProjectGraph, ProjectGraphNode } from '@nrwl/devkit'; +import { BuilderContext } from '@angular-devkit/architect'; +import { join, resolve, dirname, relative } from 'path'; +import { + fileExists, + readJsonFile, + writeJsonFile, +} from '../utilities/fileutils'; +import { stripIndents } from '@angular-devkit/core/src/utils/literals'; +import { getOutputsForTargetAndConfiguration } from '../tasks-runner/utils'; +import * as ts from 'typescript'; +import { unlinkSync } from 'fs'; + +// TODO(v13): remove this deprecated file + +function isBuildable(target: string, node: ProjectGraphNode): boolean { + return ( + node.data.targets && + node.data.targets[target] && + node.data.targets[target].executor !== '' + ); +} + +/** + * @deprecated will be removed in v13. Use `{@link DependentBuildableProjectNode}` from `utilities/buildable-libs-utils` + */ +export type DependentBuildableProjectNode = { + name: string; + outputs: string[]; + node: ProjectGraphNode; +}; + +/** + * @deprecated will be removed in v13. Use `{@link calculateProjectDependencies}` from `utilities/buildable-libs-utils` + */ +export function calculateProjectDependencies( + projGraph: ProjectGraph, + context: BuilderContext +): { target: ProjectGraphNode; dependencies: DependentBuildableProjectNode[] } { + const target = projGraph.nodes[context.target.project]; + // gather the library dependencies + const dependencies = recursivelyCollectDependencies( + context.target.project, + projGraph, + [] + ) + .map((dep) => { + const depNode = projGraph.nodes[dep]; + if ( + depNode.type === ProjectType.lib && + isBuildable(context.target.target, depNode) + ) { + const libPackageJson = readJsonFile( + join(context.workspaceRoot, depNode.data.root, 'package.json') + ); + + return { + name: libPackageJson.name, // i.e. @workspace/mylib + outputs: getOutputsForTargetAndConfiguration( + { + overrides: {}, + target: context.target, + }, + depNode + ), + node: depNode, + }; + } else if (depNode.type === 'npm') { + return { + name: depNode.data.packageName, + outputs: [], + node: depNode, + }; + } else { + return null; + } + }) + .filter((x) => !!x); + return { target, dependencies }; +} + +function recursivelyCollectDependencies( + project: string, + projGraph: ProjectGraph, + acc: string[] +) { + (projGraph.dependencies[project] || []).forEach((dependency) => { + if (acc.indexOf(dependency.target) === -1) { + acc.push(dependency.target); + recursivelyCollectDependencies(dependency.target, projGraph, acc); + } + }); + return acc; +} + +function readTsConfigWithRemappedPaths( + tsConfig: string, + generatedTsConfigPath: string, + dependencies: DependentBuildableProjectNode[] +) { + const generatedTsConfig: any = { compilerOptions: {} }; + generatedTsConfig.extends = relative( + dirname(generatedTsConfigPath), + tsConfig + ); + generatedTsConfig.compilerOptions.paths = computeCompilerOptionsPaths( + tsConfig, + dependencies + ); + return generatedTsConfig; +} + +/** + * @deprecated will be removed in v13. Use `{@link computeCompilerOptionsPaths}` from `utilities/buildable-libs-utils` + */ +export function computeCompilerOptionsPaths(tsConfig, dependencies) { + const paths = readPaths(tsConfig) || {}; + updatePaths(dependencies, paths); + return paths; +} + +function readPaths(tsConfig: string) { + try { + const parsedTSConfig = ts.readConfigFile(tsConfig, ts.sys.readFile).config; + if ( + parsedTSConfig.compilerOptions && + parsedTSConfig.compilerOptions.paths + ) { + return parsedTSConfig.compilerOptions.paths; + } else if (parsedTSConfig.extends) { + return readPaths(resolve(dirname(tsConfig), parsedTSConfig.extends)); + } else { + return null; + } + } catch (e) { + return null; + } +} + +/** + * @deprecated will be removed in v13. Use `{@link createTmpTsConfig}` from `utilities/buildable-libs-utils` + */ +export function createTmpTsConfig( + tsconfigPath: string, + workspaceRoot: string, + projectRoot: string, + dependencies: DependentBuildableProjectNode[] +) { + const tmpTsConfigPath = join( + workspaceRoot, + 'tmp', + projectRoot, + 'tsconfig.generated.json' + ); + const parsedTSConfig = readTsConfigWithRemappedPaths( + tsconfigPath, + tmpTsConfigPath, + dependencies + ); + process.on('exit', () => { + cleanupTmpTsConfigFile(tmpTsConfigPath); + }); + writeJsonFile(tmpTsConfigPath, parsedTSConfig); + return join(tmpTsConfigPath); +} + +function cleanupTmpTsConfigFile(tmpTsConfigPath) { + try { + if (tmpTsConfigPath) { + unlinkSync(tmpTsConfigPath); + } + } catch (e) {} +} + +/** + * @deprecated will be removed in v13. Use `{@link checkDependentProjectsHaveBeenBuilt}` from `utilities/buildable-libs-utils` + */ +export function checkDependentProjectsHaveBeenBuilt( + context: BuilderContext, + projectDependencies: DependentBuildableProjectNode[] +): boolean { + const depLibsToBuildFirst: DependentBuildableProjectNode[] = []; + + // verify whether all dependent libraries have been built + projectDependencies.forEach((dep) => { + if (dep.node.type !== ProjectType.lib) { + return; + } + + const paths = dep.outputs.map((p) => + join(context.workspaceRoot, p, 'package.json') + ); + + if (!paths.some(fileExists)) { + depLibsToBuildFirst.push(dep); + } + }); + + if (depLibsToBuildFirst.length > 0) { + context.logger.error(stripIndents` + Some of the project ${ + context.target.project + }'s dependencies have not been built yet. Please build these libraries before: + ${depLibsToBuildFirst.map((x) => ` - ${x.node.name}`).join('\n')} + + Try: nx run ${context.target.project}:${context.target.target} --with-deps + `); + + return false; + } else { + return true; + } +} + +/** + * @deprecated will be removed in v13. Use `{@link updatePaths}` from `utilities/buildable-libs-utils` + */ +export function updatePaths( + dependencies: DependentBuildableProjectNode[], + paths: Record +) { + const pathsKeys = Object.keys(paths); + dependencies.forEach((dep) => { + if (dep.outputs && dep.outputs.length > 0) { + paths[dep.name] = dep.outputs; + // check for secondary entrypoints, only available for ng-packagr projects + for (const path of pathsKeys) { + if (path.startsWith(`${dep.name}/`)) { + const [, nestedPart] = path.split(`${dep.name}/`); + paths[path] = dep.outputs.map((o) => `${o}/${nestedPart}`); + } + } + } + }); +} + +/** + * Updates the peerDependencies section in the `dist/lib/xyz/package.json` with + * the proper dependency and version + * + * @deprecated will be removed in v13. Use `{@link updateBuildableProjectPackageJsonDependencies}` from `utilities/buildable-libs-utils` + */ +export function updateBuildableProjectPackageJsonDependencies( + context: BuilderContext, + node: ProjectGraphNode, + dependencies: DependentBuildableProjectNode[], + typeOfDependency: 'dependencies' | 'peerDependencies' = 'dependencies' +) { + const outputs = getOutputsForTargetAndConfiguration( + { + overrides: {}, + target: context.target, + }, + node + ); + + const packageJsonPath = `${outputs[0]}/package.json`; + let packageJson; + let workspacePackageJson; + try { + packageJson = readJsonFile(packageJsonPath); + workspacePackageJson = readJsonFile( + `${context.workspaceRoot}/package.json` + ); + } catch (e) { + // cannot find or invalid package.json + return; + } + + packageJson.dependencies = packageJson.dependencies || {}; + packageJson.peerDependencies = packageJson.peerDependencies || {}; + + let updatePackageJson = false; + dependencies.forEach((entry) => { + const packageName = + entry.node.type === 'npm' ? entry.node.data.packageName : entry.name; + + if ( + !hasDependency(packageJson, 'dependencies', packageName) && + !hasDependency(packageJson, 'devDependencies', packageName) && + !hasDependency(packageJson, 'peerDependencies', packageName) + ) { + try { + let depVersion; + if (entry.node.type === ProjectType.lib) { + const outputs = getOutputsForTargetAndConfiguration( + { + overrides: {}, + target: context.target, + }, + entry.node + ); + + const depPackageJsonPath = join( + context.workspaceRoot, + outputs[0], + 'package.json' + ); + depVersion = readJsonFile(depPackageJsonPath).version; + + packageJson[typeOfDependency][packageName] = depVersion; + } else if (entry.node.type === 'npm') { + // If an npm dep is part of the workspace devDependencies, do not include it the library + if ( + !!workspacePackageJson.devDependencies?.[ + entry.node.data.packageName + ] + ) { + return; + } + + depVersion = entry.node.data.version; + + packageJson[typeOfDependency][entry.node.data.packageName] = + depVersion; + } + updatePackageJson = true; + } catch (e) { + // skip if cannot find package.json + } + } + }); + + if (updatePackageJson) { + writeJsonFile(packageJsonPath, packageJson); + } +} + +// verify whether the package.json already specifies the dep +function hasDependency(outputJson, depConfigName: string, packageName: string) { + if (outputJson[depConfigName]) { + return outputJson[depConfigName][packageName]; + } else { + return false; + } +}