Skip to content

Commit

Permalink
feat: experimental nitro tasks (#1929)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Nov 30, 2023
1 parent 081aa27 commit 5477a81
Show file tree
Hide file tree
Showing 18 changed files with 359 additions and 7 deletions.
12 changes: 12 additions & 0 deletions src/cli/commands/tasks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineCommand } from "citty";

export default defineCommand({
meta: {
name: "tasks",
description: "Operate in nitro tasks (experimental)",
},
subCommands: {
list: () => import("./list").then((r) => r.default),
run: () => import("./run").then((r) => r.default),
},
});
26 changes: 26 additions & 0 deletions src/cli/commands/tasks/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { defineCommand } from "citty";
import { resolve } from "pathe";
import { consola } from "consola";
import { listNitroTasks } from "../../../task";

export default defineCommand({
meta: {
name: "run",
description: "List available tasks (experimental)",
},
args: {
dir: {
type: "string",
description: "project root directory",
},
},
async run({ args }) {
const cwd = resolve((args.dir || args.cwd || ".") as string);
const tasks = await listNitroTasks({ cwd, buildDir: ".nitro" });
for (const [name, task] of Object.entries(tasks)) {
consola.log(
` - \`${name}\`${task.description ? ` - ${task.description}` : ""}`
);
}
},
});
46 changes: 46 additions & 0 deletions src/cli/commands/tasks/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { defineCommand } from "citty";
import { resolve } from "pathe";
import destr from "destr";
import { consola } from "consola";
import { runNitroTask } from "../../../task";

export default defineCommand({
meta: {
name: "run",
description:
"Run a runtime task in the currently running dev server (experimental)",
},
args: {
name: {
type: "positional",
description: "task name",
required: true,
},
dir: {
type: "string",
description: "project root directory",
},
payload: {
type: "string",
description: "payload json to pass to the task",
},
},
async run({ args }) {
const cwd = resolve((args.dir || args.cwd || ".") as string);
consola.info(`Running task \`${args.name}\`...`);
try {
const { result } = await runNitroTask(
args.name,
destr(args.payload || "{}"),
{
cwd,
buildDir: ".nitro",
}
);
consola.success("Result:", result);
} catch (err) {
consola.error(`Failed to run task \`${args.name}\`: ${err.message}`);
process.exit(1); // eslint-disable-line unicorn/no-process-exit
}
},
});
1 change: 1 addition & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +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),
},
});

Expand Down
6 changes: 3 additions & 3 deletions src/dev/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import defaultErrorHandler from "./error";

export interface NitroWorker {
worker: Worker;
address: { host: string; port: number } | { socketPath: string };
address: { host: string; port: number; socketPath?: string };
}

