feat(js): add nrwl/js:node executor to serve node apps
This commit is contained in:
parent
be908e2be6
commit
1139c616e1
60
docs/angular/api-js/executors/node.md
Normal file
60
docs/angular/api-js/executors/node.md
Normal 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
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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",
|
||||
|
||||
60
docs/node/api-js/executors/node.md
Normal file
60
docs/node/api-js/executors/node.md
Normal 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
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
60
docs/react/api-js/executors/node.md
Normal file
60
docs/react/api-js/executors/node.md
Normal 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
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -30,5 +30,5 @@
|
||||
"outputs": ["coverage/e2e/cli"]
|
||||
}
|
||||
},
|
||||
"implicitDependencies": ["cli"]
|
||||
"implicitDependencies": ["cli", "js"]
|
||||
}
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
4
packages/js/src/executors/node/compat.ts
Normal file
4
packages/js/src/executors/node/compat.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { convertNxExecutor } from '@nrwl/devkit';
|
||||
import nodeExecutor from './node.impl';
|
||||
|
||||
export default convertNxExecutor(nodeExecutor);
|
||||
@ -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);
|
||||
185
packages/js/src/executors/node/node.impl.ts
Normal file
185
packages/js/src/executors/node/node.impl.ts
Normal 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;
|
||||
16
packages/js/src/executors/node/schema.d.ts
vendored
Normal file
16
packages/js/src/executors/node/schema.d.ts
vendored
Normal 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;
|
||||
}
|
||||
67
packages/js/src/executors/node/schema.json
Normal file
67
packages/js/src/executors/node/schema.json
Normal 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"]
|
||||
}
|
||||
@ -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"],
|
||||
|
||||
@ -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',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
files,
|
||||
mainOutputPath: resolve(
|
||||
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;
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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 () => {
|
||||
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;
|
||||
|
||||
@ -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`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,13 +11,12 @@ export function checkDependencies(
|
||||
context: ExecutorContext,
|
||||
tsConfigPath: string
|
||||
): {
|
||||
shouldContinue: boolean;
|
||||
tmpTsConfig: string | null;
|
||||
projectRoot: string;
|
||||
projectDependencies: DependentBuildableProjectNode[];
|
||||
} {
|
||||
const projectGraph = readCachedProjectGraph();
|
||||
const { target, dependencies } = calculateProjectDependencies(
|
||||
const { target, dependencies, nonBuildableDependencies } =
|
||||
calculateProjectDependencies(
|
||||
projectGraph,
|
||||
context.root,
|
||||
context.projectName,
|
||||
@ -27,6 +25,16 @@ export function checkDependencies(
|
||||
);
|
||||
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(
|
||||
tmpTsConfig: createTmpTsConfig(
|
||||
join(context.root, tsConfigPath),
|
||||
context.root,
|
||||
projectRoot,
|
||||
dependencies
|
||||
),
|
||||
projectRoot,
|
||||
projectDependencies: dependencies,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
shouldContinue: true,
|
||||
tmpTsConfig: null,
|
||||
projectRoot,
|
||||
projectDependencies: dependencies,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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') {
|
||||
|
||||
21
packages/js/src/utils/schema.d.ts
vendored
21
packages/js/src/utils/schema.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
||||
@ -1,54 +1,153 @@
|
||||
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}`;
|
||||
|
||||
const postCompilationOperator = () =>
|
||||
tap(({ success }) => {
|
||||
if (success) {
|
||||
void postCompilationCallback();
|
||||
}
|
||||
});
|
||||
|
||||
const compile$ = new Observable<{ success: boolean }>((subscriber) => {
|
||||
if (normalizedOptions.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();
|
||||
logger.log(swcCmdLog.replace(/\n/, ''));
|
||||
await postCompilationCallback();
|
||||
return { success: true };
|
||||
subscriber.next({ success: swcCmdLog.includes('Successfully compiled') });
|
||||
|
||||
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,
|
||||
postCompilationCallback: () => void | Promise<void>
|
||||
): Promise<{ success: boolean }> {
|
||||
return new Promise((res) => {
|
||||
callback: (success: boolean) => void
|
||||
) {
|
||||
const watchProcess = exec(swcCmd);
|
||||
|
||||
watchProcess.stdout.on('data', (data) => {
|
||||
process.stdout.write(data);
|
||||
if (data.includes('Successfully compiled')) {
|
||||
postCompilationCallback();
|
||||
}
|
||||
callback(data.includes('Successfully compiled'));
|
||||
});
|
||||
|
||||
watchProcess.stderr.on('data', (err) => {
|
||||
process.stderr.write(err);
|
||||
res({ success: false });
|
||||
callback(false);
|
||||
});
|
||||
|
||||
const processExitListener = () => watchProcess.kill();
|
||||
@ -58,7 +157,8 @@ async function createSwcWatchProcess(
|
||||
process.on('exit', processExitListener);
|
||||
|
||||
watchProcess.on('exit', () => {
|
||||
res({ success: true });
|
||||
});
|
||||
callback(true);
|
||||
});
|
||||
|
||||
return { close: () => watchProcess.kill() };
|
||||
}
|
||||
|
||||
1
packages/js/src/utils/typescript/__mocks__/plugin-a.ts
Normal file
1
packages/js/src/utils/typescript/__mocks__/plugin-a.ts
Normal file
@ -0,0 +1 @@
|
||||
export const before = () => {};
|
||||
1
packages/js/src/utils/typescript/__mocks__/plugin-b.ts
Normal file
1
packages/js/src/utils/typescript/__mocks__/plugin-b.ts
Normal file
@ -0,0 +1 @@
|
||||
export const after = () => {};
|
||||
86
packages/js/src/utils/typescript/compile-typescript-files.ts
Normal file
86
packages/js/src/utils/typescript/compile-typescript-files.ts
Normal 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();
|
||||
};
|
||||
});
|
||||
}
|
||||
40
packages/js/src/utils/typescript/load-ts-plugins.spec.ts
Normal file
40
packages/js/src/utils/typescript/load-ts-plugins.spec.ts
Normal 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: [],
|
||||
});
|
||||
}
|
||||
});
|
||||
86
packages/js/src/utils/typescript/load-ts-plugins.ts
Normal file
86
packages/js/src/utils/typescript/load-ts-plugins.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -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.`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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();
|
||||
|
||||
const allDiagnostics = ts
|
||||
.getPreEmitDiagnostics(program as import('typescript').Program)
|
||||
.concat(result.diagnostics);
|
||||
|
||||
const errors = await Promise.all(
|
||||
allDiagnostics
|
||||
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))
|
||||
);
|
||||
.map((d) => getFormattedDiagnostic(ts, workspaceRoot, d));
|
||||
|
||||
const warnings = await Promise.all(
|
||||
allDiagnostics
|
||||
const warnings = allDiagnostics
|
||||
.filter((d) => d.category === ts.DiagnosticCategory.Warning)
|
||||
.map((d) => getFormattedDiagnostic(ts, workspaceRoot, d))
|
||||
);
|
||||
.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');
|
||||
|
||||
38
packages/js/src/utils/typescript/types.ts
Normal file
38
packages/js/src/utils/typescript/types.ts
Normal 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>;
|
||||
}
|
||||
@ -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.');
|
||||
|
||||
@ -88,5 +88,6 @@ function readDeps(packageJsonDeps: any) {
|
||||
return [
|
||||
...Object.keys(packageJsonDeps?.dependencies ?? {}),
|
||||
...Object.keys(packageJsonDeps?.devDependencies ?? {}),
|
||||
...Object.keys(packageJsonDeps?.peerDependencies ?? {}),
|
||||
];
|
||||
}
|
||||
|
||||
@ -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,10 +43,8 @@ export function calculateProjectDependencies(
|
||||
)
|
||||
.map((dep) => {
|
||||
const depNode = projGraph.nodes[dep] || projGraph.externalNodes[dep];
|
||||
if (
|
||||
depNode.type === ProjectType.lib &&
|
||||
isBuildable(targetName, depNode)
|
||||
) {
|
||||
if (depNode.type === ProjectType.lib) {
|
||||
if (isBuildable(targetName, depNode)) {
|
||||
const libPackageJson = readJsonFile(
|
||||
join(root, depNode.data.root, 'package.json')
|
||||
);
|
||||
@ -61,6 +64,9 @@ export function calculateProjectDependencies(
|
||||
),
|
||||
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(
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -110,7 +110,6 @@ const IGNORE_MATCHES = {
|
||||
'karma-jasmine-html-reporter',
|
||||
'webpack',
|
||||
'webpack-dev-server',
|
||||
,
|
||||
'@nrwl/cli',
|
||||
'@nrwl/jest',
|
||||
'@nrwl/linter',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user