feat(js): add nrwl/js:node executor to serve node apps

This commit is contained in:
Chau Tran 2021-12-16 10:34:18 -06:00 committed by Victor Savkin
parent be908e2be6
commit 1139c616e1
45 changed files with 1386 additions and 644 deletions

View File

@ -0,0 +1,60 @@
---
title: '@nrwl/js:node executor'
description: 'Build Node.js applications'
---
# @nrwl/js:node
Build Node.js applications
Options can be configured in `angular.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets.
## Options
### buildTarget (_**required**_)
Type: `string`
The target to run to build you the app
### args
Type: `array`
Extra args when starting the app
### host
Default: `localhost`
Type: `string`
The host to inspect the process on
### inspect
Default: `inspect`
Type: `string | boolean `
Ensures the app is starting with debugging
### port
Default: `9229`
Type: `number`
The port to inspect the process on. Setting port to 0 will assign random free ports to all forked processes.
### runtimeArgs
Type: `array`
Extra args passed to the node process
### waitUntilTargets
Type: `array`
The targets to run to before starting the node app

View File

@ -42,3 +42,11 @@ Default: `false`
Type: `boolean`
Whether to skip TypeScript type checking.
### watch
Default: `false`
Type: `boolean`
Enable re-building when files change.

View File

@ -34,3 +34,17 @@ The path to the Typescript configuration file.
Type: `array`
List of static assets.
### transformers
Type: `array`
List of TypeScript Transformer Plugins.
### watch
Default: `false`
Type: `boolean`
Enable re-building when files change.

View File

@ -374,6 +374,11 @@
"id": "library",
"file": "angular/api-js/generators/convert-to-swc"
},
{
"name": "node executor",
"id": "node",
"file": "angular/api-js/executors/node"
},
{
"name": "tsc executor",
"id": "tsc",
@ -1743,6 +1748,11 @@
"id": "library",
"file": "react/api-js/generators/convert-to-swc"
},
{
"name": "node executor",
"id": "node",
"file": "react/api-js/executors/node"
},
{
"name": "tsc executor",
"id": "tsc",
@ -3076,6 +3086,11 @@
"id": "library",
"file": "node/api-js/generators/convert-to-swc"
},
{
"name": "node executor",
"id": "node",
"file": "node/api-js/executors/node"
},
{
"name": "tsc executor",
"id": "tsc",

View File

@ -0,0 +1,60 @@
---
title: '@nrwl/js:node executor'
description: 'Build Node.js applications'
---
# @nrwl/js:node
Build Node.js applications
Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets.
## Options
### buildTarget (_**required**_)
Type: `string`
The target to run to build you the app
### args
Type: `array`
Extra args when starting the app
### host
Default: `localhost`
Type: `string`
The host to inspect the process on
### inspect
Default: `inspect`
Type: `string | boolean `
Ensures the app is starting with debugging
### port
Default: `9229`
Type: `number`
The port to inspect the process on. Setting port to 0 will assign random free ports to all forked processes.
### runtimeArgs
Type: `array`
Extra args passed to the node process
### waitUntilTargets
Type: `array`
The targets to run to before starting the node app

View File

@ -42,3 +42,11 @@ Default: `false`
Type: `boolean`
Whether to skip TypeScript type checking.
### watch
Default: `false`
Type: `boolean`
Enable re-building when files change.

View File

@ -34,3 +34,17 @@ The path to the Typescript configuration file.
Type: `array`
List of static assets.
### transformers
Type: `array`
List of TypeScript Transformer Plugins.
### watch
Default: `false`
Type: `boolean`
Enable re-building when files change.

View File

@ -0,0 +1,60 @@
---
title: '@nrwl/js:node executor'
description: 'Build Node.js applications'
---
# @nrwl/js:node
Build Node.js applications
Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets.
## Options
### buildTarget (_**required**_)
Type: `string`
The target to run to build you the app
### args
Type: `array`
Extra args when starting the app
### host
Default: `localhost`
Type: `string`
The host to inspect the process on
### inspect
Default: `inspect`
Type: `string | boolean `
Ensures the app is starting with debugging
### port
Default: `9229`
Type: `number`
The port to inspect the process on. Setting port to 0 will assign random free ports to all forked processes.
### runtimeArgs
Type: `array`
Extra args passed to the node process
### waitUntilTargets
Type: `array`
The targets to run to before starting the node app

View File

@ -42,3 +42,11 @@ Default: `false`
Type: `boolean`
Whether to skip TypeScript type checking.
### watch
Default: `false`
Type: `boolean`
Enable re-building when files change.

View File

@ -34,3 +34,17 @@ The path to the Typescript configuration file.
Type: `array`
List of static assets.
### transformers
Type: `array`
List of TypeScript Transformer Plugins.
### watch
Default: `false`
Type: `boolean`
Enable re-building when files change.

View File

@ -30,5 +30,5 @@
"outputs": ["coverage/e2e/cli"]
}
},
"implicitDependencies": ["cli"]
"implicitDependencies": ["cli", "js"]
}

View File

@ -2,6 +2,7 @@ import {
checkFilesExist,
newProject,
readJson,
readProjectConfig,
runCLI,
runCLIAsync,
runCommand,
@ -34,7 +35,7 @@ describe('js e2e', () => {
});
}, 120000);
it('should create libs and apps with js executors (--compiler=tsc)', async () => {
it('xxxshould create libs and apps with js executors (--compiler=tsc)', async () => {
const scope = newProject();
const lib = uniq('lib');
runCLI(`generate @nrwl/js:lib ${lib} --buildable --compiler=tsc`);
@ -92,7 +93,7 @@ describe('js e2e', () => {
expect(output).toContain('1 task(s) that it depends on');
expect(output).toContain('Done compiling TypeScript files');
// expect(runCommand(`node dist/apps/${app}/src/index.js`)).toContain(`Running ${lib}`)
// expect(runCommand(`serve ${app} --watch=false`)).toContain(`Running ${lib}`)
}, 120000);
// reenable when once ci runs on node 16
@ -154,6 +155,32 @@ describe('js e2e', () => {
// expect(output).toContain('1 task(s) that it depends on');
// expect(output).toContain('Successfully compiled: 2 files with swc');
//
// // expect(runCommand(`node dist/apps/${app}/src/index.js`)).toContain(`Running ${lib}`)
// expect(runCommand(`serve ${app} --watch=false`)).toContain(`Running ${lib}`)
// }, 120000);
describe('convert js:tsc to js:swc', () => {
it('should convert apps', async () => {
const app = uniq('app');
runCLI(`generate @nrwl/js:app ${app}`);
let projectConfig = readProjectConfig(app);
expect(projectConfig.targets['build'].executor).toEqual('@nrwl/js:tsc');
await runCLIAsync(`generate @nrwl/js:convert-to-swc ${app}`);
projectConfig = readProjectConfig(app);
expect(projectConfig.targets['build'].executor).toEqual('@nrwl/js:swc');
});
it('should convert libs', async () => {
const lib = uniq('lib');
runCLI(`generate @nrwl/js:lib ${lib} --buildable`);
let projectConfig = readProjectConfig(lib);
expect(projectConfig.targets['build'].executor).toEqual('@nrwl/js:tsc');
await runCLIAsync(`generate @nrwl/js:convert-to-swc ${lib}`);
projectConfig = readProjectConfig(lib);
expect(projectConfig.targets['build'].executor).toEqual('@nrwl/js:swc');
});
});
});

View File

@ -10,6 +10,11 @@
"implementation": "./src/executors/swc/swc.impl",
"schema": "./src/executors/swc/schema.json",
"description": "Build a project using SWC"
},
"node": {
"implementation": "./src/executors/node/node.impl",
"schema": "./src/executors/node/schema.json",
"description": "Build Node.js applications"
}
},
"builders": {
@ -22,6 +27,11 @@
"implementation": "./src/executors/swc/compat",
"schema": "./src/executors/swc/schema.json",
"description": "Build a project using SWC"
},
"node": {
"implementation": "./src/executors/node/compat",
"schema": "./src/executors/node/schema.json",
"description": "Build Node.js applications"
}
}
}

View File

@ -12,6 +12,10 @@
"@nrwl/jest": "*",
"@nrwl/linter": "*",
"chalk": "4.1.0",
"js-tokens": "^4.0.0"
"js-tokens": "^4.0.0",
"rxjs": "^6.5.4",
"rxjs-for-await": "0.0.2",
"source-map-support": "0.5.19",
"tree-kill": "1.2.2"
}
}

View File

@ -0,0 +1,4 @@
import { convertNxExecutor } from '@nrwl/devkit';
import nodeExecutor from './node.impl';
export default convertNxExecutor(nodeExecutor);

View File

@ -0,0 +1,20 @@
const Module = require('module');
const originalLoader = Module._load;
const mappings = JSON.parse(process.env.NX_MAPPINGS);
const keys = Object.keys(mappings);
const fileToRun = process.env.NX_FILE_TO_RUN;
Module._load = function (request, parent) {
if (!parent) return originalLoader.apply(this, arguments);
const match = keys.find((k) => request === k);
if (match) {
const newArguments = [...arguments];
newArguments[0] = mappings[match];
return originalLoader.apply(this, newArguments);
} else {
return originalLoader.apply(this, arguments);
}
};
require(fileToRun);

View File

@ -0,0 +1,185 @@
import {
ExecutorContext,
joinPathFragments,
logger,
parseTargetString,
runExecutor,
} from '@nrwl/devkit';
import { ChildProcess, fork } from 'child_process';
import * as treeKill from 'tree-kill';
import { promisify } from 'util';
import { ExecutorEvent } from '../../utils/schema';
import { InspectType, NodeExecutorOptions } from './schema';
import { readCachedProjectGraph } from '@nrwl/workspace/src/core/project-graph';
import { calculateProjectDependencies } from '@nrwl/workspace/src/utilities/buildable-libs-utils';
let subProcess: ChildProcess = null;
export async function* nodeExecutor(
options: NodeExecutorOptions,
context: ExecutorContext
) {
// for now we only run the executor in the watch mode
options.watch = true;
process.on('SIGTERM', async () => {
await killProcess();
process.exit(128 + 15);
});
process.on('SIGINT', async () => {
await killProcess();
process.exit(128 + 2);
});
process.on('SIGHUP', async () => {
await killProcess();
process.exit(128 + 1);
});
if (options.waitUntilTargets && options.waitUntilTargets.length > 0) {
const results = await runWaitUntilTargets(options, context);
for (const [i, result] of results.entries()) {
if (!result.success) {
throw new Error(
`Wait until target failed: ${options.waitUntilTargets[i]}.`
);
}
}
}
const mappings = calculateResolveMappings(context, options);
for await (const event of startBuild(options, context)) {
if (!event.success) {
logger.error('There was an error with the build. See above.');
logger.info(`${event.outfile} was not restarted.`);
}
await handleBuildEvent(event, options, mappings);
yield event;
}
}
function calculateResolveMappings(
context: ExecutorContext,
options: NodeExecutorOptions
) {
const projectGraph = readCachedProjectGraph();
const parsed = parseTargetString(options.buildTarget);
const { dependencies } = calculateProjectDependencies(
projectGraph,
context.root,
parsed.project,
parsed.target,
parsed.configuration
);
return dependencies.reduce((m, c) => {
m[c.name] = joinPathFragments(context.root, c.outputs[0]);
return m;
}, {});
}
function runProcess(
event: ExecutorEvent,
options: NodeExecutorOptions,
mappings: { [project: string]: string }
) {
subProcess = fork(
joinPathFragments(__dirname, 'node-with-require-overrides'),
options.args,
{
execArgv: getExecArgv(options),
stdio: 'inherit',
env: {
...process.env,
NX_FILE_TO_RUN: event.outfile,
NX_MAPPINGS: JSON.stringify(mappings),
},
}
);
}
function getExecArgv(options: NodeExecutorOptions) {
const args = [
'-r',
require.resolve('source-map-support/register'),
...options.runtimeArgs,
];
if (options.inspect === true) {
options.inspect = InspectType.Inspect;
}
if (options.inspect) {
args.push(`--${options.inspect}=${options.host}:${options.port}`);
}
return args;
}
async function handleBuildEvent(
event: ExecutorEvent,
options: NodeExecutorOptions,
mappings: { [project: string]: string }
) {
if ((!event.success || options.watch) && subProcess) {
await killProcess();
}
if (event.success) {
runProcess(event, options, mappings);
}
}
async function killProcess() {
const promisifiedTreeKill: (pid: number, signal: string) => Promise<void> =
promisify(treeKill);
try {
await promisifiedTreeKill(subProcess.pid, 'SIGTERM');
} catch (err) {
if (Array.isArray(err) && err[0] && err[2]) {
const errorMessage = err[2];
logger.error(errorMessage);
} else if (err.message) {
logger.error(err.message);
}
} finally {
subProcess = null;
}
}
async function* startBuild(
options: NodeExecutorOptions,
context: ExecutorContext
) {
const buildTarget = parseTargetString(options.buildTarget);
yield* await runExecutor<ExecutorEvent>(
buildTarget,
{
...options.buildTargetOptions,
watch: options.watch,
},
context
);
}
function runWaitUntilTargets(
options: NodeExecutorOptions,
context: ExecutorContext
): Promise<{ success: boolean }[]> {
return Promise.all(
options.waitUntilTargets.map(async (waitUntilTarget) => {
const target = parseTargetString(waitUntilTarget);
const output = await runExecutor(target, {}, context);
return new Promise<{ success: boolean }>(async (resolve) => {
let event = await output.next();
// Resolve after first event
resolve(event.value as { success: boolean });
// Continue iterating
while (!event.done) {
event = await output.next();
}
});
})
);
}
export default nodeExecutor;

View File

@ -0,0 +1,16 @@
export const enum InspectType {
Inspect = 'inspect',
InspectBrk = 'inspect-brk',
}
export interface NodeExecutorOptions {
inspect: boolean | InspectType;
runtimeArgs: string[];
args: string[];
waitUntilTargets: string[];
buildTarget: string;
buildTargetOptions: Record<string, any>;
host: string;
port: number;
watch: boolean;
}

View File

@ -0,0 +1,67 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"title": "Node executor",
"description": "Execute Nodejs applications",
"type": "object",
"properties": {
"buildTarget": {
"type": "string",
"description": "The target to run to build you the app"
},
"buildTargetOptions": {
"type": "object",
"description": "Additional options to pass into the build target.",
"default": {}
},
"waitUntilTargets": {
"type": "array",
"description": "The targets to run to before starting the node app",
"default": [],
"items": {
"type": "string"
}
},
"host": {
"type": "string",
"default": "localhost",
"description": "The host to inspect the process on"
},
"port": {
"type": "number",
"default": 9229,
"description": "The port to inspect the process on. Setting port to 0 will assign random free ports to all forked processes."
},
"inspect": {
"oneOf": [
{
"type": "string",
"enum": ["inspect", "inspect-brk"]
},
{
"type": "boolean"
}
],
"description": "Ensures the app is starting with debugging",
"default": "inspect"
},
"runtimeArgs": {
"type": "array",
"description": "Extra args passed to the node process",
"default": [],
"items": {
"type": "string"
}
},
"args": {
"type": "array",
"description": "Extra args when starting the app",
"default": [],
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
"required": ["buildTarget"]
}

View File

@ -17,11 +17,6 @@
"type": "string",
"description": "The path to the Typescript configuration file."
},
"skipTypeCheck": {
"type": "boolean",
"description": "Whether to skip TypeScript type checking.",
"default": false
},
"assets": {
"type": "array",
"description": "List of static assets.",
@ -29,6 +24,16 @@
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"watch": {
"type": "boolean",
"description": "Enable re-building when files change.",
"default": false
},
"skipTypeCheck": {
"type": "boolean",
"description": "Whether to skip TypeScript type checking.",
"default": false
}
},
"required": ["main", "outputPath", "tsConfig"],

View File

@ -1,176 +0,0 @@
jest.mock('@nrwl/workspace/src/core/project-graph');
jest.mock('@nrwl/workspace/src/utilities/assets');
jest.mock('@nrwl/workspace/src/utilities/buildable-libs-utils');
jest.mock('@nrwl/tao/src/utils/fileutils');
jest.mock('../../utils/swc/compile-swc');
jest.mock('../../utils/typescript/run-type-check');
import { ExecutorContext, readJsonFile, writeJsonFile } from '@nrwl/devkit';
import { copyAssetFiles } from '@nrwl/workspace/src/utilities/assets';
import {
calculateProjectDependencies,
checkDependentProjectsHaveBeenBuilt,
createTmpTsConfig,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { join } from 'path';
import { compileSwc } from '../../utils/swc/compile-swc';
import { runTypeCheck } from '../../utils/typescript/run-type-check';
import { NormalizedSwcExecutorOptions, SwcExecutorOptions } from './schema';
import {
normalizeOptions as normalizeSwcOptions,
swcExecutor,
} from './swc.impl';
describe('executor: swc', () => {
const assets = ['some-file.md'];
let options: SwcExecutorOptions;
let normalizedOptions: NormalizedSwcExecutorOptions;
let context: ExecutorContext;
let tsOptions: Record<string, unknown>;
const defaultPackageJson = { name: 'workspacelib', version: '0.0.1' };
const compileSwcMock = compileSwc as jest.Mock;
const readJsonFileMock = readJsonFile as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
(calculateProjectDependencies as jest.Mock).mockImplementation(() => ({
target: { data: { root: 'libs/workspacelib' } },
dependencies: [],
}));
(createTmpTsConfig as jest.Mock).mockImplementation(
() => '/my-app/tsconfig.app.generated.json'
);
(checkDependentProjectsHaveBeenBuilt as jest.Mock).mockReturnValue(true);
(runTypeCheck as jest.Mock).mockImplementation(() =>
Promise.resolve({ errors: [] })
);
compileSwcMock.mockImplementation((_, postCompilationCallback) =>
Promise.resolve({ success: true }).then((result) => {
postCompilationCallback?.();
return result;
})
);
readJsonFileMock.mockImplementation(() => ({ ...defaultPackageJson }));
context = {
cwd: '/root',
root: '/root',
projectName: 'workspacelib',
targetName: 'build',
workspace: {
version: 2,
projects: {
workspacelib: {
root: 'libs/workspacelib',
sourceRoot: 'libs/workspacelib/src',
targets: {},
},
},
npmScope: 'test',
},
isVerbose: false,
};
options = {
assets,
main: 'libs/workspacelib/src/index.ts',
outputPath: 'dist/libs/workspacelib',
tsConfig: 'libs/workspacelib/tsconfig.lib.json',
};
normalizedOptions = normalizeSwcOptions(options, context);
tsOptions = {
outputPath: normalizedOptions.outputPath,
projectName: context.projectName,
projectRoot: 'libs/workspacelib',
tsConfig: normalizedOptions.tsConfig,
};
});
it('should return {success: false} if deps have not been built', async () => {
(calculateProjectDependencies as jest.Mock).mockImplementation(() => ({
target: { data: { root: 'libs/workspacelib' } },
dependencies: [{}],
}));
(checkDependentProjectsHaveBeenBuilt as jest.Mock).mockReturnValue(false);
const result = await swcExecutor(options, context);
expect(result).toEqual({ success: false });
expect(compileSwcMock).not.toHaveBeenCalled();
});
it('should return {success: false} if typecheck emits errors', async () => {
(runTypeCheck as jest.Mock).mockImplementation(() =>
Promise.resolve({ errors: ['error'] })
);
const result = await swcExecutor(options, context);
expect(result).toEqual({ success: false });
expect(compileSwcMock).toHaveBeenCalledWith(
tsOptions,
expect.any(Function)
);
});
it('should success if both typecheck and compileSwc success', async () => {
const result = await swcExecutor(options, context);
expect(result).toEqual({ success: true });
expect(compileSwcMock).toHaveBeenCalledWith(
tsOptions,
expect.any(Function)
);
});
it('should copy assets files', async () => {
await swcExecutor(options, context);
expect(copyAssetFiles).toHaveBeenCalledWith(normalizedOptions.files);
});
it('should update packageJson typings', async () => {
await swcExecutor(options, context);
expect(writeJsonFile).toHaveBeenCalledWith(
join(context.root, options.outputPath, 'package.json'),
{
...defaultPackageJson,
main: './src/index.js',
typings: './src/index.d.ts',
}
);
});
describe('without typecheck', () => {
beforeEach(() => {
options.skipTypeCheck = true;
normalizedOptions = normalizeSwcOptions(options, context);
tsOptions = {
outputPath: normalizedOptions.outputPath,
projectName: context.projectName,
projectRoot: 'libs/workspacelib',
tsConfig: normalizedOptions.tsConfig,
};
});
it('should not call runTypeCheck', async () => {
await swcExecutor(options, context);
expect(runTypeCheck).not.toHaveBeenCalled();
});
it('should success if compileSwc success', async () => {
const result = await swcExecutor(options, context);
expect(result).toEqual({ success: true });
expect(compileSwcMock).toHaveBeenCalledWith(
tsOptions,
expect.any(Function)
);
});
it('should not update packageJson typings', async () => {
await swcExecutor(options, context);
expect(writeJsonFile).toHaveBeenCalledWith(
join(context.root, options.outputPath, 'package.json'),
{
...defaultPackageJson,
main: './src/index.js',
}
);
});
});
});

View File

@ -1,97 +1,93 @@
import { ExecutorContext } from '@nrwl/devkit';
import { appRootPath } from '@nrwl/tao/src/utils/app-root';
import {
assetGlobsToFiles,
copyAssetFiles,
FileInputOutput,
} from '@nrwl/workspace/src/utilities/assets';
import { join } from 'path';
import { join, resolve } from 'path';
import { eachValueFrom } from 'rxjs-for-await';
import { map } from 'rxjs/operators';
import { checkDependencies } from '../../utils/check-dependencies';
import {
ExecutorEvent,
NormalizedSwcExecutorOptions,
SwcExecutorOptions,
} from '../../utils/schema';
import { compileSwc } from '../../utils/swc/compile-swc';
import { printDiagnostics } from '../../utils/typescript/print-diagnostics';
import { runTypeCheck } from '../../utils/typescript/run-type-check';
import { updatePackageJson } from '../../utils/update-package-json';
import { NormalizedSwcExecutorOptions, SwcExecutorOptions } from './schema';
export function normalizeOptions(
options: SwcExecutorOptions,
context: ExecutorContext
contextRoot: string,
sourceRoot?: string,
projectRoot?: string
): NormalizedSwcExecutorOptions {
const outputPath = join(context.root, options.outputPath);
const outputPath = join(contextRoot, options.outputPath);
if (options.skipTypeCheck == null) {
options.skipTypeCheck = false;
}
if (options.watch == null) {
options.watch = false;
}
const files: FileInputOutput[] = assetGlobsToFiles(
options.assets,
context.root,
contextRoot,
outputPath
);
return {
...options,
mainOutputPath: resolve(
outputPath,
options.main.replace(`${projectRoot}/`, '').replace('.ts', '.js')
),
files,
root: contextRoot,
sourceRoot,
projectRoot,
outputPath,
tsConfig: join(context.root, options.tsConfig),
tsConfig: join(contextRoot, options.tsConfig),
} as NormalizedSwcExecutorOptions;
}
export async function swcExecutor(
export async function* swcExecutor(
options: SwcExecutorOptions,
context: ExecutorContext
) {
const normalizedOptions = normalizeOptions(options, context);
const { shouldContinue, tmpTsConfig, projectRoot } = checkDependencies(
const { sourceRoot, root } = context.workspace.projects[context.projectName];
const normalizedOptions = normalizeOptions(
options,
context.root,
sourceRoot,
root
);
const { tmpTsConfig, projectRoot } = checkDependencies(
context,
options.tsConfig
);
if (!shouldContinue) {
return { success: false };
}
if (tmpTsConfig) {
normalizedOptions.tsConfig = tmpTsConfig;
}
const tsOptions = {
outputPath: normalizedOptions.outputPath,
projectName: context.projectName,
projectRoot,
tsConfig: normalizedOptions.tsConfig,
const postCompilationCallback = async () => {
await updatePackageAndCopyAssets(normalizedOptions, projectRoot);
};
if (!options.skipTypeCheck) {
const ts = await import('typescript');
// start two promises, one for type checking, one for transpiling
return Promise.all([
runTypeCheck({
ts,
mode: 'emitDeclarationOnly',
tsConfigPath: tsOptions.tsConfig,
outDir: tsOptions.outputPath.replace(`/${projectRoot}`, ''),
workspaceRoot: appRootPath,
}).then((result) => {
const hasErrors = result.errors.length > 0;
if (hasErrors) {
printDiagnostics(result);
}
return Promise.resolve({ success: !hasErrors });
}),
compileSwc(tsOptions, async () => {
await updatePackageAndCopyAssets(normalizedOptions, projectRoot);
}),
]).then(([typeCheckResult, transpileResult]) => ({
success: typeCheckResult.success && transpileResult.success,
}));
}
return compileSwc(tsOptions, async () => {
await updatePackageAndCopyAssets(normalizedOptions, projectRoot);
});
return yield* eachValueFrom(
compileSwc(context, normalizedOptions, postCompilationCallback).pipe(
map(
({ success }) =>
({
success,
outfile: normalizedOptions.mainOutputPath,
} as ExecutorEvent)
)
)
);
}
async function updatePackageAndCopyAssets(

View File

@ -23,10 +23,22 @@
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"watch": {
"type": "boolean",
"description": "Enable re-building when files change.",
"default": false
},
"transformers": {
"type": "array",
"description": "List of TypeScript Transformer Plugins.",
"default": [],
"items": {
"$ref": "#/definitions/transformerPattern"
}
}
},
"required": ["main", "outputPath", "tsConfig"],
"definitions": {
"assetPattern": {
"oneOf": [
@ -60,6 +72,27 @@
"type": "string"
}
]
},
"transformerPattern": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"options": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": false,
"required": ["name"]
}
]
}
}
}

View File

@ -1,175 +0,0 @@
jest.mock('@nrwl/workspace/src/core/project-graph');
jest.mock('@nrwl/workspace/src/utilities/assets');
jest.mock('@nrwl/workspace/src/utilities/buildable-libs-utils');
jest.mock('@nrwl/tao/src/utils/fileutils');
jest.mock('@nrwl/workspace/src/utilities/typescript/compilation');
import { ExecutorContext } from '@nrwl/devkit';
import { readJsonFile, writeJsonFile } from '@nrwl/tao/src/utils/fileutils';
import {
assetGlobsToFiles,
copyAssetFiles,
} from '@nrwl/workspace/src/utilities/assets';
import {
calculateProjectDependencies,
checkDependentProjectsHaveBeenBuilt,
createTmpTsConfig,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { compileTypeScript } from '@nrwl/workspace/src/utilities/typescript/compilation';
import { join } from 'path';
import { ExecutorOptions, NormalizedExecutorOptions } from '../../utils/schema';
import { tscExecutor } from './tsc.impl';
describe('executor: tsc', () => {
const assets = ['some-file.md'];
let context: ExecutorContext;
let normalizedOptions: NormalizedExecutorOptions;
let options: ExecutorOptions;
const defaultPackageJson = { name: 'workspacelib', version: '0.0.1' };
const compileTypeScriptMock = compileTypeScript as jest.Mock;
const readJsonFileMock = readJsonFile as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
(calculateProjectDependencies as jest.Mock).mockImplementation(() => ({
target: { data: { root: 'libs/workspacelib' } },
dependencies: [],
}));
(createTmpTsConfig as jest.Mock).mockImplementation(
() => '/my-app/tsconfig.app.generated.json'
);
(checkDependentProjectsHaveBeenBuilt as jest.Mock).mockReturnValue(true);
readJsonFileMock.mockImplementation(() => ({ ...defaultPackageJson }));
context = {
cwd: '/root',
root: '/root',
projectName: 'workspacelib',
targetName: 'build',
workspace: {
version: 2,
projects: {
workspacelib: {
root: 'libs/workspacelib',
sourceRoot: 'libs/workspacelib/src',
targets: {},
},
},
npmScope: 'test',
},
isVerbose: false,
};
options = {
assets,
main: 'libs/workspacelib/src/index.ts',
outputPath: 'dist/libs/workspacelib',
tsConfig: 'libs/workspacelib/tsconfig.lib.json',
};
normalizedOptions = {
...options,
files: assetGlobsToFiles(
options.assets,
context.root,
options.outputPath
),
outputPath: join(context.root, options.outputPath),
tsConfig: join(context.root, options.tsConfig),
};
});
it('should return { success: false } when dependent projects have not been built', async () => {
(calculateProjectDependencies as jest.Mock).mockImplementation(() => ({
target: { data: { root: 'libs/workspacelib' } },
dependencies: [{}],
}));
(checkDependentProjectsHaveBeenBuilt as jest.Mock).mockReturnValue(false);
const result = await tscExecutor(options, context);
expect(result).toEqual({ success: false });
expect(compileTypeScriptMock).not.toHaveBeenCalled();
});
it('should return typescript compilation result', async () => {
const expectedResult = { success: true };
compileTypeScriptMock.mockReturnValue(expectedResult);
const result = await tscExecutor(options, context);
expect(result).toBe(expectedResult);
});
it('should copy assets before typescript compilation', async () => {
await tscExecutor(options, context);
expect(copyAssetFiles).toHaveBeenCalledWith(normalizedOptions.files);
});
describe('update package.json', () => {
it('should update the package.json when both main and typings are missing', async () => {
compileTypeScriptMock.mockReturnValue({ success: true });
await tscExecutor(options, context);
expect(writeJsonFile).toHaveBeenCalledWith(
join(context.root, options.outputPath, 'package.json'),
{
...defaultPackageJson,
main: './src/index.js',
typings: './src/index.d.ts',
}
);
});
it('should update the package.json when only main is missing', async () => {
compileTypeScriptMock.mockReturnValue({ success: true });
const packageJson = {
...defaultPackageJson,
typings: './src/index.d.ts',
};
readJsonFileMock.mockReturnValue(packageJson);
await tscExecutor(options, context);
expect(writeJsonFile).toHaveBeenCalledWith(
join(context.root, options.outputPath, 'package.json'),
{
...packageJson,
main: './src/index.js',
}
);
});
it('should update the package.json when only typings is missing', async () => {
compileTypeScriptMock.mockReturnValue({ success: true });
const packageJson = {
...defaultPackageJson,
main: './src/index.js',
};
readJsonFileMock.mockReturnValue(packageJson);
await tscExecutor(options, context);
expect(writeJsonFile).toHaveBeenCalledWith(
join(context.root, options.outputPath, 'package.json'),
{
...packageJson,
typings: './src/index.d.ts',
}
);
});
it('should not update the package.json when both main and typings are specified', async () => {
compileTypeScriptMock.mockReturnValue({ success: true });
readJsonFileMock.mockReturnValue({
...defaultPackageJson,
main: './src/index.js',
typings: './src/index.d.ts',
});
await tscExecutor(options, context);
expect(writeJsonFile).not.toHaveBeenCalled();
});
});
});

View File

@ -4,41 +4,85 @@ import {
copyAssetFiles,
FileInputOutput,
} from '@nrwl/workspace/src/utilities/assets';
import { join } from 'path';
import { join, resolve } from 'path';
import { eachValueFrom } from 'rxjs-for-await';
import { map } from 'rxjs/operators';
import { checkDependencies } from '../../utils/check-dependencies';
import { compile } from '../../utils/compile';
import { ExecutorOptions, NormalizedExecutorOptions } from '../../utils/schema';
import {
ExecutorEvent,
ExecutorOptions,
NormalizedExecutorOptions,
} from '../../utils/schema';
import { compileTypeScriptFiles } from '../../utils/typescript/compile-typescript-files';
import { updatePackageJson } from '../../utils/update-package-json';
export async function tscExecutor(
export function normalizeOptions(
options: ExecutorOptions,
contextRoot: string,
sourceRoot?: string,
projectRoot?: string
): NormalizedExecutorOptions {
const outputPath = join(contextRoot, options.outputPath);
if (options.watch == null) {
options.watch = false;
}
const files: FileInputOutput[] = assetGlobsToFiles(
options.assets,
contextRoot,
outputPath
);
return {
...options,
root: contextRoot,
sourceRoot,
projectRoot,
files,
outputPath,
tsConfig: join(contextRoot, options.tsConfig),
mainOutputPath: resolve(
outputPath,
options.main.replace(`${projectRoot}/`, '').replace('.ts', '.js')
),
};
}
export async function* tscExecutor(
options: ExecutorOptions,
context: ExecutorContext
) {
const normalizedOptions = normalizeOptions(options, context);
const { sourceRoot, root } = context.workspace.projects[context.projectName];
const normalizedOptions = normalizeOptions(
options,
context.root,
sourceRoot,
root
);
const { projectRoot, tmpTsConfig, shouldContinue } = checkDependencies(
const { projectRoot, tmpTsConfig } = checkDependencies(
context,
options.tsConfig
);
if (!shouldContinue) {
return { success: false };
}
if (tmpTsConfig) {
normalizedOptions.tsConfig = tmpTsConfig;
}
const tsOptions = {
outputPath: normalizedOptions.outputPath,
projectName: context.projectName,
projectRoot,
tsConfig: normalizedOptions.tsConfig,
};
return compile('tsc', context, tsOptions, async () => {
await updatePackageAndCopyAssets(normalizedOptions, projectRoot);
});
return yield* eachValueFrom(
compileTypeScriptFiles(normalizedOptions, context, async () => {
await updatePackageAndCopyAssets(normalizedOptions, projectRoot);
}).pipe(
map(
({ success }) =>
({
success,
outfile: normalizedOptions.mainOutputPath,
} as ExecutorEvent)
)
)
);
}
async function updatePackageAndCopyAssets(
@ -49,24 +93,4 @@ async function updatePackageAndCopyAssets(
updatePackageJson(options.main, options.outputPath, projectRoot);
}
function normalizeOptions(
options: ExecutorOptions,
context: ExecutorContext
): NormalizedExecutorOptions {
const outputPath = join(context.root, options.outputPath);
const files: FileInputOutput[] = assetGlobsToFiles(
options.assets,
context.root,
outputPath
);
return {
...options,
files,
outputPath,
tsConfig: join(context.root, options.tsConfig),
};
}
export default tscExecutor;

View File

@ -72,4 +72,18 @@ describe('app', () => {
outputs: ['{options.outputPath}'],
});
});
it('should generate a "serve" target', async () => {
await applicationGenerator(tree, {
...defaultOptions,
name: 'my-app',
});
const projectConfig = readProjectConfiguration(tree, 'my-app');
expect(projectConfig.targets.serve).toEqual({
executor: `@nrwl/js:node`,
options: {
buildTarget: `my-app:build`,
},
});
});
});

View File

@ -1,10 +1,9 @@
import { ExecutorContext } from '@nrwl/devkit';
import { ExecutorContext, ProjectGraph } from '@nrwl/devkit';
import { readCachedProjectGraph } from '@nrwl/workspace/src/core/project-graph';
import {
calculateProjectDependencies,
checkDependentProjectsHaveBeenBuilt,
createTmpTsConfig,
DependentBuildableProjectNode,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { join } from 'path';
@ -12,21 +11,30 @@ export function checkDependencies(
context: ExecutorContext,
tsConfigPath: string
): {
shouldContinue: boolean;
tmpTsConfig: string | null;
projectRoot: string;
projectDependencies: DependentBuildableProjectNode[];
} {
const projectGraph = readCachedProjectGraph();
const { target, dependencies } = calculateProjectDependencies(
projectGraph,
context.root,
context.projectName,
context.targetName,
context.configurationName
);
const { target, dependencies, nonBuildableDependencies } =
calculateProjectDependencies(
projectGraph,
context.root,
context.projectName,
context.targetName,
context.configurationName
);
const projectRoot = target.data.root;
if (nonBuildableDependencies.length > 0) {
throw new Error(
`Buildable libraries can only depend on other buildable libraries. You must define the ${
context.targetName
} target for the following libraries: ${nonBuildableDependencies
.map((t) => `"${t}"`)
.join(', ')}`
);
}
if (dependencies.length > 0) {
const areDependentProjectsBuilt = checkDependentProjectsHaveBeenBuilt(
context.root,
@ -34,25 +42,24 @@ export function checkDependencies(
context.targetName,
dependencies
);
if (!areDependentProjectsBuilt) {
throw new Error(
`Some dependencies of '${context.projectName}' have not been built. This probably due to the ${context.targetName} target being misconfigured.`
);
}
return {
shouldContinue: areDependentProjectsBuilt,
tmpTsConfig:
areDependentProjectsBuilt &&
createTmpTsConfig(
join(context.root, tsConfigPath),
context.root,
projectRoot,
dependencies
),
tmpTsConfig: createTmpTsConfig(
join(context.root, tsConfigPath),
context.root,
projectRoot,
dependencies
),
projectRoot,
projectDependencies: dependencies,
};
}
return {
shouldContinue: true,
tmpTsConfig: null,
projectRoot,
projectDependencies: dependencies,
};
}

View File

@ -1,24 +0,0 @@
import { ExecutorContext } from '@nrwl/devkit';
import {
compileTypeScript,
TypeScriptCompilationOptions,
} from '@nrwl/workspace/src/utilities/typescript/compilation';
import { Compiler } from './schema';
import { compileSwc } from './swc/compile-swc';
export async function compile(
compilerOptions: Compiler,
context: ExecutorContext,
tsCompilationOptions: TypeScriptCompilationOptions,
postCompilationCallback: () => void | Promise<void>
) {
if (compilerOptions === 'tsc') {
const result = compileTypeScript(tsCompilationOptions);
await postCompilationCallback();
return result;
}
if (compilerOptions === 'swc') {
return compileSwc(tsCompilationOptions, postCompilationCallback);
}
}

View File

@ -98,6 +98,15 @@ function addProject(
if (options.compiler === 'swc' && options.skipTypeCheck) {
projectConfiguration.targets.build.options.skipTypeCheck = true;
}
if (projectType === 'application') {
projectConfiguration.targets.serve = {
executor: `@nrwl/js:node`,
options: {
buildTarget: `${options.name}:build`,
},
};
}
}
if (options.config === 'workspace') {

View File

@ -4,6 +4,7 @@ import type {
AssetGlob,
FileInputOutput,
} from '@nrwl/workspace/src/utilities/assets';
import { TransformerEntry } from './typescript/types';
export type Compiler = 'tsc' | 'swc';
@ -33,8 +34,28 @@ export interface ExecutorOptions {
main: string;
outputPath: string;
tsConfig: string;
watch: boolean;
transformers: TransformerEntry[];
}
export interface NormalizedExecutorOptions extends ExecutorOptions {
root?: string;
sourceRoot?: string;
projectRoot?: string;
mainOutputPath: string;
files: Array<FileInputOutput>;
}
export interface SwcExecutorOptions extends ExecutorOptions {
skipTypeCheck?: boolean;
}
export interface NormalizedSwcExecutorOptions
extends NormalizedExecutorOptions {
skipTypeCheck: boolean;
}
export interface ExecutorEvent {
outfile: string;
success: boolean;
}

View File

@ -1,64 +1,164 @@
import { logger } from '@nrwl/devkit';
import { TypeScriptCompilationOptions } from '@nrwl/workspace/src/utilities/typescript/compilation';
import { ExecutorContext, logger } from '@nrwl/devkit';
import { exec, execSync } from 'child_process';
import { Observable, zip } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { normalizeTsCompilationOptions } from '../normalize-ts-compilation-options';
import { NormalizedSwcExecutorOptions } from '../schema';
import { printDiagnostics } from '../typescript/print-diagnostics';
import {
runTypeCheck,
runTypeCheckWatch,
TypeCheckOptions,
} from '../typescript/run-type-check';
export async function compileSwc(
tsCompilationOptions: TypeScriptCompilationOptions,
export function compileSwc(
context: ExecutorContext,
normalizedOptions: NormalizedSwcExecutorOptions,
postCompilationCallback: () => void | Promise<void>
): Promise<{ success: boolean }> {
const normalizedOptions = normalizeTsCompilationOptions(tsCompilationOptions);
) {
const tsOptions = {
outputPath: normalizedOptions.outputPath,
projectName: context.projectName,
projectRoot: normalizedOptions.projectRoot,
tsConfig: normalizedOptions.tsConfig,
watch: normalizedOptions.watch,
};
const outDir = tsOptions.outputPath.replace(`/${tsOptions.projectRoot}`, '');
logger.log(`Compiling with SWC for ${normalizedOptions.projectName}...`);
const srcPath = normalizedOptions.projectRoot;
const destPath = normalizedOptions.outputPath.replace(
`/${normalizedOptions.projectName}`,
const normalizedTsOptions = normalizeTsCompilationOptions(tsOptions);
logger.log(`Compiling with SWC for ${normalizedTsOptions.projectName}...`);
const srcPath = normalizedTsOptions.projectRoot;
const destPath = normalizedTsOptions.outputPath.replace(
`/${normalizedTsOptions.projectName}`,
''
);
const swcrcPath = `${normalizedOptions.projectRoot}/.swcrc`;
// TODO(chau): use `--ignore` for swc cli to exclude spec files
// Open issue: https://github.com/swc-project/cli/issues/20
const swcrcPath = `${normalizedTsOptions.projectRoot}/.swcrc`;
let swcCmd = `npx swc ${srcPath} -d ${destPath} --source-maps --config-file=${swcrcPath}`;
if (normalizedOptions.watch) {
swcCmd += ' --watch';
return createSwcWatchProcess(swcCmd, postCompilationCallback);
}
const swcCmdLog = execSync(swcCmd).toString();
logger.log(swcCmdLog.replace(/\n/, ''));
await postCompilationCallback();
return { success: true };
}
async function createSwcWatchProcess(
swcCmd: string,
postCompilationCallback: () => void | Promise<void>
): Promise<{ success: boolean }> {
return new Promise((res) => {
const watchProcess = exec(swcCmd);
watchProcess.stdout.on('data', (data) => {
process.stdout.write(data);
if (data.includes('Successfully compiled')) {
postCompilationCallback();
const postCompilationOperator = () =>
tap(({ success }) => {
if (success) {
void postCompilationCallback();
}
});
watchProcess.stderr.on('data', (err) => {
process.stderr.write(err);
res({ success: false });
});
const compile$ = new Observable<{ success: boolean }>((subscriber) => {
if (normalizedOptions.watch) {
swcCmd += ' --watch';
const watchProcess = createSwcWatchProcess(swcCmd, (success) => {
subscriber.next({ success });
});
const processExitListener = () => watchProcess.kill();
return () => {
watchProcess.close();
subscriber.complete();
};
}
process.on('SIGINT', processExitListener);
process.on('SIGTERM', processExitListener);
process.on('exit', processExitListener);
const swcCmdLog = execSync(swcCmd).toString();
logger.log(swcCmdLog.replace(/\n/, ''));
subscriber.next({ success: swcCmdLog.includes('Successfully compiled') });
watchProcess.on('exit', () => {
res({ success: true });
});
return () => {
subscriber.complete();
};
});
if (normalizedOptions.skipTypeCheck) {
return compile$.pipe(postCompilationOperator());
}
const typeCheck$ = new Observable<{ success: boolean }>((subscriber) => {
const typeCheckOptions: TypeCheckOptions = {
mode: 'emitDeclarationOnly',
tsConfigPath: tsOptions.tsConfig,
outDir,
workspaceRoot: normalizedOptions.root,
};
if (normalizedOptions.watch) {
let typeCheckRunner: { close: () => void };
let preEmit = false;
runTypeCheckWatch(
typeCheckOptions,
(diagnostic, formattedDiagnostic, errorCount) => {
// 6031 and 6032 are to skip watchCompilerHost initialization (Start watching for changes... message)
// We also skip if preEmit has been set to true, because it means that the first type check before
// the WatchCompiler emits.
if (preEmit && diagnostic.code !== 6031 && diagnostic.code !== 6032) {
const hasErrors = errorCount > 0;
if (hasErrors) {
void printDiagnostics([formattedDiagnostic]);
} else {
void printDiagnostics([], [formattedDiagnostic]);
}
subscriber.next({ success: !hasErrors });
}
}
).then(({ close, preEmitErrors, preEmitWarnings }) => {
const hasErrors = preEmitErrors.length > 0;
if (hasErrors) {
void printDiagnostics(preEmitErrors, preEmitWarnings);
}
typeCheckRunner = { close };
subscriber.next({ success: !hasErrors });
preEmit = true;
});
return () => {
if (typeCheckRunner) {
typeCheckRunner.close();
}
subscriber.complete();
};
}
runTypeCheck(typeCheckOptions).then(({ errors, warnings }) => {
const hasErrors = errors.length > 0;
if (hasErrors) {
void printDiagnostics(errors, warnings);
}
subscriber.next({ success: !hasErrors });
subscriber.complete();
});
return () => {
subscriber.complete();
};
});
return zip(compile$, typeCheck$).pipe(
map(([compileResult, typeCheckResult]) => ({
success: compileResult.success && typeCheckResult.success,
})),
postCompilationOperator()
);
}
function createSwcWatchProcess(
swcCmd: string,
callback: (success: boolean) => void
) {
const watchProcess = exec(swcCmd);
watchProcess.stdout.on('data', (data) => {
process.stdout.write(data);
callback(data.includes('Successfully compiled'));
});
watchProcess.stderr.on('data', (err) => {
process.stderr.write(err);
callback(false);
});
const processExitListener = () => watchProcess.kill();
process.on('SIGINT', processExitListener);
process.on('SIGTERM', processExitListener);
process.on('exit', processExitListener);
watchProcess.on('exit', () => {
callback(true);
});
return { close: () => watchProcess.kill() };
}

View File

@ -0,0 +1 @@
export const before = () => {};

View File

@ -0,0 +1 @@
export const after = () => {};

View File

@ -0,0 +1,86 @@
import { ExecutorContext } from '@nrwl/devkit';
import {
compileTypeScript,
compileTypeScriptWatcher,
} from '@nrwl/workspace/src/utilities/typescript/compilation';
import { Observable } from 'rxjs';
import type {
CustomTransformers,
Diagnostic,
Program,
SourceFile,
TransformerFactory,
} from 'typescript';
import { NormalizedExecutorOptions } from '../schema';
import { loadTsPlugins } from './load-ts-plugins';
export function compileTypeScriptFiles(
options: NormalizedExecutorOptions,
context: ExecutorContext,
postCompleteAction: () => void | Promise<void>
) {
const { compilerPluginHooks } = loadTsPlugins(options.transformers);
const getCustomTransformers = (program: Program): CustomTransformers => ({
before: compilerPluginHooks.beforeHooks.map(
(hook) => hook(program) as TransformerFactory<SourceFile>
),
after: compilerPluginHooks.afterHooks.map(
(hook) => hook(program) as TransformerFactory<SourceFile>
),
afterDeclarations: compilerPluginHooks.afterDeclarationsHooks.map(
(hook) => hook(program) as TransformerFactory<SourceFile>
),
});
// const tcsOptions = {
// outputPath: options.normalizedOutputPath,
// projectName: context.projectName,
// projectRoot: libRoot,
// tsConfig: tsConfigPath,
// deleteOutputPath: options.deleteOutputPath,
// rootDir: options.srcRootForCompilationRoot,
// watch: options.watch,
// getCustomTransformers,
// };
const tscOptions = {
outputPath: options.outputPath,
projectName: context.projectName,
projectRoot: options.projectRoot,
tsConfig: options.tsConfig,
// deleteOutputPath: options.deleteOutputPath,
// rootDir: options.srcRootForCompilationRoot,
watch: options.watch,
getCustomTransformers,
};
return new Observable((subscriber) => {
if (options.watch) {
const watcher = compileTypeScriptWatcher(
tscOptions,
async (d: Diagnostic) => {
if (d.code === 6194) {
await postCompleteAction();
subscriber.next({ success: true });
}
}
);
return () => {
watcher.close();
subscriber.complete();
};
}
const result = compileTypeScript(tscOptions);
(postCompleteAction() as Promise<void>).then(() => {
subscriber.next(result);
subscriber.complete();
});
return () => {
subscriber.complete();
};
});
}

View File

@ -0,0 +1,40 @@
import { loadTsPlugins } from './load-ts-plugins';
jest.mock('plugin-a');
jest.mock('plugin-b');
const mockRequireResolve = jest.fn((path) => path);
describe('loadTsPlugins', () => {
it('should return empty hooks if plugins is falsy', () => {
const result = loadTsPlugins(undefined);
assertEmptyResult(result);
});
it('should return empty hooks if plugins is []', () => {
const result = loadTsPlugins([]);
assertEmptyResult(result);
});
it('should return correct compiler hooks', () => {
const result = loadTsPlugins(
['plugin-a', 'plugin-b'],
mockRequireResolve as any
);
expect(result.hasPlugin).toEqual(true);
expect(result.compilerPluginHooks).toEqual({
beforeHooks: [expect.any(Function)],
afterHooks: [expect.any(Function)],
afterDeclarationsHooks: [],
});
});
function assertEmptyResult(result: ReturnType<typeof loadTsPlugins>) {
expect(result.hasPlugin).toEqual(false);
expect(result.compilerPluginHooks).toEqual({
beforeHooks: [],
afterHooks: [],
afterDeclarationsHooks: [],
});
}
});

View File

@ -0,0 +1,86 @@
import { logger } from '@nrwl/devkit';
import { join } from 'path';
import {
CompilerPlugin,
CompilerPluginHooks,
TransformerPlugin,
TransformerEntry,
} from './types';
export function loadTsPlugins(
plugins: TransformerEntry[],
moduleResolver: typeof require.resolve = require.resolve
): {
compilerPluginHooks: CompilerPluginHooks;
hasPlugin: boolean;
} {
const beforeHooks: CompilerPluginHooks['beforeHooks'] = [];
const afterHooks: CompilerPluginHooks['afterHooks'] = [];
const afterDeclarationsHooks: CompilerPluginHooks['afterDeclarationsHooks'] =
[];
if (!plugins || !plugins.length)
return {
compilerPluginHooks: {
beforeHooks,
afterHooks,
afterDeclarationsHooks,
},
hasPlugin: false,
};
const normalizedPlugins: TransformerPlugin[] = plugins.map((plugin) =>
typeof plugin === 'string' ? { name: plugin, options: {} } : plugin
);
const nodeModulePaths = [
join(process.cwd(), 'node_modules'),
...module.paths,
];
const pluginRefs: CompilerPlugin[] = normalizedPlugins.map(({ name }) => {
try {
const binaryPath = moduleResolver(name, {
paths: nodeModulePaths,
});
return require(binaryPath);
} catch (e) {
logger.warn(`"${name}" plugin could not be found!`);
return {};
}
});
for (let i = 0; i < pluginRefs.length; i++) {
const { name: pluginName, options: pluginOptions } = normalizedPlugins[i];
const { before, after, afterDeclarations } = pluginRefs[i];
if (!before && !after && !afterDeclarations) {
logger.warn(
`${pluginName} is not a Transformer Plugin. It does not provide neither before(), after(), nor afterDeclarations()`
);
continue;
}
if (before) {
beforeHooks.push(before.bind(before, pluginOptions));
}
if (after) {
afterHooks.push(after.bind(after, pluginOptions));
}
if (afterDeclarations) {
afterDeclarationsHooks.push(
afterDeclarations.bind(afterDeclarations, pluginOptions)
);
}
}
return {
compilerPluginHooks: {
beforeHooks,
afterHooks,
afterDeclarationsHooks,
},
hasPlugin: true,
};
}

View File

@ -1,21 +1,18 @@
import { TypeCheckResult } from './run-type-check';
export async function printDiagnostics(result: TypeCheckResult) {
if (result.errors.length > 0) {
result.errors.forEach((err) => {
export async function printDiagnostics(
errors: string[] = [],
warnings: string[] = []
) {
if (errors.length > 0) {
errors.forEach((err) => {
console.log(`${err}\n`);
});
console.log(
`Found ${result.errors.length} error${
result.errors.length > 1 ? 's' : ''
}.`
);
} else if (result.warnings.length > 0) {
result.warnings.forEach((err) => {
console.log(`Found ${errors.length} error${errors.length > 1 ? 's' : ''}.`);
} else if (warnings.length > 0) {
warnings.forEach((err) => {
console.log(`${err}\n`);
});
console.log(`Found ${result.warnings.length} warnings.`);
console.log(`Found ${warnings.length} warnings.`);
}
}

View File

@ -46,7 +46,6 @@ describe('runTypeCheck', () => {
`
);
const result = await runTypeCheck({
ts: require('typescript'),
workspaceRoot,
tsConfigPath,
mode: 'noEmit',
@ -65,7 +64,6 @@ describe('runTypeCheck', () => {
);
await runTypeCheck({
ts: require('typescript'),
workspaceRoot,
tsConfigPath,
mode: 'emitDeclarationOnly',

View File

@ -1,6 +1,7 @@
import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import * as path from 'path';
import * as chalk from 'chalk';
import * as path from 'path';
import type { BuilderProgram, Diagnostic, Program } from 'typescript';
import { codeFrameColumns } from '../code-frames/code-frames';
export interface TypeCheckResult {
@ -11,10 +12,9 @@ export interface TypeCheckResult {
incremental: boolean;
}
type TypeCheckOptions = BaseTypeCheckOptions & Mode;
export type TypeCheckOptions = BaseTypeCheckOptions & Mode;
interface BaseTypeCheckOptions {
ts: typeof import('typescript');
workspaceRoot: string;
tsConfigPath: string;
cacheDir?: string;
@ -31,10 +31,91 @@ interface EmitDeclarationOnlyMode {
outDir: string;
}
export async function runTypeCheckWatch(
options: TypeCheckOptions,
callback: (
diagnostic: Diagnostic,
formattedDiagnostic: string,
errorCount?: number
) => void | Promise<void>
) {
const { ts, workspaceRoot, config, compilerOptions } = await setupTypeScript(
options
);
const host = ts.createWatchCompilerHost(
config.fileNames,
compilerOptions,
ts.sys,
ts.createEmitAndSemanticDiagnosticsBuilderProgram
);
const originalOnWatchStatusChange = host.onWatchStatusChange;
host.onWatchStatusChange = (diagnostic, newLine, opts, errorCount) => {
originalOnWatchStatusChange?.(diagnostic, newLine, opts, errorCount);
callback(
diagnostic,
getFormattedDiagnostic(ts, workspaceRoot, diagnostic),
errorCount
);
};
const watchProgram = ts.createWatchProgram(host);
const program = watchProgram.getProgram().getProgram();
const diagnostics = ts.getPreEmitDiagnostics(program);
return {
close: watchProgram.close.bind(watchProgram),
preEmitErrors: diagnostics
.filter((d) => d.category === ts.DiagnosticCategory.Error)
.map((d) => getFormattedDiagnostic(ts, workspaceRoot, d)),
preEmitWarnings: diagnostics
.filter((d) => d.category === ts.DiagnosticCategory.Warning)
.map((d) => getFormattedDiagnostic(ts, workspaceRoot, d)),
};
}
export async function runTypeCheck(
options: TypeCheckOptions
): Promise<TypeCheckResult> {
const { ts, workspaceRoot, tsConfigPath, cacheDir } = options;
const { ts, workspaceRoot, cacheDir, config, compilerOptions } =
await setupTypeScript(options);
let program: Program | BuilderProgram;
let incremental = false;
if (compilerOptions.incremental && cacheDir) {
incremental = true;
program = ts.createIncrementalProgram({
rootNames: config.fileNames,
options: {
...compilerOptions,
incremental: true,
tsBuildInfoFile: path.join(cacheDir, '.tsbuildinfo'),
},
});
} else {
program = ts.createProgram(config.fileNames, compilerOptions);
}
const result = program.emit();
const allDiagnostics = ts
.getPreEmitDiagnostics(program as Program)
.concat(result.diagnostics);
return getTypeCheckResult(
ts,
allDiagnostics,
workspaceRoot,
config.fileNames.length,
program.getSourceFiles().length,
incremental
);
}
async function setupTypeScript(options: TypeCheckOptions) {
const ts = await import('typescript');
const { workspaceRoot, tsConfigPath, cacheDir } = options;
const config = readTsConfig(tsConfigPath);
if (config.errors.length) {
throw new Error(`Invalid config file: ${config.errors}`);
@ -50,56 +131,39 @@ export async function runTypeCheck(
skipLibCheck: true,
...emitOptions,
};
return { ts, workspaceRoot, cacheDir, config, compilerOptions };
}
let program:
| import('typescript').Program
| import('typescript').BuilderProgram;
let incremental = false;
if (compilerOptions.incremental && cacheDir) {
incremental = true;
program = ts.createIncrementalProgram({
rootNames: config.fileNames,
options: {
...compilerOptions,
incremental: true,
tsBuildInfoFile: path.join(cacheDir, '.tsbuildinfo'),
},
});
} else {
program = ts.createProgram(config.fileNames, compilerOptions);
}
const result = program.emit();
function getTypeCheckResult(
ts: typeof import('typescript'),
allDiagnostics: Diagnostic[],
workspaceRoot: string,
inputFilesCount: number,
totalFilesCount: number,
incremental: boolean = false
) {
const errors = allDiagnostics
.filter((d) => d.category === ts.DiagnosticCategory.Error)
.map((d) => getFormattedDiagnostic(ts, workspaceRoot, d));
const allDiagnostics = ts
.getPreEmitDiagnostics(program as import('typescript').Program)
.concat(result.diagnostics);
const errors = await Promise.all(
allDiagnostics
.filter((d) => d.category === ts.DiagnosticCategory.Error)
.map((d) => getFormattedDiagnostic(ts, workspaceRoot, d))
);
const warnings = await Promise.all(
allDiagnostics
.filter((d) => d.category === ts.DiagnosticCategory.Warning)
.map((d) => getFormattedDiagnostic(ts, workspaceRoot, d))
);
const warnings = allDiagnostics
.filter((d) => d.category === ts.DiagnosticCategory.Warning)
.map((d) => getFormattedDiagnostic(ts, workspaceRoot, d));
return {
warnings,
errors,
inputFilesCount: config.fileNames.length,
totalFilesCount: program.getSourceFiles().length,
inputFilesCount,
totalFilesCount,
incremental,
};
}
export async function getFormattedDiagnostic(
export function getFormattedDiagnostic(
ts: typeof import('typescript'),
workspaceRoot: string,
diagnostic: import('typescript').Diagnostic
): Promise<string> {
diagnostic: Diagnostic
): string {
let message = '';
const reason = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');

View File

@ -0,0 +1,38 @@
import type {
CustomTransformerFactory,
Node,
Program,
TransformerFactory as TypescriptTransformerFactory,
} from 'typescript';
type TransformerFactory =
| TypescriptTransformerFactory<Node>
| CustomTransformerFactory;
export interface TransformerPlugin {
name: string;
options: Record<string, unknown>;
}
export type TransformerEntry = string | TransformerPlugin;
export interface CompilerPlugin {
before?: (
options?: Record<string, unknown>,
program?: Program
) => TransformerFactory;
after?: (
options?: Record<string, unknown>,
program?: Program
) => TransformerFactory;
afterDeclarations?: (
options?: Record<string, unknown>,
program?: Program
) => TransformerFactory;
}
export interface CompilerPluginHooks {
beforeHooks: Array<(program?: Program) => TransformerFactory>;
afterHooks: Array<(program?: Program) => TransformerFactory>;
afterDeclarationsHooks: Array<(program?: Program) => TransformerFactory>;
}

View File

@ -6,15 +6,13 @@ export async function validateTypes(opts: {
projectRoot: string;
tsconfig: string;
}): Promise<void> {
const ts = await import('typescript');
const result = await runTypeCheck({
ts,
workspaceRoot: opts.workspaceRoot,
tsConfigPath: join(opts.workspaceRoot, opts.tsconfig),
mode: 'noEmit',
});
await printDiagnostics(result);
await printDiagnostics(result.errors, result.warnings);
if (result.errors.length > 0) {
throw new Error('Found type errors. See above.');

View File

@ -88,5 +88,6 @@ function readDeps(packageJsonDeps: any) {
return [
...Object.keys(packageJsonDeps?.dependencies ?? {}),
...Object.keys(packageJsonDeps?.devDependencies ?? {}),
...Object.keys(packageJsonDeps?.peerDependencies ?? {}),
];
}

View File

@ -28,9 +28,14 @@ export function calculateProjectDependencies(
projectName: string,
targetName: string,
configurationName: string
): { target: ProjectGraphNode; dependencies: DependentBuildableProjectNode[] } {
): {
target: ProjectGraphNode;
dependencies: DependentBuildableProjectNode[];
nonBuildableDependencies: string[];
} {
const target = projGraph.nodes[projectName];
// gather the library dependencies
const nonBuildableDependencies = [];
const dependencies = recursivelyCollectDependencies(
projectName,
projGraph,
@ -38,29 +43,30 @@ export function calculateProjectDependencies(
)
.map((dep) => {
const depNode = projGraph.nodes[dep] || projGraph.externalNodes[dep];
if (
depNode.type === ProjectType.lib &&
isBuildable(targetName, depNode)
) {
const libPackageJson = readJsonFile(
join(root, depNode.data.root, 'package.json')
);
if (depNode.type === ProjectType.lib) {
if (isBuildable(targetName, depNode)) {
const libPackageJson = readJsonFile(
join(root, depNode.data.root, 'package.json')
);
return {
name: libPackageJson.name, // i.e. @workspace/mylib
outputs: getOutputsForTargetAndConfiguration(
{
overrides: {},
target: {
project: projectName,
target: targetName,
configuration: configurationName,
return {
name: libPackageJson.name, // i.e. @workspace/mylib
outputs: getOutputsForTargetAndConfiguration(
{
overrides: {},
target: {
project: projectName,
target: targetName,
configuration: configurationName,
},
},
},
depNode
),
node: depNode,
};
depNode
),
node: depNode,
};
} else {
nonBuildableDependencies.push(dep);
}
} else if (depNode.type === 'npm') {
return {
name: depNode.data.packageName,
@ -72,7 +78,7 @@ export function calculateProjectDependencies(
}
})
.filter((x) => !!x);
return { target, dependencies };
return { target, dependencies, nonBuildableDependencies };
}
function recursivelyCollectDependencies(

View File

@ -43,7 +43,7 @@ export function compileTypeScriptWatcher(
options: ts.CompilerOptions,
errorCount: number
) => void | Promise<void>
): Promise<any> {
) {
const normalizedOptions = normalizeOptions(options);
const tsConfig = getNormalizedTsConfig(normalizedOptions);
@ -67,13 +67,13 @@ export function compileTypeScriptWatcher(
emitOnlyDtsFiles,
customTransformers
) => {
const consumerCustomTransfomers = options.getCustomTransformers?.(
const consumerCustomTransformers = options.getCustomTransformers?.(
builderProgram.getProgram()
);
const mergedCustomTransformers = mergeCustomTransformers(
customTransformers,
consumerCustomTransfomers
consumerCustomTransformers
);
return originalProgramEmit(
@ -94,30 +94,29 @@ export function compileTypeScriptWatcher(
await callback?.(a, b, c, d);
};
ts.createWatchProgram(host);
return new Promise(() => {});
return ts.createWatchProgram(host);
}
function mergeCustomTransformers(
originalCustomTransfomers: CustomTransformers | undefined,
originalCustomTransformers: CustomTransformers | undefined,
consumerCustomTransformers: CustomTransformers | undefined
): CustomTransformers | undefined {
if (!consumerCustomTransformers) return originalCustomTransfomers;
if (!consumerCustomTransformers) return originalCustomTransformers;
const mergedCustomTransformers: CustomTransformers = {};
if (consumerCustomTransformers.before) {
mergedCustomTransformers.before = originalCustomTransfomers?.before
mergedCustomTransformers.before = originalCustomTransformers?.before
? [
...originalCustomTransfomers.before,
...originalCustomTransformers.before,
...consumerCustomTransformers.before,
]
: consumerCustomTransformers.before;
}
if (consumerCustomTransformers.after) {
mergedCustomTransformers.after = originalCustomTransfomers?.after
mergedCustomTransformers.after = originalCustomTransformers?.after
? [
...originalCustomTransfomers.after,
...originalCustomTransformers.after,
...consumerCustomTransformers.after,
]
: consumerCustomTransformers.after;
@ -125,9 +124,9 @@ function mergeCustomTransformers(
if (consumerCustomTransformers.afterDeclarations) {
mergedCustomTransformers.afterDeclarations =
originalCustomTransfomers?.afterDeclarations
originalCustomTransformers?.afterDeclarations
? [
...originalCustomTransfomers.afterDeclarations,
...originalCustomTransformers.afterDeclarations,
...consumerCustomTransformers.afterDeclarations,
]
: consumerCustomTransformers.afterDeclarations;

View File

@ -110,7 +110,6 @@ const IGNORE_MATCHES = {
'karma-jasmine-html-reporter',
'webpack',
'webpack-dev-server',
,
'@nrwl/cli',
'@nrwl/jest',
'@nrwl/linter',