@metaobjectsdev/codegen-ts 0.7.0-rc.9 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generator.d.ts +9 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/docs-data-builder.d.ts +16 -0
- package/dist/generators/docs-data-builder.d.ts.map +1 -0
- package/dist/generators/docs-data-builder.js +381 -0
- package/dist/generators/docs-data-builder.js.map +1 -0
- package/dist/generators/docs-data.d.ts +98 -0
- package/dist/generators/docs-data.d.ts.map +1 -0
- package/dist/generators/docs-data.js +43 -0
- package/dist/generators/docs-data.js.map +1 -0
- package/dist/generators/docs-file.d.ts +8 -0
- package/dist/generators/docs-file.d.ts.map +1 -0
- package/dist/generators/docs-file.js +77 -0
- package/dist/generators/docs-file.js.map +1 -0
- package/dist/generators/index.d.ts +4 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +3 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/template-generator.d.ts +41 -0
- package/dist/generators/template-generator.d.ts.map +1 -0
- package/dist/generators/template-generator.js +62 -0
- package/dist/generators/template-generator.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/overwrite-policy.d.ts +39 -2
- package/dist/overwrite-policy.d.ts.map +1 -1
- package/dist/overwrite-policy.js +233 -13
- package/dist/overwrite-policy.js.map +1 -1
- package/dist/render-engine/framework-provider.d.ts +28 -0
- package/dist/render-engine/framework-provider.d.ts.map +1 -0
- package/dist/render-engine/framework-provider.js +104 -0
- package/dist/render-engine/framework-provider.js.map +1 -0
- package/dist/runner.d.ts +15 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +44 -6
- package/dist/runner.js.map +1 -1
- package/dist/templates/docs-file.d.ts +17 -0
- package/dist/templates/docs-file.d.ts.map +1 -0
- package/dist/templates/docs-file.js +37 -0
- package/dist/templates/docs-file.js.map +1 -0
- package/package.json +5 -5
- package/src/generator.ts +9 -0
- package/src/generators/docs-data-builder.ts +470 -0
- package/src/generators/docs-data.ts +154 -0
- package/src/generators/docs-file.ts +87 -0
- package/src/generators/index.ts +16 -0
- package/src/generators/template-generator.ts +106 -0
- package/src/index.ts +33 -2
- package/src/overwrite-policy.ts +325 -14
- package/src/render-engine/framework-provider.ts +107 -0
- package/src/runner.ts +65 -6
- package/src/templates/docs-file.ts +51 -0
- package/templates/docs/entity-page.md.mustache +54 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// FrameworkTemplateProvider — resolves template refs (e.g. "docs/entity-page.md")
|
|
2
|
+
// against the codegen-ts package's `templates/` directory. Adopters who want
|
|
3
|
+
// to override a framework template create their own `templates/<ref>.mustache`
|
|
4
|
+
// file in their project; `ProviderChain` (below) consults the project first
|
|
5
|
+
// and falls back to the framework defaults.
|
|
6
|
+
//
|
|
7
|
+
// Decision D1 from the template-driven-codegen design — hybrid: framework
|
|
8
|
+
// ships defaults, adopters override by file-system convention.
|
|
9
|
+
//
|
|
10
|
+
// The framework Provider is filesystem-backed so it works identically whether
|
|
11
|
+
// codegen-ts runs from source (bun, dev) or from `dist/` (npm install). The
|
|
12
|
+
// `templates/` directory is included in the published tarball via
|
|
13
|
+
// package.json `files: ["dist", "src", "templates", ...]`.
|
|
14
|
+
|
|
15
|
+
import type { Provider } from "@metaobjectsdev/render";
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { join, resolve, dirname } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
/** Canonical shipped template — used to verify a candidate framework
|
|
21
|
+
* templates directory actually contains our defaults. Without this check a
|
|
22
|
+
* hoisted-install layout (pnpm/bun workspaces) can walk up to a CONSUMER
|
|
23
|
+
* package.json and silently return a templates dir that doesn't exist. */
|
|
24
|
+
const CANONICAL_TEMPLATE_REL = "docs/entity-page.md.mustache";
|
|
25
|
+
|
|
26
|
+
/** Walk up from `start` until we find a `package.json` whose neighbour
|
|
27
|
+
* `templates/` directory contains our canonical shipped template (i.e., the
|
|
28
|
+
* codegen-ts package root). Works the same way from
|
|
29
|
+
* `src/render-engine/framework-provider.ts` (during dev) and
|
|
30
|
+
* `dist/render-engine/framework-provider.js` (after `npm install`). */
|
|
31
|
+
function findFrameworkTemplatesDir(start: string): string {
|
|
32
|
+
let dir = start;
|
|
33
|
+
while (true) {
|
|
34
|
+
const pkgJson = join(dir, "package.json");
|
|
35
|
+
if (existsSync(pkgJson)) {
|
|
36
|
+
const templatesDir = join(dir, "templates");
|
|
37
|
+
// Assert we landed at the codegen-ts package root, not a consumer's.
|
|
38
|
+
if (existsSync(join(templatesDir, CANONICAL_TEMPLATE_REL))) {
|
|
39
|
+
return templatesDir;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const parent = dirname(dir);
|
|
43
|
+
if (parent === dir) break;
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(
|
|
47
|
+
`framework templates dir unresolved: walked up from ${start} without finding a package.json ` +
|
|
48
|
+
`whose templates/${CANONICAL_TEMPLATE_REL} exists. This usually means codegen-ts was installed ` +
|
|
49
|
+
`via a hoisted layout (pnpm/bun workspaces) into an unexpected location, or the published ` +
|
|
50
|
+
`tarball is missing the templates/ directory.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// In ESM (CLAUDE.md: "ESM only. No CommonJS."), `import.meta.url` is
|
|
55
|
+
// guaranteed to be a file: URL; no defensive try/catch needed.
|
|
56
|
+
const SELF_DIR = dirname(fileURLToPath(import.meta.url));
|
|
57
|
+
|
|
58
|
+
const FRAMEWORK_TEMPLATES_DIR = findFrameworkTemplatesDir(SELF_DIR);
|
|
59
|
+
|
|
60
|
+
/** Provider backed by an arbitrary on-disk template directory. References
|
|
61
|
+
* resolve as `<dir>/<ref>.mustache`. Used by both the framework default
|
|
62
|
+
* and adopter override paths. */
|
|
63
|
+
export class FileSystemProvider implements Provider {
|
|
64
|
+
constructor(private readonly root: string) {}
|
|
65
|
+
resolve(ref: string): string | undefined {
|
|
66
|
+
const path = join(this.root, `${ref}.mustache`);
|
|
67
|
+
if (!existsSync(path)) return undefined;
|
|
68
|
+
return readFileSync(path, "utf-8");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The framework defaults provider — resolves refs against codegen-ts's own
|
|
73
|
+
* `templates/` directory. */
|
|
74
|
+
export const frameworkTemplatesProvider: Provider = new FileSystemProvider(
|
|
75
|
+
FRAMEWORK_TEMPLATES_DIR,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
/** Compose providers: first match wins. Adopters typically chain
|
|
79
|
+
* `[projectProvider, frameworkTemplatesProvider]` so their own templates
|
|
80
|
+
* override the framework defaults. */
|
|
81
|
+
export class ProviderChain implements Provider {
|
|
82
|
+
constructor(private readonly providers: readonly Provider[]) {}
|
|
83
|
+
resolve(ref: string): string | undefined {
|
|
84
|
+
for (const p of this.providers) {
|
|
85
|
+
const text = p.resolve(ref);
|
|
86
|
+
if (text !== undefined) return text;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Build a project-scoped Provider: layers an optional project `templates/`
|
|
93
|
+
* directory over the framework defaults. Returns just the framework provider
|
|
94
|
+
* when `projectRoot` is undefined / its `templates/` dir doesn't exist. */
|
|
95
|
+
export function projectProvider(projectRoot?: string): Provider {
|
|
96
|
+
if (projectRoot === undefined) return frameworkTemplatesProvider;
|
|
97
|
+
const projTemplates = resolve(projectRoot, "templates");
|
|
98
|
+
if (!existsSync(projTemplates)) return frameworkTemplatesProvider;
|
|
99
|
+
return new ProviderChain([
|
|
100
|
+
new FileSystemProvider(projTemplates),
|
|
101
|
+
frameworkTemplatesProvider,
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Exposed for tests that want to inspect / clear the resolved framework
|
|
106
|
+
* templates directory (don't use outside tests). */
|
|
107
|
+
export const __frameworkTemplatesDirForTests = FRAMEWORK_TEMPLATES_DIR;
|
package/src/runner.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
1
|
+
import { join, relative, resolve, isAbsolute } from "node:path";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
2
3
|
import type { MetaData, MetaObject } from "@metaobjectsdev/metadata";
|
|
3
4
|
import { MetaRoot } from "@metaobjectsdev/metadata";
|
|
4
5
|
import type { Generator, GenContext, EmittedFile } from "./generator.js";
|
|
@@ -8,7 +9,12 @@ import type { ResolvedTarget } from "./import-path.js";
|
|
|
8
9
|
import { buildPkMap } from "./pk-resolver.js";
|
|
9
10
|
import { buildRelationMap } from "./relation-resolver.js";
|
|
10
11
|
import { makeRenderContext } from "./render-context.js";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
decideAndWrite,
|
|
14
|
+
type WriteResult,
|
|
15
|
+
type MergeStrategy,
|
|
16
|
+
type BaselineMode,
|
|
17
|
+
} from "./overwrite-policy.js";
|
|
12
18
|
|
|
13
19
|
/** JS-identifier-shape only. Prevents filesystem traversal when metadata comes
|
|
14
20
|
* from untrusted sources (e.g. MCP). Mirrors the guard in legacy generate.ts. */
|
|
@@ -21,16 +27,48 @@ export interface RunGenOpts {
|
|
|
21
27
|
entityFilter?: string[];
|
|
22
28
|
/** Overwrite strategy passed to decideAndWrite. Defaults to "overwrite". */
|
|
23
29
|
mergeStrategy?: MergeStrategy;
|
|
30
|
+
/** Project root (used to derive the .gen-state/ snapshot directory and to
|
|
31
|
+
* key snapshots by project-relative output path). When omitted, falls back
|
|
32
|
+
* to process.cwd(). */
|
|
33
|
+
projectRoot?: string;
|
|
34
|
+
/** Override the snapshot directory location. Defaults to
|
|
35
|
+
* `<projectRoot>/.metaobjects/.gen-state/`. */
|
|
36
|
+
genStateDir?: string;
|
|
37
|
+
/** First-time-on-existing-file behavior. Defaults to "default" (write-if-
|
|
38
|
+
* different). "fresh" → overwrite and re-baseline (the `--baseline=fresh`
|
|
39
|
+
* CLI flag). */
|
|
40
|
+
baseline?: BaselineMode;
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
export interface RunGenResult {
|
|
27
44
|
files: WriteResult[];
|
|
28
45
|
warnings: string[];
|
|
46
|
+
/** Subset of `files` with status "conflict" — surfaced separately so the
|
|
47
|
+
* CLI can print the end-of-run summary. */
|
|
48
|
+
conflicts: WriteResult[];
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
32
52
|
const warnings: string[] = [];
|
|
33
53
|
const strategy = opts.mergeStrategy ?? "overwrite";
|
|
54
|
+
const baseline = opts.baseline ?? "default";
|
|
55
|
+
// When projectRoot is not supplied we DON'T fall back to process.cwd() —
|
|
56
|
+
// that would leak .gen-state/ into whatever directory happens to be cwd
|
|
57
|
+
// (e.g. the package dir during a unit-test run). Instead, fall back to a
|
|
58
|
+
// process-isolated tmpdir, which gives the new-write semantics every call
|
|
59
|
+
// (the snapshot exists only for the current process). The CLI's
|
|
60
|
+
// `genCommand` always passes a real projectRoot, so this fallback only
|
|
61
|
+
// affects programmatic callers (tests, library embedding).
|
|
62
|
+
const projectRoot = opts.projectRoot !== undefined
|
|
63
|
+
? (isAbsolute(opts.projectRoot) ? opts.projectRoot : resolve(opts.projectRoot))
|
|
64
|
+
: undefined;
|
|
65
|
+
const genStateDir = opts.genStateDir !== undefined
|
|
66
|
+
? (isAbsolute(opts.genStateDir)
|
|
67
|
+
? opts.genStateDir
|
|
68
|
+
: resolve(projectRoot ?? process.cwd(), opts.genStateDir))
|
|
69
|
+
: (projectRoot !== undefined
|
|
70
|
+
? join(projectRoot, ".metaobjects", ".gen-state")
|
|
71
|
+
: join(tmpdir(), `meta-gen-state-${process.pid}`));
|
|
34
72
|
|
|
35
73
|
// loadMemory now returns MetaRoot; guard here also covers callers that pass a
|
|
36
74
|
// plain MetaData (e.g. test helpers that build trees programmatically).
|
|
@@ -50,7 +88,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
50
88
|
? "no object children match the provided entityFilter"
|
|
51
89
|
: "root has no object children";
|
|
52
90
|
warnings.push(`No entities to generate — ${reason}.`);
|
|
53
|
-
return { files: [], warnings };
|
|
91
|
+
return { files: [], warnings, conflicts: [] };
|
|
54
92
|
}
|
|
55
93
|
|
|
56
94
|
const safeEntities: MetaObject[] = [];
|
|
@@ -64,7 +102,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
64
102
|
safeEntities.push(entity);
|
|
65
103
|
}
|
|
66
104
|
if (safeEntities.length === 0) {
|
|
67
|
-
return { files: [], warnings };
|
|
105
|
+
return { files: [], warnings, conflicts: [] };
|
|
68
106
|
}
|
|
69
107
|
|
|
70
108
|
// 2. Resolve targets + entity-module target.
|
|
@@ -136,6 +174,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
136
174
|
outputLayout: selfTarget.outputLayout,
|
|
137
175
|
},
|
|
138
176
|
renderContext,
|
|
177
|
+
...(projectRoot !== undefined && { projectRoot }),
|
|
139
178
|
warn: (msg) => warnings.push(`[${generator.name}] ${msg}`),
|
|
140
179
|
};
|
|
141
180
|
|
|
@@ -163,9 +202,29 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
163
202
|
|
|
164
203
|
// 5. Write phase.
|
|
165
204
|
const writes: WriteResult[] = [];
|
|
205
|
+
const conflicts: WriteResult[] = [];
|
|
166
206
|
for (const file of emitted) {
|
|
167
|
-
|
|
207
|
+
// Key the snapshot by project-relative path so multi-target projects keep
|
|
208
|
+
// distinct entries (e.g. `database/Post.ts` vs `web/Post.queries.ts`).
|
|
209
|
+
// Without an explicit projectRoot we let decideAndWrite derive a stable
|
|
210
|
+
// hash-of-path key — fine for ephemeral test runs.
|
|
211
|
+
const policyOpts: import("./overwrite-policy.js").DecideAndWriteOpts = {
|
|
212
|
+
strategy,
|
|
213
|
+
genStateDir,
|
|
214
|
+
baseline,
|
|
215
|
+
};
|
|
216
|
+
if (projectRoot !== undefined) {
|
|
217
|
+
policyOpts.outputRelPath = relative(projectRoot, file.fullPath);
|
|
218
|
+
}
|
|
219
|
+
const result = decideAndWrite(file.fullPath, file.content, policyOpts);
|
|
168
220
|
writes.push(result);
|
|
221
|
+
if (result.status === "conflict") {
|
|
222
|
+
conflicts.push(result);
|
|
223
|
+
warnings.push(
|
|
224
|
+
`Merge conflict in ${file.fullPath}: resolve diff3 markers and re-run ` +
|
|
225
|
+
`'meta gen' to advance the canonical state.`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
169
228
|
if (result.status === "refused") {
|
|
170
229
|
warnings.push(
|
|
171
230
|
`Refused to overwrite ${file.fullPath}: file exists without @generated header. ` +
|
|
@@ -174,5 +233,5 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
174
233
|
}
|
|
175
234
|
}
|
|
176
235
|
|
|
177
|
-
return { files: writes, warnings };
|
|
236
|
+
return { files: writes, warnings, conflicts };
|
|
178
237
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Backward-compatible facade over the rc.12+ template-driven docsFile()
|
|
2
|
+
// implementation. The Markdown structure now lives in
|
|
3
|
+
// `templates/docs/entity-page.md.mustache`; this module preserves the
|
|
4
|
+
// `renderDocsFile()` + `DocsRenderOpts` exports so the per-section unit
|
|
5
|
+
// tests in `test/templates/docs-file.test.ts` keep working without
|
|
6
|
+
// modification.
|
|
7
|
+
//
|
|
8
|
+
// Adopters writing custom docs templates should import the public surface
|
|
9
|
+
// from `@metaobjectsdev/codegen-ts/generators` instead:
|
|
10
|
+
// - `buildEntityDocData(entity, opts)` for the typed data dict
|
|
11
|
+
// - `templateGenerator({ template: "docs/entity-page.md", ... })` for the
|
|
12
|
+
// standard end-to-end generator
|
|
13
|
+
|
|
14
|
+
import type { MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
|
|
15
|
+
import type { Dialect } from "../column-mapper.js";
|
|
16
|
+
import type { ColumnNamingStrategy } from "../metaobjects-config.js";
|
|
17
|
+
import { render } from "@metaobjectsdev/render";
|
|
18
|
+
import { buildEntityDocData } from "../generators/docs-data-builder.js";
|
|
19
|
+
import { frameworkTemplatesProvider } from "../render-engine/framework-provider.js";
|
|
20
|
+
|
|
21
|
+
export interface DocsRenderOpts {
|
|
22
|
+
dialect: Dialect;
|
|
23
|
+
columnNamingStrategy?: ColumnNamingStrategy;
|
|
24
|
+
loadedRoot: MetaRoot;
|
|
25
|
+
/** Names of generators present in the pipeline — drives the "Generated code"
|
|
26
|
+
* section. Always includes "entity-file" implicitly. Recognized names:
|
|
27
|
+
* "queries-file", "routes-file", "routes-file-hono". */
|
|
28
|
+
generatorNames?: ReadonlySet<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Backward-compatible entry point: builds the EntityDocData payload and
|
|
32
|
+
* renders it via the framework template. Byte-identical to the hand-coded
|
|
33
|
+
* rc.11 output (gated by `docs-file-conformance.test.ts`). */
|
|
34
|
+
export function renderDocsFile(entity: MetaObject, opts: DocsRenderOpts): string {
|
|
35
|
+
const data = buildEntityDocData(entity, {
|
|
36
|
+
dialect: opts.dialect,
|
|
37
|
+
...(opts.columnNamingStrategy !== undefined
|
|
38
|
+
? { columnNamingStrategy: opts.columnNamingStrategy }
|
|
39
|
+
: {}),
|
|
40
|
+
loadedRoot: opts.loadedRoot,
|
|
41
|
+
...(opts.generatorNames !== undefined
|
|
42
|
+
? { generatorNames: opts.generatorNames }
|
|
43
|
+
: {}),
|
|
44
|
+
});
|
|
45
|
+
return render({
|
|
46
|
+
ref: "docs/entity-page.md",
|
|
47
|
+
payload: data,
|
|
48
|
+
provider: frameworkTemplatesProvider,
|
|
49
|
+
format: "markdown",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{{{generatedMarker}}}
|
|
2
|
+
|
|
3
|
+
# {{entity.name}}
|
|
4
|
+
{{#descriptionQuote}}
|
|
5
|
+
|
|
6
|
+
{{{.}}}
|
|
7
|
+
{{/descriptionQuote}}
|
|
8
|
+
|
|
9
|
+
{{{preambleHeader}}}
|
|
10
|
+
{{#hasStorage}}
|
|
11
|
+
|
|
12
|
+
## Storage
|
|
13
|
+
|
|
14
|
+
{{{storage.tableHeader}}}
|
|
15
|
+
{{#storage.rows}}
|
|
16
|
+
{{{rowLine}}}
|
|
17
|
+
{{/storage.rows}}
|
|
18
|
+
{{/hasStorage}}
|
|
19
|
+
{{#hasIdentities}}
|
|
20
|
+
|
|
21
|
+
## Identity
|
|
22
|
+
|
|
23
|
+
{{#identities}}
|
|
24
|
+
- {{{bullet}}}
|
|
25
|
+
{{/identities}}
|
|
26
|
+
{{/hasIdentities}}
|
|
27
|
+
{{#hasRelationships}}
|
|
28
|
+
|
|
29
|
+
## Relationships
|
|
30
|
+
|
|
31
|
+
{{#relationships}}
|
|
32
|
+
- {{{bullet}}}
|
|
33
|
+
{{/relationships}}
|
|
34
|
+
{{/hasRelationships}}
|
|
35
|
+
|
|
36
|
+
## Validation
|
|
37
|
+
|
|
38
|
+
- `{{validation.insertSchema}}` (Zod) — for creating new {{validation.lower}}s.
|
|
39
|
+
- `{{validation.updateSchema}}` (Zod) — for partial updates.
|
|
40
|
+
- See `{{validation.entityFile}}` for the exported schemas.
|
|
41
|
+
{{#hasUsedBy}}
|
|
42
|
+
|
|
43
|
+
## Used by
|
|
44
|
+
|
|
45
|
+
{{#usedBy}}
|
|
46
|
+
- {{{bullet}}}
|
|
47
|
+
{{/usedBy}}
|
|
48
|
+
{{/hasUsedBy}}
|
|
49
|
+
|
|
50
|
+
## Generated code
|
|
51
|
+
|
|
52
|
+
{{#generated}}
|
|
53
|
+
- `{{filename}}` — {{{description}}}
|
|
54
|
+
{{/generated}}
|