Skip to content

Commit 4e2da72

Browse files
nam-hleclaude
andauthored
feat: add NodeTask builtin task for running Node.js scripts (#521)
## Summary - Add `NodeTask` builtin task type for running Node.js scripts (`node <script> <args>`) - Uses `script` (required) + `args` (optional) interface, distinct from ExecTask/PnpxTask patterns - Update ESLint plugin `prefer-builtin-task` rule to detect `execa("node", ...)` and suggest `NodeTask` - Update spec (bumped to 1.4.0), docs, type tests, and integration tests ## Test plan - [x] Integration tests pass (3 tests: pass, info log level, fail exit code) - [x] Type-level tests pass (17 tests including 2 new NodeTask tests) - [x] ESLint plugin tests pass (33 tests including new `preferNode` detection) - [x] Pre-commit checks pass (eslint, prettier, spell, knip, validate) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 78f2ebd commit 4e2da72

File tree

32 files changed

+307
-55
lines changed

32 files changed

+307
-55
lines changed

packages/docs/docs/getting-started/features.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,18 @@ tasks.register("lint", ExecTask, {
201201
});
202202
```
203203

204+
### NodeTask
205+
206+
Run Node.js scripts:
207+
208+
```typescript
209+
import { tasks, NodeTask } from "nadle";
210+
211+
tasks.register("seed", NodeTask, {
212+
script: "scripts/seed-db.mjs"
213+
});
214+
```
215+
204216
### NpmTask
205217

206218
Run npm commands:

packages/docs/docs/guides/defining-task.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ keywords: [nadle, defineTask, custom task, TypeScript, API, reusable]
77

88
To create reusable and configurable task logic, use the `defineTask` API. This allows you to encapsulate behavior in a type-safe, shareable way that can be used across multiple task registrations.
99

10-
This is particularly useful for implementing tasks like `CopyTask`, `ExecTask`, `PnpxTask`, or any custom logic that accepts parameters and integrates cleanly with Nadle’s runtime.
10+
This is particularly useful for implementing tasks like `CopyTask`, `ExecTask`, `NodeTask`, `PnpxTask`, or any custom logic that accepts parameters and integrates cleanly with Nadle’s runtime.
1111

1212
---
1313

packages/eslint-plugin/src/rules/prefer-builtin-task.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { isInTaskAction } from "../utils/ast-helpers.js";
55

66
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/nadlejs/nadle/blob/main/packages/eslint-plugin/docs/rules/${name}.md`);
77

8-
type MessageId = "preferExec" | "preferNpm" | "preferNpx" | "preferPnpm" | "preferPnpx" | "preferCopy" | "preferDelete";
8+
type MessageId = "preferExec" | "preferNode" | "preferNpm" | "preferNpx" | "preferPnpm" | "preferPnpx" | "preferCopy" | "preferDelete";
99

1010
const EXEC_APIS = new Set(["execa", "exec", "execFile", "spawn"]);
1111

@@ -95,6 +95,11 @@ function detectPattern(node: TSESTree.CallExpression): MessageId | undefined {
9595
return "preferNpx";
9696
}
9797

98+
// NodeTask: execa("node", ...)
99+
if (name === "execa" && !isMember && hasFirstArg(node, "node")) {
100+
return "preferNode";
101+
}
102+
98103
// NpmTask: execa("npm", ...)
99104
if (name === "execa" && !isMember && hasFirstArg(node, "npm")) {
100105
return "preferNpm";
@@ -157,6 +162,7 @@ export default createRule({
157162
messages: {
158163
preferNpm: "Consider using NpmTask instead of calling npm via '{{name}}'.",
159164
preferNpx: "Consider using NpxTask instead of calling npx via '{{name}}'.",
165+
preferNode: "Consider using NodeTask instead of calling node via '{{name}}'.",
160166
preferPnpm: "Consider using PnpmTask instead of calling pnpm via '{{name}}'.",
161167
preferCopy: "Consider using CopyTask instead of '{{name}}' for file copying.",
162168
preferPnpx: "Consider using PnpxTask instead of calling pnpm exec via '{{name}}'.",

packages/eslint-plugin/test/rules/prefer-builtin-task.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ ruleTester.run("prefer-builtin-task", rule, {
8181
errors: [{ messageId: "preferNpx", data: { name: "execa" } }],
8282
code: 'tasks.register("build", async () => { await execa("npx", ["tsc"]); });'
8383
},
84+
// NodeTask: execa("node", ...)
85+
{
86+
errors: [{ messageId: "preferNode", data: { name: "execa" } }],
87+
code: 'tasks.register("run", async () => { await execa("node", ["script.js"]); });'
88+
},
8489
// NpmTask: execa("npm", ...)
8590
{
8691
errors: [{ messageId: "preferNpm", data: { name: "execa" } }],

packages/nadle/index.api.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ export interface NadleFileOptions extends Partial<NadleBaseOptions> {
116116
readonly alias?: AliasOption;
117117
}
118118

119+
// @public
120+
export const NodeTask: Task<NodeTaskOptions>;
121+
122+
// @public
123+
export interface NodeTaskOptions {
124+
readonly args?: MaybeArray<string>;
125+
readonly script: string;
126+
}
127+
119128
// @public
120129
export const NpmTask: Task<NpmTaskOptions>;
121130

packages/nadle/src/builtin-tasks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./npm-task.js";
22
export * from "./npx-task.js";
3+
export * from "./node-task.js";
34
export * from "./pnpm-task.js";
45
export * from "./pnpx-task.js";
56
export * from "./exec-task.js";
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { execa } from "execa";
2+
3+
import type { MaybeArray } from "../core/index.js";
4+
import { defineTask } from "../core/registration/define-task.js";
5+
6+
/**
7+
* Options for the NodeTask.
8+
*/
9+
export interface NodeTaskOptions {
10+
/** The script to execute via node. */
11+
readonly script: string;
12+
/** Arguments for the script. Defaults to none. */
13+
readonly args?: MaybeArray<string>;
14+
}
15+
16+
/**
17+
* Task for running Node.js scripts.
18+
*
19+
* Executes `node <script> <args>` in the given working directory.
20+
*/
21+
export const NodeTask = defineTask<NodeTaskOptions>({
22+
run: async ({ options, context }) => {
23+
const args = options.args == null ? [] : typeof options.args === "string" ? [options.args] : options.args;
24+
25+
context.logger.info(`Running node script: node ${options.script} ${args.join(" ")}`);
26+
27+
const subprocess = execa("node", [options.script, ...args], { all: true, cwd: context.workingDir, env: { FORCE_COLOR: "1" } });
28+
29+
subprocess.all?.on("data", (chunk) => {
30+
context.logger.log(chunk.toString());
31+
});
32+
33+
await subprocess;
34+
35+
context.logger.info(`Node script completed successfully.`);
36+
}
37+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { tasks, NodeTask } from "nadle";
2+
3+
tasks.register("pass", NodeTask, { script: "./src/pass.js" });
4+
tasks.register("fail", NodeTask, { script: "./src/fail.js" });
5+
tasks.register("echo", NodeTask, { args: "hello", script: "./src/echo.js" });
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "@nadle/internal-nadle-test-fixtures-node-task",
3+
"type": "module",
4+
"private": true,
5+
"dependencies": {
6+
"nadle": "workspace:*"
7+
},
8+
"nadle": {
9+
"root": true
10+
}
11+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log(process.argv.slice(2).join(" "));

0 commit comments

Comments
 (0)