@intentius/chant-lexicon-render 0.124.31 → 0.124.32
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/catalog.js +221 -0
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.js +110 -0
- package/dist/codegen/generate-cli.js +26 -0
- package/dist/codegen/generate-lexicon.js +38 -0
- package/dist/codegen/generate-typescript.js +46 -0
- package/dist/codegen/generate.js +82 -0
- package/dist/codegen/naming.js +43 -0
- package/dist/codegen/package.js +27 -0
- package/dist/composites/render-deploy.js +71 -0
- package/dist/coverage.js +29 -0
- package/dist/describe-resources.js +325 -0
- package/dist/detect.js +27 -0
- package/dist/generated/index.js +40 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/index.js +21 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +23 -0
- package/dist/lint/audit-lineage.js +8 -0
- package/dist/lint/post-synth/index.js +8 -0
- package/dist/lint/post-synth/ren010-native-runtime-requires-commands.js +43 -0
- package/dist/lint/post-synth/ren011-service-requires-source.js +45 -0
- package/dist/lint/post-synth/ren012-free-plan-cannot-scale.js +41 -0
- package/dist/lint/post-synth/render-helpers.js +34 -0
- package/dist/lint/rules/index.js +8 -0
- package/dist/lint/rules/no-secret-literals.js +68 -0
- package/dist/lint/rules/valid-cron-schedule.js +44 -0
- package/dist/lint/rules/valid-region.js +43 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +20 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +49 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/fake-render.js +271 -0
- package/dist/op/activities/index.js +8 -0
- package/dist/op/activities/render-apply.js +873 -0
- package/dist/ownership.js +38 -0
- package/dist/package-cli.js +17 -0
- package/dist/plugin.js +229 -0
- package/dist/pseudo.js +23 -0
- package/dist/serializer.js +238 -0
- package/dist/spec/fetch.js +30 -0
- package/dist/spec/parse.js +600 -0
- package/dist/validate-cli.js +4 -0
- package/dist/validate.js +41 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Render resource catalog — the one place that knows, per chant entity
|
|
3
|
+
* type, which REST collection it lives in, how it is identified, and how it is
|
|
4
|
+
* ordered against its neighbours. The serializer, the applier, the observation
|
|
5
|
+
* seam, and the lint rules all read this table so the four cannot drift apart
|
|
6
|
+
* on a resource's wire shape.
|
|
7
|
+
*
|
|
8
|
+
* Render's Public API is a plain REST surface (`https://api.render.com/v1`):
|
|
9
|
+
* - collections are `POST /services`, `POST /postgres`, ...; children of a
|
|
10
|
+
* service are `POST /services/{serviceId}/custom-domains`;
|
|
11
|
+
* - a resource is identified by an opaque id (`srv-…`, `dpg-…`) Render
|
|
12
|
+
* assigns on create; the *name* is the human key chant reconciles by, and
|
|
13
|
+
* is unique per workspace for services (Render enforces it) and, in
|
|
14
|
+
* practice, for the rest of what chant declares;
|
|
15
|
+
* - lists paginate by cursor and filter by `name` + `ownerId`;
|
|
16
|
+
* - updates are `PATCH` with the resource's PATCH DTO — a strict subset of
|
|
17
|
+
* the POST body (`ownerId`, `type`, `envVars` and friends are excluded).
|
|
18
|
+
*/
|
|
19
|
+
// ── Entity types ───────────────────────────────────────────────────
|
|
20
|
+
export const ENTITY_TYPES = {
|
|
21
|
+
webService: "Render::Services::WebService",
|
|
22
|
+
staticSite: "Render::Services::StaticSite",
|
|
23
|
+
privateService: "Render::Services::PrivateService",
|
|
24
|
+
backgroundWorker: "Render::Services::BackgroundWorker",
|
|
25
|
+
cronJob: "Render::Services::CronJob",
|
|
26
|
+
postgres: "Render::Datastores::Postgres",
|
|
27
|
+
keyValue: "Render::Datastores::KeyValue",
|
|
28
|
+
envGroup: "Render::Config::EnvGroup",
|
|
29
|
+
registryCredential: "Render::Config::RegistryCredential",
|
|
30
|
+
webhook: "Render::Config::Webhook",
|
|
31
|
+
project: "Render::Projects::Project",
|
|
32
|
+
environment: "Render::Projects::Environment",
|
|
33
|
+
disk: "Render::Services::Disk",
|
|
34
|
+
customDomain: "Render::Services::CustomDomain",
|
|
35
|
+
};
|
|
36
|
+
/** The five service entity types (share `POST /services`). */
|
|
37
|
+
export const SERVICE_ENTITY_TYPES = [
|
|
38
|
+
ENTITY_TYPES.webService,
|
|
39
|
+
ENTITY_TYPES.staticSite,
|
|
40
|
+
ENTITY_TYPES.privateService,
|
|
41
|
+
ENTITY_TYPES.backgroundWorker,
|
|
42
|
+
ENTITY_TYPES.cronJob,
|
|
43
|
+
];
|
|
44
|
+
/** Service entity type → the `type` discriminator `POST /services` takes. */
|
|
45
|
+
export const SERVICE_TYPE_OF = {
|
|
46
|
+
[ENTITY_TYPES.webService]: "web_service",
|
|
47
|
+
[ENTITY_TYPES.staticSite]: "static_site",
|
|
48
|
+
[ENTITY_TYPES.privateService]: "private_service",
|
|
49
|
+
[ENTITY_TYPES.backgroundWorker]: "background_worker",
|
|
50
|
+
[ENTITY_TYPES.cronJob]: "cron_job",
|
|
51
|
+
};
|
|
52
|
+
/** The reverse: a live service's `type` → chant entity type. */
|
|
53
|
+
export const ENTITY_TYPE_OF_SERVICE = Object.fromEntries(Object.entries(SERVICE_TYPE_OF).map(([k, v]) => [v, k]));
|
|
54
|
+
export function isServiceEntityType(entityType) {
|
|
55
|
+
return !!entityType && SERVICE_ENTITY_TYPES.includes(entityType);
|
|
56
|
+
}
|
|
57
|
+
const SERVICE_PATCH_FIELDS = [
|
|
58
|
+
"autoDeploy",
|
|
59
|
+
"repo",
|
|
60
|
+
"branch",
|
|
61
|
+
"image",
|
|
62
|
+
"name",
|
|
63
|
+
"buildFilter",
|
|
64
|
+
"rootDir",
|
|
65
|
+
"serviceDetails",
|
|
66
|
+
];
|
|
67
|
+
function serviceEntry(kind) {
|
|
68
|
+
return {
|
|
69
|
+
kind,
|
|
70
|
+
collection: "/services",
|
|
71
|
+
listKey: "service",
|
|
72
|
+
filters: { name: true, ownerId: true },
|
|
73
|
+
patchable: true,
|
|
74
|
+
patchFields: SERVICE_PATCH_FIELDS,
|
|
75
|
+
marked: true,
|
|
76
|
+
ownerScoped: true,
|
|
77
|
+
order: 40,
|
|
78
|
+
nonBodyProps: [],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export const CATALOG = {
|
|
82
|
+
[ENTITY_TYPES.project]: {
|
|
83
|
+
kind: "Project",
|
|
84
|
+
collection: "/projects",
|
|
85
|
+
listKey: "project",
|
|
86
|
+
filters: { name: true, ownerId: true },
|
|
87
|
+
patchable: true,
|
|
88
|
+
patchFields: ["name"],
|
|
89
|
+
marked: false,
|
|
90
|
+
ownerScoped: true,
|
|
91
|
+
order: 10,
|
|
92
|
+
nonBodyProps: [],
|
|
93
|
+
},
|
|
94
|
+
[ENTITY_TYPES.environment]: {
|
|
95
|
+
kind: "Environment",
|
|
96
|
+
collection: "/environments",
|
|
97
|
+
listKey: "environment",
|
|
98
|
+
filters: { name: true, ownerId: false },
|
|
99
|
+
patchable: true,
|
|
100
|
+
patchFields: ["name", "protectedStatus", "networkIsolationEnabled", "ipAllowList"],
|
|
101
|
+
marked: false,
|
|
102
|
+
ownerScoped: false,
|
|
103
|
+
order: 20,
|
|
104
|
+
nonBodyProps: [],
|
|
105
|
+
},
|
|
106
|
+
[ENTITY_TYPES.registryCredential]: {
|
|
107
|
+
kind: "RegistryCredential",
|
|
108
|
+
collection: "/registrycredentials",
|
|
109
|
+
listKey: "registryCredential",
|
|
110
|
+
filters: { name: true, ownerId: true },
|
|
111
|
+
patchable: true,
|
|
112
|
+
patchFields: ["registry", "name", "username", "authToken"],
|
|
113
|
+
marked: false,
|
|
114
|
+
ownerScoped: true,
|
|
115
|
+
order: 25,
|
|
116
|
+
nonBodyProps: [],
|
|
117
|
+
},
|
|
118
|
+
[ENTITY_TYPES.envGroup]: {
|
|
119
|
+
kind: "EnvGroup",
|
|
120
|
+
collection: "/env-groups",
|
|
121
|
+
listKey: "envGroup",
|
|
122
|
+
filters: { name: true, ownerId: true },
|
|
123
|
+
patchable: true,
|
|
124
|
+
patchFields: ["name", "environmentId"],
|
|
125
|
+
marked: true,
|
|
126
|
+
ownerScoped: true,
|
|
127
|
+
order: 30,
|
|
128
|
+
nonBodyProps: [],
|
|
129
|
+
},
|
|
130
|
+
[ENTITY_TYPES.postgres]: {
|
|
131
|
+
kind: "Postgres",
|
|
132
|
+
collection: "/postgres",
|
|
133
|
+
listKey: "postgres",
|
|
134
|
+
filters: { name: true, ownerId: true },
|
|
135
|
+
patchable: true,
|
|
136
|
+
patchFields: [
|
|
137
|
+
"name",
|
|
138
|
+
"plan",
|
|
139
|
+
"enableHighAvailability",
|
|
140
|
+
"diskSizeGB",
|
|
141
|
+
"enableDiskAutoscaling",
|
|
142
|
+
"readReplicas",
|
|
143
|
+
"ipAllowList",
|
|
144
|
+
"parameterOverrides",
|
|
145
|
+
],
|
|
146
|
+
marked: false,
|
|
147
|
+
ownerScoped: true,
|
|
148
|
+
order: 30,
|
|
149
|
+
nonBodyProps: [],
|
|
150
|
+
},
|
|
151
|
+
[ENTITY_TYPES.keyValue]: {
|
|
152
|
+
kind: "KeyValue",
|
|
153
|
+
collection: "/key-value",
|
|
154
|
+
listKey: "keyValue",
|
|
155
|
+
filters: { name: true, ownerId: true },
|
|
156
|
+
patchable: true,
|
|
157
|
+
patchFields: ["name", "plan", "maxmemoryPolicy", "ipAllowList"],
|
|
158
|
+
marked: false,
|
|
159
|
+
ownerScoped: true,
|
|
160
|
+
order: 30,
|
|
161
|
+
nonBodyProps: [],
|
|
162
|
+
},
|
|
163
|
+
[ENTITY_TYPES.webService]: serviceEntry("WebService"),
|
|
164
|
+
[ENTITY_TYPES.staticSite]: serviceEntry("StaticSite"),
|
|
165
|
+
[ENTITY_TYPES.privateService]: serviceEntry("PrivateService"),
|
|
166
|
+
[ENTITY_TYPES.backgroundWorker]: serviceEntry("BackgroundWorker"),
|
|
167
|
+
[ENTITY_TYPES.cronJob]: serviceEntry("CronJob"),
|
|
168
|
+
[ENTITY_TYPES.disk]: {
|
|
169
|
+
kind: "Disk",
|
|
170
|
+
collection: "/disks",
|
|
171
|
+
listKey: "disk",
|
|
172
|
+
filters: { name: true, ownerId: true },
|
|
173
|
+
patchable: true,
|
|
174
|
+
patchFields: ["name", "sizeGB", "mountPath"],
|
|
175
|
+
marked: false,
|
|
176
|
+
boundary: "service",
|
|
177
|
+
ownerScoped: false,
|
|
178
|
+
order: 50,
|
|
179
|
+
nonBodyProps: [],
|
|
180
|
+
},
|
|
181
|
+
[ENTITY_TYPES.customDomain]: {
|
|
182
|
+
kind: "CustomDomain",
|
|
183
|
+
collection: "/services/{serviceId}/custom-domains",
|
|
184
|
+
listKey: "customDomain",
|
|
185
|
+
filters: { name: true, ownerId: false },
|
|
186
|
+
patchable: false,
|
|
187
|
+
patchFields: [],
|
|
188
|
+
marked: false,
|
|
189
|
+
boundary: "service",
|
|
190
|
+
ownerScoped: false,
|
|
191
|
+
order: 50,
|
|
192
|
+
nonBodyProps: ["serviceId"],
|
|
193
|
+
},
|
|
194
|
+
[ENTITY_TYPES.webhook]: {
|
|
195
|
+
kind: "Webhook",
|
|
196
|
+
collection: "/webhooks",
|
|
197
|
+
listKey: "webhook",
|
|
198
|
+
filters: { name: false, ownerId: true },
|
|
199
|
+
patchable: true,
|
|
200
|
+
patchFields: ["url", "name", "enabled", "eventFilter"],
|
|
201
|
+
marked: false,
|
|
202
|
+
ownerScoped: true,
|
|
203
|
+
order: 60,
|
|
204
|
+
nonBodyProps: [],
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
/** Look up a catalog entry, or throw — an unknown entity type is a lexicon bug, not user error. */
|
|
208
|
+
export function catalogEntry(entityType) {
|
|
209
|
+
const entry = CATALOG[entityType];
|
|
210
|
+
if (!entry)
|
|
211
|
+
throw new Error(`render: no catalog entry for entity type ${entityType}`);
|
|
212
|
+
return entry;
|
|
213
|
+
}
|
|
214
|
+
/** Entity type ← short kind (`WebService` → `Render::Services::WebService`). */
|
|
215
|
+
export function entityTypeOfKind(kind) {
|
|
216
|
+
for (const [type, entry] of Object.entries(CATALOG)) {
|
|
217
|
+
if (entry.kind === kind)
|
|
218
|
+
return type;
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render documentation generator.
|
|
3
|
+
*
|
|
4
|
+
* Calls the core docsPipeline with render-specific config: service grouping and
|
|
5
|
+
* overview content. writeDocsSite puts the pages in the main docs site under
|
|
6
|
+
* docs/src/content/docs/lexicons/render/ (chant #3993).
|
|
7
|
+
*/
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
|
|
11
|
+
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
12
|
+
/**
|
|
13
|
+
* Group a render resource type into a bucket for the reference sidebar. Type
|
|
14
|
+
* names are `Render::<Group>::<Kind>`; the middle segment is the group
|
|
15
|
+
* (Services, Datastores, Config, Projects). Property types are all filed under
|
|
16
|
+
* Services by the generator; keep them there.
|
|
17
|
+
*/
|
|
18
|
+
export function serviceFromType(resourceType) {
|
|
19
|
+
return resourceType.split("::")[1] ?? resourceType;
|
|
20
|
+
}
|
|
21
|
+
const overview = `The **Render** lexicon defines Render services, datastores, env groups, projects, and environments using chant's declarative TypeScript syntax. Resources are serialized to the JSON create bodies the [Render Public API](https://api-docs.render.com/) accepts, so the applier can POST them straight through — no Blueprint, no CLI, no state file.
|
|
22
|
+
|
|
23
|
+
This lexicon is generated from Render's published [OpenAPI spec](https://api-docs.render.com/v1.0/openapi/render-public-api-1.json).
|
|
24
|
+
|
|
25
|
+
Install it with:
|
|
26
|
+
|
|
27
|
+
\`\`\`bash
|
|
28
|
+
npm install --save-dev @intentius/chant-lexicon-render
|
|
29
|
+
\`\`\`
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
\`\`\`typescript
|
|
34
|
+
import {
|
|
35
|
+
WebService, WebServiceDetails, NativeEnvironmentDetails, EnvVar, GeneratedEnvVar, Postgres, Render,
|
|
36
|
+
} from "@intentius/chant-lexicon-render";
|
|
37
|
+
|
|
38
|
+
const db = new Postgres({ name: "app-db", plan: "free", version: "16", region: Render.Region });
|
|
39
|
+
|
|
40
|
+
const web = new WebService({
|
|
41
|
+
name: "app-web",
|
|
42
|
+
repo: "https://github.com/render-examples/express-hello-world",
|
|
43
|
+
serviceDetails: new WebServiceDetails({
|
|
44
|
+
runtime: "node",
|
|
45
|
+
plan: "starter",
|
|
46
|
+
region: Render.Region,
|
|
47
|
+
envSpecificDetails: new NativeEnvironmentDetails({ buildCommand: "npm ci", startCommand: "npm start" }),
|
|
48
|
+
}),
|
|
49
|
+
envVars: [
|
|
50
|
+
new EnvVar({ key: "DATABASE_URL", value: db.internalConnectionString }),
|
|
51
|
+
new GeneratedEnvVar({ key: "SESSION_SECRET", generateValue: true }),
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export { db, web };
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
\`Render.OwnerId\` and \`Render.Region\` are pseudo-parameters resolved from the environment at build time (\`RENDER_OWNER_ID\`, \`RENDER_REGION\`). Every \`ownerId\` chant fills in defaults to \`Render.OwnerId\`, so a stack rarely names it. \`db.internalConnectionString\` is an attribute read the applier resolves from the live database.`;
|
|
59
|
+
const outputFormat = `The render lexicon serializes resources into the JSON create bodies the Render Public API accepts.
|
|
60
|
+
|
|
61
|
+
## Building
|
|
62
|
+
|
|
63
|
+
Each declared resource becomes a single request keyed by its logical name:
|
|
64
|
+
|
|
65
|
+
\`\`\`json
|
|
66
|
+
{
|
|
67
|
+
"web": {
|
|
68
|
+
"kind": "WebService",
|
|
69
|
+
"entityType": "Render::Services::WebService",
|
|
70
|
+
"endpoint": "/services",
|
|
71
|
+
"method": "POST",
|
|
72
|
+
"name": "app-web",
|
|
73
|
+
"body": { "type": "web_service", "name": "app-web", "ownerId": "tea-…", "serviceDetails": { ... }, "envVars": [ ... ] }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
- Each service type becomes \`POST /services\` with its \`type\` discriminator re-injected and \`serviceDetails\` in that type's shape.
|
|
79
|
+
- \`Postgres\` → \`POST /postgres\`; \`KeyValue\` → \`POST /key-value\`; \`EnvGroup\` → \`POST /env-groups\`; \`Project\` → \`POST /projects\`; \`Environment\` → \`POST /environments\`; \`Disk\` → \`POST /disks\`; \`RegistryCredential\` → \`POST /registrycredentials\`; \`Webhook\` → \`POST /webhooks\`.
|
|
80
|
+
- \`CustomDomain\` → \`POST /services/{serviceId}/custom-domains\`; the service is a path segment carried in \`pathParams\`.
|
|
81
|
+
|
|
82
|
+
Three markers may appear in a body, resolved by the applier once their target is live: \`{ "$ref": "<entity>" }\` (a declared resource's id), \`{ "$attr": { "entity", "attribute" } }\` (an attribute read — \`id\`, \`dashboardUrl\`, or a datastore connection string), and \`{ "$owner": true }\` (the workspace, when neither the author nor \`RENDER_OWNER_ID\` named one).
|
|
83
|
+
|
|
84
|
+
Every serialized service and env group carries the \`CHANT_MANAGED_BY=chant\` ownership marker (plus \`CHANT_STACK\` / \`CHANT_ENV\`) in its \`envVars\`, which the owned-only prune reads back.
|
|
85
|
+
|
|
86
|
+
## Applying
|
|
87
|
+
|
|
88
|
+
The output is applied against the Public API directly by \`renderApply\` (via \`chant run\` and the \`renderDeploy\` Op). Endpoint and auth come from \`RENDER_API_BASE_URL\` and \`RENDER_API_KEY\`; the workspace from \`RENDER_OWNER_ID\` (or the sole workspace the key can see). Each resource is found by name and created or PATCHed; created services are waited to \`live\`; with \`prune\` the owned services and env groups no longer declared — and the disks and custom domains under owned services no longer declared — are deleted.`;
|
|
89
|
+
/**
|
|
90
|
+
* Generate documentation for the render lexicon.
|
|
91
|
+
*/
|
|
92
|
+
export async function generateDocs(options) {
|
|
93
|
+
const config = {
|
|
94
|
+
name: "render",
|
|
95
|
+
displayName: "Render",
|
|
96
|
+
description: "Typed constructors for Render services, datastores, and env groups, serialized to Public API create bodies",
|
|
97
|
+
distDir: join(pkgDir, "dist"),
|
|
98
|
+
outDir: join(pkgDir, "docs"),
|
|
99
|
+
srcDir: join(pkgDir, "src"),
|
|
100
|
+
basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/render/",
|
|
101
|
+
overview,
|
|
102
|
+
outputFormat,
|
|
103
|
+
serviceFromType,
|
|
104
|
+
};
|
|
105
|
+
const result = docsPipeline(config);
|
|
106
|
+
writeDocsSite(config, result);
|
|
107
|
+
if (options?.verbose) {
|
|
108
|
+
console.error(`Generated docs: ${result.stats.resources} resources, ${result.stats.properties} properties, ${result.stats.services} services, ${result.stats.rules} rules`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for render lexicon generation.
|
|
4
|
+
*/
|
|
5
|
+
import { dirname } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { generate, writeGeneratedFiles } from "./generate.js";
|
|
8
|
+
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
9
|
+
async function main() {
|
|
10
|
+
const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
11
|
+
const force = process.argv.includes("--force") || process.argv.includes("-f");
|
|
12
|
+
console.error("Generating render lexicon...");
|
|
13
|
+
const result = await generate({ verbose, force });
|
|
14
|
+
writeGeneratedFiles(result, pkgDir);
|
|
15
|
+
console.error(`Generated ${result.resources} resources, ${result.properties} property types, ${result.enums} enums`);
|
|
16
|
+
if (result.warnings.length > 0) {
|
|
17
|
+
console.error(`${result.warnings.length} warnings:`);
|
|
18
|
+
for (const w of result.warnings) {
|
|
19
|
+
console.error(` ${w.file}: ${w.error}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
main().catch((err) => {
|
|
24
|
+
console.error("Generation failed:", err);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexicon JSON generator — produces lexicon-render.json with metadata for the
|
|
3
|
+
* render resource and property types.
|
|
4
|
+
*/
|
|
5
|
+
import { renderShortName } from "../spec/parse.js";
|
|
6
|
+
import { buildRegistry, serializeRegistry } from "@intentius/chant/codegen/generate-registry";
|
|
7
|
+
/**
|
|
8
|
+
* Generate the lexicon-render.json content.
|
|
9
|
+
*/
|
|
10
|
+
export function generateLexiconJSON(results, naming) {
|
|
11
|
+
const registryResources = results.map((r) => ({
|
|
12
|
+
typeName: r.resource.typeName,
|
|
13
|
+
attributes: r.resource.attributes,
|
|
14
|
+
properties: r.resource.properties,
|
|
15
|
+
propertyTypes: [],
|
|
16
|
+
}));
|
|
17
|
+
const entries = buildRegistry(registryResources, naming, {
|
|
18
|
+
shortName: renderShortName,
|
|
19
|
+
buildEntry: (resource, _tsName, _attrs, propConstraints) => {
|
|
20
|
+
const r = results.find((res) => res.resource.typeName === resource.typeName);
|
|
21
|
+
const entry = {
|
|
22
|
+
resourceType: resource.typeName,
|
|
23
|
+
kind: r?.isProperty ? "property" : "resource",
|
|
24
|
+
lexicon: "render",
|
|
25
|
+
};
|
|
26
|
+
if (propConstraints && Object.keys(propConstraints).length > 0) {
|
|
27
|
+
entry.constraints = propConstraints;
|
|
28
|
+
}
|
|
29
|
+
return entry;
|
|
30
|
+
},
|
|
31
|
+
buildPropertyEntry: (resourceType, propertyType) => ({
|
|
32
|
+
resourceType: `${resourceType}.${propertyType}`,
|
|
33
|
+
kind: "property",
|
|
34
|
+
lexicon: "render",
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
return serializeRegistry(entries);
|
|
38
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript declaration file generator — produces index.d.ts with the render
|
|
3
|
+
* resource classes (WebService, Postgres, EnvGroup, ...) and their
|
|
4
|
+
* property-type classes.
|
|
5
|
+
*/
|
|
6
|
+
import { writeResourceClass, writePropertyClass, } from "@intentius/chant/codegen/generate-typescript";
|
|
7
|
+
/**
|
|
8
|
+
* Generate the complete .d.ts file content.
|
|
9
|
+
*/
|
|
10
|
+
export function generateTypeScriptDeclarations(results, naming) {
|
|
11
|
+
const lines = [];
|
|
12
|
+
lines.push("// Code generated by chant render generate. DO NOT EDIT.");
|
|
13
|
+
const entries = [];
|
|
14
|
+
for (const r of results) {
|
|
15
|
+
const tsName = naming.resolve(r.resource.typeName);
|
|
16
|
+
if (!tsName)
|
|
17
|
+
continue;
|
|
18
|
+
const properties = r.resource.properties.map((p) => ({
|
|
19
|
+
name: p.name,
|
|
20
|
+
type: p.tsType,
|
|
21
|
+
required: p.required,
|
|
22
|
+
description: p.description,
|
|
23
|
+
}));
|
|
24
|
+
const attributes = r.resource.attributes.map((a) => ({ name: a.name, type: a.tsType }));
|
|
25
|
+
entries.push({ tsName, isProperty: !!r.isProperty, properties, attributes });
|
|
26
|
+
}
|
|
27
|
+
entries.sort((a, b) => a.tsName.localeCompare(b.tsName));
|
|
28
|
+
const resources = entries.filter((e) => !e.isProperty);
|
|
29
|
+
const propertyTypes = entries.filter((e) => e.isProperty);
|
|
30
|
+
if (resources.length > 0) {
|
|
31
|
+
lines.push("");
|
|
32
|
+
lines.push("// --- Render Resource classes ---");
|
|
33
|
+
for (const re of resources) {
|
|
34
|
+
writeResourceClass(lines, re.tsName, re.properties, re.attributes);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (propertyTypes.length > 0) {
|
|
38
|
+
lines.push("");
|
|
39
|
+
lines.push("// --- Property type classes ---");
|
|
40
|
+
for (const pe of propertyTypes) {
|
|
41
|
+
writePropertyClass(lines, pe.tsName, pe.properties);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
lines.push("");
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render generation pipeline — uses core generatePipeline with render-specific
|
|
3
|
+
* fetch, parse, naming, and generation callbacks.
|
|
4
|
+
*/
|
|
5
|
+
import { generatePipeline, writeGeneratedArtifacts, } from "@intentius/chant/codegen/generate";
|
|
6
|
+
import { fetchSchemas } from "../spec/fetch.js";
|
|
7
|
+
import { parseRenderOpenAPI } from "../spec/parse.js";
|
|
8
|
+
import { NamingStrategy } from "./naming.js";
|
|
9
|
+
import { generateLexiconJSON } from "./generate-lexicon.js";
|
|
10
|
+
import { generateTypeScriptDeclarations } from "./generate-typescript.js";
|
|
11
|
+
import { generateRuntimeIndex as coreGenerateRuntimeIndex, } from "@intentius/chant/codegen/generate-runtime-index";
|
|
12
|
+
/**
|
|
13
|
+
* Run the full render generation pipeline.
|
|
14
|
+
*/
|
|
15
|
+
export async function generate(opts = {}) {
|
|
16
|
+
// The Render spec is a single document — parseRenderOpenAPI returns multiple
|
|
17
|
+
// results. The pipeline calls parseSchema once, so we return the first result
|
|
18
|
+
// and inject the rest via augmentResults.
|
|
19
|
+
let pendingResults = [];
|
|
20
|
+
const config = {
|
|
21
|
+
fetchSchemas: (fetchOpts) => fetchSchemas({ force: fetchOpts.force }),
|
|
22
|
+
parseSchema: (_typeName, data) => {
|
|
23
|
+
const results = parseRenderOpenAPI(data);
|
|
24
|
+
if (results.length === 0)
|
|
25
|
+
return null;
|
|
26
|
+
pendingResults = results.slice(1);
|
|
27
|
+
return results[0];
|
|
28
|
+
},
|
|
29
|
+
createNaming: (results) => new NamingStrategy(results),
|
|
30
|
+
augmentResults: (results, _opts, log) => {
|
|
31
|
+
if (pendingResults.length > 0) {
|
|
32
|
+
results.push(...pendingResults);
|
|
33
|
+
log(`Added ${pendingResults.length} additional render schemas from OpenAPI spec`);
|
|
34
|
+
pendingResults = [];
|
|
35
|
+
}
|
|
36
|
+
log(`Total: ${results.length} render resource/property schemas`);
|
|
37
|
+
return { results };
|
|
38
|
+
},
|
|
39
|
+
generateRegistry: (results, naming) => generateLexiconJSON(results, naming),
|
|
40
|
+
generateTypes: (results, naming) => generateTypeScriptDeclarations(results, naming),
|
|
41
|
+
generateRuntimeIndex: (results, naming) => generateRuntimeIndex(results, naming),
|
|
42
|
+
};
|
|
43
|
+
return generatePipeline(config, opts);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Write generated artifacts to disk.
|
|
47
|
+
*/
|
|
48
|
+
export function writeGeneratedFiles(result, baseDir) {
|
|
49
|
+
writeGeneratedArtifacts({
|
|
50
|
+
baseDir,
|
|
51
|
+
files: {
|
|
52
|
+
"lexicon-render.json": result.lexiconJSON,
|
|
53
|
+
"index.d.ts": result.typesDTS,
|
|
54
|
+
"index.ts": result.indexTS,
|
|
55
|
+
"runtime.ts": `/**\n * Runtime factory constructors — re-exported from core.\n */\nexport { createResource, createProperty } from "@intentius/chant/runtime";\n`,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Generate the runtime index.ts with factory constructor exports.
|
|
61
|
+
*/
|
|
62
|
+
function generateRuntimeIndex(results, naming) {
|
|
63
|
+
const resourceEntries = [];
|
|
64
|
+
const propertyEntries = [];
|
|
65
|
+
for (const r of results) {
|
|
66
|
+
const typeName = r.resource.typeName;
|
|
67
|
+
const tsName = naming.resolve(typeName);
|
|
68
|
+
if (!tsName)
|
|
69
|
+
continue;
|
|
70
|
+
if (r.isProperty) {
|
|
71
|
+
propertyEntries.push({ tsName, resourceType: typeName });
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const attrs = {};
|
|
75
|
+
for (const attr of r.resource.attributes) {
|
|
76
|
+
attrs[attr.name] = attr.name;
|
|
77
|
+
}
|
|
78
|
+
resourceEntries.push({ tsName, resourceType: typeName, attrs });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return coreGenerateRuntimeIndex(resourceEntries, propertyEntries, { lexiconName: "render" });
|
|
82
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render naming strategy — maps Render::{Group}::{Kind} type names to
|
|
3
|
+
* user-friendly TypeScript class names.
|
|
4
|
+
*/
|
|
5
|
+
import { NamingStrategy as CoreNamingStrategy, } from "@intentius/chant/codegen/naming";
|
|
6
|
+
import { renderShortName, renderServiceName } from "../spec/parse.js";
|
|
7
|
+
export { propertyTypeName, extractDefName } from "@intentius/chant/codegen/naming";
|
|
8
|
+
const renderNamingConfig = {
|
|
9
|
+
priorityNames: {
|
|
10
|
+
"Render::Services::WebService": "WebService",
|
|
11
|
+
"Render::Services::StaticSite": "StaticSite",
|
|
12
|
+
"Render::Services::PrivateService": "PrivateService",
|
|
13
|
+
"Render::Services::BackgroundWorker": "BackgroundWorker",
|
|
14
|
+
"Render::Services::CronJob": "CronJob",
|
|
15
|
+
"Render::Datastores::Postgres": "Postgres",
|
|
16
|
+
"Render::Datastores::KeyValue": "KeyValue",
|
|
17
|
+
"Render::Config::EnvGroup": "EnvGroup",
|
|
18
|
+
"Render::Projects::Project": "Project",
|
|
19
|
+
"Render::Projects::Environment": "Environment",
|
|
20
|
+
},
|
|
21
|
+
priorityAliases: {},
|
|
22
|
+
priorityPropertyAliases: {},
|
|
23
|
+
serviceAbbreviations: {
|
|
24
|
+
Services: "Services",
|
|
25
|
+
Datastores: "Datastores",
|
|
26
|
+
Config: "Config",
|
|
27
|
+
Projects: "Projects",
|
|
28
|
+
},
|
|
29
|
+
shortName: renderShortName,
|
|
30
|
+
serviceName: renderServiceName,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Render-specific naming strategy. Extends the core strategy with the render config.
|
|
34
|
+
*/
|
|
35
|
+
export class NamingStrategy extends CoreNamingStrategy {
|
|
36
|
+
constructor(results) {
|
|
37
|
+
const inputs = results.map((r) => ({
|
|
38
|
+
typeName: r.resource.typeName,
|
|
39
|
+
propertyTypes: r.propertyTypes,
|
|
40
|
+
}));
|
|
41
|
+
super(inputs, renderNamingConfig);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { packagePipeline, collectSkills } from "@intentius/chant/codegen/package";
|
|
2
|
+
import { generate } from "./generate.js";
|
|
3
|
+
import { renderPlugin } from "../plugin.js";
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
7
|
+
/**
|
|
8
|
+
* Package the render lexicon for distribution.
|
|
9
|
+
*/
|
|
10
|
+
export async function packageLexicon(options) {
|
|
11
|
+
// This file is src/codegen/package.ts — rules and skills are collected
|
|
12
|
+
// relative to src/, so srcDir must be the parent of this directory.
|
|
13
|
+
const srcDir = dirname(moduleSourceDir(import.meta.url));
|
|
14
|
+
const pkgJson = JSON.parse(readFileSync(join(srcDir, "..", "package.json"), "utf-8"));
|
|
15
|
+
const { spec, stats } = await packagePipeline({
|
|
16
|
+
generate: (opts) => generate({ verbose: opts?.verbose, force: opts?.force }),
|
|
17
|
+
buildManifest: (_genResult) => ({
|
|
18
|
+
name: "render",
|
|
19
|
+
version: pkgJson.version ?? "0.0.0",
|
|
20
|
+
chantVersion: ">=0.1.0",
|
|
21
|
+
namespace: "Render",
|
|
22
|
+
}),
|
|
23
|
+
srcDir,
|
|
24
|
+
collectSkills: () => collectSkills(renderPlugin.skills?.() ?? []),
|
|
25
|
+
});
|
|
26
|
+
return { spec, stats };
|
|
27
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render deploy Op — `build → renderApply [→ verify]` on the local executor.
|
|
3
|
+
* The peer of `flyDeploy`, for a platform with no local emulator: it targets a
|
|
4
|
+
* real Render workspace with a `RENDER_API_KEY`.
|
|
5
|
+
*
|
|
6
|
+
* The phases compose modeled activities, no raw shell:
|
|
7
|
+
* Build run the project's `build:render` npm script → the serialized plan
|
|
8
|
+
* Apply `renderApply` the plan straight to the Render Public API (creates
|
|
9
|
+
* or PATCHes each resource by name, waits each created service's
|
|
10
|
+
* deploy to `live`, prunes owned-only when asked)
|
|
11
|
+
* Verify optional: GET a public URL and assert on its body
|
|
12
|
+
*
|
|
13
|
+
* The activity steps are the generic `activity(fn, args)` builder tagged with
|
|
14
|
+
* the exported activity function names the render lexicon contributes
|
|
15
|
+
* (`renderApply`/`renderDelete` from `src/op/activities`). The core activity
|
|
16
|
+
* registry resolves them by name when a project lists the `render` lexicon.
|
|
17
|
+
*/
|
|
18
|
+
import { Op, phase, build, activity, httpCheck } from "@intentius/chant/op";
|
|
19
|
+
/**
|
|
20
|
+
* Apply the serialized render plan straight to the Render Public API — the
|
|
21
|
+
* typed twin of `flyApplyStep`. Resolves to the `renderApply` activity.
|
|
22
|
+
* Defaults to the `longInfra` profile (a first deploy builds and can take
|
|
23
|
+
* minutes); override via `opts.profile`.
|
|
24
|
+
*/
|
|
25
|
+
export const renderApplyStep = (planPath, opts) => {
|
|
26
|
+
const { profile, ...rest } = opts ?? {};
|
|
27
|
+
return activity("renderApply", { planPath, ...rest }, profile ?? "longInfra");
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Delete everything the plan declares, in reverse order — the teardown twin.
|
|
31
|
+
* Resolves to the `renderDelete` activity.
|
|
32
|
+
*/
|
|
33
|
+
export const renderDeleteStep = (planPath, opts) => {
|
|
34
|
+
const { profile, ...rest } = opts ?? {};
|
|
35
|
+
return activity("renderDelete", { planPath, ...rest }, profile ?? "longInfra");
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Build a render deploy Op: build the plan, apply it to the workspace, verify
|
|
39
|
+
* a URL when asked, tear down when asked. See the module docstring.
|
|
40
|
+
*/
|
|
41
|
+
export function renderDeploy(opts = {}) {
|
|
42
|
+
const path = opts.path ?? ".";
|
|
43
|
+
const planPath = opts.planPath ?? "dist/render.json";
|
|
44
|
+
const applyOpts = {
|
|
45
|
+
...(opts.endpoint ? { endpoint: opts.endpoint } : {}),
|
|
46
|
+
...(opts.ownerId ? { ownerId: opts.ownerId } : {}),
|
|
47
|
+
...(opts.prune ? { prune: true } : {}),
|
|
48
|
+
...(opts.wait ? { wait: opts.wait } : {}),
|
|
49
|
+
};
|
|
50
|
+
const phases = [
|
|
51
|
+
phase("Build", [build(path, { script: opts.buildScript ?? "build:render" })]),
|
|
52
|
+
phase("Apply", [renderApplyStep(planPath, applyOpts)]),
|
|
53
|
+
];
|
|
54
|
+
if (opts.verify) {
|
|
55
|
+
phases.push(phase("Verify", [
|
|
56
|
+
httpCheck(opts.verify.url, {
|
|
57
|
+
...(opts.verify.contains ? { contains: opts.verify.contains } : {}),
|
|
58
|
+
retries: opts.verify.retries ?? 30,
|
|
59
|
+
intervalMs: opts.verify.intervalMs ?? 5000,
|
|
60
|
+
}),
|
|
61
|
+
]));
|
|
62
|
+
}
|
|
63
|
+
if (opts.teardown) {
|
|
64
|
+
phases.push(phase("Teardown", [renderDeleteStep(planPath, applyOpts)]));
|
|
65
|
+
}
|
|
66
|
+
return Op({
|
|
67
|
+
name: opts.name ?? "render",
|
|
68
|
+
overview: opts.overview ?? "Render: build the plan, apply it to the workspace over the Public API, verify",
|
|
69
|
+
phases,
|
|
70
|
+
});
|
|
71
|
+
}
|