@@ -17,6 +17,16 @@ import type { MetaRoot, MetaObject } from "@metaobjectsdev/metadata";
1717import { render , type Provider , type RenderFormat } from "@metaobjectsdev/render" ;
1818import type { Generator , GenContext , EmittedFile , GeneratorFactory } from "../generator.js" ;
1919import { projectProvider } from "../render-engine/framework-provider.js" ;
20+ import { expandOutputPattern } from "../template-codegen/output-pattern.js" ;
21+ import {
22+ buildEntityTemplateData ,
23+ buildPackageTemplateData ,
24+ buildModelTemplateData ,
25+ } from "../template-codegen/template-data.js" ;
26+
27+ /** The three built-in walk scopes (SP-1 §3.1). Same vocabulary as the engine
28+ * helpers perEntity/perPackage/perModel. */
29+ export type TemplateScope = "perEntity" | "perPackage" | "perModel" ;
2030
2131export type TemplateFormat = RenderFormat ;
2232
@@ -34,8 +44,17 @@ export interface TemplateGeneratorOpts {
3444 name : string ;
3545 /** Walk the loaded metadata tree and produce `{ data, outputPath }` tuples
3646 * — one per emitted file. Pattern A (per-entity), pattern B (single
37- * aggregator), pattern C (mixed), pattern D (filter inline) all fit. */
38- walk : ( root : MetaRoot ) => TemplateWalkResult [ ] | Promise < TemplateWalkResult [ ] > ;
47+ * aggregator), pattern C (mixed), pattern D (filter inline) all fit.
48+ * Mutually exclusive with `scope` — provide exactly one. The power-user
49+ * escape hatch; most consumers declare a `scope` + `outputPattern` instead. */
50+ walk ?: ( root : MetaRoot ) => TemplateWalkResult [ ] | Promise < TemplateWalkResult [ ] > ;
51+ /** Built-in walk scope (SP-1 §3.1) — declarative alternative to `walk`. The
52+ * generator derives the neutral data dict (template-data.ts) per unit and
53+ * names each file via `outputPattern`. Mutually exclusive with `walk`. */
54+ scope ?: TemplateScope ;
55+ /** Output path pattern for the built-in `scope` walk: `{name}` `{Name}`
56+ * `{package}` (SP-1 §3.3). Required with `scope`; ignored with `walk`. */
57+ outputPattern ?: string ;
3958 /** Template reference. Resolved by the configured Provider chain — by
4059 * default the project's `templates/<ref>.mustache` first, then the
4160 * framework defaults at `codegen-ts/templates/<ref>.mustache`. */
@@ -57,10 +76,53 @@ export interface TemplateGeneratorOpts {
5776 target ?: string ;
5877}
5978
79+ /** Derive a `walk` from a built-in scope + output pattern. Each scope yields the
80+ * neutral data dict for its unit and names the file via the pattern. */
81+ function scopeWalk (
82+ scope : TemplateScope ,
83+ pattern : string ,
84+ ) : ( root : MetaRoot ) => TemplateWalkResult [ ] {
85+ return ( root ) => {
86+ const concrete = root . objects ( ) . filter ( ( o ) => o . isAbstract !== true ) ;
87+ if ( scope === "perEntity" ) {
88+ return concrete . map ( ( e ) => ( {
89+ data : buildEntityTemplateData ( e ) ,
90+ outputPath : expandOutputPattern ( pattern , { name : e . name , package : e . package ?? "" } ) ,
91+ } ) ) ;
92+ }
93+ if ( scope === "perPackage" ) {
94+ const byPkg = new Map < string , MetaObject [ ] > ( ) ;
95+ for ( const o of concrete ) {
96+ const pkg = o . package ?? "" ;
97+ let bucket = byPkg . get ( pkg ) ;
98+ if ( bucket === undefined ) { bucket = [ ] ; byPkg . set ( pkg , bucket ) ; }
99+ bucket . push ( o ) ;
100+ }
101+ return [ ...byPkg . keys ( ) ] . sort ( ) . map ( ( pkg ) => ( {
102+ data : buildPackageTemplateData ( pkg , byPkg . get ( pkg ) ! ) ,
103+ outputPath : expandOutputPattern ( pattern , { package : pkg } ) ,
104+ } ) ) ;
105+ }
106+ // perModel — one file over the whole model.
107+ return [ { data : buildModelTemplateData ( root ) , outputPath : expandOutputPattern ( pattern , { } ) } ] ;
108+ } ;
109+ }
110+
60111export const templateGenerator = function templateGenerator (
61112 opts : TemplateGeneratorOpts ,
62113) : Generator {
63114 const fmt : TemplateFormat = opts . format ?? "text" ;
115+ const hasWalk = typeof opts . walk === "function" ;
116+ const hasScope = opts . scope !== undefined ;
117+ if ( hasWalk === hasScope ) {
118+ throw new Error (
119+ `templateGenerator(${ opts . name } ): provide exactly one of \`walk\` or (\`scope\` + \`outputPattern\`)` ,
120+ ) ;
121+ }
122+ if ( hasScope && ( opts . outputPattern === undefined || opts . outputPattern === "" ) ) {
123+ throw new Error ( `templateGenerator(${ opts . name } ): \`scope\` requires a non-empty \`outputPattern\`` ) ;
124+ }
125+ const walk = hasWalk ? opts . walk ! : scopeWalk ( opts . scope ! , opts . outputPattern ! ) ;
64126 const generator : Generator = {
65127 name : opts . name ,
66128 async generate ( ctx : GenContext ) : Promise < EmittedFile [ ] > {
@@ -77,7 +139,7 @@ export const templateGenerator = function templateGenerator(
77139 ) ;
78140 provider = projectProvider ( process . cwd ( ) ) ;
79141 }
80- const walkRes = await opts . walk ( ctx . loadedRoot ) ;
142+ const walkRes = await walk ( ctx . loadedRoot ) ;
81143 const files : EmittedFile [ ] = [ ] ;
82144 for ( const { data, outputPath } of walkRes ) {
83145 let content : string ;
0 commit comments