/** * Runnable house-pattern generators (WP-43). `npm run gen ` or the `gen:*` scripts. * These emit the boilerplate the skills used to describe by hand; each generated unit ships * with its co-located spec so the "domain/pure logic must have a spec" rule holds by default. * * Generators here are the pure-TS patterns (no Angular-template escaping): value objects and * form/wizard state machines. The UI-component and bff-endpoint patterns stay skill-driven — * they span the template `{{ }}` syntax / the C# backend + `gen:api` regen, where a generator * adds little over the recipe. `context` (WP-44) mechanises the `new-context` skill's manual * multi-file edit: folders + tsconfig alias + boundary entry + lazy route. * * Positional args skip the prompts, e.g. `npx plop value-object registratie KvkNummer`. */ export default function (plop) { // WP-67: shared/beheer moved to libs/ (cross-app libraries); every other context is // ssp-only today (behandelportal has its own auth/behandeling, not covered by this // ssp-assuming heuristic — run these generators by hand for a behandelportal context). const LIBS = ['shared', 'beheer']; plop.setHelper('ctxRoot', (context) => LIBS.includes(context) ? `libs/${context}/src` : `apps/ssp/src/app/${context}`, ); const CONTEXT_PROMPT = { type: 'input', name: 'context', message: 'Context (shared | beheer | auth | registratie | herregistratie | brief):', }; const remindEnXlf = () => '⚠ The generated $localize id needs an English in apps/ssp/src/locale/messages.en.xlf before `npm run ci` (the localize gate fails otherwise).'; plop.setGenerator('value-object', { description: 'Branded value object + parser + spec ("parse, don\'t validate")', prompts: [ CONTEXT_PROMPT, { type: 'input', name: 'name', message: 'Name (PascalCase, e.g. KvkNummer):' }, ], actions: [ { type: 'add', path: '{{ctxRoot context}}/domain/value-objects/{{kebabCase name}}.ts', templateFile: 'plop-templates/value-object.hbs', }, { type: 'add', path: '{{ctxRoot context}}/domain/value-objects/{{kebabCase name}}.spec.ts', templateFile: 'plop-templates/value-object.spec.hbs', }, remindEnXlf, ], }); plop.setGenerator('form-machine', { description: 'Elm-style form/wizard state machine (Model/Msg/pure reduce) + spec', prompts: [ CONTEXT_PROMPT, { type: 'input', name: 'name', message: 'Machine name (PascalCase, e.g. Adreswijziging):' }, ], actions: [ { type: 'add', path: '{{ctxRoot context}}/domain/{{kebabCase name}}.machine.ts', templateFile: 'plop-templates/form-machine.hbs', }, { type: 'add', path: '{{ctxRoot context}}/domain/{{kebabCase name}}.machine.spec.ts', templateFile: 'plop-templates/form-machine.spec.hbs', }, remindEnXlf, ], }); plop.setGenerator('context', { description: 'Scaffold a bounded context: folders + tsconfig alias + boundary entry + lazy route', prompts: [ { type: 'input', name: 'name', message: 'Context name (lowercase, single Dutch ubiquitous term, e.g. vergunning):', }, ], actions: [ // WP-67: this generator only scaffolds an ssp context — behandelportal's contexts // (auth, behandeling) predate it and aren't covered by this heuristic. { type: 'add', path: 'apps/ssp/src/app/{{kebabCase name}}/ui/{{kebabCase name}}.page.ts', templateFile: 'plop-templates/context.page.hbs', }, { type: 'add', path: 'apps/ssp/src/app/{{kebabCase name}}/domain/.gitkeep', templateFile: 'plop-templates/gitkeep.hbs', }, { type: 'add', path: 'apps/ssp/src/app/{{kebabCase name}}/application/.gitkeep', templateFile: 'plop-templates/gitkeep.hbs', }, { type: 'add', path: 'apps/ssp/src/app/{{kebabCase name}}/infrastructure/.gitkeep', templateFile: 'plop-templates/gitkeep.hbs', }, { type: 'add', path: 'apps/ssp/src/app/{{kebabCase name}}/contracts/.gitkeep', templateFile: 'plop-templates/gitkeep.hbs', }, { // tsconfig path alias — inserted right after the `"paths": {` line so it's stable // across repeated runs regardless of what's already been added. type: 'modify', path: 'apps/ssp/tsconfig.json', pattern: /"paths": \{\n/, template: '"paths": {\n "@{{kebabCase name}}/*": ["src/app/{{kebabCase name}}/*"],\n', }, { // Boundary entry (WP-38's single source of truth) — inserted right after the // ssp dep-cruiser config's CONTEXT_ALLOWED object literal opens, which never moves. // (Previously anchored on the `showcase: null,` line, but create-frontend.mjs strips // that entry before running this generator, so the modify silently no-op'd and the // new context got no fence at all — dep:check stayed green with the boundary rule // simply missing.) type: 'modify', path: '.dependency-cruiser.ssp.js', pattern: /\.dependency-cruiser\.base\.js'\)\(\n {2}\{\n/, template: ".dependency-cruiser.base.js')(\n {\n {{kebabCase name}}: [],\n", }, { // Lazy route — inserted right before the catch-all, which never moves. type: 'modify', path: 'apps/ssp/src/app/app.routes.ts', pattern: " { path: '**', redirectTo: 'login' },", template: ` { path: '{{kebabCase name}}', canActivate: [authGuard], loadComponent: () => import('@{{kebabCase name}}/ui/{{kebabCase name}}.page').then( (m) => m.{{pascalCase name}}Page, ), }, { path: '**', redirectTo: 'login' },`, }, () => 'Next: npm run dep:check && npm run lint && npm run build to verify, then build the first feature with the new-feature skill.', ], }); }