export interface NitroDevServer {
Expand Down Expand Up @@ -97,7 +97,7 @@ async function killWorker(worker: NitroWorker, nitro: Nitro) {
await worker.worker.terminate();
worker.worker = null;
}
if ("socketPath" in worker.address && existsSync(worker.address.socketPath)) {
if (worker.address.socketPath && existsSync(worker.address.socketPath)) {
await fsp.rm(worker.address.socketPath).catch(() => {});
}
}
Expand Down Expand Up @@ -210,7 +210,7 @@ export function createDevServer(nitro: Nitro): NitroDevServer {
if (!address) {
return;
}
if ("socketPath" in address) {
if (address.socketPath) {
try {
accessSync(address.socketPath);
} catch (err) {
Expand Down
2 changes: 2 additions & 0 deletions src/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const nitroImports: Preset[] = [
"getRouteRules",
"useAppConfig",
"useEvent",
"defineNitroTask",
"runNitroTask",
"defineNitroErrorHandler",
],
},
Expand Down
54 changes: 52 additions & 2 deletions src/nitro.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { existsSync } from "node:fs";
import { resolve } from "pathe";
import { resolve, normalize } from "pathe";
import { createHooks, createDebugger } from "hookable";
import { createUnimport } from "unimport";
import { defu } from "defu";
Expand All @@ -11,7 +11,7 @@ import {
normalizeRouteRules,
normalizeRuntimeConfig,
} from "./options";
import { scanModules, scanPlugins } from "./scan";
import { scanModules, scanPlugins, scanTasks } from "./scan";
import { createStorage } from "./storage";
import { resolveNitroModule } from "./module";

Expand Down Expand Up @@ -107,6 +107,56 @@ 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;
}
} 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)
.map(
([name, task]) =>
`"${name}": {
description: ${JSON.stringify(task.description)},
get: ${
task.handler
? `() => import("${normalize(task.handler)}")`
: "undefined"
},
}`
)
.join(",\n")}
};
`;

// Auto imports
if (nitro.options.imports) {
nitro.unimport = createUnimport(nitro.options.imports);
Expand Down
1 change: 1 addition & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const NitroDefaults: NitroConfig = {
publicAssets: [],
serverAssets: [],
plugins: [],
tasks: {},
imports: {
exclude: [],
dirs: [],
Expand Down
36 changes: 35 additions & 1 deletion src/runtime/entries/nitro-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ import { join } from "node:path";
import { mkdirSync } from "node:fs";
import { threadId, parentPort } from "node:worker_threads";
import { isWindows, provider } from "std-env";
import { toNodeListener } from "h3";
import {
defineEventHandler,
getQuery,
getRouterParam,
toNodeListener,
readBody,
} from "h3";
import { nitroApp } from "../app";
import { trapUnhandledNodeErrors } from "../utils";
import { runNitroTask } from "../task";
import { tasks } from "#internal/nitro/virtual/tasks";

const server = new Server(toNodeListener(nitroApp.h3App));

Expand Down Expand Up @@ -41,6 +49,32 @@ const listener = server.listen(listenAddress, () => {
});
});

// Register tasks handlers
nitroApp.router.get(
"/_nitro/tasks",
defineEventHandler((event) => {
return {
tasks: Object.fromEntries(
Object.entries(tasks).map(([name, task]) => [
name,
{ description: task.description },
])
),
};
})
);
nitroApp.router.use(
"/_nitro/tasks/:name",
defineEventHandler(async (event) => {
const name = getRouterParam(event, "name");
const payload = {
...getQuery(event),
...(await readBody(event).catch(() => ({}))),
};
return await runNitroTask(name, payload);
})
);

// Trap unhandled errors
trapUnhandledNodeErrors();

Expand Down
1 change: 1 addition & 0 deletions src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { useRuntimeConfig, useAppConfig } from "./config";
export * from "./cache";
export { useNitroApp } from "./app";
export * from "./plugin";
export * from "./task";
export * from "./renderer";
export { getRouteRules } from "./route-rules";
export { useStorage } from "./storage";
Expand Down
62 changes: 62 additions & 0 deletions src/runtime/task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { createError } from "h3";
import { useNitroApp, type NitroApp } from "./app";
import { tasks } from "#internal/nitro/virtual/tasks";

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

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

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

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

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

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

export const tasks: Record<
string,
{ get: () => Promise<{ default: NitroTask }>; description?: string }
> = {};
11 changes: 11 additions & 0 deletions src/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ export async function scanPlugins(nitro: Nitro) {
return files.map((f) => f.fullPath);
}

export async function scanTasks(nitro: Nitro) {
const files = await scanFiles(nitro, "tasks");
return files.map((f) => {
const name = f.path
.replace(/\/index$/, "")
.replace(/\.[A-Za-z]+$/, "")
.replace(/\//g, ":");
return { name, handler: f.fullPath };
});
}

export async function scanModules(nitro: Nitro) {
const files = await scanFiles(nitro, "modules");
return files.map((f) => f.fullPath);
Expand Down
Loading

0 comments on commit 5477a81

Please sign in to comment.