@metaobjectsdev/codegen-ts 0.9.0 → 0.10.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/README.md +1 -1
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +24 -8
- package/dist/column-mapper.js.map +1 -1
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -1
- package/dist/docs-paths.d.ts +58 -0
- package/dist/docs-paths.d.ts.map +1 -0
- package/dist/docs-paths.js +89 -0
- package/dist/docs-paths.js.map +1 -0
- package/dist/enum-import.d.ts +14 -0
- package/dist/enum-import.d.ts.map +1 -0
- package/dist/enum-import.js +35 -0
- package/dist/enum-import.js.map +1 -0
- package/dist/enum-shared.d.ts +32 -0
- package/dist/enum-shared.d.ts.map +1 -0
- package/dist/enum-shared.js +83 -0
- package/dist/enum-shared.js.map +1 -0
- package/dist/generator-registry.d.ts +22 -0
- package/dist/generator-registry.d.ts.map +1 -0
- package/dist/generator-registry.js +161 -0
- package/dist/generator-registry.js.map +1 -0
- package/dist/generator.d.ts +6 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/api-doc-render.d.ts +17 -0
- package/dist/generators/api-doc-render.d.ts.map +1 -0
- package/dist/generators/api-doc-render.js +431 -0
- package/dist/generators/api-doc-render.js.map +1 -0
- package/dist/generators/api-docs-file.d.ts +21 -0
- package/dist/generators/api-docs-file.d.ts.map +1 -0
- package/dist/generators/api-docs-file.js +112 -0
- package/dist/generators/api-docs-file.js.map +1 -0
- package/dist/generators/api-field-shape.d.ts +39 -0
- package/dist/generators/api-field-shape.d.ts.map +1 -0
- package/dist/generators/api-field-shape.js +92 -0
- package/dist/generators/api-field-shape.js.map +1 -0
- package/dist/generators/api-label.d.ts +3 -0
- package/dist/generators/api-label.d.ts.map +1 -0
- package/dist/generators/api-label.js +8 -0
- package/dist/generators/api-label.js.map +1 -0
- package/dist/generators/api-model.d.ts +122 -0
- package/dist/generators/api-model.d.ts.map +1 -0
- package/dist/generators/api-model.js +809 -0
- package/dist/generators/api-model.js.map +1 -0
- package/dist/generators/docs-data-builder.d.ts +26 -4
- package/dist/generators/docs-data-builder.d.ts.map +1 -1
- package/dist/generators/docs-data-builder.js +436 -164
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/docs-data.d.ts +136 -27
- package/dist/generators/docs-data.d.ts.map +1 -1
- package/dist/generators/docs-data.js +1 -1
- package/dist/generators/docs-data.js.map +1 -1
- package/dist/generators/docs-file.d.ts +19 -0
- package/dist/generators/docs-file.d.ts.map +1 -1
- package/dist/generators/docs-file.js +154 -27
- package/dist/generators/docs-file.js.map +1 -1
- package/dist/generators/entity-file.d.ts.map +1 -1
- package/dist/generators/entity-file.js +29 -14
- package/dist/generators/entity-file.js.map +1 -1
- package/dist/generators/extractor-file.d.ts.map +1 -1
- package/dist/generators/extractor-file.js +2 -1
- package/dist/generators/extractor-file.js.map +1 -1
- package/dist/generators/field-anchor.d.ts +7 -0
- package/dist/generators/field-anchor.d.ts.map +1 -0
- package/dist/generators/field-anchor.js +23 -0
- package/dist/generators/field-anchor.js.map +1 -0
- package/dist/generators/index.d.ts +8 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +6 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/mermaid-er.d.ts +14 -0
- package/dist/generators/mermaid-er.d.ts.map +1 -1
- package/dist/generators/mermaid-er.js +14 -0
- package/dist/generators/mermaid-er.js.map +1 -1
- package/dist/generators/output-parser-file.d.ts.map +1 -1
- package/dist/generators/output-parser-file.js +3 -4
- package/dist/generators/output-parser-file.js.map +1 -1
- package/dist/generators/output-prompt-file.d.ts.map +1 -1
- package/dist/generators/output-prompt-file.js +2 -2
- package/dist/generators/output-prompt-file.js.map +1 -1
- package/dist/generators/prompt-render-file.d.ts.map +1 -1
- package/dist/generators/prompt-render-file.js +3 -4
- package/dist/generators/prompt-render-file.js.map +1 -1
- package/dist/generators/queries-file.d.ts.map +1 -1
- package/dist/generators/queries-file.js +8 -3
- package/dist/generators/queries-file.js.map +1 -1
- package/dist/generators/render-helper-file.d.ts.map +1 -1
- package/dist/generators/render-helper-file.js +2 -2
- package/dist/generators/render-helper-file.js.map +1 -1
- package/dist/generators/routes-file-hono.d.ts.map +1 -1
- package/dist/generators/routes-file-hono.js +5 -1
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts +3 -0
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +6 -1
- package/dist/generators/routes-file.js.map +1 -1
- package/dist/generators/template-doc-builder.d.ts +19 -0
- package/dist/generators/template-doc-builder.d.ts.map +1 -0
- package/dist/generators/template-doc-builder.js +220 -0
- package/dist/generators/template-doc-builder.js.map +1 -0
- package/dist/generators/template-doc-data.d.ts +62 -0
- package/dist/generators/template-doc-data.d.ts.map +1 -0
- package/dist/generators/template-doc-data.js +16 -0
- package/dist/generators/template-doc-data.js.map +1 -0
- package/dist/generators/template-payload-tree.d.ts +15 -0
- package/dist/generators/template-payload-tree.d.ts.map +1 -0
- package/dist/generators/template-payload-tree.js +61 -0
- package/dist/generators/template-payload-tree.js.map +1 -0
- package/dist/generators/template-source-annotate.d.ts +74 -0
- package/dist/generators/template-source-annotate.d.ts.map +1 -0
- package/dist/generators/template-source-annotate.js +184 -0
- package/dist/generators/template-source-annotate.js.map +1 -0
- package/dist/generators/template-source-render.d.ts +24 -0
- package/dist/generators/template-source-render.d.ts.map +1 -0
- package/dist/generators/template-source-render.js +175 -0
- package/dist/generators/template-source-render.js.map +1 -0
- package/dist/generators/trace-helper-file.d.ts +9 -0
- package/dist/generators/trace-helper-file.d.ts.map +1 -0
- package/dist/generators/trace-helper-file.js +196 -0
- package/dist/generators/trace-helper-file.js.map +1 -0
- package/dist/index.d.ts +29 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -2
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +75 -2
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +43 -0
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/naming.d.ts +19 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +41 -0
- package/dist/naming.js.map +1 -1
- package/dist/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +12 -4
- package/dist/payload-codegen.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +51 -25
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/relation-resolver.d.ts +16 -0
- package/dist/relation-resolver.d.ts.map +1 -1
- package/dist/relation-resolver.js +82 -1
- package/dist/relation-resolver.js.map +1 -1
- package/dist/render-context.d.ts +4 -0
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js.map +1 -1
- package/dist/render-engine/embedded-templates.generated.d.ts +2 -0
- package/dist/render-engine/embedded-templates.generated.d.ts.map +1 -0
- package/dist/render-engine/embedded-templates.generated.js +15 -0
- package/dist/render-engine/embedded-templates.generated.js.map +1 -0
- package/dist/render-engine/framework-provider.d.ts.map +1 -1
- package/dist/render-engine/framework-provider.js +26 -13
- package/dist/render-engine/framework-provider.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +17 -0
- package/dist/runner.js.map +1 -1
- package/dist/templates/docs-file.d.ts +2 -6
- package/dist/templates/docs-file.d.ts.map +1 -1
- package/dist/templates/docs-file.js +2 -5
- package/dist/templates/docs-file.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +30 -2
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-constants.d.ts +7 -0
- package/dist/templates/entity-constants.d.ts.map +1 -1
- package/dist/templates/entity-constants.js +1 -1
- package/dist/templates/entity-constants.js.map +1 -1
- package/dist/templates/entity-file.d.ts.map +1 -1
- package/dist/templates/entity-file.js +16 -5
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/enums-file.d.ts +11 -0
- package/dist/templates/enums-file.d.ts.map +1 -0
- package/dist/templates/enums-file.js +44 -0
- package/dist/templates/enums-file.js.map +1 -0
- package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
- package/dist/templates/extract-delegate-emitter.js +5 -7
- package/dist/templates/extract-delegate-emitter.js.map +1 -1
- package/dist/templates/extract-schema-emitter.d.ts.map +1 -1
- package/dist/templates/extract-schema-emitter.js +5 -1
- package/dist/templates/extract-schema-emitter.js.map +1 -1
- package/dist/templates/extractor.d.ts.map +1 -1
- package/dist/templates/extractor.js +56 -39
- package/dist/templates/extractor.js.map +1 -1
- package/dist/templates/field-meta.d.ts.map +1 -1
- package/dist/templates/field-meta.js +1 -5
- package/dist/templates/field-meta.js.map +1 -1
- package/dist/templates/filter-allowlist.d.ts +7 -2
- package/dist/templates/filter-allowlist.d.ts.map +1 -1
- package/dist/templates/filter-allowlist.js +17 -9
- package/dist/templates/filter-allowlist.js.map +1 -1
- package/dist/templates/filter-type.d.ts +7 -1
- package/dist/templates/filter-type.d.ts.map +1 -1
- package/dist/templates/filter-type.js +9 -5
- package/dist/templates/filter-type.js.map +1 -1
- package/dist/templates/find-templates.d.ts +4 -0
- package/dist/templates/find-templates.d.ts.map +1 -0
- package/dist/templates/find-templates.js +15 -0
- package/dist/templates/find-templates.js.map +1 -0
- package/dist/templates/fr010-field-mapping.d.ts +2 -0
- package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
- package/dist/templates/fr010-field-mapping.js +10 -6
- package/dist/templates/fr010-field-mapping.js.map +1 -1
- package/dist/templates/inferred-types.d.ts +44 -7
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +107 -16
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/mermaid-er.d.ts +35 -2
- package/dist/templates/mermaid-er.d.ts.map +1 -1
- package/dist/templates/mermaid-er.js +174 -7
- package/dist/templates/mermaid-er.js.map +1 -1
- package/dist/templates/output-parser.d.ts.map +1 -1
- package/dist/templates/output-parser.js +30 -79
- package/dist/templates/output-parser.js.map +1 -1
- package/dist/templates/output-prompt.d.ts.map +1 -1
- package/dist/templates/output-prompt.js +2 -2
- package/dist/templates/output-prompt.js.map +1 -1
- package/dist/templates/queries-file.d.ts.map +1 -1
- package/dist/templates/queries-file.js +112 -4
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/queries.d.ts +5 -0
- package/dist/templates/queries.d.ts.map +1 -1
- package/dist/templates/queries.js +7 -7
- package/dist/templates/queries.js.map +1 -1
- package/dist/templates/recover-schema-emitter.d.ts +8 -0
- package/dist/templates/recover-schema-emitter.d.ts.map +1 -0
- package/dist/templates/recover-schema-emitter.js +64 -0
- package/dist/templates/recover-schema-emitter.js.map +1 -0
- package/dist/templates/relations-block.js +10 -0
- package/dist/templates/relations-block.js.map +1 -1
- package/dist/templates/render-helper.d.ts.map +1 -1
- package/dist/templates/render-helper.js +4 -4
- package/dist/templates/render-helper.js.map +1 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +183 -6
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/tph-discriminator.d.ts +56 -0
- package/dist/templates/tph-discriminator.d.ts.map +1 -0
- package/dist/templates/tph-discriminator.js +180 -0
- package/dist/templates/tph-discriminator.js.map +1 -0
- package/dist/templates/value-object-file.d.ts +2 -1
- package/dist/templates/value-object-file.d.ts.map +1 -1
- package/dist/templates/value-object-file.js +32 -4
- package/dist/templates/value-object-file.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +64 -1
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +181 -8
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +103 -34
- package/src/column-mapper.ts +25 -8
- package/src/constants.ts +18 -0
- package/src/docs-paths.ts +128 -0
- package/src/enum-import.ts +43 -0
- package/src/enum-shared.ts +95 -0
- package/src/generator-registry.ts +204 -0
- package/src/generator.ts +6 -0
- package/src/generators/api-doc-render.ts +572 -0
- package/src/generators/api-docs-file.ts +146 -0
- package/src/generators/api-field-shape.ts +114 -0
- package/src/generators/api-label.ts +7 -0
- package/src/generators/api-model.ts +1067 -0
- package/src/generators/docs-data-builder.ts +479 -185
- package/src/generators/docs-data.ts +139 -28
- package/src/generators/docs-file.ts +205 -39
- package/src/generators/entity-file.ts +31 -15
- package/src/generators/extractor-file.ts +2 -1
- package/src/generators/field-anchor.ts +24 -0
- package/src/generators/index.ts +8 -1
- package/src/generators/mermaid-er.ts +14 -0
- package/src/generators/output-parser-file.ts +3 -4
- package/src/generators/output-prompt-file.ts +2 -1
- package/src/generators/prompt-render-file.ts +3 -4
- package/src/generators/queries-file.ts +9 -3
- package/src/generators/render-helper-file.ts +2 -1
- package/src/generators/routes-file-hono.ts +5 -1
- package/src/generators/routes-file.ts +7 -1
- package/src/generators/template-doc-builder.ts +306 -0
- package/src/generators/template-doc-data.ts +85 -0
- package/src/generators/template-payload-tree.ts +71 -0
- package/src/generators/template-source-annotate.ts +290 -0
- package/src/generators/template-source-render.ts +203 -0
- package/src/generators/trace-helper-file.ts +301 -0
- package/src/index.ts +55 -4
- package/src/metaobjects-config.ts +117 -2
- package/src/naming.ts +48 -0
- package/src/payload-codegen.ts +14 -3
- package/src/projection/extract-view-spec.ts +49 -30
- package/src/relation-resolver.ts +103 -1
- package/src/render-context.ts +4 -0
- package/src/render-engine/embedded-templates.generated.ts +14 -0
- package/src/render-engine/framework-provider.ts +25 -11
- package/src/runner.ts +21 -0
- package/src/templates/docs-file.ts +2 -9
- package/src/templates/drizzle-schema.ts +31 -1
- package/src/templates/entity-constants.ts +1 -1
- package/src/templates/entity-file.ts +16 -5
- package/src/templates/enums-file.ts +50 -0
- package/src/templates/extract-delegate-emitter.ts +5 -6
- package/src/templates/extractor.ts +68 -38
- package/src/templates/field-meta.ts +0 -6
- package/src/templates/filter-allowlist.ts +17 -10
- package/src/templates/filter-type.ts +8 -6
- package/src/templates/find-templates.ts +15 -0
- package/src/templates/fr010-field-mapping.ts +10 -8
- package/src/templates/inferred-types.ts +108 -18
- package/src/templates/mermaid-er.ts +176 -8
- package/src/templates/output-parser.ts +30 -79
- package/src/templates/output-prompt.ts +2 -1
- package/src/templates/queries-file.ts +132 -3
- package/src/templates/queries.ts +15 -7
- package/src/templates/relations-block.ts +17 -0
- package/src/templates/render-helper.ts +4 -3
- package/src/templates/routes-file.ts +233 -6
- package/src/templates/tph-discriminator.ts +232 -0
- package/src/templates/value-object-file.ts +38 -4
- package/src/templates/zod-validators.ts +204 -7
- package/templates/api/agent-api.md.mustache +30 -0
- package/templates/api/entity-api.md.mustache +69 -0
- package/templates/api/index.md.mustache +21 -0
- package/templates/docs/entity-page.md.mustache +33 -21
- package/templates/docs/template-page.md.mustache +56 -0
- package/src/templates/extract-schema-emitter.ts +0 -111
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// Custom output directory:
|
|
10
10
|
// generators: [..., outputParser({ outDir: "src/generated/outputs" })]
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { TEMPLATE_SUBTYPE_OUTPUT } from "@metaobjectsdev/metadata";
|
|
13
|
+
import { findTemplates } from "../templates/find-templates.js";
|
|
13
14
|
import {
|
|
14
15
|
type EmittedFile,
|
|
15
16
|
type Generator,
|
|
@@ -30,9 +31,7 @@ export const outputParser = function outputParser(opts?: OutputParserOpts): Gene
|
|
|
30
31
|
const generator: Generator = {
|
|
31
32
|
name: "output-parser",
|
|
32
33
|
generate: oncePerRun((_entities, ctx) => {
|
|
33
|
-
const outputs = ctx.loadedRoot
|
|
34
|
-
.ownChildren()
|
|
35
|
-
.filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_OUTPUT);
|
|
34
|
+
const outputs = findTemplates(ctx.loadedRoot, TEMPLATE_SUBTYPE_OUTPUT);
|
|
36
35
|
const files: EmittedFile[] = [];
|
|
37
36
|
for (const t of outputs) {
|
|
38
37
|
files.push({
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
TYPE_TEMPLATE,
|
|
18
18
|
TEMPLATE_SUBTYPE_OUTPUT,
|
|
19
19
|
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
20
|
+
refMatchesObject,
|
|
20
21
|
} from "@metaobjectsdev/metadata";
|
|
21
22
|
import {
|
|
22
23
|
type EmittedFile,
|
|
@@ -49,7 +50,7 @@ export const outputPrompt = function outputPrompt(opts?: OutputPromptOpts): Gene
|
|
|
49
50
|
// @payloadRef must resolve to a value-object (same contract as the parser).
|
|
50
51
|
const payloadRef = t.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
51
52
|
if (typeof payloadRef !== "string") continue;
|
|
52
|
-
const vo = root.ownChildren().find((c) => c.type === TYPE_OBJECT && c
|
|
53
|
+
const vo = root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, payloadRef));
|
|
53
54
|
if (!vo) continue;
|
|
54
55
|
files.push({
|
|
55
56
|
path: `${dirPrefix}${t.name}.prompt.ts`,
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// Custom output path:
|
|
10
10
|
// generators: [..., promptRender({ outFile: "src/render/generated/prompts.ts" })]
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { TEMPLATE_SUBTYPE_PROMPT, OBJECT_SUBTYPE_VALUE } from "@metaobjectsdev/metadata";
|
|
13
|
+
import { findTemplates } from "../templates/find-templates.js";
|
|
13
14
|
import {
|
|
14
15
|
type Generator,
|
|
15
16
|
type GeneratorFactory,
|
|
@@ -45,9 +46,7 @@ export const promptRender = function promptRender(opts?: PromptRenderOpts): Gene
|
|
|
45
46
|
name: "prompt-render",
|
|
46
47
|
generate: oncePerRun((entities, ctx) => {
|
|
47
48
|
const payloads = entities.filter((e) => e.subType === OBJECT_SUBTYPE_VALUE);
|
|
48
|
-
const prompts = ctx.loadedRoot
|
|
49
|
-
.ownChildren()
|
|
50
|
-
.filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_PROMPT);
|
|
49
|
+
const prompts = findTemplates(ctx.loadedRoot, TEMPLATE_SUBTYPE_PROMPT);
|
|
51
50
|
|
|
52
51
|
if (payloads.length === 0 && prompts.length === 0) {
|
|
53
52
|
return [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OBJECT_SUBTYPE_VALUE, type MetaObject } from "@metaobjectsdev/metadata";
|
|
2
2
|
import { perEntity, type Generator, type GeneratorFactory } from "../generator.js";
|
|
3
3
|
import { renderQueriesFile } from "../templates/queries-file.js";
|
|
4
|
+
import { isTphSubtype } from "../templates/zod-validators.js";
|
|
4
5
|
import { formatTs } from "../format.js";
|
|
5
6
|
import { entityOutputPath } from "../import-path.js";
|
|
6
7
|
|
|
@@ -13,13 +14,18 @@ export interface QueriesFileOpts {
|
|
|
13
14
|
// emits findById/updateById/deleteById against a non-existent column. Skipping
|
|
14
15
|
// value subtypes is unconditional — the user-supplied filter (if any) is applied
|
|
15
16
|
// on top via boolean AND.
|
|
16
|
-
|
|
17
|
+
//
|
|
18
|
+
// FR-017 Tier 2: TPH subtypes are ALSO skipped — they emit no standalone
|
|
19
|
+
// queries file. Their per-subtype CRUD helpers live in the discriminator
|
|
20
|
+
// base's queries file (which targets the single shared table).
|
|
21
|
+
const skipNonQueryable = (e: MetaObject): boolean =>
|
|
22
|
+
e.subType !== OBJECT_SUBTYPE_VALUE && !isTphSubtype(e);
|
|
17
23
|
|
|
18
24
|
export const queriesFile = function queriesFile(opts?: QueriesFileOpts): Generator {
|
|
19
25
|
const userFilter = opts?.filter;
|
|
20
26
|
const filter: (e: MetaObject) => boolean = userFilter
|
|
21
|
-
? (e) =>
|
|
22
|
-
:
|
|
27
|
+
? (e) => skipNonQueryable(e) && userFilter(e)
|
|
28
|
+
: skipNonQueryable;
|
|
23
29
|
|
|
24
30
|
const generator: Generator = {
|
|
25
31
|
name: "queries-file",
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
TYPE_TEMPLATE,
|
|
21
21
|
TEMPLATE_SUBTYPE_OUTPUT,
|
|
22
22
|
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
23
|
+
refMatchesObject,
|
|
23
24
|
} from "@metaobjectsdev/metadata";
|
|
24
25
|
import {
|
|
25
26
|
type EmittedFile,
|
|
@@ -55,7 +56,7 @@ export const renderHelper = function renderHelper(opts?: RenderHelperOpts): Gene
|
|
|
55
56
|
// @payloadRef must resolve to a value-object (same contract as the parser).
|
|
56
57
|
const payloadRef = t.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
57
58
|
if (typeof payloadRef !== "string") continue;
|
|
58
|
-
const vo = root.ownChildren().find((c) => c.type === TYPE_OBJECT && c
|
|
59
|
+
const vo = root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, payloadRef));
|
|
59
60
|
if (!vo) continue;
|
|
60
61
|
files.push({
|
|
61
62
|
// renderRenderHelper THROWS (fails codegen) on a mustache↔VO drift —
|
|
@@ -3,6 +3,7 @@ import { perEntity, type Generator, type GeneratorFactory } from "../generator.j
|
|
|
3
3
|
import { renderRoutesFileHono } from "../templates/routes-file-hono.js";
|
|
4
4
|
import { formatTs } from "../format.js";
|
|
5
5
|
import { entityOutputPath } from "../import-path.js";
|
|
6
|
+
import { CODEGEN_ATTR_EMIT_ROUTES } from "../constants.js";
|
|
6
7
|
|
|
7
8
|
export interface RoutesFileHonoOpts {
|
|
8
9
|
filter?: (entity: MetaObject) => boolean;
|
|
@@ -26,7 +27,10 @@ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts)
|
|
|
26
27
|
const userFilter = opts?.filter ?? (() => true);
|
|
27
28
|
const generator: Generator = {
|
|
28
29
|
name: "routes-file-hono",
|
|
29
|
-
|
|
30
|
+
// Marks this as the Hono routes generator so the runner can aggregate
|
|
31
|
+
// `ctx.config.includeHonoRoutes` and api-docs auto-documents the Hono surface.
|
|
32
|
+
emitsHonoRoutes: true,
|
|
33
|
+
filter: (e: MetaObject) => e.ownAttr(CODEGEN_ATTR_EMIT_ROUTES) !== false && userFilter(e),
|
|
30
34
|
generate: perEntity(async (entity, ctx) => {
|
|
31
35
|
if (!ctx.renderContext) {
|
|
32
36
|
throw new Error("routes-file-hono: renderContext is required (provided by runGen)");
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
2
2
|
import { perEntity, type Generator, type GeneratorFactory } from "../generator.js";
|
|
3
3
|
import { renderRoutesFile } from "../templates/routes-file.js";
|
|
4
|
+
import { isTphSubtype } from "../templates/zod-validators.js";
|
|
4
5
|
import { formatTs } from "../format.js";
|
|
5
6
|
import { entityOutputPath } from "../import-path.js";
|
|
7
|
+
import { CODEGEN_ATTR_EMIT_ROUTES } from "../constants.js";
|
|
6
8
|
|
|
7
9
|
export interface RoutesFileOpts {
|
|
8
10
|
filter?: (entity: MetaObject) => boolean;
|
|
@@ -12,13 +14,17 @@ export interface RoutesFileOpts {
|
|
|
12
14
|
/**
|
|
13
15
|
* Per-entity opt-out via `@emitRoutes: false` is honored. If the user supplies
|
|
14
16
|
* their own filter, both must pass (AND).
|
|
17
|
+
*
|
|
18
|
+
* FR-017 Tier 2: TPH subtypes get no standalone routes file — their per-subtype
|
|
19
|
+
* route set lives in the discriminator base's routes file.
|
|
15
20
|
*/
|
|
16
21
|
export const routesFile = function routesFile(opts?: RoutesFileOpts): Generator {
|
|
17
22
|
const userFilter = opts?.filter ?? (() => true);
|
|
18
23
|
const generator: Generator = {
|
|
19
24
|
name: "routes-file",
|
|
20
25
|
// Always set: AND-composes metadata opt-out with optional user filter.
|
|
21
|
-
filter: (e: MetaObject) =>
|
|
26
|
+
filter: (e: MetaObject) =>
|
|
27
|
+
e.ownAttr(CODEGEN_ATTR_EMIT_ROUTES) !== false && !isTphSubtype(e) && userFilter(e),
|
|
22
28
|
generate: perEntity(async (entity, ctx) => {
|
|
23
29
|
if (!ctx.renderContext) {
|
|
24
30
|
throw new Error("routes-file: renderContext is required (provided by runGen)");
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// Walk one `template.output` node (+ root) into the NEUTRAL TemplateDocData
|
|
2
|
+
// shape the `docs/template-page.md` Mustache template consumes. The attr reads
|
|
3
|
+
// MIRROR render-helper-file.ts / templates/render-helper.ts (@kind, @payloadRef,
|
|
4
|
+
// @textRef, @format, @maxChars, @requiredTags; for email @subjectRef /
|
|
5
|
+
// @htmlBodyRef / @textBodyRef) — but this builder emits DESCRIPTION, never code:
|
|
6
|
+
// no helper signatures, no language types. `capability` is a FIXED, neutral
|
|
7
|
+
// sentence per @kind.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type MetaData,
|
|
11
|
+
type MetaRoot,
|
|
12
|
+
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
13
|
+
TEMPLATE_ATTR_TEXT_REF,
|
|
14
|
+
TEMPLATE_ATTR_FORMAT,
|
|
15
|
+
TEMPLATE_ATTR_MAX_CHARS,
|
|
16
|
+
TEMPLATE_ATTR_KIND,
|
|
17
|
+
TEMPLATE_KIND_EMAIL,
|
|
18
|
+
TEMPLATE_KIND_DOCUMENT,
|
|
19
|
+
TEMPLATE_KIND_DEFAULT,
|
|
20
|
+
TEMPLATE_ATTR_SUBJECT_REF,
|
|
21
|
+
TEMPLATE_ATTR_HTML_BODY_REF,
|
|
22
|
+
TEMPLATE_ATTR_TEXT_BODY_REF,
|
|
23
|
+
TEMPLATE_ATTR_REQUIRED_TAGS,
|
|
24
|
+
DOC_ATTR_DESCRIPTION,
|
|
25
|
+
stripPackage,
|
|
26
|
+
} from "@metaobjectsdev/metadata";
|
|
27
|
+
import {
|
|
28
|
+
TYPE_TEMPLATE,
|
|
29
|
+
TEMPLATE_SUBTYPE_OUTPUT,
|
|
30
|
+
} from "@metaobjectsdev/metadata";
|
|
31
|
+
import type { Provider } from "@metaobjectsdev/render";
|
|
32
|
+
import { GENERATED_HEADER } from "../constants.js";
|
|
33
|
+
import type { OutputLayout } from "../import-path.js";
|
|
34
|
+
import { docPageHref, docPageNode, type DocPageNode } from "../docs-paths.js";
|
|
35
|
+
import { fieldAnchorSlug } from "./field-anchor.js";
|
|
36
|
+
import type { TemplateDocData, TemplateOutputPart } from "./template-doc-data.js";
|
|
37
|
+
import { buildEnrichedPayloadTree } from "./template-payload-tree.js";
|
|
38
|
+
import { annotateTemplate, type AnnotatePayloadField } from "./template-source-annotate.js";
|
|
39
|
+
import {
|
|
40
|
+
renderSourceBlock,
|
|
41
|
+
renderVariablesTable,
|
|
42
|
+
renderRichLinkedHtml,
|
|
43
|
+
} from "./template-source-render.js";
|
|
44
|
+
|
|
45
|
+
export interface BuildTemplateDocDataOpts {
|
|
46
|
+
/** Page-placement layout. Defaults to "flat" (back-compat: same-dir links). */
|
|
47
|
+
layout?: OutputLayout;
|
|
48
|
+
/** Root used to resolve the @payloadRef target's package for a correct
|
|
49
|
+
* cross-link in package layout. Optional: flat layout never needs it. */
|
|
50
|
+
loadedRoot?: MetaRoot;
|
|
51
|
+
/** The page provider — the SAME one the render-helper drift gate / page
|
|
52
|
+
* rendering use (e.g. `projectProvider(projectRoot)`). Used to resolve each
|
|
53
|
+
* referenced mustache's SOURCE TEXT for the "## Template source" section.
|
|
54
|
+
* Optional: when absent (or a ref doesn't resolve) the section is omitted. */
|
|
55
|
+
provider?: Provider;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// FIXED, language-NEUTRAL capability sentences. NO type names, NO signatures.
|
|
59
|
+
const CAPABILITY_DOCUMENT =
|
|
60
|
+
"A render helper is generated for this template: it takes the payload and " +
|
|
61
|
+
"returns the rendered output as a single string.";
|
|
62
|
+
const CAPABILITY_EMAIL =
|
|
63
|
+
"A render helper is generated for this template: it takes the payload and " +
|
|
64
|
+
"returns the rendered email — subject, HTML body, and an optional text body.";
|
|
65
|
+
|
|
66
|
+
/** Read an attr that may be a string or string[] (string-array attrs come back
|
|
67
|
+
* as string[]; a bare comma string is split defensively). Returns a trimmed,
|
|
68
|
+
* non-empty string list. */
|
|
69
|
+
function attrStringList(value: unknown): string[] {
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return value.filter((v): v is string => typeof v === "string" && v.length > 0);
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
74
|
+
return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readMaxChars(value: unknown): number | undefined {
|
|
80
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
81
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
82
|
+
const n = Number(value);
|
|
83
|
+
if (Number.isFinite(n)) return n;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function templateDescription(t: MetaData): string | undefined {
|
|
89
|
+
const v = t.attr(DOC_ATTR_DESCRIPTION);
|
|
90
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Page-placement node for a metadata object resolved by short name, with the
|
|
94
|
+
* SAME package-less fallback the Payload cross-link and field hrefs share: when
|
|
95
|
+
* the object can't be resolved off the root, fall back to a root-level node so a
|
|
96
|
+
* link is still emitted. Keeps every inbound href routing through the one
|
|
97
|
+
* `docPageHref(layout, …)` placement. */
|
|
98
|
+
function pageNodeByName(root: MetaRoot | undefined, name: string): DocPageNode {
|
|
99
|
+
const obj = root?.findObject(name);
|
|
100
|
+
return obj !== undefined ? docPageNode(obj) : { name };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Build the TemplateDocData for one `template.output` node. */
|
|
104
|
+
export function buildTemplateDocData(
|
|
105
|
+
template: MetaData,
|
|
106
|
+
opts?: BuildTemplateDocDataOpts,
|
|
107
|
+
): TemplateDocData {
|
|
108
|
+
const layout = opts?.layout ?? "flat";
|
|
109
|
+
const root = opts?.loadedRoot;
|
|
110
|
+
const kindRaw = ((template.ownAttr(TEMPLATE_ATTR_KIND) as string | undefined) ??
|
|
111
|
+
TEMPLATE_KIND_DEFAULT).toLowerCase();
|
|
112
|
+
const isEmail = kindRaw === TEMPLATE_KIND_EMAIL;
|
|
113
|
+
const kind: "document" | "email" = isEmail ? TEMPLATE_KIND_EMAIL : TEMPLATE_KIND_DOCUMENT;
|
|
114
|
+
|
|
115
|
+
const payloadRefRaw = template.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
116
|
+
const payloadName =
|
|
117
|
+
typeof payloadRefRaw === "string" && payloadRefRaw.length > 0
|
|
118
|
+
? stripPackage(payloadRefRaw)
|
|
119
|
+
: "unknown";
|
|
120
|
+
|
|
121
|
+
const requiredTags = attrStringList(template.ownAttr(TEMPLATE_ATTR_REQUIRED_TAGS));
|
|
122
|
+
const maxChars = readMaxChars(template.ownAttr(TEMPLATE_ATTR_MAX_CHARS));
|
|
123
|
+
|
|
124
|
+
let format = "";
|
|
125
|
+
let parts: TemplateOutputPart[] | undefined;
|
|
126
|
+
const sourceRefs: string[] = [];
|
|
127
|
+
|
|
128
|
+
if (isEmail) {
|
|
129
|
+
const subjectRef = template.ownAttr(TEMPLATE_ATTR_SUBJECT_REF);
|
|
130
|
+
const htmlBodyRef = template.ownAttr(TEMPLATE_ATTR_HTML_BODY_REF);
|
|
131
|
+
const textBodyRef = template.ownAttr(TEMPLATE_ATTR_TEXT_BODY_REF);
|
|
132
|
+
parts = [];
|
|
133
|
+
if (typeof subjectRef === "string") {
|
|
134
|
+
parts.push({ label: "Subject", ref: subjectRef, format: "text", escaped: false });
|
|
135
|
+
sourceRefs.push(subjectRef);
|
|
136
|
+
}
|
|
137
|
+
if (typeof htmlBodyRef === "string") {
|
|
138
|
+
parts.push({ label: "HTML body", ref: htmlBodyRef, format: "html", escaped: true });
|
|
139
|
+
sourceRefs.push(htmlBodyRef);
|
|
140
|
+
}
|
|
141
|
+
if (typeof textBodyRef === "string") {
|
|
142
|
+
parts.push({ label: "Text body", ref: textBodyRef, format: "text", escaped: false });
|
|
143
|
+
sourceRefs.push(textBodyRef);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
format = ((template.ownAttr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
|
|
147
|
+
const textRef = template.ownAttr(TEMPLATE_ATTR_TEXT_REF);
|
|
148
|
+
if (typeof textRef === "string") sourceRefs.push(textRef);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Cross-link to the payload entity's page. The href is derived from the SAME
|
|
152
|
+
// page-placement function used to write that entity page, so it resolves in
|
|
153
|
+
// BOTH layouts (package layout folds the correct relative path).
|
|
154
|
+
const payloadLink = docPageHref(
|
|
155
|
+
layout,
|
|
156
|
+
docPageNode(template),
|
|
157
|
+
pageNodeByName(root, payloadName),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const data: TemplateDocData = {
|
|
161
|
+
generatedMarker: `<!-- ${GENERATED_HEADER} — DO NOT EDIT. -->`,
|
|
162
|
+
name: template.name,
|
|
163
|
+
kind,
|
|
164
|
+
isEmail,
|
|
165
|
+
format,
|
|
166
|
+
payload: { name: payloadName, link: payloadLink },
|
|
167
|
+
sourceRefs,
|
|
168
|
+
capability: isEmail ? CAPABILITY_EMAIL : CAPABILITY_DOCUMENT,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (parts !== undefined) data.parts = parts;
|
|
172
|
+
if (requiredTags.length > 0) {
|
|
173
|
+
data.requiredTags = requiredTags;
|
|
174
|
+
data.hasRequiredTags = true;
|
|
175
|
+
}
|
|
176
|
+
if (maxChars !== undefined) data.maxChars = maxChars;
|
|
177
|
+
|
|
178
|
+
// ── "## Template source" section (Task 4) ────────────────────────────────
|
|
179
|
+
// Resolve each referenced mustache's SOURCE TEXT via the page provider, then
|
|
180
|
+
// annotate it against the ENRICHED payload tree (owner/type/required per node)
|
|
181
|
+
// and pre-render the three doc forms. Only when a provider + a resolvable
|
|
182
|
+
// payload VO are available; a ref that doesn't resolve is skipped (no crash).
|
|
183
|
+
if (opts?.provider !== undefined && root !== undefined && payloadName !== "unknown") {
|
|
184
|
+
const section = buildTemplateSourceSection({
|
|
185
|
+
provider: opts.provider,
|
|
186
|
+
root,
|
|
187
|
+
layout,
|
|
188
|
+
template,
|
|
189
|
+
payloadName,
|
|
190
|
+
// Document: the single @textRef (unlabeled). Email: one labeled part each.
|
|
191
|
+
refs:
|
|
192
|
+
parts !== undefined
|
|
193
|
+
? parts.map((p) => ({ label: p.label, ref: p.ref }))
|
|
194
|
+
: sourceRefs.map((ref) => ({ ref })),
|
|
195
|
+
});
|
|
196
|
+
if (section !== undefined) data.templateSourceSection = section;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const desc = templateDescription(template);
|
|
200
|
+
if (desc !== undefined) {
|
|
201
|
+
data.descriptionQuote = desc.split("\n").map((l) => `> ${l}`.trimEnd()).join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return data;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// One referenced template source to document: an optional human label (email
|
|
208
|
+
// part name) + the logical mustache ref (`@textRef` / `@subjectRef` / …).
|
|
209
|
+
interface SourceRefSpec {
|
|
210
|
+
label?: string;
|
|
211
|
+
ref: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
interface BuildSectionArgs {
|
|
215
|
+
provider: Provider;
|
|
216
|
+
root: MetaRoot;
|
|
217
|
+
/** Page-placement layout — threaded so field/partial hrefs route through the
|
|
218
|
+
* SAME docPageHref the Payload cross-link uses (resolves under package layout). */
|
|
219
|
+
layout: OutputLayout;
|
|
220
|
+
/** The `template.output` node whose page is being built — the FROM page for
|
|
221
|
+
* every relative href on this section. */
|
|
222
|
+
template: MetaData;
|
|
223
|
+
payloadName: string;
|
|
224
|
+
refs: SourceRefSpec[];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Build the PRE-RENDERED "## Template source" section markdown. For each ref:
|
|
229
|
+
* resolve its mustache source via the provider, annotate it against the enriched
|
|
230
|
+
* payload tree, and emit the fenced source block + the linked variables table +
|
|
231
|
+
* the rich `<details>` linked view. A document template yields one block; an
|
|
232
|
+
* email yields one `### <label>` sub-section per part. Refs whose source the
|
|
233
|
+
* provider can't resolve are skipped (graceful — the build-time drift gate
|
|
234
|
+
* already errors on a truly unresolved ref). Returns `undefined` when NOTHING
|
|
235
|
+
* resolved, so the caller omits the section entirely.
|
|
236
|
+
*/
|
|
237
|
+
function buildTemplateSourceSection(args: BuildSectionArgs): string | undefined {
|
|
238
|
+
const { provider, root, layout, template, payloadName, refs } = args;
|
|
239
|
+
const tree: AnnotatePayloadField[] = buildEnrichedPayloadTree(root, payloadName);
|
|
240
|
+
const fromNode = docPageNode(template);
|
|
241
|
+
|
|
242
|
+
// Layout-aware field href: route through the SAME docPageHref the Payload
|
|
243
|
+
// cross-link uses, so the link lands on the owner VO's REAL page in BOTH
|
|
244
|
+
// layouts (flat → `./Owner.md`, package → `../<pkg>/Owner.md`).
|
|
245
|
+
const fieldHref = (owner: string, name: string): string =>
|
|
246
|
+
`${docPageHref(layout, fromNode, pageNodeByName(root, owner))}#${fieldAnchorSlug(name)}`;
|
|
247
|
+
|
|
248
|
+
const resolvePartialHref = makePartialHrefResolver(root, layout, fromNode);
|
|
249
|
+
const isMultipart = refs.length > 1 || refs.some((r) => r.label !== undefined);
|
|
250
|
+
|
|
251
|
+
const blocks: string[] = [];
|
|
252
|
+
for (const spec of refs) {
|
|
253
|
+
const source = provider.resolve(spec.ref);
|
|
254
|
+
if (source === undefined) continue; // missing source → skip this part.
|
|
255
|
+
const tokens = annotateTemplate(source, tree, {
|
|
256
|
+
ownerVoName: payloadName,
|
|
257
|
+
resolvePartialHref,
|
|
258
|
+
fieldHref,
|
|
259
|
+
});
|
|
260
|
+
const parts: string[] = [];
|
|
261
|
+
if (isMultipart && spec.label !== undefined) parts.push(`### ${spec.label}`);
|
|
262
|
+
parts.push(renderSourceBlock(tokens));
|
|
263
|
+
const table = renderVariablesTable(tokens);
|
|
264
|
+
if (table !== "") parts.push(table);
|
|
265
|
+
parts.push(renderRichLinkedHtml(tokens));
|
|
266
|
+
blocks.push(parts.join("\n\n"));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (blocks.length === 0) return undefined;
|
|
270
|
+
return ["## Template source", "", blocks.join("\n\n")].join("\n");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* A `{{>ref}}` partial-href resolver: returns a relative href to the page of the
|
|
275
|
+
* `template.output` node that documents `ref` (it is one of that template's
|
|
276
|
+
* source refs — @textRef or an email part ref), else undefined (the partial is
|
|
277
|
+
* highlight-only). The href routes through the SAME `docPageHref(layout, …)` the
|
|
278
|
+
* field/Payload links use, so it resolves in BOTH layouts (flat → `./Name.md`,
|
|
279
|
+
* package → a correct relative path like `../comms/OrderEmail.md`).
|
|
280
|
+
*/
|
|
281
|
+
function makePartialHrefResolver(
|
|
282
|
+
root: MetaRoot,
|
|
283
|
+
layout: OutputLayout,
|
|
284
|
+
fromNode: DocPageNode,
|
|
285
|
+
): (ref: string) => string | undefined {
|
|
286
|
+
// Map each documented source ref → the template node that documents it.
|
|
287
|
+
const refToTemplate = new Map<string, MetaData>();
|
|
288
|
+
for (const child of root.ownChildren()) {
|
|
289
|
+
if (child.type !== TYPE_TEMPLATE || child.subType !== TEMPLATE_SUBTYPE_OUTPUT) continue;
|
|
290
|
+
for (const attr of [
|
|
291
|
+
TEMPLATE_ATTR_TEXT_REF,
|
|
292
|
+
TEMPLATE_ATTR_SUBJECT_REF,
|
|
293
|
+
TEMPLATE_ATTR_HTML_BODY_REF,
|
|
294
|
+
TEMPLATE_ATTR_TEXT_BODY_REF,
|
|
295
|
+
]) {
|
|
296
|
+
const v = child.ownAttr(attr);
|
|
297
|
+
if (typeof v === "string" && v.length > 0 && !refToTemplate.has(v)) {
|
|
298
|
+
refToTemplate.set(v, child);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return (ref: string) => {
|
|
303
|
+
const node = refToTemplate.get(ref);
|
|
304
|
+
return node !== undefined ? docPageHref(layout, fromNode, docPageNode(node)) : undefined;
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Data-dict shape for the NEUTRAL template.output doc page (the "render
|
|
2
|
+
// contract" page). Sibling of EntityDocData (docs-data.ts) — same public-API
|
|
3
|
+
// stability contract: template authors who write a custom Mustache file for
|
|
4
|
+
// `docs/template-page.md` reference these keys.
|
|
5
|
+
//
|
|
6
|
+
// CRITICAL — NEUTRALITY: this shape carries NO language assumptions. There are
|
|
7
|
+
// no generated-helper signatures, no language type names, no SDK tokens. The
|
|
8
|
+
// `capability` field is a FIXED, language-neutral English sentence per @kind
|
|
9
|
+
// (see template-doc-builder.ts). The page describes the RENDER CONTRACT (what
|
|
10
|
+
// the template references, validates, and produces), not any one port's code.
|
|
11
|
+
//
|
|
12
|
+
// Markdown-flavored, like EntityDocData: a few fields are pre-rendered (the
|
|
13
|
+
// parts table is structural, but escaping / raw-vs-escaped wording lives in the
|
|
14
|
+
// builder so the template stays trivial and cross-port walks don't re-derive it).
|
|
15
|
+
|
|
16
|
+
/** One part of a multipart (email) template — a single rendered source ref with
|
|
17
|
+
* its format and whether the renderer escapes its output for that format. */
|
|
18
|
+
export interface TemplateOutputPart {
|
|
19
|
+
/** Human-readable part label, e.g. "Subject", "HTML body", "Text body". */
|
|
20
|
+
label: string;
|
|
21
|
+
/** The logical text ref rendered for this part (e.g. "email/welcome.html"). */
|
|
22
|
+
ref: string;
|
|
23
|
+
/** Render format for this part: "text" | "html" (subject/text = text, html = html). */
|
|
24
|
+
format: string;
|
|
25
|
+
/** True iff the renderer escapes output for this part's format (html → true). */
|
|
26
|
+
escaped: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TemplateDocData {
|
|
30
|
+
/** @markdown — the generated-by marker, echoed at the top of the page. */
|
|
31
|
+
generatedMarker: string;
|
|
32
|
+
|
|
33
|
+
/** Raw template node name — also the emitted filename (`<name>.md`). */
|
|
34
|
+
name: string;
|
|
35
|
+
|
|
36
|
+
/** @kind — "document" | "email". */
|
|
37
|
+
kind: "document" | "email";
|
|
38
|
+
/** Convenience flag for the Mustache template (no "is X" primitive). */
|
|
39
|
+
isEmail: boolean;
|
|
40
|
+
|
|
41
|
+
/** @markdown — description as a blockquote (one `> ` per line). Present iff
|
|
42
|
+
* the template declares a @description. Mirrors EntityDocData. */
|
|
43
|
+
descriptionQuote?: string;
|
|
44
|
+
|
|
45
|
+
/** Document: the @format (e.g. "html"). Email: "" — the parts carry format. */
|
|
46
|
+
format: string;
|
|
47
|
+
|
|
48
|
+
/** Email only: the ordered multipart parts (subject, html body, text body?). */
|
|
49
|
+
parts?: TemplateOutputPart[];
|
|
50
|
+
|
|
51
|
+
/** The payload object the template renders from. `link` is `./<rawName>.md` —
|
|
52
|
+
* the payload entity's own doc page (raw-name convention, matching the
|
|
53
|
+
* entity-page filename + the entity's Used-by back-link). */
|
|
54
|
+
payload: { name: string; link: string };
|
|
55
|
+
|
|
56
|
+
/** @requiredTags, if declared. Drives both the Input "Required fields" line
|
|
57
|
+
* and the Render-contract "Required tags" bullet. */
|
|
58
|
+
requiredTags?: string[];
|
|
59
|
+
/** Present-and-non-empty flag for @requiredTags (Mustache has no "non-empty
|
|
60
|
+
* array" primitive; same idiom as EntityDocData's `has*` flags). */
|
|
61
|
+
hasRequiredTags?: boolean;
|
|
62
|
+
|
|
63
|
+
/** @maxChars, if declared (document only in practice). */
|
|
64
|
+
maxChars?: number;
|
|
65
|
+
|
|
66
|
+
/** The template refs this page renders: [@textRef] for a document, or the
|
|
67
|
+
* 2–3 email part refs. Drives the Source section. */
|
|
68
|
+
sourceRefs: string[];
|
|
69
|
+
|
|
70
|
+
/** FIXED, language-NEUTRAL capability sentence per @kind. No type names, no
|
|
71
|
+
* function signatures. See template-doc-builder.ts. */
|
|
72
|
+
capability: string;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @markdown — the PRE-RENDERED "## Template source" section body (linked-
|
|
76
|
+
* template-source-docs, Task 4). Present iff at least one referenced mustache
|
|
77
|
+
* resolved to source via the page provider. For a document template it is one
|
|
78
|
+
* block; for an email it is one `### <part>` sub-section per part. Each carries
|
|
79
|
+
* the fenced ```mustache source, a linked variables table, and the rich
|
|
80
|
+
* `<details>` linked view — ALL pre-rendered in the builder (the page template
|
|
81
|
+
* just emits this markdown verbatim, like `descriptionQuote`), so the mustache
|
|
82
|
+
* page never re-parses the embedded `{{field}}` tokens. Absent when no
|
|
83
|
+
* referenced source resolved (the section is omitted, not empty). */
|
|
84
|
+
templateSourceSection?: string;
|
|
85
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Enriched payload-field tree for the template-source annotator
|
|
2
|
+
// (linked-template-source-docs, Task 4).
|
|
3
|
+
//
|
|
4
|
+
// The render-helper drift gate walks the payload VO into a bare `PayloadField[]`
|
|
5
|
+
// (name + nested fields) — enough to VERIFY that a `{{field}}` exists. The doc
|
|
6
|
+
// page needs MORE per node: which VO OWNS the field (so the link points at the
|
|
7
|
+
// right entity page), the field's NEUTRAL type, and whether it's required — the
|
|
8
|
+
// exact same facts the entity Constraints table shows.
|
|
9
|
+
//
|
|
10
|
+
// This walk produces that `AnnotatePayloadField[]`. It is the same recursion the
|
|
11
|
+
// render-helper's `derivePayloadFieldTree` does (object-ref fields recurse into
|
|
12
|
+
// their referenced VO; a `seen` set guards a reference cycle) — but it carries
|
|
13
|
+
// owner/type/required and SWITCHES owner to the nested VO when it descends, so a
|
|
14
|
+
// `{{address.city}}` resolves to `Address.city` and links to `./Address.md`.
|
|
15
|
+
//
|
|
16
|
+
// Reuse, not reimplementation:
|
|
17
|
+
// • isFieldRequired / neutralTypeStr — the SAME helpers the entity Constraints
|
|
18
|
+
// table uses, so the documented type/required can never drift from the
|
|
19
|
+
// entity page.
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
type MetaObject,
|
|
23
|
+
type MetaRoot,
|
|
24
|
+
FIELD_SUBTYPE_OBJECT,
|
|
25
|
+
FIELD_ATTR_OBJECT_REF,
|
|
26
|
+
stripPackage,
|
|
27
|
+
} from "@metaobjectsdev/metadata";
|
|
28
|
+
import { isFieldRequired, neutralTypeStr } from "./docs-data-builder.js";
|
|
29
|
+
import type { AnnotatePayloadField } from "./template-source-annotate.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Walk the payload VO `voName` into an enriched `AnnotatePayloadField[]`. Each
|
|
33
|
+
* node carries `owner` (the short name of the VO that DECLARES the field — the
|
|
34
|
+
* link target), `type` (the neutral logical type), and `required`. Object-ref
|
|
35
|
+
* fields recurse into their referenced VO with `owner` switched to that nested
|
|
36
|
+
* VO; a `seen` set guards a (pathological) reference cycle (returns `[]` rather
|
|
37
|
+
* than recursing forever).
|
|
38
|
+
*
|
|
39
|
+
* Returns `[]` when the VO can't be resolved — the annotator then leaves the
|
|
40
|
+
* referencing variables unresolved (flagged "not on payload"), never throws.
|
|
41
|
+
*/
|
|
42
|
+
export function buildEnrichedPayloadTree(
|
|
43
|
+
root: MetaRoot,
|
|
44
|
+
voName: string,
|
|
45
|
+
seen: ReadonlySet<string> = new Set(),
|
|
46
|
+
): AnnotatePayloadField[] {
|
|
47
|
+
if (seen.has(voName)) return [];
|
|
48
|
+
const vo: MetaObject | undefined = root.findObject(voName);
|
|
49
|
+
if (vo === undefined) return [];
|
|
50
|
+
const owner = stripPackage(vo.name);
|
|
51
|
+
const nextSeen = new Set(seen).add(voName);
|
|
52
|
+
const out: AnnotatePayloadField[] = [];
|
|
53
|
+
for (const f of vo.fields()) {
|
|
54
|
+
const node: AnnotatePayloadField = {
|
|
55
|
+
name: f.name,
|
|
56
|
+
owner,
|
|
57
|
+
type: neutralTypeStr(f),
|
|
58
|
+
required: isFieldRequired(f),
|
|
59
|
+
};
|
|
60
|
+
if (f.subType === FIELD_SUBTYPE_OBJECT) {
|
|
61
|
+
const ref = f.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
62
|
+
if (typeof ref === "string" && ref.length > 0) {
|
|
63
|
+
// Owner switches to the nested VO for its fields (the recursion below
|
|
64
|
+
// re-derives `owner` from the resolved VO's own name).
|
|
65
|
+
node.fields = buildEnrichedPayloadTree(root, stripPackage(ref), nextSeen);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
out.push(node);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|