Skip to content

Commit

Permalink
feat(js): implement a smarter compiler (tsc, swc) helper dependency d… (
Browse files Browse the repository at this point in the history
#10297)

* 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
  • Loading branch information
nartc committed May 20, 2022
1 parent 8fddedf commit 36010c1
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 65 deletions.
27 changes: 26 additions & 1 deletion e2e/js/src/js.test.ts
@@ -1,7 +1,7 @@
import {
checkFilesDoNotExist,
checkFilesExist,
expectJestTestsToPass,
checkFilesDoNotExist,
newProject,
readFile,
readJson,
Expand Down Expand Up @@ -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)', () => {
Expand Down
21 changes: 17 additions & 4 deletions packages/js/src/executors/swc/swc.impl.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 13 additions & 2 deletions packages/js/src/executors/tsc/tsc.impl.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
171 changes: 171 additions & 0 deletions 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[]
);
}
12 changes: 12 additions & 0 deletions 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');
}
50 changes: 0 additions & 50 deletions packages/js/src/utils/tslib-dependency.ts

This file was deleted.

0 comments on commit 36010c1

Please sign in to comment.