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` Type: `boolean`
Whether to skip TypeScript type checking. 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` Type: `array`
List of static assets. 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", "id": "library",
"file": "angular/api-js/generators/convert-to-swc" "file": "angular/api-js/generators/convert-to-swc"
}, },
{
"name": "node executor",
"id": "node",
"file": "angular/api-js/executors/node"
},
{ {
"name": "tsc executor", "name": "tsc executor",
"id": "tsc", "id": "tsc",
@ -1743,6 +1748,11 @@
"id": "library", "id": "library",
"file": "react/api-js/generators/convert-to-swc" "file": "react/api-js/generators/convert-to-swc"
}, },
{
"name": "node executor",
"id": "node",
"file": "react/api-js/executors/node"
},
{ {
"name": "tsc executor", "name": "tsc executor",
"id": "tsc", "id": "tsc",
@ -3076,6 +3086,11 @@
"id": "library", "id": "library",
"file": "node/api-js/generators/convert-to-swc" "file": "node/api-js/generators/convert-to-swc"
}, },
{
"name": "node executor",
"id": "node",
"file": "node/api-js/executors/node"
},
{ {
"name": "tsc executor", "name": "tsc executor",
"id": "tsc", "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` Type: `boolean`
Whether to skip TypeScript type checking. 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` Type: `array`
List of static assets. 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` Type: `boolean`
Whether to skip TypeScript type checking. 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` Type: `array`
List of static assets. 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"] "outputs": ["coverage/e2e/cli"]
} }
}, },
"implicitDependencies": ["cli"] "implicitDependencies": ["cli", "js"]
} }

View File

@ -2,6 +2,7 @@ import {
checkFilesExist, checkFilesExist,
newProject, newProject,
readJson, readJson,
readProjectConfig,
runCLI, runCLI,
runCLIAsync, runCLIAsync,
runCommand, runCommand,
@ -34,7 +35,7 @@ describe('js e2e', () => {
}); });
}, 120000); }, 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 scope = newProject();
const lib = uniq('lib'); const lib = uniq('lib');
runCLI(`generate @nrwl/js:lib ${lib} --buildable --compiler=tsc`); 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('1 task(s) that it depends on');
expect(output).toContain('Done compiling TypeScript files'); 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); }, 120000);
// reenable when once ci runs on node 16 // 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('1 task(s) that it depends on');
// expect(output).toContain('Successfully compiled: 2 files with swc'); // 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); // }, 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", "implementation": "./src/executors/swc/swc.impl",
"schema": "./src/executors/swc/schema.json", "schema": "./src/executors/swc/schema.json",
"description": "Build a project using SWC" "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": { "builders": {
@ -22,6 +27,11 @@
"implementation": "./src/executors/swc/compat", "implementation": "./src/executors/swc/compat",
"schema": "./src/executors/swc/schema.json", "schema": "./src/executors/swc/schema.json",
"description": "Build a project using SWC" "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/jest": "*",
"@nrwl/linter": "*", "@nrwl/linter": "*",
"chalk": "4.1.0", "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", "type": "string",
"description": "The path to the Typescript configuration file." "description": "The path to the Typescript configuration file."
}, },
"skipTypeCheck": {
"type": "boolean",
"description": "Whether to skip TypeScript type checking.",
"default": false
},
"assets": { "assets": {
"type": "array", "type": "array",
"description": "List of static assets.", "description": "List of static assets.",
@ -29,6 +24,16 @@
"items": { "items": {
"$ref": "#/definitions/assetPattern" "$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"], "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 { ExecutorContext } from '@nrwl/devkit';
import { appRootPath } from '@nrwl/tao/src/utils/app-root';
import { import {
assetGlobsToFiles, assetGlobsToFiles,
copyAssetFiles, copyAssetFiles,
FileInputOutput, FileInputOutput,
} from '@nrwl/workspace/src/utilities/assets'; } 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 { checkDependencies } from '../../utils/check-dependencies';
import {
ExecutorEvent,
NormalizedSwcExecutorOptions,
SwcExecutorOptions,
} from '../../utils/schema';
import { compileSwc } from '../../utils/swc/compile-swc'; 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 { updatePackageJson } from '../../utils/update-package-json';
import { NormalizedSwcExecutorOptions, SwcExecutorOptions } from './schema';
export function normalizeOptions( export function normalizeOptions(
options: SwcExecutorOptions, options: SwcExecutorOptions,
context: ExecutorContext contextRoot: string,
sourceRoot?: string,
projectRoot?: string
): NormalizedSwcExecutorOptions { ): NormalizedSwcExecutorOptions {
const outputPath = join(context.root, options.outputPath); const outputPath = join(contextRoot, options.outputPath);
if (options.skipTypeCheck == null) { if (options.skipTypeCheck == null) {
options.skipTypeCheck = false; options.skipTypeCheck = false;
} }
if (options.watch == null) {
options.watch = false;
}
const files: FileInputOutput[] = assetGlobsToFiles( const files: FileInputOutput[] = assetGlobsToFiles(
options.assets, options.assets,
context.root, contextRoot,
outputPath outputPath
); );
return { return {
...options, ...options,
files, mainOutputPath: resolve(
outputPath, outputPath,
tsConfig: join(context.root, options.tsConfig), options.main.replace(`${projectRoot}/`, '').replace('.ts', '.js')
),
files,
root: contextRoot,
sourceRoot,
projectRoot,
outputPath,
tsConfig: join(contextRoot, options.tsConfig),
} as NormalizedSwcExecutorOptions; } as NormalizedSwcExecutorOptions;
} }
export async function swcExecutor( export async function* swcExecutor(
options: SwcExecutorOptions, options: SwcExecutorOptions,
context: ExecutorContext context: ExecutorContext
) { ) {
const normalizedOptions = normalizeOptions(options, context); const { sourceRoot, root } = context.workspace.projects[context.projectName];
const { shouldContinue, tmpTsConfig, projectRoot } = checkDependencies( const normalizedOptions = normalizeOptions(
options,
context.root,
sourceRoot,
root
);
const { tmpTsConfig, projectRoot } = checkDependencies(
context, context,
options.tsConfig options.tsConfig
); );
if (!shouldContinue) {
return { success: false };
}
if (tmpTsConfig) { if (tmpTsConfig) {
normalizedOptions.tsConfig = tmpTsConfig; normalizedOptions.tsConfig = tmpTsConfig;
} }
const tsOptions = { const postCompilationCallback = async () => {
outputPath: normalizedOptions.outputPath, await updatePackageAndCopyAssets(normalizedOptions, projectRoot);
projectName: context.projectName,
projectRoot,
tsConfig: normalizedOptions.tsConfig,
}; };
if (!options.skipTypeCheck) { return yield* eachValueFrom(
const ts = await import('typescript'); compileSwc(context, normalizedOptions, postCompilationCallback).pipe(
// start two promises, one for type checking, one for transpiling map(
return Promise.all([ ({ success }) =>
runTypeCheck({ ({
ts, success,
mode: 'emitDeclarationOnly', outfile: normalizedOptions.mainOutputPath,
tsConfigPath: tsOptions.tsConfig, } as ExecutorEvent)
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);
});
} }
async function updatePackageAndCopyAssets( async function updatePackageAndCopyAssets(

View File

@ -23,10 +23,22 @@
"items": { "items": {
"$ref": "#/definitions/assetPattern" "$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"], "required": ["main", "outputPath", "tsConfig"],
"definitions": { "definitions": {
"assetPattern": { "assetPattern": {
"oneOf": [ "oneOf": [
@ -60,6 +72,27 @@
"type": "string" "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, copyAssetFiles,
FileInputOutput, FileInputOutput,
} from '@nrwl/workspace/src/utilities/assets'; } 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 { checkDependencies } from '../../utils/check-dependencies';
import { compile } from '../../utils/compile'; import {
import { ExecutorOptions, NormalizedExecutorOptions } from '../../utils/schema'; ExecutorEvent,
ExecutorOptions,
NormalizedExecutorOptions,
} from '../../utils/schema';
import { compileTypeScriptFiles } from '../../utils/typescript/compile-typescript-files';
import { updatePackageJson } from '../../utils/update-package-json'; 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, options: ExecutorOptions,
context: ExecutorContext 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, context,
options.tsConfig options.tsConfig
); );
if (!shouldContinue) {
return { success: false };
}
if (tmpTsConfig) { if (tmpTsConfig) {
normalizedOptions.tsConfig = tmpTsConfig; normalizedOptions.tsConfig = tmpTsConfig;
} }
const tsOptions = { return yield* eachValueFrom(
outputPath: normalizedOptions.outputPath, compileTypeScriptFiles(normalizedOptions, context, async () => {
projectName: context.projectName,
projectRoot,
tsConfig: normalizedOptions.tsConfig,
};
return compile('tsc', context, tsOptions, async () => {
await updatePackageAndCopyAssets(normalizedOptions, projectRoot); await updatePackageAndCopyAssets(normalizedOptions, projectRoot);
}); }).pipe(
map(
({ success }) =>
({
success,
outfile: normalizedOptions.mainOutputPath,
} as ExecutorEvent)
)
)
);
} }
async function updatePackageAndCopyAssets( async function updatePackageAndCopyAssets(
@ -49,24 +93,4 @@ async function updatePackageAndCopyAssets(
updatePackageJson(options.main, options.outputPath, projectRoot); 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; export default tscExecutor;

View File

@ -72,4 +72,18 @@ describe('app', () => {
outputs: ['{options.outputPath}'], 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 { readCachedProjectGraph } from '@nrwl/workspace/src/core/project-graph';
import { import {
calculateProjectDependencies, calculateProjectDependencies,
checkDependentProjectsHaveBeenBuilt, checkDependentProjectsHaveBeenBuilt,
createTmpTsConfig, createTmpTsConfig,
DependentBuildableProjectNode,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils'; } from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { join } from 'path'; import { join } from 'path';
@ -12,13 +11,12 @@ export function checkDependencies(
context: ExecutorContext, context: ExecutorContext,
tsConfigPath: string tsConfigPath: string
): { ): {
shouldContinue: boolean;
tmpTsConfig: string | null; tmpTsConfig: string | null;
projectRoot: string; projectRoot: string;
projectDependencies: DependentBuildableProjectNode[];
} { } {
const projectGraph = readCachedProjectGraph(); const projectGraph = readCachedProjectGraph();
const { target, dependencies } = calculateProjectDependencies( const { target, dependencies, nonBuildableDependencies } =
calculateProjectDependencies(
projectGraph, projectGraph,
context.root, context.root,
context.projectName, context.projectName,
@ -27,6 +25,16 @@ export function checkDependencies(
); );
const projectRoot = target.data.root; 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) { if (dependencies.length > 0) {
const areDependentProjectsBuilt = checkDependentProjectsHaveBeenBuilt( const areDependentProjectsBuilt = checkDependentProjectsHaveBeenBuilt(
context.root, context.root,
@ -34,25 +42,24 @@ export function checkDependencies(
context.targetName, context.targetName,
dependencies 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 { return {
shouldContinue: areDependentProjectsBuilt, tmpTsConfig: createTmpTsConfig(
tmpTsConfig:
areDependentProjectsBuilt &&
createTmpTsConfig(
join(context.root, tsConfigPath), join(context.root, tsConfigPath),
context.root, context.root,
projectRoot, projectRoot,
dependencies dependencies
), ),
projectRoot, projectRoot,
projectDependencies: dependencies,
}; };
} }
return { return {
shouldContinue: true,
tmpTsConfig: null, tmpTsConfig: null,
projectRoot, 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) { if (options.compiler === 'swc' && options.skipTypeCheck) {
projectConfiguration.targets.build.options.skipTypeCheck = true; 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') { if (options.config === 'workspace') {

View File

@ -4,6 +4,7 @@ import type {
AssetGlob, AssetGlob,
FileInputOutput, FileInputOutput,
} from '@nrwl/workspace/src/utilities/assets'; } from '@nrwl/workspace/src/utilities/assets';
import { TransformerEntry } from './typescript/types';
export type Compiler = 'tsc' | 'swc'; export type Compiler = 'tsc' | 'swc';
@ -33,8 +34,28 @@ export interface ExecutorOptions {
main: string; main: string;
outputPath: string; outputPath: string;
tsConfig: string; tsConfig: string;
watch: boolean;
transformers: TransformerEntry[];
} }
export interface NormalizedExecutorOptions extends ExecutorOptions { export interface NormalizedExecutorOptions extends ExecutorOptions {
root?: string;
sourceRoot?: string;
projectRoot?: string;
mainOutputPath: string;
files: Array<FileInputOutput>; 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,54 +1,153 @@
import { logger } from '@nrwl/devkit'; import { ExecutorContext, logger } from '@nrwl/devkit';
import { TypeScriptCompilationOptions } from '@nrwl/workspace/src/utilities/typescript/compilation';
import { exec, execSync } from 'child_process'; 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 { 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( export function compileSwc(
tsCompilationOptions: TypeScriptCompilationOptions, context: ExecutorContext,
normalizedOptions: NormalizedSwcExecutorOptions,
postCompilationCallback: () => void | Promise<void> 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 normalizedTsOptions = normalizeTsCompilationOptions(tsOptions);
const srcPath = normalizedOptions.projectRoot; logger.log(`Compiling with SWC for ${normalizedTsOptions.projectName}...`);
const destPath = normalizedOptions.outputPath.replace( const srcPath = normalizedTsOptions.projectRoot;
`/${normalizedOptions.projectName}`, const destPath = normalizedTsOptions.outputPath.replace(
`/${normalizedTsOptions.projectName}`,
'' ''
); );
const swcrcPath = `${normalizedOptions.projectRoot}/.swcrc`; const swcrcPath = `${normalizedTsOptions.projectRoot}/.swcrc`;
// TODO(chau): use `--ignore` for swc cli to exclude spec files
// Open issue: https://github.com/swc-project/cli/issues/20
let swcCmd = `npx swc ${srcPath} -d ${destPath} --source-maps --config-file=${swcrcPath}`; let swcCmd = `npx swc ${srcPath} -d ${destPath} --source-maps --config-file=${swcrcPath}`;
const postCompilationOperator = () =>
tap(({ success }) => {
if (success) {
void postCompilationCallback();
}
});
const compile$ = new Observable<{ success: boolean }>((subscriber) => {
if (normalizedOptions.watch) { if (normalizedOptions.watch) {
swcCmd += ' --watch'; swcCmd += ' --watch';
return createSwcWatchProcess(swcCmd, postCompilationCallback); const watchProcess = createSwcWatchProcess(swcCmd, (success) => {
subscriber.next({ success });
});
return () => {
watchProcess.close();
subscriber.complete();
};
} }
const swcCmdLog = execSync(swcCmd).toString(); const swcCmdLog = execSync(swcCmd).toString();
logger.log(swcCmdLog.replace(/\n/, '')); logger.log(swcCmdLog.replace(/\n/, ''));
await postCompilationCallback(); subscriber.next({ success: swcCmdLog.includes('Successfully compiled') });
return { 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()
);
} }
async function createSwcWatchProcess( function createSwcWatchProcess(
swcCmd: string, swcCmd: string,
postCompilationCallback: () => void | Promise<void> callback: (success: boolean) => void
): Promise<{ success: boolean }> { ) {
return new Promise((res) => {
const watchProcess = exec(swcCmd); const watchProcess = exec(swcCmd);
watchProcess.stdout.on('data', (data) => { watchProcess.stdout.on('data', (data) => {
process.stdout.write(data); process.stdout.write(data);
if (data.includes('Successfully compiled')) { callback(data.includes('Successfully compiled'));
postCompilationCallback();
}
}); });
watchProcess.stderr.on('data', (err) => { watchProcess.stderr.on('data', (err) => {
process.stderr.write(err); process.stderr.write(err);
res({ success: false }); callback(false);
}); });
const processExitListener = () => watchProcess.kill(); const processExitListener = () => watchProcess.kill();
@ -58,7 +157,8 @@ async function createSwcWatchProcess(
process.on('exit', processExitListener); process.on('exit', processExitListener);
watchProcess.on('exit', () => { watchProcess.on('exit', () => {
res({ success: true }); 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(
errors: string[] = [],
export async function printDiagnostics(result: TypeCheckResult) { warnings: string[] = []
if (result.errors.length > 0) { ) {
result.errors.forEach((err) => { if (errors.length > 0) {
errors.forEach((err) => {
console.log(`${err}\n`); console.log(`${err}\n`);
}); });
console.log( console.log(`Found ${errors.length} error${errors.length > 1 ? 's' : ''}.`);
`Found ${result.errors.length} error${ } else if (warnings.length > 0) {
result.errors.length > 1 ? 's' : '' warnings.forEach((err) => {
}.`
);
} else if (result.warnings.length > 0) {
result.warnings.forEach((err) => {
console.log(`${err}\n`); 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({ const result = await runTypeCheck({
ts: require('typescript'),
workspaceRoot, workspaceRoot,
tsConfigPath, tsConfigPath,
mode: 'noEmit', mode: 'noEmit',
@ -65,7 +64,6 @@ describe('runTypeCheck', () => {
); );
await runTypeCheck({ await runTypeCheck({
ts: require('typescript'),
workspaceRoot, workspaceRoot,
tsConfigPath, tsConfigPath,
mode: 'emitDeclarationOnly', mode: 'emitDeclarationOnly',

View File

@ -1,6 +1,7 @@
import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript'; import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import * as path from 'path';
import * as chalk from 'chalk'; import * as chalk from 'chalk';
import * as path from 'path';
import type { BuilderProgram, Diagnostic, Program } from 'typescript';
import { codeFrameColumns } from '../code-frames/code-frames'; import { codeFrameColumns } from '../code-frames/code-frames';
export interface TypeCheckResult { export interface TypeCheckResult {
@ -11,10 +12,9 @@ export interface TypeCheckResult {
incremental: boolean; incremental: boolean;
} }
type TypeCheckOptions = BaseTypeCheckOptions & Mode; export type TypeCheckOptions = BaseTypeCheckOptions & Mode;
interface BaseTypeCheckOptions { interface BaseTypeCheckOptions {
ts: typeof import('typescript');
workspaceRoot: string; workspaceRoot: string;
tsConfigPath: string; tsConfigPath: string;
cacheDir?: string; cacheDir?: string;
@ -31,10 +31,91 @@ interface EmitDeclarationOnlyMode {
outDir: string; 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( export async function runTypeCheck(
options: TypeCheckOptions options: TypeCheckOptions
): Promise<TypeCheckResult> { ): 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); const config = readTsConfig(tsConfigPath);
if (config.errors.length) { if (config.errors.length) {
throw new Error(`Invalid config file: ${config.errors}`); throw new Error(`Invalid config file: ${config.errors}`);
@ -50,56 +131,39 @@ export async function runTypeCheck(
skipLibCheck: true, skipLibCheck: true,
...emitOptions, ...emitOptions,
}; };
return { ts, workspaceRoot, cacheDir, config, compilerOptions };
}
let program: function getTypeCheckResult(
| import('typescript').Program ts: typeof import('typescript'),
| import('typescript').BuilderProgram; allDiagnostics: Diagnostic[],
let incremental = false; workspaceRoot: string,
if (compilerOptions.incremental && cacheDir) { inputFilesCount: number,
incremental = true; totalFilesCount: number,
program = ts.createIncrementalProgram({ incremental: boolean = false
rootNames: config.fileNames, ) {
options: { const errors = allDiagnostics
...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 import('typescript').Program)
.concat(result.diagnostics);
const errors = await Promise.all(
allDiagnostics
.filter((d) => d.category === ts.DiagnosticCategory.Error) .filter((d) => d.category === ts.DiagnosticCategory.Error)
.map((d) => getFormattedDiagnostic(ts, workspaceRoot, d)) .map((d) => getFormattedDiagnostic(ts, workspaceRoot, d));
);
const warnings = await Promise.all( const warnings = allDiagnostics
allDiagnostics
.filter((d) => d.category === ts.DiagnosticCategory.Warning) .filter((d) => d.category === ts.DiagnosticCategory.Warning)
.map((d) => getFormattedDiagnostic(ts, workspaceRoot, d)) .map((d) => getFormattedDiagnostic(ts, workspaceRoot, d));
);
return { return {
warnings, warnings,
errors, errors,
inputFilesCount: config.fileNames.length, inputFilesCount,
totalFilesCount: program.getSourceFiles().length, totalFilesCount,
incremental, incremental,
}; };
} }
export async function getFormattedDiagnostic( export function getFormattedDiagnostic(
ts: typeof import('typescript'), ts: typeof import('typescript'),
workspaceRoot: string, workspaceRoot: string,
diagnostic: import('typescript').Diagnostic diagnostic: Diagnostic
): Promise<string> { ): string {
let message = ''; let message = '';
const reason = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); 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; projectRoot: string;
tsconfig: string; tsconfig: string;
}): Promise<void> { }): Promise<void> {
const ts = await import('typescript');
const result = await runTypeCheck({ const result = await runTypeCheck({
ts,
workspaceRoot: opts.workspaceRoot, workspaceRoot: opts.workspaceRoot,
tsConfigPath: join(opts.workspaceRoot, opts.tsconfig), tsConfigPath: join(opts.workspaceRoot, opts.tsconfig),
mode: 'noEmit', mode: 'noEmit',
}); });
await printDiagnostics(result); await printDiagnostics(result.errors, result.warnings);
if (result.errors.length > 0) { if (result.errors.length > 0) {
throw new Error('Found type errors. See above.'); throw new Error('Found type errors. See above.');

View File

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

View File

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

View File

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

View File

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