@intentius/chant-lexicon-render 0.124.31 → 0.124.33

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.
Files changed (49) hide show
  1. package/dist/catalog.js +221 -0
  2. package/dist/codegen/docs-cli.js +6 -0
  3. package/dist/codegen/docs.js +110 -0
  4. package/dist/codegen/generate-cli.js +26 -0
  5. package/dist/codegen/generate-lexicon.js +38 -0
  6. package/dist/codegen/generate-typescript.js +46 -0
  7. package/dist/codegen/generate.js +82 -0
  8. package/dist/codegen/naming.js +43 -0
  9. package/dist/codegen/package.js +27 -0
  10. package/dist/composites/render-deploy.js +71 -0
  11. package/dist/coverage.js +29 -0
  12. package/dist/describe-resources.js +325 -0
  13. package/dist/detect.js +27 -0
  14. package/dist/generated/index.js +40 -0
  15. package/dist/generated/runtime.js +4 -0
  16. package/dist/index.js +21 -0
  17. package/dist/integrity.json +2 -2
  18. package/dist/lint/audit-catalog.js +23 -0
  19. package/dist/lint/audit-lineage.js +8 -0
  20. package/dist/lint/post-synth/index.js +8 -0
  21. package/dist/lint/post-synth/ren010-native-runtime-requires-commands.js +43 -0
  22. package/dist/lint/post-synth/ren011-service-requires-source.js +45 -0
  23. package/dist/lint/post-synth/ren012-free-plan-cannot-scale.js +41 -0
  24. package/dist/lint/post-synth/render-helpers.js +34 -0
  25. package/dist/lint/rules/index.js +8 -0
  26. package/dist/lint/rules/no-secret-literals.js +68 -0
  27. package/dist/lint/rules/valid-cron-schedule.js +44 -0
  28. package/dist/lint/rules/valid-region.js +43 -0
  29. package/dist/lsp/completions.d.ts.map +1 -1
  30. package/dist/lsp/completions.js +20 -0
  31. package/dist/lsp/hover.d.ts.map +1 -1
  32. package/dist/lsp/hover.js +49 -0
  33. package/dist/manifest.json +1 -1
  34. package/dist/op/activities/fake-render.js +271 -0
  35. package/dist/op/activities/index.js +8 -0
  36. package/dist/op/activities/render-apply.js +873 -0
  37. package/dist/ownership.js +38 -0
  38. package/dist/package-cli.js +17 -0
  39. package/dist/plugin.js +229 -0
  40. package/dist/pseudo.js +23 -0
  41. package/dist/serializer.js +238 -0
  42. package/dist/spec/fetch.js +30 -0
  43. package/dist/spec/parse.js +600 -0
  44. package/dist/validate-cli.js +4 -0
  45. package/dist/validate.js +41 -0
  46. package/package.json +7 -7
  47. package/src/codegen/package.ts +2 -2
  48. package/src/lsp/completions.ts +2 -1
  49. package/src/lsp/hover.ts +2 -1
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The render lexicon's ownership convention, declared here so core carries no
3
+ * render-specific branch — the #686 seam: each lexicon owns its key names; core
4
+ * keeps only the generic stamp/detect logic (@intentius/chant/ownership).
5
+ *
6
+ * Render has no tags or labels on any resource. What it does have, on the two
7
+ * kinds chant creates most and prunes — services and env groups — is a
8
+ * key/value store the API writes and reads back verbatim: environment
9
+ * variables. chant stamps `CHANT_MANAGED_BY=chant` (+ `CHANT_STACK` /
10
+ * `CHANT_ENV`) into a service's or env group's env vars at serialize time and
11
+ * reads them back through `GET .../env-vars`; that is the primary marker and
12
+ * the basis for owned-only prune. A service without it is never modified or
13
+ * deleted by a prune. The keys are upper-snake because that is what an env var
14
+ * is; the values are exactly what every other lexicon stamps.
15
+ *
16
+ * Disks and custom domains carry no marker either, but they hang off a
17
+ * service, so they inherit the parent's verdict — the service boundary, one
18
+ * level down from fly's app boundary. An undeclared disk under a chant-owned
19
+ * service is chant's and is pruned; one under a foreign service is foreign.
20
+ *
21
+ * Datastores, projects, environments, registry credentials, and webhooks
22
+ * carry no channel and no boundary. Their verdict is `unknown` (never
23
+ * `foreign`, never `owned`): the change set never escalates `unknown` to a
24
+ * delete, so an undeclared Postgres is never pruned. Removing one is an
25
+ * explicit `renderDelete` of a plan that names it.
26
+ */
27
+ /** Env-var keys for chant's ownership markers on services and env groups. */
28
+ export const RENDER_ENV_OWNERSHIP_KEYS = {
29
+ managedBy: "CHANT_MANAGED_BY",
30
+ stack: "CHANT_STACK",
31
+ env: "CHANT_ENV",
32
+ };
33
+ /** True when a key is one of chant's marker env vars. */
34
+ export function isOwnershipKey(key) {
35
+ return (key === RENDER_ENV_OWNERSHIP_KEYS.managedBy ||
36
+ key === RENDER_ENV_OWNERSHIP_KEYS.stack ||
37
+ key === RENDER_ENV_OWNERSHIP_KEYS.env);
38
+ }
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Thin entry point for `npm run bundle` in lexicon-render.
4
+ * Generates src/generated/ files and writes dist/ bundle.
5
+ */
6
+ import { packageLexicon } from "./codegen/package.js";
7
+ import { writeBundleSpec } from "@intentius/chant/codegen/package";
8
+ import { dirname, join } from "path";
9
+ import { fileURLToPath } from "url";
10
+ const pkgDir = dirname(fileURLToPath(import.meta.url));
11
+ const distDir = join(pkgDir, "..", "dist");
12
+ const verbose = process.argv.includes("--verbose") || !process.argv.includes("--quiet");
13
+ const force = process.argv.includes("--force");
14
+ const { spec, stats } = await packageLexicon({ verbose, force });
15
+ writeBundleSpec(spec, distDir);
16
+ console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
17
+ console.error(`dist/ written to ${distDir}`);
package/dist/plugin.js ADDED
@@ -0,0 +1,229 @@
1
+ import { createSkillsLoader } from "@intentius/chant/lexicon-plugin-helpers";
2
+ import { renderSerializer } from "./serializer.js";
3
+ import { rules } from "./lint/rules/index.js";
4
+ import { postSynthChecks } from "./lint/post-synth/index.js";
5
+ import { renderAuditCatalog } from "./lint/audit-catalog.js";
6
+ import { completions } from "./lsp/completions.js";
7
+ import { hover } from "./lsp/hover.js";
8
+ import { detectTemplate as detectRenderTemplate } from "./detect.js";
9
+ import { RENDER_ENV_OWNERSHIP_KEYS } from "./ownership.js";
10
+ /**
11
+ * render lexicon plugin.
12
+ *
13
+ * Implements all required LexiconPlugin lifecycle methods.
14
+ */
15
+ export const renderPlugin = {
16
+ name: "render",
17
+ // Services and env groups carry the env-var marker; describeResources reads
18
+ // it back. See ./ownership.ts for the two-tier story.
19
+ ownershipChannel: { keys: RENDER_ENV_OWNERSHIP_KEYS, reads: ["describeResources"] },
20
+ auditCatalog: () => renderAuditCatalog,
21
+ serializer: renderSerializer,
22
+ // ── Required lifecycle methods ────────────────────────────────
23
+ async generate(options) {
24
+ const { generate } = await import("./codegen/generate.js");
25
+ await generate(options);
26
+ },
27
+ async validate(options) {
28
+ const { validate } = await import("./validate.js");
29
+ const { printValidationResult } = await import("@intentius/chant/codegen/validate");
30
+ const result = await validate();
31
+ printValidationResult(result);
32
+ },
33
+ async coverage(options) {
34
+ const { analyzeRenderCoverage } = await import("./coverage.js");
35
+ await analyzeRenderCoverage({ verbose: options?.verbose, minOverall: options?.minOverall });
36
+ },
37
+ async package(options) {
38
+ const { packageLexicon } = await import("./codegen/package.js");
39
+ const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
40
+ const { join, dirname } = await import("path");
41
+ const { fileURLToPath } = await import("url");
42
+ const { spec, stats } = await packageLexicon(options);
43
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
44
+ writeBundleSpec(spec, join(pkgDir, "dist"));
45
+ console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
46
+ },
47
+ // ── Optional extensions ────────────────────────────────────
48
+ lintRules() {
49
+ return rules;
50
+ },
51
+ postSynthChecks() {
52
+ return postSynthChecks;
53
+ },
54
+ skills: createSkillsLoader(import.meta.url, [
55
+ {
56
+ file: "chant-render.md",
57
+ name: "chant-render",
58
+ description: "Author, lint, and deploy Render services, datastores, and env groups from a chant project, applied straight to the Render Public API",
59
+ triggers: [
60
+ { type: "file-pattern", value: "*.render.ts" },
61
+ { type: "context", value: "render" },
62
+ { type: "context", value: "render.com" },
63
+ ],
64
+ parameters: [
65
+ {
66
+ name: "resourceType",
67
+ type: "string",
68
+ description: "Render resource type to work with (WebService, StaticSite, PrivateService, BackgroundWorker, CronJob, Postgres, KeyValue, EnvGroup, Project, Environment, Disk, CustomDomain, RegistryCredential, Webhook)",
69
+ },
70
+ ],
71
+ examples: [
72
+ {
73
+ title: "Author a web service and a database",
74
+ output: 'new Postgres({ name: "app-db", plan: "free", version: "16" });\nnew WebService({ name: "app-web", repo: "https://github.com/…", serviceDetails: new WebServiceDetails({ runtime: "node", envSpecificDetails: new NativeEnvironmentDetails({ buildCommand: "npm ci", startCommand: "npm start" }) }), envVars: [new EnvVar({ key: "DATABASE_URL", value: db.internalConnectionString })] })',
75
+ },
76
+ {
77
+ title: "Deploy to a workspace",
78
+ input: "Apply the stack to Render",
79
+ output: "export RENDER_API_KEY=… RENDER_OWNER_ID=… && chant run render",
80
+ },
81
+ ],
82
+ },
83
+ {
84
+ file: "chant-render-patterns.md",
85
+ name: "chant-render-patterns",
86
+ description: "Render patterns in chant — env groups, projects and environments, disks, custom domains, image-backed and cron services, and the ownership model",
87
+ triggers: [
88
+ { type: "context", value: "render env group" },
89
+ { type: "context", value: "render disk" },
90
+ { type: "context", value: "render custom domain" },
91
+ { type: "context", value: "render cron" },
92
+ ],
93
+ parameters: [],
94
+ examples: [
95
+ {
96
+ title: "Env group shared by two services",
97
+ input: "Share env vars between a web service and a worker",
98
+ output: 'new EnvGroup({ name: "shared", envVars: [new EnvVar({ key: "LOG_LEVEL", value: "info" })], serviceIds: [web, worker] })',
99
+ },
100
+ ],
101
+ },
102
+ ]),
103
+ mcpTools() {
104
+ // No MCP tools yet: Render's read-only context is the chant-render skills.
105
+ return [];
106
+ },
107
+ mcpResources() {
108
+ return [];
109
+ },
110
+ detectTemplate(data) {
111
+ return detectRenderTemplate(data);
112
+ },
113
+ pseudoParameters() {
114
+ return ["Render::OwnerId", "Render::Region"];
115
+ },
116
+ initTemplates(template) {
117
+ if (template === "database") {
118
+ return {
119
+ src: {
120
+ "infra.ts": `import {
121
+ WebService,
122
+ WebServiceDetails,
123
+ NativeEnvironmentDetails,
124
+ EnvVar,
125
+ GeneratedEnvVar,
126
+ Postgres,
127
+ Render,
128
+ } from "@intentius/chant-lexicon-render";
129
+
130
+ // A Postgres database and a web service wired to it. The connection string is
131
+ // read from the live database at apply time, after it exists. The owner comes
132
+ // from Render.OwnerId (RENDER_OWNER_ID); the region from Render.Region
133
+ // (RENDER_REGION, default "oregon").
134
+ const db = new Postgres({
135
+ name: "app-db",
136
+ plan: "free",
137
+ version: "16",
138
+ region: Render.Region,
139
+ });
140
+
141
+ const web = new WebService({
142
+ name: "app-web",
143
+ repo: "https://github.com/render-examples/express-hello-world",
144
+ branch: "main",
145
+ serviceDetails: new WebServiceDetails({
146
+ runtime: "node",
147
+ plan: "starter",
148
+ region: Render.Region,
149
+ envSpecificDetails: new NativeEnvironmentDetails({ buildCommand: "npm ci", startCommand: "npm start" }),
150
+ }),
151
+ envVars: [
152
+ new EnvVar({ key: "DATABASE_URL", value: db.internalConnectionString }),
153
+ new GeneratedEnvVar({ key: "SESSION_SECRET", generateValue: true }),
154
+ ],
155
+ });
156
+
157
+ export { db, web };
158
+ `,
159
+ },
160
+ };
161
+ }
162
+ if (template === "static") {
163
+ return {
164
+ src: {
165
+ "infra.ts": `import { StaticSite, StaticSiteDetails, Route } from "@intentius/chant-lexicon-render";
166
+
167
+ // A static site built from a repo and served from its publish path, with an
168
+ // SPA rewrite. The owner comes from Render.OwnerId (RENDER_OWNER_ID).
169
+ const site = new StaticSite({
170
+ name: "my-site",
171
+ repo: "https://github.com/render-examples/vite-react-hello-world",
172
+ branch: "main",
173
+ serviceDetails: new StaticSiteDetails({
174
+ buildCommand: "npm ci && npm run build",
175
+ publishPath: "dist",
176
+ routes: [new Route({ type: "rewrite", source: "/*", destination: "/index.html" })],
177
+ }),
178
+ });
179
+
180
+ export { site };
181
+ `,
182
+ },
183
+ };
184
+ }
185
+ // Default: one web service — the smallest complete deploy.
186
+ return {
187
+ src: {
188
+ "infra.ts": `import { WebService, WebServiceDetails, NativeEnvironmentDetails, Render } from "@intentius/chant-lexicon-render";
189
+
190
+ // One Render web service — the smallest complete deploy. The serializer turns
191
+ // this into the POST /services body renderApply issues. The owner comes from
192
+ // Render.OwnerId (RENDER_OWNER_ID); the region from Render.Region
193
+ // (RENDER_REGION, default "oregon").
194
+ const web = new WebService({
195
+ name: "my-web",
196
+ repo: "https://github.com/render-examples/express-hello-world",
197
+ branch: "main",
198
+ serviceDetails: new WebServiceDetails({
199
+ runtime: "node",
200
+ plan: "starter",
201
+ region: Render.Region,
202
+ envSpecificDetails: new NativeEnvironmentDetails({ buildCommand: "npm ci", startCommand: "npm start" }),
203
+ }),
204
+ });
205
+
206
+ export { web };
207
+ `,
208
+ },
209
+ };
210
+ },
211
+ completionProvider(ctx) {
212
+ return completions(ctx);
213
+ },
214
+ hoverProvider(ctx) {
215
+ return hover(ctx);
216
+ },
217
+ async docs(options) {
218
+ const { generateDocs } = await import("./codegen/docs.js");
219
+ return generateDocs(options);
220
+ },
221
+ // The read-back seam for plan + drift. Lists live Render resources over the
222
+ // Public API, keyed by chant entity name, with an ownership verdict core's
223
+ // change set reads. Endpoint + auth reuse the applier (RENDER_API_BASE_URL /
224
+ // RENDER_API_KEY / RENDER_OWNER_ID), so it reads the same target renderApply writes.
225
+ async describeResources(options) {
226
+ const { describeResources } = await import("./describe-resources.js");
227
+ return describeResources(options);
228
+ },
229
+ };
package/dist/pseudo.js ADDED
@@ -0,0 +1,23 @@
1
+ import { PseudoParameter, createPseudoParameters } from "@intentius/chant/pseudo-parameter";
2
+ export { PseudoParameter };
3
+ /**
4
+ * Render pseudo-parameters — environment-resolved values a project references
5
+ * without hard-coding. Each serializes to a `{ Ref: "Render::..." }` marker
6
+ * that the serializer resolves from an environment variable at build time (see
7
+ * PSEUDO_ENV_MAP in ./serializer.ts), mirroring fly's `Fly.OrgSlug`.
8
+ *
9
+ * - OwnerId → the workspace (team or personal) id (RENDER_OWNER_ID), e.g. `tea-…`.
10
+ * Every ownerId chant fills in defaults to this, so a stack rarely names it.
11
+ * - Region → the target region (RENDER_REGION), default `oregon`.
12
+ */
13
+ export const { OwnerId, Region } = createPseudoParameters({
14
+ OwnerId: "Render::OwnerId",
15
+ Region: "Render::Region",
16
+ });
17
+ /**
18
+ * Render namespace containing all pseudo-parameters.
19
+ */
20
+ export const Render = {
21
+ OwnerId,
22
+ Region,
23
+ };
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Render serializer.
3
+ *
4
+ * Turns declared `WebService`, `Postgres`, `EnvGroup`, `Project`, ... resources
5
+ * into the JSON create bodies the Render Public API accepts, so the applier can
6
+ * POST them straight through.
7
+ *
8
+ * Output shape — a JSON object keyed by entity name. Each value is a single
9
+ * Render request plus the identity the applier reconciles by:
10
+ *
11
+ * {
12
+ * "<entityName>": {
13
+ * "kind": "WebService", // generated class name
14
+ * "entityType": "Render::Services::WebService",
15
+ * "endpoint": "/services", // create collection
16
+ * "method": "POST",
17
+ * "name": "my-web", // reconcile key (Render has no client-chosen ids)
18
+ * "body": { ... }, // the create body
19
+ * "pathParams": { "serviceId": { "$ref": "web" } } // for child collections
20
+ * }
21
+ * }
22
+ *
23
+ * Cross-resource references. Render identifies resources by server-assigned
24
+ * ids, so a Disk's `serviceId` or an Environment's `projectId` cannot be known
25
+ * at build time. A declared resource passed where an id is expected serializes
26
+ * to `{ "$ref": "<entityName>" }`; an attribute read (`db.internalConnectionString`)
27
+ * to `{ "$attr": { "entity": "<entityName>", "attribute": "..." } }`. The applier
28
+ * resolves both once the target exists.
29
+ *
30
+ * Ownership. Every serialized service and env group carries the
31
+ * `CHANT_MANAGED_BY=chant` marker (plus the stack/env identity from
32
+ * `context.ownership`) merged into `envVars`, even when the author supplied
33
+ * none. See ./ownership.ts for why env vars.
34
+ *
35
+ * `ownerId`. Every owner-scoped body gets `ownerId` filled from `Render.OwnerId`
36
+ * (`RENDER_OWNER_ID`) when the author omitted it. When that is unset too, an
37
+ * `{ "$owner": true }` marker is left for the applier, which resolves it from
38
+ * `GET /owners` when the token sees exactly one workspace.
39
+ */
40
+ import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
41
+ import { ownershipEntries, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
42
+ import { walkValue } from "@intentius/chant/serializer-walker";
43
+ import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
44
+ import { RENDER_ENV_OWNERSHIP_KEYS } from "./ownership.js";
45
+ import { CATALOG, SERVICE_TYPE_OF, catalogEntry, isServiceEntityType } from "./catalog.js";
46
+ export function isRefMarker(v) {
47
+ return !!v && typeof v === "object" && typeof v.$ref === "string";
48
+ }
49
+ export function isAttrMarker(v) {
50
+ const a = v;
51
+ return !!a && typeof a === "object" && !!a.$attr && typeof a.$attr.entity === "string";
52
+ }
53
+ export function isOwnerMarker(v) {
54
+ return !!v && typeof v === "object" && v.$owner === true;
55
+ }
56
+ /**
57
+ * Visitor for the generic serializer walker. Property declarables
58
+ * (WebServiceDetails, Image, ServiceDisk, ...) are unwrapped to plain objects;
59
+ * resource references and attribute reads become the applier's markers.
60
+ */
61
+ function renderVisitor() {
62
+ return {
63
+ attrRef: (entity, attribute) => ({ $attr: { entity, attribute } }),
64
+ resourceRef: (name) => ({ $ref: name }),
65
+ propertyDeclarable: (entity, walk) => {
66
+ const props = isResourceDeclarable(entity) ? entity.props : undefined;
67
+ if (!props || typeof props !== "object")
68
+ return undefined;
69
+ const result = {};
70
+ for (const [key, value] of Object.entries(props)) {
71
+ if (value !== undefined)
72
+ result[key] = walk(value);
73
+ }
74
+ return result;
75
+ },
76
+ };
77
+ }
78
+ function readProps(entity) {
79
+ const props = isResourceDeclarable(entity) ? entity.props : undefined;
80
+ return props && typeof props === "object" ? props : {};
81
+ }
82
+ /**
83
+ * Pseudo-parameter → environment variable mapping. Render bodies are plain
84
+ * JSON, so `Render.OwnerId` / `Render.Region` (which the walker lowers to a
85
+ * `{ Ref: "Render::..." }` marker) are resolved from the environment at build
86
+ * time. Region has a fallback (Render's own default); the owner does not — an
87
+ * unset owner becomes the `$owner` marker for the applier.
88
+ */
89
+ const PSEUDO_ENV_MAP = {
90
+ "Render::OwnerId": { envVars: ["RENDER_OWNER_ID"], fallback: { $owner: true } },
91
+ "Render::Region": { envVars: ["RENDER_REGION"], fallback: "oregon" },
92
+ };
93
+ function resolvePseudoRef(ref) {
94
+ const mapping = PSEUDO_ENV_MAP[ref];
95
+ if (!mapping)
96
+ return undefined;
97
+ for (const envVar of mapping.envVars) {
98
+ const value = process.env[envVar];
99
+ if (value)
100
+ return value;
101
+ }
102
+ return mapping.fallback;
103
+ }
104
+ /**
105
+ * Recursively replace pseudo-parameters with their resolved environment value.
106
+ * Handles both a raw `PseudoParameter` instance and the already-walked
107
+ * `{ Ref: "Render::X" }` shape. Everything else passes through untouched.
108
+ */
109
+ export function resolvePseudoParameters(value) {
110
+ if (Array.isArray(value)) {
111
+ return value.map(resolvePseudoParameters);
112
+ }
113
+ if (value && typeof value === "object") {
114
+ const record = value;
115
+ if (INTRINSIC_MARKER in record && typeof record.toJSON === "function") {
116
+ return resolvePseudoParameters(record.toJSON());
117
+ }
118
+ const keys = Object.keys(record);
119
+ if (keys.length === 1 && keys[0] === "Ref" && typeof record.Ref === "string") {
120
+ const resolved = resolvePseudoRef(record.Ref);
121
+ if (resolved !== undefined)
122
+ return resolved;
123
+ }
124
+ const result = {};
125
+ for (const [key, val] of Object.entries(record)) {
126
+ result[key] = resolvePseudoParameters(val);
127
+ }
128
+ return result;
129
+ }
130
+ return value;
131
+ }
132
+ /** The Render name a resource gets: an explicit `name` prop, else the entity name. */
133
+ function resourceName(props, entityName) {
134
+ return typeof props.name === "string" && props.name ? props.name : entityName;
135
+ }
136
+ /**
137
+ * Merge chant's ownership marker into an `envVars` list. The marker wins over
138
+ * any colliding user key so ownership can never be silently unset; the user's
139
+ * ordering is otherwise preserved.
140
+ */
141
+ export function stampOwnership(envVars, marker) {
142
+ const list = Array.isArray(envVars) ? [...envVars] : [];
143
+ const out = list.filter((e) => !(e && typeof e === "object" && e.key !== undefined && e.key in marker));
144
+ for (const [key, value] of Object.entries(marker))
145
+ out.push({ key, value });
146
+ return out;
147
+ }
148
+ /**
149
+ * Serialize a render partition into the plan document `renderApply` consumes.
150
+ *
151
+ * Split out of the `Serializer` object so callers that need the concrete
152
+ * document — the applier, its tests, the `describeResources` fixtures — get
153
+ * `string` rather than the interface's `string | SerializerResult` union.
154
+ */
155
+ export function serializeRender(entities, _outputs, context) {
156
+ const entityNames = new Map();
157
+ for (const [name, entity] of entities)
158
+ entityNames.set(entity, name);
159
+ const visitor = renderVisitor();
160
+ // Ownership marker. `CHANT_MANAGED_BY=chant` is always stamped; stack/env
161
+ // are added when the build threads an ownership marker through the context.
162
+ const ownershipEnv = context?.ownership
163
+ ? ownershipEntries(RENDER_ENV_OWNERSHIP_KEYS, context.ownership)
164
+ : { [RENDER_ENV_OWNERSHIP_KEYS.managedBy]: OWNERSHIP_MANAGED_BY_VALUE };
165
+ const requests = {};
166
+ for (const [entityName, entity] of entities) {
167
+ if (isPropertyDeclarable(entity))
168
+ continue;
169
+ const entityType = entity.entityType;
170
+ if (!entityType || !(entityType in CATALOG))
171
+ continue;
172
+ const entry = catalogEntry(entityType);
173
+ const props = readProps(entity);
174
+ // Body: every prop except association hints, walked so nested property
175
+ // declarables flatten and references become markers.
176
+ const body = {};
177
+ const pathParams = {};
178
+ for (const [key, value] of Object.entries(props)) {
179
+ if (value === undefined)
180
+ continue;
181
+ const walked = walkValue(value, entityNames, visitor);
182
+ if (entry.nonBodyProps.includes(key)) {
183
+ pathParams[key] = walked;
184
+ }
185
+ else {
186
+ body[key] = walked;
187
+ }
188
+ }
189
+ // Services: re-inject the fixed `type` discriminator the authoring
190
+ // surface hides, and stamp the marker.
191
+ if (isServiceEntityType(entityType)) {
192
+ body.type = SERVICE_TYPE_OF[entityType];
193
+ }
194
+ if (entry.marked) {
195
+ body.envVars = stampOwnership(body.envVars, ownershipEnv);
196
+ }
197
+ // Owner: fill from the pseudo-parameter when omitted; the walked value
198
+ // of `Render.OwnerId` is a `{ Ref }` resolved below with the rest.
199
+ if (entry.ownerScoped && body.ownerId === undefined) {
200
+ body.ownerId = { Ref: "Render::OwnerId" };
201
+ }
202
+ // `image.ownerId` must match the service's; default it the same way.
203
+ if (isServiceEntityType(entityType) && body.image && typeof body.image === "object") {
204
+ const image = body.image;
205
+ if (image.ownerId === undefined)
206
+ image.ownerId = body.ownerId;
207
+ }
208
+ // Child collections: fill the endpoint placeholder from pathParams when
209
+ // the value is a literal id; a `{ $ref }` stays for the applier.
210
+ let endpoint = entry.collection;
211
+ for (const [key, value] of Object.entries(pathParams)) {
212
+ if (typeof value === "string")
213
+ endpoint = endpoint.replace(`{${key}}`, encodeURIComponent(value));
214
+ }
215
+ const request = {
216
+ kind: entry.kind,
217
+ entityType,
218
+ endpoint,
219
+ method: "POST",
220
+ name: resourceName(props, entityName),
221
+ body,
222
+ };
223
+ if (Object.keys(pathParams).length > 0)
224
+ request.pathParams = pathParams;
225
+ requests[entityName] = request;
226
+ }
227
+ // Resolve pseudo-parameter markers (Render.OwnerId, Render.Region) to their
228
+ // environment value so bodies carry plain strings, not `{ Ref }`.
229
+ return JSON.stringify(resolvePseudoParameters(requests), null, 2);
230
+ }
231
+ /**
232
+ * Render serializer.
233
+ */
234
+ export const renderSerializer = {
235
+ name: "render",
236
+ rulePrefix: "REN",
237
+ serialize: serializeRender,
238
+ };
@@ -0,0 +1,30 @@
1
+ import { join } from "path";
2
+ import { homedir } from "os";
3
+ import { fetchWithCache } from "@intentius/chant/codegen/fetch";
4
+ /**
5
+ * Render's published Public API OpenAPI 3.0 spec.
6
+ *
7
+ * Render serves the current spec at a fixed URL with no version or release tag
8
+ * to pin (the document's own `info.version` has sat at "1.0.0" across many
9
+ * additive changes), so — like fly — the plugin declares no `upstreamPin`.
10
+ * Re-running `generate` picks up whatever the URL currently serves.
11
+ */
12
+ export const SCHEMA_URL = "https://api-docs.render.com/v1.0/openapi/render-public-api-1.json";
13
+ /**
14
+ * Cache under `~/.chant` so CI's `~/.chant` schema cache covers it — an
15
+ * api-docs.render.com hiccup then falls back to the cache instead of failing
16
+ * the generate step.
17
+ */
18
+ const CACHE_FILE = join(homedir(), ".chant", "render-public-api-openapi.json");
19
+ /**
20
+ * Fetch the Render OpenAPI spec and return it as a Map<typeName, Buffer>
21
+ * compatible with the generatePipeline fetchSchemas callback.
22
+ *
23
+ * The spec is a single document, so we return a single entry keyed by
24
+ * "Render::OpenAPI" — the parse step splits it into the curated resources and
25
+ * their reachable property types.
26
+ */
27
+ export async function fetchSchemas(options) {
28
+ const raw = await fetchWithCache({ url: SCHEMA_URL, cacheFile: CACHE_FILE }, options?.force);
29
+ return new Map([["Render::OpenAPI", raw]]);
30
+ }