Skip to content

Commit

Permalink
fix(js): add watchIgnore and runBuildTargetDependencies options to sp…
Browse files Browse the repository at this point in the history
…eed up build (#17953)

(cherry picked from commit c6a0615)
  • Loading branch information
jaysoo authored and FrozenPandaz committed Jul 7, 2023
1 parent ea28f16 commit b39cafa
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 56 deletions.
27 changes: 20 additions & 7 deletions docs/generated/packages/js/executors/node.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,46 +28,59 @@
"host": {
"type": "string",
"default": "localhost",
"description": "The host to inspect the process on."
"description": "The host to inspect the process on.",
"x-priority": "important"
},
"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."
"description": "The port to inspect the process on. Setting port to 0 will assign random free ports to all forked processes.",
"x-priority": "important"
},
"inspect": {
"oneOf": [
{ "type": "string", "enum": ["inspect", "inspect-brk"] },
{ "type": "boolean" }
],
"description": "Ensures the app is starting with debugging.",
"default": "inspect"
"default": "inspect",
"x-priority": "important"
},
"runtimeArgs": {
"type": "array",
"description": "Extra args passed to the node process.",
"default": [],
"items": { "type": "string" }
"items": { "type": "string" },
"x-priority": "important"
},
"args": {
"type": "array",
"description": "Extra args when starting the app.",
"default": [],
"items": { "type": "string" }
"items": { "type": "string" },
"x-priority": "important"
},
"watch": {
"type": "boolean",
"description": "Enable re-building when files change.",
"default": true
"default": true,
"x-priority": "important"
},
"debounce": {
"type": "number",
"description": "Delay in milliseconds to wait before restarting. Useful to batch multiple file changes events together. Set to zero (0) to disable.",
"default": 500
"default": 500,
"x-priority": "important"
},
"runBuildTargetDependencies": {
"type": "boolean",
"description": "Whether to run dependencies before running the build. Set this to true if the project does not build libraries from source (e.g. 'buildLibsFromSource: false').",
"default": false
}
},
"additionalProperties": false,
"required": ["buildTarget"],
"examplesFile": "---\ntitle: JS Node executor examples\ndescription: This page contains examples for the @nx/js:node executor.\n---\n\nThe `@nx/js:node` executor runs the output of a build target. For example, an application uses esbuild ([`@nx/esbuild:esbuild`](/packages/esbuild/executors/esbuild)) to output the bundle to `dist/my-app` folder, which can then be executed by `@nx/js:node`.\n\n`project.json`:\n\n```json\n\"my-app\": {\n \"targets\": {\n \"serve\": {\n \"executor\": \"@nx/js:node\",\n \"options\": {\n \"buildTarget\": \"my-app:build\"\n }\n },\n \"build\": {\n \"executor\": \"@nx/esbuild:esbuild\",\n \"options\": {\n \"main\": \"my-app/src/main.ts\",\n \"output\": [\"dist/my-app\"],\n //...\n }\n },\n }\n}\n```\n\n```bash\nnpx nx serve my-app\n```\n\n## Examples\n\n{% tabs %}\n{% tab label=\"Pass extra Node CLI arguments\" %}\n\nUsing `runtimeArgs`, you can pass arguments to the underlying `node` command. For example, if you want to set [`--no-warnings`](https://nodejs.org/api/cli.html#--no-warnings) to silence all Node warnings, then add the following to the `project.json` file.\n\n```json\n\"my-app\": {\n \"targets\": {\n \"serve\": {\n \"executor\": \"@nx/js:node\",\n \"options\": {\n \"runtimeArgs\": [\"--no-warnings\"],\n //...\n },\n },\n }\n}\n```\n\n{% /tab %}\n\n{% tab label=\"Run all task dependencies\" %}\n\nIf your application build depends on other tasks, and you want those tasks to also be executed, then set the `runBuildTargetDependencies` to `true`. For example, a library may have a task to generate GraphQL schemas, which is consume by the application. In this case, you want to run the generate task before building and running the application.\n\nThis option is also useful when the build consumes a library from its output, not its source. For example, if an executor that supports `buildLibsFromSource` option has it set to `false` (e.g. [`@nx/webpack:webpack`](/packages/webpack/executors/webpack)).\n\nNote that this option will increase the build time, so use it only when necessary.\n\n```json\n\"my-app\": {\n \"targets\": {\n \"serve\": {\n \"executor\": \"@nx/js:node\",\n \"options\": {\n \"runBuildTargetDependencies\": true,\n //...\n },\n },\n }\n}\n```\n\n{% /tab %}\n\n{% /tabs %}\n",
"presets": []
},
"description": "Execute a Node application.",
Expand Down
52 changes: 52 additions & 0 deletions e2e/node/src/node-webpack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import {
readFile,
runCLI,
runCLIAsync,
runCommandUntil,
waitUntil,
tmpProjPath,
uniq,
updateFile,
updateProjectConfig,
} from '@nx/e2e/utils';
import { execSync } from 'child_process';

