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: various improvements around tasks api #2175

Merged
merged 5 commits into from
Feb 26, 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
124 changes: 124 additions & 0 deletions docs/1.guide/10.tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
icon: codicon:run-all
---

# Tasks

> Nitro tasks allow on-off operations in runtime.

## Opt-in to the experimental feature

> [!IMPORTANT]
> Tasks support is currently experimental and available in [nightly channel](/guide/nightly).
> See [unjs/nitro#1974](https://github.com/unjs/nitro/issues/1974) for the relavant discussion.

In order to tasks API you need to enable experimental feature flag.

::code-group
```ts [nitro.config.ts]
export default defineNitroConfig({
experimental: {
tasks: true
}
})
```

```ts [nuxt.config.ts]
export default defineNuxtConfig({
nitro: {
experimental: {
tasks: true
}
}
})
```
::


## Define Tasks

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

Nested directories are supported. The task name will be joined with `:`. (Example: `tasks/db/migrate.ts`task name will be `db:migrate`)

**Example:**

```ts [tasks/db/migrate.ts]
export default defineNitroTask({
description: "Run database migrations",
run(payload, context) {
console.log("Running DB migration task...", { payload });
return "Success";
},
});
```

> [!NOTE]
> Use `server/tasks/db/migrate.ts` for Nuxt.

## Run tasks

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

**Example:**

```ts
// api/migrate.ts
export default eventHandler(async (event) => {
// IMPORTANT: Authenticate user and validate payload!
const payload = { ...getQuery(event) };
const { result } = await runNitroTask("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.

### Using API routes

#### `/_nitro/tasks`

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

**Example:**

```json
{
"tasks": {
"db:migrate": {
"description": "Run database migrations"
}
}
}
```

#### `/_nitro/tasks/:name`

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

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

```json
{
"result": "Database migrations completed!"
}
```

### Usin CLI

> [!IMPORTANT]
> It is only possible to run these commands while the **dev server is running**. You should run them in a second terminal.

#### List tasks

```sh
nitro tasks list
```

#### Run a task

```sh
nitro tasks run db:migrate --payload "{}"
```
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions examples/database/nitro.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export default defineNitroConfig({
experimental: {
database: true,
tasks: true,
},
});
16 changes: 16 additions & 0 deletions examples/database/tasks/db/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default defineNitroTask({
description: "Run database migrations",
async run(payload, context) {
const db = useDatabase();

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

// Create users table
await db.sql`DROP TABLE IF EXISTS users`;
await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`;

return {
result: "Database migrations complete!",
};
},
});
41 changes: 13 additions & 28 deletions src/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,37 +108,22 @@ export async function createNitro(
}

// Tasks
const scannedTasks = await scanTasks(nitro);
for (const scannedTask of scannedTasks) {
if (scannedTask.name in nitro.options.tasks) {
if (!nitro.options.tasks[scannedTask.name].handler) {
nitro.options.tasks[scannedTask.name].handler = scannedTask.handler;
if (nitro.options.experimental.tasks) {
const scannedTasks = await scanTasks(nitro);
for (const scannedTask of scannedTasks) {
if (scannedTask.name in nitro.options.tasks) {
if (!nitro.options.tasks[scannedTask.name].handler) {
nitro.options.tasks[scannedTask.name].handler = scannedTask.handler;
}
} else {
nitro.options.tasks[scannedTask.name] = {
handler: scannedTask.handler,
description: "",
};
}
} else {
nitro.options.tasks[scannedTask.name] = {
handler: scannedTask.handler,
description: "",
};
}
}
const taskNames = Object.keys(nitro.options.tasks).sort();
if (taskNames.length > 0) {
consola.warn(
`Nitro tasks are experimental and API may change in the future releases!`
);
consola.log(
`Available Tasks:\n\n${taskNames
.map(
(t) =>
` - \`${t}\`${
nitro.options.tasks[t].description
? ` - ${nitro.options.tasks[t].description}`
: ""
}`
)
.join("\n")}`
);
}

nitro.options.virtual["#internal/nitro/virtual/tasks"] = () => `
export const tasks = {
${Object.entries(nitro.options.tasks)
Expand Down
15 changes: 8 additions & 7 deletions src/runtime/entries/nitro-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,15 @@ const listener = server.listen(listenAddress, () => {
// Register tasks handlers
nitroApp.router.get(
"/_nitro/tasks",
defineEventHandler((event) => {
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 }];
})
);
return {
tasks: Object.fromEntries(
Object.entries(tasks).map(([name, task]) => [
name,
{ description: task.description },
])
),
tasks: Object.fromEntries(_tasks),
};
})
);
Expand Down
7 changes: 4 additions & 3 deletions src/runtime/task.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { createError } from "h3";
import { useNitroApp, type NitroApp } from "./app";
import { tasks } from "#internal/nitro/virtual/tasks";

/** @experimental */
Expand All @@ -16,12 +15,14 @@ export interface NitroTaskMeta {
description?: string;
}

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

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

/** @experimental */
Expand Down Expand Up @@ -55,7 +56,7 @@ export async function runNitroTask<RT = unknown>(
}
const context: NitroTaskContext = {};
const handler = await tasks[name].get().then((mod) => mod.default);
const { result } = handler.run(payload, context);
const { result } = await handler.run(payload, context);
return {
result: result as RT,
};
Expand Down
6 changes: 6 additions & 0 deletions src/types/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,12 @@ export interface NitroOptions extends PresetOptions {
* @see https://nitro.unjs.io/guide/websocket
*/
database?: boolean;
/**
* Enable experimental Tasks support
*
* @see https://nitro.unjs.io/guide/tasks
*/
tasks?: boolean;
};
future: {
nativeSWR: boolean;
Expand Down
1 change: 1 addition & 0 deletions test/fixture/nitro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export default defineNitroConfig({
wasm: true,
envExpansion: true,
database: true,
tasks: true,
},
cloudflare: {
pages: {
Expand Down