import { existsSync } from "node:fs"; import { join } from "node:path"; import type { PlopTypes } from "@turbo/gen"; import { assertAnchors } from "./lib/anchor-validate.js"; /** * Turbo generator: `feature` * * Scaffolds a Lazar-conformant feature package under `packages//` * matching the shape of the existing `navigation` reference feature. * * Phase 1 scope (intentionally limited): * - Single entity, single use case (`getX`) * - Skips Payload CMS collection/global templates (integrations/cms/**) * - Skips UI query helpers (ui/query.ts) — emits an empty barrel * - Skips faker-driven factories — emits empty stubs * - Skips multi-entity / multi-use-case * - Skips aggregator wiring (core-api/root.ts, core-cms/**, apps/web-next/server/bind-production.ts) * * After running, the developer must hand-wire the new feature into: * - apps/web-next/src/server/bind-production.ts (bindAll dispatcher) * - packages/core-api/src/root.ts (mount on app router) * - packages/core-cms/... (when CMS templates are added later) * * 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)", prompts: [ { type: "input", name: "name", message: "Feature package name (kebab-case, becomes @repo/ and packages//):", validate: (input: string) => { if (!input) return "Required"; if (!/^[a-z][a-z0-9-]*$/.test(input)) { return "Must be kebab-case (lowercase letters, digits, hyphens; must start with a letter)"; } return true; }, }, { type: "input", name: "entity", message: "Entity name (PascalCase singular, e.g. 'Widget' for a getWidget use case):", validate: (input: string) => { if (!input) return "Required"; if (!/^[A-Z][A-Za-z0-9]*$/.test(input)) { return "Must be PascalCase (e.g. Widget, BlogPost)"; } return true; }, }, { type: "input", name: "entityPlural", message: "Entity plural slug (kebab-case, used for Payload collection slug; e.g. 'widgets'):", validate: (input: string) => { if (!input) return "Required"; if (!/^[a-z][a-z0-9-]*$/.test(input)) { return "Must be kebab-case"; } return true; }, }, ], actions: [ // Top-level package files { type: "add", path: "packages/{{kebabCase name}}/package.json", templateFile: "templates/feature/package.json.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/tsconfig.json", templateFile: "templates/feature/tsconfig.json.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/vitest.config.ts", templateFile: "templates/feature/vitest.config.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/eslint.config.js", templateFile: "templates/feature/eslint.config.js.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/turbo.json", templateFile: "templates/feature/turbo.json.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/AGENTS.md", templateFile: "templates/feature/AGENTS.md.hbs", }, // Public surface { type: "add", path: "packages/{{kebabCase name}}/src/index.ts", templateFile: "templates/feature/src/index.ts.hbs", }, // Entities — models + errors { type: "add", path: "packages/{{kebabCase name}}/src/entities/models/{{kebabCase entity}}.ts", templateFile: "templates/feature/src/entities/models/entity.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/entities/models/{{kebabCase entity}}.test.ts", templateFile: "templates/feature/src/entities/models/entity.test.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/entities/errors/common.ts", templateFile: "templates/feature/src/entities/errors/common.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/entities/errors/{{kebabCase entity}}.ts", templateFile: "templates/feature/src/entities/errors/entity.ts.hbs", }, // Application layer { type: "add", path: "packages/{{kebabCase name}}/src/application/repositories/{{kebabCase entity}}.repository.interface.ts", templateFile: "templates/feature/src/application/repositories/entity.repository.interface.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/application/use-cases/get-{{kebabCase entity}}.use-case.ts", templateFile: "templates/feature/src/application/use-cases/get-entity.use-case.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/application/use-cases/get-{{kebabCase entity}}.use-case.test.ts", templateFile: "templates/feature/src/application/use-cases/get-entity.use-case.test.ts.hbs", }, // Infrastructure { type: "add", path: "packages/{{kebabCase name}}/src/infrastructure/repositories/{{kebabCase entity}}.repository.ts", templateFile: "templates/feature/src/infrastructure/repositories/entity.repository.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/infrastructure/repositories/{{kebabCase entity}}.repository.test.ts", templateFile: "templates/feature/src/infrastructure/repositories/entity.repository.test.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/infrastructure/repositories/{{kebabCase entity}}.repository.mock.ts", templateFile: "templates/feature/src/infrastructure/repositories/entity.repository.mock.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/infrastructure/repositories/{{kebabCase entity}}.repository.mock.test.ts", templateFile: "templates/feature/src/infrastructure/repositories/entity.repository.mock.test.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/infrastructure/repositories/{{kebabCase entity}}.repository.span.test.ts", templateFile: "templates/feature/src/infrastructure/repositories/entity.repository.span.test.ts.hbs", }, // Interface adapters { type: "add", path: "packages/{{kebabCase name}}/src/interface-adapters/controllers/get-{{kebabCase entity}}.controller.ts", templateFile: "templates/feature/src/interface-adapters/controllers/get-entity.controller.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/interface-adapters/controllers/get-{{kebabCase entity}}.controller.test.ts", templateFile: "templates/feature/src/interface-adapters/controllers/get-entity.controller.test.ts.hbs", }, // DI { type: "add", path: "packages/{{kebabCase name}}/src/di/symbols.ts", templateFile: "templates/feature/src/di/symbols.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/di/module.ts", templateFile: "templates/feature/src/di/module.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/di/container.ts", templateFile: "templates/feature/src/di/container.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/di/container.test.ts", templateFile: "templates/feature/src/di/container.test.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/di/bind-production.ts", templateFile: "templates/feature/src/di/bind-production.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/di/bind-dev-seed.ts", templateFile: "templates/feature/src/di/bind-dev-seed.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/di/bind-dev-seed.test.ts", templateFile: "templates/feature/src/di/bind-dev-seed.test.ts.hbs", }, // Integrations: api { type: "add", path: "packages/{{kebabCase name}}/src/integrations/api/procedures.ts", templateFile: "templates/feature/src/integrations/api/procedures.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/integrations/api/router.ts", templateFile: "templates/feature/src/integrations/api/router.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/integrations/api/router.test.ts", templateFile: "templates/feature/src/integrations/api/router.test.ts.hbs", }, // Seeds + factories + contracts (Phase 1: minimal stubs that still typecheck/test) { type: "add", path: "packages/{{kebabCase name}}/src/__seeds__/dev.ts", templateFile: "templates/feature/src/__seeds__/dev.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/__factories__/index.ts", templateFile: "templates/feature/src/__factories__/index.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/__factories__/{{kebabCase entity}}.factory.ts", templateFile: "templates/feature/src/__factories__/entity.factory.ts.hbs", }, { type: "add", path: "packages/{{kebabCase name}}/src/__contracts__/{{kebabCase entity}}-repository.contract.ts", templateFile: "templates/feature/src/__contracts__/entity-repository.contract.ts.hbs", }, // ui — empty barrel for Phase 1 (./ui subpath kept reserved) { type: "add", path: "packages/{{kebabCase name}}/src/ui/index.ts", templateFile: "templates/feature/src/ui/index.ts.hbs", }, // Final manual-wiring instructions printed to the user function printNextSteps(answers: Record): string { const a = answers as { name: string; entity: string; entityPlural: string }; const kebab = a.name; const constSym = a.name.toUpperCase().replace(/-/g, "_"); const pkg = `@repo/${kebab}`; return [ "", "─────────────────────────────────────────────────────────────", `Feature package ${pkg} scaffolded.`, "", "Next steps (manual aggregator wiring — not generated):", "", ` 1. pnpm install # link the new workspace package`, "", ` 2. apps/web-next/src/server/bind-production.ts:`, ` - import { bindProduction${cap(a.name)} } from "${pkg}/di/bind-production";`, ` - import { bindDevSeed${cap(a.name)} } from "${pkg}/di/bind-dev-seed";`, ` - call bindProduction${cap(a.name)}(config, tracer, logger) (production branch)`, ` - call await bindDevSeed${cap(a.name)}(tracer, logger) (dev-seed branch)`, "", ` 3. packages/core-api/src/root.ts:`, ` - import { ${camel(a.name)}Router } from "${pkg}/api";`, ` - mount it on the app router (e.g. ${camel(a.name)}: ${camel(a.name)}Router)`, "", ` 4. packages/core-api/package.json:`, ` - add "${pkg}": "workspace:*" to dependencies`, "", ` 5. apps/web-next/package.json:`, ` - add "${pkg}": "workspace:*" to dependencies`, "", ` 6. (Later) Add Payload CMS collection/global at:`, ` packages/${kebab}/src/integrations/cms/collections/${a.entityPlural}.ts`, ` and register in packages/core-cms/...`, "", ` 7. Verify: pnpm --filter ${pkg} lint typecheck test`, "", `Reference symbols exported by this package:`, ` - ${constSym}_SYMBOLS (from ${pkg}/* internal)`, ` - bindProduction${cap(a.name)}, bindDevSeed${cap(a.name)}`, ` - ${camel(a.name)}Router (tRPC)`, "─────────────────────────────────────────────────────────────", "", ].join("\n"); }, ], }); /** * Turbo generator: `event` * * Mode `publish` scaffolds an event contract under * `packages//src/events/.event.ts` (+ test) and * re-exports it from the feature's public surface at the `// ` * anchor. Mode `consume` (Task 40) scaffolds a handler in the consumer * feature. */ plop.setGenerator("event", { description: "Scaffold an event contract (publish) or handler (consume)", prompts: [ { type: "list", name: "mode", message: "Mode:", choices: ["publish", "consume"], }, { type: "input", name: "feature", message: "Owning feature (kebab-case; for publish: contract package; for consume: consumer package):", 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: "event", message: "Event slug (dotted-kebab past tense, e.g. 'user.signed-up'):", validate(input: string) { if (!/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/.test(input)) { return "Must be dotted-kebab past tense (e.g. user.signed-up)"; } return true; }, }, { type: "input", name: "publisher", message: "Publisher feature (kebab-case; only for consume mode):", when(answers: { mode: string }) { return answers.mode === "consume"; }, validate(input: string, answers: { event: string }) { if (!/^[a-z][a-z0-9-]*$/.test(input)) return "Must be kebab-case"; const eventKebab = answers.event.replace(/\./g, "-"); const path = join( process.cwd(), "packages", input, "src", "events", `${eventKebab}.event.ts`, ); if (!existsSync(path)) { return `Publisher contract not found: ${path}`; } return true; }, }, ], actions(answers) { const a = answers as { mode: "publish" | "consume"; feature: string; event: string; publisher?: string; }; if (a.mode === "publish") return publishActions(a); if (!a.publisher) { throw new Error("consume mode requires a publisher feature"); } return consumeActions(a as Required); }, }); /** * 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, ["// "]); assertAnchors(process.cwd(), bindProdFile, ["// "]); assertAnchors(process.cwd(), bindDevFile, ["// "]); assertAnchors(process.cwd(), cmsIndexFile, ["// "]); 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: /\/\/ /, template: `// \nexport { ${camel(a.job)}Task } from "./jobs/${a.job}.task";`, }, { type: "modify", path: symbolFile, pattern: /\/\/ /, template: `// \n I${pascalCase(a.job)}Job: Symbol.for("@repo/${a.feature}/${camel(a.job)}Job"),`, }, { type: "modify", path: bindProdFile, pattern: /\/\/ /, template: jobBindBlock(a), }, { type: "modify", path: bindDevFile, pattern: /\/\/ /, 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 `// 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: { feature: string; event: string; }): PlopTypes.ActionType[] { const eventKebab = a.event.replace(/\./g, "-"); const indexPath = `packages/${a.feature}/src/index.ts`; return [ () => { assertAnchors(process.cwd(), indexPath, ["// "]); return `Anchors verified in ${indexPath}`; }, { type: "add", path: `packages/${a.feature}/src/events/${eventKebab}.event.ts`, templateFile: "templates/event/publish/event.ts.hbs", data: { event: a.event, feature: a.feature, eventKebab }, }, { type: "add", path: `packages/${a.feature}/src/events/${eventKebab}.event.test.ts`, templateFile: "templates/event/publish/event.test.ts.hbs", data: { event: a.event, feature: a.feature, eventKebab }, }, { type: "modify", path: indexPath, pattern: /\/\/ /, template: `// \nexport {\n {{camelCase event}}Event,\n {{camelCase event}}EventSchema,\n type {{pascalCase event}}Event,\n} from "./events/{{kebabCase event}}.event";`, data: { event: a.event }, }, () => printPublishNextSteps(a), ]; } function consumeActions(a: { feature: string; event: string; publisher: string; }): PlopTypes.ActionType[] { const eventKebab = a.event.replace(/\./g, "-"); const handlerName = `on${pascalCase(a.publisher)}${pascalCase(a.event)}`; 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, ["// "]); assertAnchors(process.cwd(), bindProdFile, ["// "]); assertAnchors(process.cwd(), bindDevFile, ["// "]); assertAnchors(process.cwd(), cmsIndexFile, ["// "]); return "All required anchors present"; }, { type: "add", path: `packages/${a.feature}/src/events/handlers/on-${a.publisher}-${eventKebab}.handler.ts`, templateFile: "templates/event/consume/handler.ts.hbs", data: a, }, { type: "add", path: `packages/${a.feature}/src/events/handlers/on-${a.publisher}-${eventKebab}.handler.test.ts`, templateFile: "templates/event/consume/handler.test.ts.hbs", data: a, }, { type: "add", path: `packages/${a.feature}/src/integrations/cms/jobs/__events-${a.publisher}-${eventKebab}.task.ts`, templateFile: "templates/event/consume/event-task.ts.hbs", data: a, }, { type: "modify", path: symbolFile, pattern: /\/\/ /, template: `// \n IOn${pascalCase(a.publisher)}${pascalCase(a.event)}Handler: Symbol.for("@repo/${a.feature}/${handlerName}"),`, }, { type: "modify", path: bindProdFile, pattern: /\/\/ /, template: handlerBindBlock(a), }, { type: "modify", path: bindDevFile, pattern: /\/\/ /, template: handlerBindBlock(a), }, { type: "modify", path: cmsIndexFile, pattern: /\/\/ /, template: `// \nexport { on${pascalCase(a.publisher)}${pascalCase(a.event)}EventTask } from "./jobs/__events-${a.publisher}-${eventKebab}.task";`, }, () => printConsumeNextSteps(a), ]; } function handlerBindBlock(a: { feature: string; event: string; publisher: string; }): string { const handlerFn = `on${pascalCase(a.publisher)}${pascalCase(a.event)}Handler`; const eventConst = `${camel(a.event.replace(/\./g, "-"))}Event`; const handlerSymbol = `${constantCase(a.feature)}_SYMBOLS.IOn${pascalCase(a.publisher)}${pascalCase(a.event)}Handler`; const wrappedVar = `wrapped${pascalCase(a.publisher)}${pascalCase(a.event)}`; const containerVar = `${camel(a.feature)}Container`; return `// // ${handlerFn} subscription — generated, edit the handler file (not this block) for behavior. const ${wrappedVar} = withSpan( tracer, { name: "${a.feature}.${handlerFn}", op: "event-handler" }, withCapture( logger, { feature: "${a.feature}", layer: "event-handler", name: "${a.feature}.${handlerFn}", }, ${handlerFn}(), ), ); if (${containerVar}.isBound(${handlerSymbol})) { ${containerVar}.unbind(${handlerSymbol}); } ${containerVar}.bind(${handlerSymbol}).toConstantValue(${wrappedVar}); bus.subscribe(${eventConst}, "${a.feature}", ${wrappedVar});`; } function printConsumeNextSteps(a: { feature: string; event: string; publisher: string; }): string { const eventKebab = a.event.replace(/\./g, "-"); const eventConst = `${camel(a.event.replace(/\./g, "-"))}Event`; return [ "", "─────────────────────────────────────────────────────────────", `Handler on-${a.publisher}-${eventKebab} scaffolded in ${a.feature}.`, "", "Next steps (manual):", ` 1. Implement the handler body in packages/${a.feature}/src/events/handlers/on-${a.publisher}-${eventKebab}.handler.ts`, ` 2. Add the import at the top of bind-production.ts AND bind-dev-seed.ts:`, ` import { ${eventConst} } from "@repo/${a.publisher}";`, ` import { ${camel(`on-${a.publisher}-${a.event.replace(/\./g, "-")}-handler`)} } from "../events/handlers/on-${a.publisher}-${eventKebab}.handler";`, ` 3. If your handler needs deps, extend the factory signature and pass them in the generated bind block.`, ` 4. Add "@repo/${a.publisher}": "workspace:*" to packages/${a.feature}/package.json dependencies (if not already present).`, ` 5. Verify: pnpm --filter @repo/${a.feature} lint typecheck test`, "─────────────────────────────────────────────────────────────", "", ].join("\n"); } function printPublishNextSteps(a: { feature: string; event: string }): string { const eventKebab = a.event.replace(/\./g, "-"); return [ "", "─────────────────────────────────────────────────────────────", `Event ${a.feature}.${a.event} contract scaffolded.`, "", "Next steps (manual):", ` 1. Fill in the schema in packages/${a.feature}/src/events/${eventKebab}.event.ts`, ` 2. Pick a use case in packages/${a.feature}/src/application/use-cases/ that should publish.`, ` - Add 'bus: IEventBus' to the factory signature.`, ` - Call 'await bus.publish(${camel(a.event.replace(/\./g, "-"))}Event, payload)' after success.`, ` - Update the use case's DI binding to inject the bus.`, ` 3. Verify: pnpm --filter @repo/${a.feature} lint typecheck test`, "─────────────────────────────────────────────────────────────", "", ].join("\n"); } // Local helpers used inside the printNextSteps action — Plop's helpers aren't // available outside template strings, so we replicate the bits we need. function cap(input: string): string { return input .split(/[-_\s]+/) .filter(Boolean) .map((s) => s[0]!.toUpperCase() + s.slice(1)) .join(""); } function camel(input: string): string { const p = cap(input); return p ? p[0]!.toLowerCase() + p.slice(1) : ""; } function pascalCase(input: string): string { // Same as `cap` but also splits on dots, so "user.signed-up" → "UserSignedUp". return input .split(/[-_.\s]+/) .filter(Boolean) .map((s) => s[0]!.toUpperCase() + s.slice(1)) .join(""); } function constantCase(input: string): string { return input.toUpperCase().replace(/[-.\s]/g, "_"); }