Expand Down Expand Up @@ -47,5 +50,54 @@ describe('Node Applications + webpack', () => {
await runCLIAsync(`build ${app} --optimization`);
const optimizedContent = readFile(`dist/apps/${app}/main.js`);
expect(optimizedContent).toContain('console.log("foo "+"bar")');

// Test that serve can re-run dependency builds.
const lib = uniq('nodelib');
runCLI(`generate @nx/js:lib ${lib} --bundler=esbuild --no-interactive`);

updateProjectConfig(app, (config) => {
// Since we read from lib from dist, we should re-build it when lib changes.
config.targets.build.options.buildLibsFromSource = false;
config.targets.serve.options.runBuildTargetDependencies = true;
return config;
});

updateFile(
`apps/${app}/src/main.ts`,
`
import { ${lib} } from '@proj/${lib}';
console.log('Hello ' + ${lib}());
`
);

const serveProcess = await runCommandUntil(
`serve ${app} --watch --runBuildTargetDependencies`,
(output) => {
return output.includes(`Hello`);
}
);

// Update library source and check that it triggers rebuild.
const terminalOutputs: string[] = [];
serveProcess.stdout.on('data', (chunk) => {
const data = chunk.toString();
terminalOutputs.push(data);
});

updateFile(
`libs/${lib}/src/index.ts`,
`export function ${lib}() { return 'should rebuild lib'; }`
);

await waitUntil(
() => {
return terminalOutputs.some((output) =>
output.includes(`should rebuild lib`)
);
},
{ timeout: 30_000, ms: 200 }
);

serveProcess.kill();
}, 300_000);
});
82 changes: 82 additions & 0 deletions packages/js/docs/node-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
title: JS Node executor examples
description: This page contains examples for the @nx/js:node executor.
---

The `@nx/js:node` executor runs the output of a build target. For example, an application uses esbuild ([`@nx/esbuild:esbuild`](/packages/esbuild/executors/esbuild)) to output the bundle to `dist/my-app` folder, which can then be executed by `@nx/js:node`.

`project.json`:

```json
"my-app": {
"targets": {
"serve": {
"executor": "@nx/js:node",
"options": {
"buildTarget": "my-app:build"
}
},
"build": {
"executor": "@nx/esbuild:esbuild",
"options": {
"main": "my-app/src/main.ts",
"output": ["dist/my-app"],
//...
}
},
}
}
```

```bash
npx nx serve my-app
```

## Examples

{% tabs %}
{% tab label="Pass extra Node CLI arguments" %}

