feat(turbo-gen): job generator
Adds `pnpm turbo gen job <feature> <slug> <void|typed>` which scaffolds the factory + test + Payload TaskConfig, then modifies the feature's job-symbol, job-bind, and cms job-task anchors to wire the wrapped job into the per-feature container. Also registers a custom Handlebars `eq` helper used by the void/typed branch in the job templates.
This commit is contained in:
@@ -25,6 +25,10 @@ import { assertAnchors } from "./lib/anchor-validate.js";
|
||||
* The generator prints these manual steps when it finishes.
|
||||
*/
|
||||
export default function generator(plop: PlopTypes.NodePlopAPI): void {
|
||||
// Handlebars helper used by templates that branch on prompt values
|
||||
// (e.g. `gen job`'s inputShape void/typed split).
|
||||
plop.setHelper("eq", (a: unknown, b: unknown) => a === b);
|
||||
|
||||
plop.setGenerator("feature", {
|
||||
description:
|
||||
"Scaffold a Lazar-conformant feature package (single entity / single use case)",
|
||||
@@ -410,6 +414,159 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void {
|
||||
return consumeActions(a as Required<typeof a>);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Turbo generator: `job`
|
||||
*
|
||||
* Scaffolds a background job inside an existing feature: factory + test +
|
||||
* Payload TaskConfig, plus the per-feature DI binding (span+capture wrap)
|
||||
* and re-export from `integrations/cms/index.ts`.
|
||||
*/
|
||||
plop.setGenerator("job", {
|
||||
description: "Scaffold a background job in an existing feature",
|
||||
prompts: [
|
||||
{
|
||||
type: "input",
|
||||
name: "feature",
|
||||
message: "Feature (kebab-case, must exist):",
|
||||
validate(input: string) {
|
||||
if (!/^[a-z][a-z0-9-]*$/.test(input)) return "Must be kebab-case";
|
||||
if (!existsSync(join(process.cwd(), "packages", input, "src"))) {
|
||||
return `packages/${input}/src does not exist`;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "job",
|
||||
message: "Job slug (verb-noun kebab, e.g. 'send-welcome-email'):",
|
||||
validate(input: string) {
|
||||
if (!/^[a-z][a-z0-9-]+$/.test(input)) return "Must be kebab-case";
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
name: "inputShape",
|
||||
message: "Input shape:",
|
||||
choices: ["void", "typed"],
|
||||
default: "void",
|
||||
},
|
||||
],
|
||||
actions(answers) {
|
||||
const a = answers as {
|
||||
feature: string;
|
||||
job: string;
|
||||
inputShape: "void" | "typed";
|
||||
};
|
||||
return jobActions(a);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function jobActions(a: {
|
||||
feature: string;
|
||||
job: string;
|
||||
inputShape: "void" | "typed";
|
||||
}): PlopTypes.ActionType[] {
|
||||
const symbolFile = `packages/${a.feature}/src/di/symbols.ts`;
|
||||
const bindProdFile = `packages/${a.feature}/src/di/bind-production.ts`;
|
||||
const bindDevFile = `packages/${a.feature}/src/di/bind-dev-seed.ts`;
|
||||
const cmsIndexFile = `packages/${a.feature}/src/integrations/cms/index.ts`;
|
||||
return [
|
||||
() => {
|
||||
assertAnchors(process.cwd(), symbolFile, ["// <gen:job-symbols>"]);
|
||||
assertAnchors(process.cwd(), bindProdFile, ["// <gen:jobs>"]);
|
||||
assertAnchors(process.cwd(), bindDevFile, ["// <gen:jobs>"]);
|
||||
assertAnchors(process.cwd(), cmsIndexFile, ["// <gen:job-tasks>"]);
|
||||
return "All required anchors present";
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: `packages/${a.feature}/src/jobs/${a.job}.job.ts`,
|
||||
templateFile: "templates/job/job.ts.hbs",
|
||||
data: a,
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: `packages/${a.feature}/src/jobs/${a.job}.job.test.ts`,
|
||||
templateFile: "templates/job/job.test.ts.hbs",
|
||||
data: a,
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: `packages/${a.feature}/src/integrations/cms/jobs/${a.job}.task.ts`,
|
||||
templateFile: "templates/job/task.ts.hbs",
|
||||
data: a,
|
||||
},
|
||||
{
|
||||
type: "modify",
|
||||
path: cmsIndexFile,
|
||||
pattern: /\/\/ <gen:job-tasks>/,
|
||||
template: `// <gen:job-tasks>\nexport { ${camel(a.job)}Task } from "./jobs/${a.job}.task";`,
|
||||
},
|
||||
{
|
||||
type: "modify",
|
||||
path: symbolFile,
|
||||
pattern: /\/\/ <gen:job-symbols>/,
|
||||
template: `// <gen:job-symbols>\n I${pascalCase(a.job)}Job: Symbol.for("@repo/${a.feature}/${camel(a.job)}Job"),`,
|
||||
},
|
||||
{
|
||||
type: "modify",
|
||||
path: bindProdFile,
|
||||
pattern: /\/\/ <gen:jobs>/,
|
||||
template: jobBindBlock(a),
|
||||
},
|
||||
{
|
||||
type: "modify",
|
||||
path: bindDevFile,
|
||||
pattern: /\/\/ <gen:jobs>/,
|
||||
template: jobBindBlock(a),
|
||||
},
|
||||
() => printJobNextSteps(a),
|
||||
];
|
||||
}
|
||||
|
||||
function jobBindBlock(a: { feature: string; job: string }): string {
|
||||
const factoryFn = `${camel(a.job)}Job`;
|
||||
const symbol = `${constantCase(a.feature)}_SYMBOLS.I${pascalCase(a.job)}Job`;
|
||||
const containerVar = `${camel(a.feature)}Container`;
|
||||
return `// <gen:jobs>
|
||||
const wrapped${pascalCase(a.job)} = withSpan(
|
||||
tracer,
|
||||
{ name: "${a.feature}.${camel(a.job)}", op: "job" },
|
||||
withCapture(
|
||||
logger,
|
||||
{
|
||||
feature: "${a.feature}",
|
||||
layer: "job",
|
||||
name: "${a.feature}.${camel(a.job)}",
|
||||
},
|
||||
${factoryFn}(),
|
||||
),
|
||||
);
|
||||
if (${containerVar}.isBound(${symbol})) {
|
||||
${containerVar}.unbind(${symbol});
|
||||
}
|
||||
${containerVar}.bind(${symbol}).toConstantValue(wrapped${pascalCase(a.job)});`;
|
||||
}
|
||||
|
||||
function printJobNextSteps(a: { feature: string; job: string }): string {
|
||||
return [
|
||||
"",
|
||||
"─────────────────────────────────────────────────────────────",
|
||||
`Job ${a.feature}.${a.job} scaffolded.`,
|
||||
"",
|
||||
"Next steps (manual):",
|
||||
` 1. Fill in the job body in packages/${a.feature}/src/jobs/${a.job}.job.ts`,
|
||||
` 2. Add Payload field config to inputSchema in ${a.job}.task.ts (matches your Zod schema).`,
|
||||
` 3. Add the import for ${camel(a.job)}Job at the top of bind-production.ts and bind-dev-seed.ts.`,
|
||||
` 4. (Optional) Add a cron schedule in core-cms's buildConfig if this job runs periodically.`,
|
||||
` 5. Verify: pnpm --filter @repo/${a.feature} lint typecheck test`,
|
||||
"─────────────────────────────────────────────────────────────",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function publishActions(a: {
|
||||
|
||||
Reference in New Issue
Block a user