diff --git a/docs/1.guide/10.tasks.md b/docs/1.guide/10.tasks.md index ae975932ed..3fa4d6100d 100644 --- a/docs/1.guide/10.tasks.md +++ b/docs/1.guide/10.tasks.md @@ -35,7 +35,7 @@ export default defineNuxtConfig({ :: -## Define Tasks +## Define tasks Tasks can be defined in `tasks/[name].ts` files. @@ -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"; }, }); @@ -58,7 +61,7 @@ export default defineNitroTask({ ## Run tasks -To execute tasks, you can use `runNitroTask(name, )` utility. +To execute tasks, you can use `runTask(name, { payload? })` utility. **Example:** @@ -67,14 +70,14 @@ To execute tasks, you can use `runNitroTask(name, )` 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 @@ -82,9 +85,8 @@ Nitro's built-in dev server exposes tasks to be easily executed without programm This endpoint returns a list of available task names and their meta. -**Example:** - ```json +// [GET] /_nitro/tasks { "tasks": { "db:migrate": { @@ -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!" } @@ -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 "{}" ``` diff --git a/examples/database/tasks/db/migrate.ts b/examples/database/tasks/db/migrate.ts index 9bf525b493..11155f34a7 100644 --- a/examples/database/tasks/db/migrate.ts +++ b/examples/database/tasks/db/migrate.ts @@ -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`; diff --git a/src/cli/commands/tasks/index.ts b/src/cli/commands/task/index.ts similarity index 93% rename from src/cli/commands/tasks/index.ts rename to src/cli/commands/task/index.ts index 1eded9c6b6..6758b11342 100644 --- a/src/cli/commands/tasks/index.ts +++ b/src/cli/commands/task/index.ts @@ -2,7 +2,7 @@ import { defineCommand } from "citty"; export default defineCommand({ meta: { - name: "tasks", + name: "task", description: "Operate in nitro tasks (experimental)", }, subCommands: { diff --git a/src/cli/commands/tasks/list.ts b/src/cli/commands/task/list.ts similarity index 72% rename from src/cli/commands/tasks/list.ts rename to src/cli/commands/task/list.ts index aea14134b6..9ffc599fb1 100644 --- a/src/cli/commands/tasks/list.ts +++ b/src/cli/commands/task/list.ts @@ -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: { @@ -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}` : ""}` ); } }, diff --git a/src/cli/commands/tasks/run.ts b/src/cli/commands/task/run.ts similarity index 71% rename from src/cli/commands/tasks/run.ts rename to src/cli/commands/task/run.ts index d5e0e49e3b..708439c6f9 100644 --- a/src/cli/commands/tasks/run.ts +++ b/src/cli/commands/task/run.ts @@ -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: { @@ -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", diff --git a/src/cli/index.ts b/src/cli/index.ts index 308be13c10..b8d48ea5f7 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -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), }, }); diff --git a/src/imports.ts b/src/imports.ts index dd6e3248cd..fc3824b053 100644 --- a/src/imports.ts +++ b/src/imports.ts @@ -17,8 +17,8 @@ export const nitroImports: Preset[] = [ "getRouteRules", "useAppConfig", "useEvent", - "defineNitroTask", - "runNitroTask", + "defineTask", + "runTask", "defineNitroErrorHandler", ], }, diff --git a/src/nitro.ts b/src/nitro.ts index 9d7ee0767e..dfd6acbfb7 100644 --- a/src/nitro.ts +++ b/src/nitro.ts @@ -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" }, }` diff --git a/src/runtime/entries/nitro-dev.ts b/src/runtime/entries/nitro-dev.ts index f8a1ae3660..84dbfaaa24 100644 --- a/src/runtime/entries/nitro-dev.ts +++ b/src/runtime/entries/nitro-dev.ts @@ -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)); @@ -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 { @@ -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 }); }) ); diff --git a/src/runtime/task.ts b/src/runtime/task.ts index 8467d917d5..1f6857bce2 100644 --- a/src/runtime/task.ts +++ b/src/runtime/task.ts @@ -1,63 +1,70 @@ import { createError } from "h3"; import { tasks } from "#internal/nitro/virtual/tasks"; +type MaybePromise = T | Promise; + /** @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 | Promise; +/** @experimental */ +export interface TaskEvent { + name: string; + payload: TaskPayload; + context: TaskContext; +} + +export interface TaskResult { + result?: RT; +} /** @experimental */ -export interface NitroTask extends NitroTaskMeta { - run( - payload: NitroTaskPayload, - context: NitroTaskContext - ): MaybePromise<{ result?: RT }>; +export interface Task { + meta?: TaskMeta; + run(event: TaskEvent): MaybePromise<{ result?: RT }>; } /** @experimental */ -export function defineNitroTask( - def: NitroTask -): NitroTask { +export function defineTask(def: Task): Task { 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( +export async function runTask( name: string, - payload: NitroTaskPayload = {} -): Promise<{ result: RT }> { + { + payload = {}, + context = {}, + }: { payload?: TaskPayload; context?: TaskContext } = {} +): Promise> { 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; + const taskEvent: TaskEvent = { name, payload, context }; + return handler.run(taskEvent); } diff --git a/src/runtime/virtual/tasks.ts b/src/runtime/virtual/tasks.ts index 884d590590..8a8f09d021 100644 --- a/src/runtime/virtual/tasks.ts +++ b/src/runtime/virtual/tasks.ts @@ -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; meta: TaskMeta } > = {}; diff --git a/src/task.ts b/src/task.ts index 40d2762e59..65eb815dcf 100644 --- a/src/task.ts +++ b/src/task.ts @@ -2,42 +2,48 @@ import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { resolve } from "pathe"; import { ofetch } from "ofetch"; -import type { NitroTaskPayload } from "./runtime"; +import type { TaskEvent } from "./runtime"; import { NitroBuildInfo } from "nitropack"; -interface TaskRunnerOptions { +/** @experimental */ +export interface TaskRunnerOptions { cwd?: string; buildDir?: string; } -export async function runNitroTask( - name: string, - payload?: NitroTaskPayload, +/** @experimental */ +export async function runTask( + taskEvent: TaskEvent, opts?: TaskRunnerOptions ): Promise<{ result: unknown }> { - const ctx = await getTasksContext(opts); - const result = await ctx.devFetch("/_nitro/tasks/" + name); + const ctx = await _getTasksContext(opts); + const result = await ctx.devFetch(`/_nitro/tasks/${taskEvent.name}`, { + method: "POST", + body: taskEvent, + }); return result; } -export async function listNitroTasks(opts?: TaskRunnerOptions) { - const ctx = await getTasksContext(opts); +/** @experimental */ +export async function listTasks(opts?: TaskRunnerOptions) { + const ctx = await _getTasksContext(opts); const res = (await ctx.devFetch("/_nitro/tasks")) as { - tasks: Record; + tasks: Record; }; return res.tasks; } -const devHint = `(is dev server running?)`; +// --- internal --- -/** @experimental */ -async function getTasksContext(opts: TaskRunnerOptions) { +const _devHint = `(is dev server running?)`; + +async function _getTasksContext(opts: TaskRunnerOptions) { const cwd = resolve(process.cwd(), opts.cwd); const outDir = resolve(cwd, opts.buildDir || ".nitro"); const buildInfoPath = resolve(outDir, "nitro.json"); if (!existsSync(buildInfoPath)) { - throw new Error(`Missing info file: \`${buildInfoPath}\` ${devHint}`); + throw new Error(`Missing info file: \`${buildInfoPath}\` ${_devHint}`); } const buildInfo = JSON.parse( @@ -46,11 +52,11 @@ async function getTasksContext(opts: TaskRunnerOptions) { if (!buildInfo.dev?.pid || !buildInfo.dev?.workerAddress) { throw new Error( - `Missing dev server info in: \`${buildInfoPath}\` ${devHint}` + `Missing dev server info in: \`${buildInfoPath}\` ${_devHint}` ); } - if (!pidIsRunning(buildInfo.dev.pid)) { + if (!_pidIsRunning(buildInfo.dev.pid)) { throw new Error(`Dev server is not running (pid: ${buildInfo.dev.pid})`); } @@ -68,7 +74,7 @@ async function getTasksContext(opts: TaskRunnerOptions) { }; } -function pidIsRunning(pid) { +function _pidIsRunning(pid) { try { process.kill(pid, 0); return true; diff --git a/test/fixture/routes/tasks/[...name].ts b/test/fixture/routes/tasks/[...name].ts index 597f9000a4..81e834b509 100644 --- a/test/fixture/routes/tasks/[...name].ts +++ b/test/fixture/routes/tasks/[...name].ts @@ -1,7 +1,7 @@ export default eventHandler(async (event) => { const name = getRouterParam(event, "name"); const payload = { ...getQuery(event) }; - const { result } = await runNitroTask(name, payload); + const { result } = await runTask(name, { payload }); return { name, payload, diff --git a/test/fixture/tasks/db/migrate.ts b/test/fixture/tasks/db/migrate.ts index efb6d3d139..2dd0f67a06 100644 --- a/test/fixture/tasks/db/migrate.ts +++ b/test/fixture/tasks/db/migrate.ts @@ -1,7 +1,9 @@ -export default defineNitroTask({ - description: "Run database migrations", - run(payload, context) { - console.log("Running DB migration task...", { payload }); +export default defineTask({ + meta: { + description: "Run database migrations", + }, + run() { + console.log("Running DB migration task..."); return { result: "Success" }; }, }); diff --git a/test/fixture/tasks/test.ts b/test/fixture/tasks/test.ts new file mode 100644 index 0000000000..b34e736096 --- /dev/null +++ b/test/fixture/tasks/test.ts @@ -0,0 +1,9 @@ +export default defineTask({ + meta: { + description: "task to debug", + }, + run(taskEvent) { + console.log("test task", taskEvent); + return { result: taskEvent }; + }, +});