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`
|
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.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
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`
|
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.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
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`
|
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.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -30,5 +30,5 @@
|
|||||||
"outputs": ["coverage/e2e/cli"]
|
"outputs": ["coverage/e2e/cli"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"implicitDependencies": ["cli"]
|
"implicitDependencies": ["cli", "js"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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",
|
"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"],
|
||||||
|
|||||||
@ -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 { 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(
|
||||||
|
|||||||
@ -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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
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;
|
||||||
|
|||||||
@ -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`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
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') {
|
||||||
|
|||||||
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,
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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() };
|
||||||
}
|
}
|
||||||
|
|||||||
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(
|
||||||
|
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.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
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;
|
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.');
|
||||||
|
|||||||
@ -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 ?? {}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user