Using `runtimeArgs`, you can pass arguments to the underlying `node` command. For example, if you want to set [`--no-warnings`](https://nodejs.org/api/cli.html#--no-warnings) to silence all Node warnings, then add the following to the `project.json` file.

```json
"my-app": {
"targets": {
"serve": {
"executor": "@nx/js:node",
"options": {
"runtimeArgs": ["--no-warnings"],
//...
},
},
}
}
```

{% /tab %}

{% tab label="Run all task dependencies" %}

If your application build depends on other tasks, and you want those tasks to also be executed, then set the `runBuildTargetDependencies` to `true`. For example, a library may have a task to generate GraphQL schemas, which is consume by the application. In this case, you want to run the generate task before building and running the application.

This option is also useful when the build consumes a library from its output, not its source. For example, if an executor that supports `buildLibsFromSource` option has it set to `false` (e.g. [`@nx/webpack:webpack`](/packages/webpack/executors/webpack)).

Note that this option will increase the build time, so use it only when necessary.

```json
"my-app": {
"targets": {
"serve": {
"executor": "@nx/js:node",
"options": {
"runBuildTargetDependencies": true,
//...
},
},
}
}
```

{% /tab %}

{% /tabs %}
106 changes: 65 additions & 41 deletions packages/js/src/executors/node/node.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,48 +114,67 @@ export async function* nodeExecutor(
childProcess: null,
promise: null,
start: async () => {
let buildFailed = false;
// Run the build
task.promise = new Promise<void>(async (resolve, reject) => {
task.childProcess = exec(
`npx nx run ${context.projectName}:${buildTarget.target}${
buildTarget.configuration ? `:${buildTarget.configuration}` : ''
}`,
if (options.runBuildTargetDependencies) {
// If task dependencies are to be run, then we need to run through CLI since `runExecutor` doesn't support it.
task.promise = new Promise<void>(async (resolve, reject) => {
task.childProcess = fork(
require.resolve('nx'),
[
'run',
`${context.projectName}:${buildTarget.target}${
buildTarget.configuration
? `:${buildTarget.configuration}`
: ''
}`,
],
{
cwd: context.root,
stdio: 'inherit',
}
);
task.childProcess.once('exit', (code) => {
if (code === 0) resolve();
else reject();
});
});
} else {
const output = await runExecutor(
buildTarget,
{
cwd: context.root,
...options.buildTargetOptions,
watch: false, // we'll handle the watch in this executor
},
(error, stdout, stderr) => {
if (
// Build succeeded
!error ||
// If task was killed then another build process has started, ignore errors.
task.killed
) {
resolve();
return;
}

logger.info(stdout);
buildFailed = true;
if (options.watch) {
logger.error(
`Build failed, waiting for changes to restart...`
);
resolve(); // Don't reject because it'll error out and kill the Nx process.
} else {
logger.error(`Build failed. See above for errors.`);
reject();
}
}
context
);
});
task.promise = new Promise(async (resolve, reject) => {
let error = false;
let event;
do {
event = await output.next();
if (event.value?.success === false) {
error = true;
}
} while (!event.done);
if (error) reject();
else resolve();
});
}

// Wait for build to finish
await task.promise;
// Wait for build to finish.
try {
await task.promise;
} catch {
// If in watch-mode, don't throw or else the process exits.
if (options.watch) {
logger.error(`Build failed, waiting for changes to restart...`);
return;
} else {
throw new Error(`Build failed. See above for errors.`);
}
}

// Task may have been stopped due to another running task.
// OR build failed, so don't start the process.
if (task.killed || buildFailed) return;
// Before running the program, check if the task has been killed (by a new change during watch).
if (task.killed) return;

// Run the program
task.promise = new Promise<void>((resolve, reject) => {
Expand All @@ -173,16 +192,17 @@ export async function* nodeExecutor(
}
);

task.childProcess.stderr.on('data', (data) => {
const handleStdErr = (data) => {
// Don't log out error if task is killed and new one has started.
// This could happen if a new build is triggered while new process is starting, since the operation is not atomic.
// Log the error in normal mode
if (!options.watch || !task.killed) {
logger.error(data.toString());
}
});

};
task.childProcess.stderr.on('data', handleStdErr);
task.childProcess.once('exit', (code) => {
task.childProcess.off('data', handleStdErr);
if (options.watch && !task.killed) {
logger.info(
`NX Process exited with code ${code}, waiting for changes to restart...`
Expand All @@ -203,7 +223,11 @@ export async function* nodeExecutor(
if (task.childProcess) {
await killTree(task.childProcess.pid, signal);
}
await task.promise;
try {
await task.promise;
} catch {
// Doesn't matter if task fails, we just need to wait until it finishes.
}
},
};

Expand Down
1 change: 1 addition & 0 deletions packages/js/src/executors/node/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export interface NodeExecutorOptions {
port: number;
watch?: boolean;
debounce?: number;
runBuildTargetDependencies?: boolean;
}
Loading

0 comments on commit b39cafa

Please sign in to comment.