From 36010c1c0e310b6724b883eb172ce09c728dcd04 Mon Sep 17 00:00:00 2001 From: Chau Tran Date: Fri, 20 May 2022 14:14:30 -0500 Subject: [PATCH] =?UTF-8?q?feat(js):=20implement=20a=20smarter=20compiler?= =?UTF-8?q?=20(tsc,=20swc)=20helper=20dependency=20d=E2=80=A6=20(#10297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(js): implement a smarter compiler (tsc, swc) helper dependency detection ISSUES CLOSED: #10270 * chore(js): add test to check for swc/helpers upon build lib --- e2e/js/src/js.test.ts | 27 ++- packages/js/src/executors/swc/swc.impl.ts | 21 ++- packages/js/src/executors/tsc/tsc.impl.ts | 15 +- .../src/utils/compiler-helper-dependency.ts | 171 ++++++++++++++++++ packages/js/src/utils/swc/get-swcrc-path.ts | 12 ++ packages/js/src/utils/tslib-dependency.ts | 50 ----- .../src/executors/webpack/webpack.impl.ts | 23 ++- 7 files changed, 254 insertions(+), 65 deletions(-) create mode 100644 packages/js/src/utils/compiler-helper-dependency.ts create mode 100644 packages/js/src/utils/swc/get-swcrc-path.ts delete mode 100644 packages/js/src/utils/tslib-dependency.ts diff --git a/e2e/js/src/js.test.ts b/e2e/js/src/js.test.ts index 60756ac3d7c2f..d7605f140d839 100644 --- a/e2e/js/src/js.test.ts +++ b/e2e/js/src/js.test.ts @@ -1,7 +1,7 @@ import { + checkFilesDoNotExist, checkFilesExist, expectJestTestsToPass, - checkFilesDoNotExist, newProject, readFile, readJson, @@ -233,6 +233,31 @@ describe('js e2e', () => { const output = runCLI(`build ${parentLib}`); expect(output).toContain('1 task(s) it depends on'); expect(output).toContain('Successfully compiled: 2 files with swc'); + + updateJson(`libs/${lib}/.lib.swcrc`, (json) => { + json.jsc.externalHelpers = true; + return json; + }); + + runCLI(`build ${lib}`); + + const rootPackageJson = readJson(`package.json`); + + expect(readJson(`dist/libs/${lib}/package.json`)).toHaveProperty( + 'peerDependencies.@swc/helpers', + rootPackageJson.dependencies['@swc/helpers'] + ); + + updateJson(`libs/${lib}/.lib.swcrc`, (json) => { + json.jsc.externalHelpers = false; + return json; + }); + + runCLI(`build ${lib}`); + + expect(readJson(`dist/libs/${lib}/package.json`)).not.toHaveProperty( + 'peerDependencies.@swc/helpers' + ); }, 120000); it('should not create a `.babelrc` file when creating libs with js executors (--compiler=tsc)', () => { diff --git a/packages/js/src/executors/swc/swc.impl.ts b/packages/js/src/executors/swc/swc.impl.ts index 8000b027068d6..5377d1dd5b24d 100644 --- a/packages/js/src/executors/swc/swc.impl.ts +++ b/packages/js/src/executors/swc/swc.impl.ts @@ -7,16 +7,21 @@ import { DependentBuildableProjectNode } from '@nrwl/workspace/src/utilities/bui import { join, relative, resolve } from 'path'; import { checkDependencies } from '../../utils/check-dependencies'; +import { + getHelperDependency, + HelperDependency, +} from '../../utils/compiler-helper-dependency'; import { CopyAssetsHandler } from '../../utils/copy-assets-handler'; import { NormalizedSwcExecutorOptions, SwcExecutorOptions, } from '../../utils/schema'; import { compileSwc, compileSwcWatch } from '../../utils/swc/compile-swc'; +import { getSwcrcPath } from '../../utils/swc/get-swcrc-path'; import { updatePackageJson } from '../../utils/update-package-json'; import { watchForSingleFileChanges } from '../../utils/watch-for-single-file-changes'; -function normalizeOptions( +export function normalizeOptions( options: SwcExecutorOptions, contextRoot: string, sourceRoot?: string, @@ -45,9 +50,7 @@ function normalizeOptions( // default to current directory if projectRootParts is []. // Eg: when a project is at the root level, outside of layout dir const swcCwd = projectRootParts.join('/') || '.'; - const swcrcPath = options.swcrc - ? join(contextRoot, options.swcrc) - : join(contextRoot, projectRoot, '.lib.swcrc'); + const swcrcPath = getSwcrcPath(options, contextRoot, projectRoot); const swcCliOptions = { srcPath: projectDir, @@ -106,6 +109,16 @@ export async function* swcExecutor( options.tsConfig = tmpTsConfig; } + const swcHelperDependency = getHelperDependency( + HelperDependency.swc, + options.swcCliOptions.swcrcPath, + dependencies + ); + + if (swcHelperDependency) { + dependencies.push(swcHelperDependency); + } + const assetHandler = new CopyAssetsHandler({ projectDir: projectRoot, rootDir: context.root, diff --git a/packages/js/src/executors/tsc/tsc.impl.ts b/packages/js/src/executors/tsc/tsc.impl.ts index d61994472c8b6..13bc8a87a43f4 100644 --- a/packages/js/src/executors/tsc/tsc.impl.ts +++ b/packages/js/src/executors/tsc/tsc.impl.ts @@ -5,9 +5,12 @@ import { } from '@nrwl/workspace/src/utilities/assets'; import { join, resolve } from 'path'; import { checkDependencies } from '../../utils/check-dependencies'; +import { + getHelperDependency, + HelperDependency, +} from '../../utils/compiler-helper-dependency'; import { CopyAssetsHandler } from '../../utils/copy-assets-handler'; import { ExecutorOptions, NormalizedExecutorOptions } from '../../utils/schema'; -import { addTslibDependencyIfNeeded } from '../../utils/tslib-dependency'; import { compileTypeScriptFiles } from '../../utils/typescript/compile-typescript-files'; import { updatePackageJson } from '../../utils/update-package-json'; import { watchForSingleFileChanges } from '../../utils/watch-for-single-file-changes'; @@ -61,7 +64,15 @@ export async function* tscExecutor( options.tsConfig = tmpTsConfig; } - addTslibDependencyIfNeeded(options, context, dependencies); + const tsLibDependency = getHelperDependency( + HelperDependency.tsc, + options.tsConfig, + dependencies + ); + + if (tsLibDependency) { + dependencies.push(tsLibDependency); + } const assetHandler = new CopyAssetsHandler({ projectDir: projectRoot, diff --git a/packages/js/src/utils/compiler-helper-dependency.ts b/packages/js/src/utils/compiler-helper-dependency.ts new file mode 100644 index 0000000000000..2b1dfdf606ce7 --- /dev/null +++ b/packages/js/src/utils/compiler-helper-dependency.ts @@ -0,0 +1,171 @@ +import { + logger, + ProjectGraphDependency, + readCachedProjectGraph, + readJsonFile, +} from '@nrwl/devkit'; +import { DependentBuildableProjectNode } from '@nrwl/workspace/src/utilities/buildable-libs-utils'; +import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript'; +import { join } from 'path'; +import { ExecutorOptions, SwcExecutorOptions } from './schema'; +import { getSwcrcPath } from './swc/get-swcrc-path'; + +export enum HelperDependency { + tsc = 'npm:tslib', + swc = 'npm:@swc/helpers', +} + +const jsExecutors = { + '@nrwl/js:tsc': { + helperDependency: HelperDependency.tsc, + getConfigPath: (options: ExecutorOptions, contextRoot: string, _: string) => + join(contextRoot, options.tsConfig), + } as const, + '@nrwl/js:swc': { + helperDependency: HelperDependency.swc, + getConfigPath: ( + options: SwcExecutorOptions, + contextRoot: string, + projectRoot: string + ) => getSwcrcPath(options, contextRoot, projectRoot), + } as const, +} as const; + +/** + * Check and return a DependencyNode for the compiler's external helpers npm package. Return "null" + * if it doesn't need it or it cannot be found in the Project Graph + * + * @param {HelperDependency} helperDependency + * @param {string} configPath + * @param {DependentBuildableProjectNode[]} dependencies + * @param {boolean=false} returnDependencyIfFound + */ +export function getHelperDependency( + helperDependency: HelperDependency, + configPath: string, + dependencies: DependentBuildableProjectNode[], + returnDependencyIfFound = false +): DependentBuildableProjectNode | null { + const dependency = dependencies.find((dep) => dep.name === helperDependency); + + if (!!dependency) { + // if a helperDependency is found, we either return null or the found dependency + // We return the found return dependency for the cases where it is a part of a + // project's dependency's dependency instead + // eg: app-a -> lib-a (helperDependency is on lib-a instead of app-a) + // When building app-a, we'd want to know about the found helper dependency still + return returnDependencyIfFound ? dependency : null; + } + + let isHelperNeeded = false; + + switch (helperDependency) { + case HelperDependency.tsc: + isHelperNeeded = !!readTsConfig(configPath).options['importHelpers']; + break; + case HelperDependency.swc: + isHelperNeeded = !!readJsonFile(configPath)['jsc']['externalHelpers']; + break; + } + + if (!isHelperNeeded) return null; + + const projectGraph = readCachedProjectGraph(); + const libNode = projectGraph.externalNodes[helperDependency]; + + if (!libNode) { + logger.warn( + `Your library compilation option specifies that the compiler external helper (${ + helperDependency.split(':')[1] + }) is needed but it is not installed.` + ); + return null; + } + + return { + name: helperDependency, + outputs: [], + node: libNode, + }; +} + +export function getHelperDependenciesFromProjectGraph( + contextRoot: string, + sourceProject: string +): ProjectGraphDependency[] { + const projectGraph = readCachedProjectGraph(); + + // if the source project isn't part of the projectGraph nodes; skip + if (!projectGraph.nodes[sourceProject]) return []; + + // if the source project does not have any dependencies; skip + if ( + !projectGraph.dependencies[sourceProject] || + !projectGraph.dependencies[sourceProject].length + ) + return; + + const sourceDependencies = projectGraph.dependencies[sourceProject]; + const internalDependencies = sourceDependencies.reduce( + (result, dependency) => { + // we check if a dependency is part of the workspace and if it's a library + // because we wouldn't want to include external dependencies (npm packages) + if ( + !dependency.target.startsWith('npm:') && + !!projectGraph.nodes[dependency.target] && + projectGraph.nodes[dependency.target].type === 'lib' + ) { + const targetData = projectGraph.nodes[dependency.target].data; + + // check if the dependency has a buildable target with one of the jsExecutors + const targetExecutor = Object.values(targetData.targets).find( + ({ executor }) => !!jsExecutors[executor] + ); + if (targetExecutor) { + const jsExecutor = jsExecutors[targetExecutor['executor']]; + + const { root: projectRoot } = targetData; + const configPath = jsExecutor.getConfigPath( + targetExecutor['options'], + contextRoot, + projectRoot + ); + + // construct the correct helperDependency configurations + // so we can compute the ProjectGraphDependency later + result.push({ + helperDependency: + jsExecutors[targetExecutor['executor']].helperDependency, + dependencies: projectGraph.dependencies[dependency.target], + configPath, + }); + } + } + + return result; + }, + [] + ); + + return internalDependencies.reduce( + (result, { helperDependency, configPath, dependencies }) => { + const dependency = getHelperDependency( + helperDependency, + configPath, + dependencies, + true + ); + + if (dependency) { + result.push({ + type: 'static', + source: sourceProject, + target: dependency.name, + } as ProjectGraphDependency); + } + + return result; + }, + [] as ProjectGraphDependency[] + ); +} diff --git a/packages/js/src/utils/swc/get-swcrc-path.ts b/packages/js/src/utils/swc/get-swcrc-path.ts new file mode 100644 index 0000000000000..781c83faa70b8 --- /dev/null +++ b/packages/js/src/utils/swc/get-swcrc-path.ts @@ -0,0 +1,12 @@ +import { join } from 'path'; +import { SwcExecutorOptions } from '../schema'; + +export function getSwcrcPath( + options: SwcExecutorOptions, + contextRoot: string, + projectRoot: string +) { + return options.swcrc + ? join(contextRoot, options.swcrc) + : join(contextRoot, projectRoot, '.lib.swcrc'); +} diff --git a/packages/js/src/utils/tslib-dependency.ts b/packages/js/src/utils/tslib-dependency.ts deleted file mode 100644 index 0716bf72eed6c..0000000000000 --- a/packages/js/src/utils/tslib-dependency.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - ExecutorContext, - getPackageManagerCommand, - readCachedProjectGraph, -} from '@nrwl/devkit'; -import { DependentBuildableProjectNode } from '@nrwl/workspace/src/utilities/buildable-libs-utils'; -import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript'; -import { NormalizedExecutorOptions } from './schema'; - -const tslibNodeName = 'npm:tslib'; - -function shouldAddTslibDependency( - tsConfig: string, - dependencies: DependentBuildableProjectNode[] -): boolean { - if (dependencies.some((dep) => dep.name === tslibNodeName)) { - return false; - } - - const config = readTsConfig(tsConfig); - return !!config.options.importHelpers; -} - -export function addTslibDependencyIfNeeded( - options: NormalizedExecutorOptions, - context: ExecutorContext, - dependencies: DependentBuildableProjectNode[] -): void { - if (!shouldAddTslibDependency(options.tsConfig, dependencies)) { - return; - } - - const depGraph = readCachedProjectGraph(); - const tslibNode = depGraph.externalNodes[tslibNodeName]; - - if (!tslibNode) { - const pmc = getPackageManagerCommand(); - throw new Error( - `"importHelpers" is enabled for ${context.targetName} but tslib is not installed. Use "${pmc.add} tslib" to install it.` - ); - } - - const tslibDependency: DependentBuildableProjectNode = { - name: tslibNodeName, - outputs: [], - node: tslibNode, - }; - - dependencies.push(tslibDependency); -} diff --git a/packages/node/src/executors/webpack/webpack.impl.ts b/packages/node/src/executors/webpack/webpack.impl.ts index de91f2204b9d7..8457bdbaae3c0 100644 --- a/packages/node/src/executors/webpack/webpack.impl.ts +++ b/packages/node/src/executors/webpack/webpack.impl.ts @@ -1,24 +1,23 @@ -import 'dotenv/config'; -import { ExecutorContext } from '@nrwl/devkit'; - -import { readCachedProjectGraph } from '@nrwl/devkit'; +import { ExecutorContext, readCachedProjectGraph } from '@nrwl/devkit'; +import { getHelperDependenciesFromProjectGraph } from '@nrwl/js/src/utils/compiler-helper-dependency'; import { calculateProjectDependencies, checkDependentProjectsHaveBeenBuilt, createTmpTsConfig, } from '@nrwl/workspace/src/utilities/buildable-libs-utils'; import { getRootTsConfigPath } from '@nrwl/workspace/src/utilities/typescript'; +import 'dotenv/config'; +import { resolve } from 'path'; +import { eachValueFrom } from 'rxjs-for-await'; import { map, tap } from 'rxjs/operators'; -import { eachValueFrom } from 'rxjs-for-await'; -import { resolve } from 'path'; import { register } from 'ts-node'; +import { generatePackageJson } from '../../utils/generate-package-json'; import { getNodeWebpackConfig } from '../../utils/node.config'; -import { BuildNodeBuilderOptions } from '../../utils/types'; import { normalizeBuildOptions } from '../../utils/normalize'; -import { generatePackageJson } from '../../utils/generate-package-json'; import { runWebpack } from '../../utils/run-webpack'; +import { BuildNodeBuilderOptions } from '../../utils/types'; export type NodeBuildEvent = { outfile: string; @@ -79,6 +78,14 @@ export async function* webpackExecutor( } if (options.generatePackageJson) { + const helperDependencies = getHelperDependenciesFromProjectGraph( + context.root, + context.projectName + ); + if (helperDependencies.length > 0) { + projGraph.dependencies[context.projectName] = + projGraph.dependencies[context.projectName].concat(helperDependencies); + } generatePackageJson(context.projectName, projGraph, options); } const config = await options.webpackConfig.reduce(