Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(tasks): stablize api #2178

Merged
merged 6 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions docs/1.guide/10.tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default defineNuxtConfig({
::


## Define Tasks
## Define tasks

Tasks can be defined in `tasks/[name].ts` files.

Expand All @@ -44,10 +44,13 @@ Nested directories are supported. The task name will be joined with `:`. (Exampl
**Example:**

```ts [tasks/db/migrate.ts]
export default defineNitroTask({
description: "Run database migrations",
run(payload, context) {
console.log("Running DB migration task...", { payload });
export default defineTask({
meta: {
name: "db:migrate",
description: "Run database migrations",
},
run({ payload, context }) {
console.log("Running DB migration task...");
return "Success";
},
});
Expand All @@ -58,7 +61,7 @@ export default defineNitroTask({

## Run tasks

To execute tasks, you can use `runNitroTask(name, <payload>)` utility.
To execute tasks, you can use `runTask(name, { payload? })` utility.

**Example:**

Expand All @@ -67,24 +70,23 @@ To execute tasks, you can use `runNitroTask(name, <payload>)` utility.
export default eventHandler(async (event) => {
// IMPORTANT: Authenticate user and validate payload!
const payload = { ...getQuery(event) };
const { result } = await runNitroTask("db:migrate", payload);
const { result } = await runTask("db:migrate", { payload });
return { result };
});
```

## Run tasks with dev server

Nitro's built-in dev server exposes tasks to be easily executed without programmatic .usage.
Nitro's built-in dev server exposes tasks to be easily executed without programmatic usage.

### Using API routes

#### `/_nitro/tasks`

This endpoint returns a list of available task names and their meta.

**Example:**

```json
// [GET] /_nitro/tasks
{
"tasks": {
"db:migrate": {
Expand All @@ -98,9 +100,8 @@ This endpoint returns a list of available task names and their meta.

This endpoint executes a task. You can provide a payload using both query parameters and body JSON payload.

**Example:** (`/_nitro/tasks/db:migrate`)

```json
// [GET] /_nitro/tasks/db:migrate
{
"result": "Database migrations completed!"
}
Expand All @@ -114,11 +115,11 @@ This endpoint executes a task. You can provide a payload using both query parame
#### List tasks

```sh
nitro tasks list
nitro task list
```

#### Run a task

```sh
nitro tasks run db:migrate --payload "{}"
nitro task run db:migrate --payload "{}"
```
10 changes: 6 additions & 4 deletions examples/database/tasks/db/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export default defineNitroTask({
description: "Run database migrations",
async run(payload, context) {
export default defineTask({
meta: {
description: "Run database migrations",
},
async run() {
const db = useDatabase();

console.log("Running database migrations...", { payload, context });
console.log("Running database migrations...");

// Create users table
await db.sql`DROP TABLE IF EXISTS users`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineCommand } from "citty";

export default defineCommand({
meta: {
name: "tasks",
name: "task",
description: "Operate in nitro tasks (experimental)",
},
subCommands: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineCommand } from "citty";
import { resolve } from "pathe";
import { consola } from "consola";
import { listNitroTasks } from "../../../task";
import { listTasks } from "../../../task";

export default defineCommand({
meta: {
Expand All @@ -16,10 +16,10 @@ export default defineCommand({
},
async run({ args }) {
const cwd = resolve((args.dir || args.cwd || ".") as string);
const tasks = await listNitroTasks({ cwd, buildDir: ".nitro" });
const tasks = await listTasks({ cwd, buildDir: ".nitro" });
for (const [name, task] of Object.entries(tasks)) {
consola.log(
` - \`${name}\`${task.description ? ` - ${task.description}` : ""}`
` - \`${name}\`${task.meta?.description ? ` - ${task.meta.description}` : ""}`
);
}
},
Expand Down
18 changes: 14 additions & 4 deletions src/cli/commands/tasks/run.ts β†’ src/cli/commands/task/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineCommand } from "citty";
import { resolve } from "pathe";
import destr from "destr";
import { consola } from "consola";
import { runNitroTask } from "../../../task";
import { runTask } from "../../../task";

export default defineCommand({
meta: {
Expand All @@ -28,10 +28,20 @@ export default defineCommand({
async run({ args }) {
const cwd = resolve((args.dir || args.cwd || ".") as string);
consola.info(`Running task \`${args.name}\`...`);
let payload: any = destr(args.payload || "{}");
if (typeof payload !== "object") {
consola.error(
`Invalid payload: \`${args.payload}\` (it should be a valid JSON object)`
);
payload = undefined;
}
try {
const { result } = await runNitroTask(
args.name,
destr(args.payload || "{}"),
const { result } = await runTask(
{
name: args.name,
context: {},
payload,
},
{
cwd,
buildDir: ".nitro",
Expand Down
2 changes: 1 addition & 1 deletion src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const main = defineCommand({
dev: () => import("./commands/dev").then((r) => r.default),
build: () => import("./commands/build").then((r) => r.default),
prepare: () => import("./commands/prepare").then((r) => r.default),
tasks: () => import("./commands/tasks").then((r) => r.default),
task: () => import("./commands/task").then((r) => r.default),
},
});

Expand Down
4 changes: 2 additions & 2 deletions src/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export const nitroImports: Preset[] = [
"getRouteRules",
"useAppConfig",
"useEvent",
"defineNitroTask",
"runNitroTask",
"defineTask",
"runTask",
"defineNitroErrorHandler",
],
},
Expand Down
8 changes: 5 additions & 3 deletions src/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,12 @@ export const tasks = {
.map(
([name, task]) =>
`"${name}": {
description: ${JSON.stringify(task.description)},
get: ${
meta: {
description: ${JSON.stringify(task.description)},
},
resolve: ${
task.handler
? `() => import("${normalize(task.handler)}")`
? `() => import("${normalize(task.handler)}").then(r => r.default || r)`
: "undefined"
},
}`
Expand Down
12 changes: 7 additions & 5 deletions src/runtime/entries/nitro-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import wsAdapter from "crossws/adapters/node";
import { nitroApp } from "../app";
import { trapUnhandledNodeErrors } from "../utils";
import { runNitroTask } from "../task";
import { runTask } from "../task";
import { tasks } from "#internal/nitro/virtual/tasks";

const server = new Server(toNodeListener(nitroApp.h3App));
Expand Down Expand Up @@ -61,8 +61,8 @@ nitroApp.router.get(
defineEventHandler(async (event) => {
const _tasks = await Promise.all(
Object.entries(tasks).map(async ([name, task]) => {
const _task = await task.get().then((r) => r.default);
return [name, { description: _task.description }];
const _task = await task.resolve?.();
return [name, { description: _task?.meta?.description }];
})
);
return {
Expand All @@ -76,9 +76,11 @@ nitroApp.router.use(
const name = getRouterParam(event, "name");
const payload = {
...getQuery(event),
...(await readBody(event).catch(() => ({}))),
...(await readBody(event)
.then((r) => r?.payload)
.catch(() => ({}))),
};
return await runNitroTask(name, payload);
return await runTask(name, { payload });
})
);

Expand Down
57 changes: 32 additions & 25 deletions src/runtime/task.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,70 @@
import { createError } from "h3";
import { tasks } from "#internal/nitro/virtual/tasks";

type MaybePromise<T> = T | Promise<T>;

/** @experimental */
export interface NitroTaskContext {}
export interface TaskContext {}

/** @experimental */
export interface NitroTaskPayload {
export interface TaskPayload {
[key: string]: unknown;
}

/** @experimental */
export interface NitroTaskMeta {
export interface TaskMeta {
name?: string;
description?: string;
}

type MaybePromise<T> = T | Promise<T>;
/** @experimental */
export interface TaskEvent {
name: string;
payload: TaskPayload;
context: TaskContext;
}

export interface TaskResult<RT = unknown> {
result?: RT;
}

/** @experimental */
export interface NitroTask<RT = unknown> extends NitroTaskMeta {
run(
payload: NitroTaskPayload,
context: NitroTaskContext
): MaybePromise<{ result?: RT }>;
export interface Task<RT = unknown> {
meta?: TaskMeta;
run(event: TaskEvent): MaybePromise<{ result?: RT }>;
}

/** @experimental */
export function defineNitroTask<RT = unknown>(
def: NitroTask<RT>
): NitroTask<RT> {
export function defineTask<RT = unknown>(def: Task<RT>): Task<RT> {
if (typeof def.run !== "function") {
def.run = () => {
throw new TypeError("Nitro task must implement a `run` method!");
throw new TypeError("Task must implement a `run` method!");
};
}
return def;
}

/** @experimental */
export async function runNitroTask<RT = unknown>(
export async function runTask<RT = unknown>(
name: string,
payload: NitroTaskPayload = {}
): Promise<{ result: RT }> {
{
payload = {},
context = {},
}: { payload?: TaskPayload; context?: TaskContext } = {}
): Promise<TaskResult<RT>> {
if (!(name in tasks)) {
throw createError({
message: `Nitro task \`${name}\` is not available!`,
message: `Task \`${name}\` is not available!`,
statusCode: 404,
});
}
if (!tasks[name].get) {
if (!tasks[name].resolve) {
throw createError({
message: `Nitro task \`${name}\` is not implemented!`,
message: `Task \`${name}\` is not implemented!`,
statusCode: 501,
});
}
const context: NitroTaskContext = {};
const handler = await tasks[name].get().then((mod) => mod.default);
const { result } = await handler.run(payload, context);
return {
result: result as RT,
};
const handler = (await tasks[name].resolve()) as Task<RT>;
const taskEvent: TaskEvent = { name, payload, context };
return handler.run(taskEvent);
}
4 changes: 2 additions & 2 deletions src/runtime/virtual/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NitroTask } from "../task";
import type { Task, TaskMeta } from "../task";

export const tasks: Record<
string,
{ get: () => Promise<{ default: NitroTask }>; description?: string }
{ resolve?: () => Promise<Task>; meta: TaskMeta }
> = {};
Loading