@intentius/chant-lexicon-render 0.53.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 +89 -0
- package/dist/catalog.d.ts +100 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +21 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +6 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate-lexicon.d.ts +18 -0
- package/dist/codegen/generate-lexicon.d.ts.map +1 -0
- package/dist/codegen/generate-typescript.d.ts +12 -0
- package/dist/codegen/generate-typescript.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +15 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/naming.d.ts +14 -0
- package/dist/codegen/naming.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +17 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/composites/render-deploy.d.ts +88 -0
- package/dist/composites/render-deploy.d.ts.map +1 -0
- package/dist/coverage.d.ts +15 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +60 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/detect.d.ts +12 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/generated/index.d.ts +38 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/runtime.d.ts +5 -0
- package/dist/generated/runtime.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/integrity.json +18 -0
- package/dist/lint/audit-catalog.d.ts +13 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/post-synth/ren010-native-runtime-requires-commands.d.ts +12 -0
- package/dist/lint/post-synth/ren010-native-runtime-requires-commands.d.ts.map +1 -0
- package/dist/lint/post-synth/ren011-service-requires-source.d.ts +12 -0
- package/dist/lint/post-synth/ren011-service-requires-source.d.ts.map +1 -0
- package/dist/lint/post-synth/ren012-free-plan-cannot-scale.d.ts +11 -0
- package/dist/lint/post-synth/ren012-free-plan-cannot-scale.d.ts.map +1 -0
- package/dist/lint/post-synth/render-helpers.d.ts +19 -0
- package/dist/lint/post-synth/render-helpers.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +7 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/no-secret-literals.d.ts +13 -0
- package/dist/lint/rules/no-secret-literals.d.ts.map +1 -0
- package/dist/lint/rules/valid-cron-schedule.d.ts +12 -0
- package/dist/lint/rules/valid-cron-schedule.d.ts.map +1 -0
- package/dist/lint/rules/valid-region.d.ts +16 -0
- package/dist/lint/rules/valid-region.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +8 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +6 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/manifest.json +6 -0
- package/dist/meta.json +304 -0
- package/dist/okf/index.md +29 -0
- package/dist/okf/rules/REN001.md +11 -0
- package/dist/okf/rules/REN002.md +15 -0
- package/dist/okf/rules/REN003.md +15 -0
- package/dist/okf/rules/REN010.md +11 -0
- package/dist/okf/rules/REN011.md +11 -0
- package/dist/okf/rules/REN012.md +11 -0
- package/dist/okf/types/BackgroundWorker.md +24 -0
- package/dist/okf/types/CronJob.md +28 -0
- package/dist/okf/types/CustomDomain.md +14 -0
- package/dist/okf/types/Disk.md +16 -0
- package/dist/okf/types/EnvGroup.md +22 -0
- package/dist/okf/types/Environment.md +17 -0
- package/dist/okf/types/KeyValue.md +20 -0
- package/dist/okf/types/Postgres.md +29 -0
- package/dist/okf/types/PrivateService.md +24 -0
- package/dist/okf/types/Project.md +15 -0
- package/dist/okf/types/RegistryCredential.md +17 -0
- package/dist/okf/types/StaticSite.md +24 -0
- package/dist/okf/types/WebService.md +24 -0
- package/dist/okf/types/Webhook.md +17 -0
- package/dist/op/activities/fake-render.d.ts +52 -0
- package/dist/op/activities/fake-render.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +10 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/activities/render-apply.d.ts +229 -0
- package/dist/op/activities/render-apply.d.ts.map +1 -0
- package/dist/ownership.d.ts +32 -0
- package/dist/ownership.d.ts.map +1 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/pseudo.d.ts +21 -0
- package/dist/pseudo.d.ts.map +1 -0
- package/dist/rules/no-secret-literals.ts +73 -0
- package/dist/rules/ren010-native-runtime-requires-commands.ts +47 -0
- package/dist/rules/ren011-service-requires-source.ts +50 -0
- package/dist/rules/ren012-free-plan-cannot-scale.ts +43 -0
- package/dist/rules/render-helpers.ts +38 -0
- package/dist/rules/valid-cron-schedule.ts +51 -0
- package/dist/rules/valid-region.ts +49 -0
- package/dist/serializer.d.ts +99 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/skills/chant-render-patterns.md +126 -0
- package/dist/skills/chant-render.md +112 -0
- package/dist/spec/fetch.d.ts +21 -0
- package/dist/spec/fetch.d.ts.map +1 -0
- package/dist/spec/parse.d.ts +120 -0
- package/dist/spec/parse.d.ts.map +1 -0
- package/dist/types/index.d.ts +573 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate.d.ts +15 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +73 -0
- package/src/catalog.ts +286 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +120 -0
- package/src/codegen/generate-cli.ts +36 -0
- package/src/codegen/generate-lexicon.ts +52 -0
- package/src/codegen/generate-typescript.ts +70 -0
- package/src/codegen/generate.ts +106 -0
- package/src/codegen/naming.ts +55 -0
- package/src/codegen/package.ts +30 -0
- package/src/composites/render-deploy.ts +133 -0
- package/src/coverage.ts +49 -0
- package/src/describe-resources.test.ts +143 -0
- package/src/describe-resources.ts +366 -0
- package/src/detect.ts +29 -0
- package/src/generated/index.d.ts +573 -0
- package/src/generated/index.ts +43 -0
- package/src/generated/lexicon-render.json +304 -0
- package/src/generated/runtime.ts +4 -0
- package/src/index.ts +30 -0
- package/src/lint/audit-catalog.ts +47 -0
- package/src/lint/post-synth/index.ts +11 -0
- package/src/lint/post-synth/post-synth.test.ts +151 -0
- package/src/lint/post-synth/ren010-native-runtime-requires-commands.ts +47 -0
- package/src/lint/post-synth/ren011-service-requires-source.ts +50 -0
- package/src/lint/post-synth/ren012-free-plan-cannot-scale.ts +43 -0
- package/src/lint/post-synth/render-helpers.ts +38 -0
- package/src/lint/rules/index.ts +11 -0
- package/src/lint/rules/no-secret-literals.ts +73 -0
- package/src/lint/rules/rules.test.ts +73 -0
- package/src/lint/rules/valid-cron-schedule.ts +51 -0
- package/src/lint/rules/valid-region.ts +49 -0
- package/src/lsp/completions.test.ts +28 -0
- package/src/lsp/completions.ts +22 -0
- package/src/lsp/hover.test.ts +32 -0
- package/src/lsp/hover.ts +50 -0
- package/src/op/activities/fake-render.ts +275 -0
- package/src/op/activities/index.ts +51 -0
- package/src/op/activities/render-apply.test.ts +403 -0
- package/src/op/activities/render-apply.ts +1042 -0
- package/src/ownership.ts +44 -0
- package/src/package-cli.ts +23 -0
- package/src/plugin.test.ts +54 -0
- package/src/plugin.ts +258 -0
- package/src/pseudo.ts +26 -0
- package/src/serializer.test.ts +221 -0
- package/src/serializer.ts +286 -0
- package/src/skills/chant-render-patterns.md +126 -0
- package/src/skills/chant-render.md +112 -0
- package/src/spec/fetch.ts +33 -0
- package/src/spec/parse.ts +721 -0
- package/src/validate-cli.ts +5 -0
- package/src/validate.ts +47 -0
package/src/ownership.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
|
|
28
|
+
import type { ChannelKeys } from "@intentius/chant/ownership";
|
|
29
|
+
|
|
30
|
+
/** Env-var keys for chant's ownership markers on services and env groups. */
|
|
31
|
+
export const RENDER_ENV_OWNERSHIP_KEYS: ChannelKeys = {
|
|
32
|
+
managedBy: "CHANT_MANAGED_BY",
|
|
33
|
+
stack: "CHANT_STACK",
|
|
34
|
+
env: "CHANT_ENV",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** True when a key is one of chant's marker env vars. */
|
|
38
|
+
export function isOwnershipKey(key: string): boolean {
|
|
39
|
+
return (
|
|
40
|
+
key === RENDER_ENV_OWNERSHIP_KEYS.managedBy ||
|
|
41
|
+
key === RENDER_ENV_OWNERSHIP_KEYS.stack ||
|
|
42
|
+
key === RENDER_ENV_OWNERSHIP_KEYS.env
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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";
|
|
7
|
+
import { writeBundleSpec } from "@intentius/chant/codegen/package";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
|
|
11
|
+
const pkgDir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const distDir = join(pkgDir, "..", "dist");
|
|
13
|
+
|
|
14
|
+
const verbose = process.argv.includes("--verbose") || !process.argv.includes("--quiet");
|
|
15
|
+
const force = process.argv.includes("--force");
|
|
16
|
+
|
|
17
|
+
const { spec, stats } = await packageLexicon({ verbose, force });
|
|
18
|
+
writeBundleSpec(spec, distDir);
|
|
19
|
+
|
|
20
|
+
console.error(
|
|
21
|
+
`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`,
|
|
22
|
+
);
|
|
23
|
+
console.error(`dist/ written to ${distDir}`);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderPlugin } from "./plugin";
|
|
3
|
+
import { isLexiconPlugin } from "@intentius/chant/lexicon";
|
|
4
|
+
|
|
5
|
+
describe("render plugin", () => {
|
|
6
|
+
it("is a valid LexiconPlugin", () => {
|
|
7
|
+
expect(isLexiconPlugin(renderPlugin)).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("has the correct name and serializer", () => {
|
|
11
|
+
expect(renderPlugin.name).toBe("render");
|
|
12
|
+
expect(renderPlugin.serializer.name).toBe("render");
|
|
13
|
+
expect(renderPlugin.serializer.rulePrefix).toBe("REN");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("ships lint rules and post-synth checks under the REN prefix", () => {
|
|
17
|
+
const rules = renderPlugin.lintRules?.() ?? [];
|
|
18
|
+
const checks = renderPlugin.postSynthChecks?.() ?? [];
|
|
19
|
+
expect(rules.map((r) => r.id)).toEqual(["REN001", "REN002", "REN003"]);
|
|
20
|
+
expect(checks.map((c) => c.id)).toEqual(["REN010", "REN011", "REN012"]);
|
|
21
|
+
for (const id of checks.map((c) => c.id)) {
|
|
22
|
+
expect(renderPlugin.auditCatalog?.()[id]).toBeDefined();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("declares its ownership channel on the read path it implements", () => {
|
|
27
|
+
expect(renderPlugin.ownershipChannel?.reads).toEqual(["describeResources"]);
|
|
28
|
+
expect(typeof renderPlugin.describeResources).toBe("function");
|
|
29
|
+
expect(renderPlugin.ownershipChannel?.keys.managedBy).toBe("CHANT_MANAGED_BY");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("loads its skills with content", () => {
|
|
33
|
+
const skills = renderPlugin.skills?.() ?? [];
|
|
34
|
+
expect(skills.map((s) => s.name)).toEqual(["chant-render", "chant-render-patterns"]);
|
|
35
|
+
for (const s of skills) expect(s.content.length).toBeGreaterThan(500);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("detects its own serializer plan and nothing else", () => {
|
|
39
|
+
expect(
|
|
40
|
+
renderPlugin.detectTemplate?.({
|
|
41
|
+
web: { kind: "WebService", entityType: "Render::Services::WebService", endpoint: "/services", method: "POST", name: "web", body: {} },
|
|
42
|
+
}),
|
|
43
|
+
).toBe(true);
|
|
44
|
+
expect(renderPlugin.detectTemplate?.({ Resources: {} })).toBe(false);
|
|
45
|
+
expect(renderPlugin.detectTemplate?.([])).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("offers init templates that mention the package", () => {
|
|
49
|
+
for (const t of [undefined, "database", "static"]) {
|
|
50
|
+
const set = renderPlugin.initTemplates?.(t);
|
|
51
|
+
expect(set?.src["infra.ts"]).toContain("@intentius/chant-lexicon-render");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { LexiconPlugin, InitTemplateSet } from "@intentius/chant/lexicon";
|
|
2
|
+
import type { CompletionContext, HoverContext } from "@intentius/chant/lsp/types";
|
|
3
|
+
import { createSkillsLoader } from "@intentius/chant/lexicon-plugin-helpers";
|
|
4
|
+
import { renderSerializer } from "./serializer";
|
|
5
|
+
import { rules } from "./lint/rules";
|
|
6
|
+
import { postSynthChecks } from "./lint/post-synth";
|
|
7
|
+
import { renderAuditCatalog } from "./lint/audit-catalog";
|
|
8
|
+
import { completions } from "./lsp/completions";
|
|
9
|
+
import { hover } from "./lsp/hover";
|
|
10
|
+
import { detectTemplate as detectRenderTemplate } from "./detect";
|
|
11
|
+
import { RENDER_ENV_OWNERSHIP_KEYS } from "./ownership";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* render lexicon plugin.
|
|
15
|
+
*
|
|
16
|
+
* Implements all required LexiconPlugin lifecycle methods.
|
|
17
|
+
*/
|
|
18
|
+
export const renderPlugin: LexiconPlugin = {
|
|
19
|
+
name: "render",
|
|
20
|
+
// Services and env groups carry the env-var marker; describeResources reads
|
|
21
|
+
// it back. See ./ownership.ts for the two-tier story.
|
|
22
|
+
ownershipChannel: { keys: RENDER_ENV_OWNERSHIP_KEYS, reads: ["describeResources"] },
|
|
23
|
+
auditCatalog: () => renderAuditCatalog,
|
|
24
|
+
serializer: renderSerializer,
|
|
25
|
+
|
|
26
|
+
// ── Required lifecycle methods ────────────────────────────────
|
|
27
|
+
|
|
28
|
+
async generate(options?: { verbose?: boolean }): Promise<void> {
|
|
29
|
+
const { generate } = await import("./codegen/generate");
|
|
30
|
+
await generate(options);
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async validate(options?: { verbose?: boolean }): Promise<void> {
|
|
34
|
+
const { validate } = await import("./validate");
|
|
35
|
+
const { printValidationResult } = await import("@intentius/chant/codegen/validate");
|
|
36
|
+
const result = await validate();
|
|
37
|
+
printValidationResult(result);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
|
|
41
|
+
const { analyzeRenderCoverage } = await import("./coverage");
|
|
42
|
+
await analyzeRenderCoverage({ verbose: options?.verbose, minOverall: options?.minOverall });
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
|
|
46
|
+
const { packageLexicon } = await import("./codegen/package");
|
|
47
|
+
const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
|
|
48
|
+
const { join, dirname } = await import("path");
|
|
49
|
+
const { fileURLToPath } = await import("url");
|
|
50
|
+
|
|
51
|
+
const { spec, stats } = await packageLexicon(options);
|
|
52
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
53
|
+
writeBundleSpec(spec, join(pkgDir, "dist"));
|
|
54
|
+
|
|
55
|
+
console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// ── Optional extensions ────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
lintRules() {
|
|
61
|
+
return rules;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
postSynthChecks() {
|
|
65
|
+
return postSynthChecks;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
skills: createSkillsLoader(import.meta.url, [
|
|
69
|
+
{
|
|
70
|
+
file: "chant-render.md",
|
|
71
|
+
name: "chant-render",
|
|
72
|
+
description:
|
|
73
|
+
"Author, lint, and deploy Render services, datastores, and env groups from a chant project, applied straight to the Render Public API",
|
|
74
|
+
triggers: [
|
|
75
|
+
{ type: "file-pattern" as const, value: "*.render.ts" },
|
|
76
|
+
{ type: "context" as const, value: "render" },
|
|
77
|
+
{ type: "context" as const, value: "render.com" },
|
|
78
|
+
],
|
|
79
|
+
parameters: [
|
|
80
|
+
{
|
|
81
|
+
name: "resourceType",
|
|
82
|
+
type: "string",
|
|
83
|
+
description:
|
|
84
|
+
"Render resource type to work with (WebService, StaticSite, PrivateService, BackgroundWorker, CronJob, Postgres, KeyValue, EnvGroup, Project, Environment, Disk, CustomDomain, RegistryCredential, Webhook)",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
examples: [
|
|
88
|
+
{
|
|
89
|
+
title: "Author a web service and a database",
|
|
90
|
+
output:
|
|
91
|
+
'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 })] })',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
title: "Deploy to a workspace",
|
|
95
|
+
input: "Apply the stack to Render",
|
|
96
|
+
output: "export RENDER_API_KEY=… RENDER_OWNER_ID=… && chant run render",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
file: "chant-render-patterns.md",
|
|
102
|
+
name: "chant-render-patterns",
|
|
103
|
+
description:
|
|
104
|
+
"Render patterns in chant — env groups, projects and environments, disks, custom domains, image-backed and cron services, and the ownership model",
|
|
105
|
+
triggers: [
|
|
106
|
+
{ type: "context" as const, value: "render env group" },
|
|
107
|
+
{ type: "context" as const, value: "render disk" },
|
|
108
|
+
{ type: "context" as const, value: "render custom domain" },
|
|
109
|
+
{ type: "context" as const, value: "render cron" },
|
|
110
|
+
],
|
|
111
|
+
parameters: [],
|
|
112
|
+
examples: [
|
|
113
|
+
{
|
|
114
|
+
title: "Env group shared by two services",
|
|
115
|
+
input: "Share env vars between a web service and a worker",
|
|
116
|
+
output: 'new EnvGroup({ name: "shared", envVars: [new EnvVar({ key: "LOG_LEVEL", value: "info" })], serviceIds: [web, worker] })',
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
]),
|
|
121
|
+
|
|
122
|
+
mcpTools() {
|
|
123
|
+
// No MCP tools yet: Render's read-only context is the chant-render skills.
|
|
124
|
+
return [];
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
mcpResources() {
|
|
128
|
+
return [];
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
detectTemplate(data: unknown) {
|
|
132
|
+
return detectRenderTemplate(data);
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
pseudoParameters(): string[] {
|
|
136
|
+
return ["Render::OwnerId", "Render::Region"];
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
initTemplates(template?: string): InitTemplateSet {
|
|
140
|
+
if (template === "database") {
|
|
141
|
+
return {
|
|
142
|
+
src: {
|
|
143
|
+
"infra.ts": `import {
|
|
144
|
+
WebService,
|
|
145
|
+
WebServiceDetails,
|
|
146
|
+
NativeEnvironmentDetails,
|
|
147
|
+
EnvVar,
|
|
148
|
+
GeneratedEnvVar,
|
|
149
|
+
Postgres,
|
|
150
|
+
Render,
|
|
151
|
+
} from "@intentius/chant-lexicon-render";
|
|
152
|
+
|
|
153
|
+
// A Postgres database and a web service wired to it. The connection string is
|
|
154
|
+
// read from the live database at apply time, after it exists. The owner comes
|
|
155
|
+
// from Render.OwnerId (RENDER_OWNER_ID); the region from Render.Region
|
|
156
|
+
// (RENDER_REGION, default "oregon").
|
|
157
|
+
const db = new Postgres({
|
|
158
|
+
name: "app-db",
|
|
159
|
+
plan: "free",
|
|
160
|
+
version: "16",
|
|
161
|
+
region: Render.Region,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const web = new WebService({
|
|
165
|
+
name: "app-web",
|
|
166
|
+
repo: "https://github.com/render-examples/express-hello-world",
|
|
167
|
+
branch: "main",
|
|
168
|
+
serviceDetails: new WebServiceDetails({
|
|
169
|
+
runtime: "node",
|
|
170
|
+
plan: "starter",
|
|
171
|
+
region: Render.Region,
|
|
172
|
+
envSpecificDetails: new NativeEnvironmentDetails({ buildCommand: "npm ci", startCommand: "npm start" }),
|
|
173
|
+
}),
|
|
174
|
+
envVars: [
|
|
175
|
+
new EnvVar({ key: "DATABASE_URL", value: db.internalConnectionString }),
|
|
176
|
+
new GeneratedEnvVar({ key: "SESSION_SECRET", generateValue: true }),
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export { db, web };
|
|
181
|
+
`,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (template === "static") {
|
|
187
|
+
return {
|
|
188
|
+
src: {
|
|
189
|
+
"infra.ts": `import { StaticSite, StaticSiteDetails, Route } from "@intentius/chant-lexicon-render";
|
|
190
|
+
|
|
191
|
+
// A static site built from a repo and served from its publish path, with an
|
|
192
|
+
// SPA rewrite. The owner comes from Render.OwnerId (RENDER_OWNER_ID).
|
|
193
|
+
const site = new StaticSite({
|
|
194
|
+
name: "my-site",
|
|
195
|
+
repo: "https://github.com/render-examples/vite-react-hello-world",
|
|
196
|
+
branch: "main",
|
|
197
|
+
serviceDetails: new StaticSiteDetails({
|
|
198
|
+
buildCommand: "npm ci && npm run build",
|
|
199
|
+
publishPath: "dist",
|
|
200
|
+
routes: [new Route({ type: "rewrite", source: "/*", destination: "/index.html" })],
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
export { site };
|
|
205
|
+
`,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Default: one web service — the smallest complete deploy.
|
|
211
|
+
return {
|
|
212
|
+
src: {
|
|
213
|
+
"infra.ts": `import { WebService, WebServiceDetails, NativeEnvironmentDetails, Render } from "@intentius/chant-lexicon-render";
|
|
214
|
+
|
|
215
|
+
// One Render web service — the smallest complete deploy. The serializer turns
|
|
216
|
+
// this into the POST /services body renderApply issues. The owner comes from
|
|
217
|
+
// Render.OwnerId (RENDER_OWNER_ID); the region from Render.Region
|
|
218
|
+
// (RENDER_REGION, default "oregon").
|
|
219
|
+
const web = new WebService({
|
|
220
|
+
name: "my-web",
|
|
221
|
+
repo: "https://github.com/render-examples/express-hello-world",
|
|
222
|
+
branch: "main",
|
|
223
|
+
serviceDetails: new WebServiceDetails({
|
|
224
|
+
runtime: "node",
|
|
225
|
+
plan: "starter",
|
|
226
|
+
region: Render.Region,
|
|
227
|
+
envSpecificDetails: new NativeEnvironmentDetails({ buildCommand: "npm ci", startCommand: "npm start" }),
|
|
228
|
+
}),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
export { web };
|
|
232
|
+
`,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
completionProvider(ctx: CompletionContext) {
|
|
238
|
+
return completions(ctx);
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
hoverProvider(ctx: HoverContext) {
|
|
242
|
+
return hover(ctx);
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
async docs(options?) {
|
|
246
|
+
const { generateDocs } = await import("./codegen/docs");
|
|
247
|
+
return generateDocs(options);
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
// The read-back seam for plan + drift. Lists live Render resources over the
|
|
251
|
+
// Public API, keyed by chant entity name, with an ownership verdict core's
|
|
252
|
+
// change set reads. Endpoint + auth reuse the applier (RENDER_API_BASE_URL /
|
|
253
|
+
// RENDER_API_KEY / RENDER_OWNER_ID), so it reads the same target renderApply writes.
|
|
254
|
+
async describeResources(options) {
|
|
255
|
+
const { describeResources } = await import("./describe-resources");
|
|
256
|
+
return describeResources(options);
|
|
257
|
+
},
|
|
258
|
+
};
|
package/src/pseudo.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PseudoParameter, createPseudoParameters } from "@intentius/chant/pseudo-parameter";
|
|
2
|
+
|
|
3
|
+
export { PseudoParameter };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Render pseudo-parameters — environment-resolved values a project references
|
|
7
|
+
* without hard-coding. Each serializes to a `{ Ref: "Render::..." }` marker
|
|
8
|
+
* that the serializer resolves from an environment variable at build time (see
|
|
9
|
+
* PSEUDO_ENV_MAP in ./serializer.ts), mirroring fly's `Fly.OrgSlug`.
|
|
10
|
+
*
|
|
11
|
+
* - OwnerId → the workspace (team or personal) id (RENDER_OWNER_ID), e.g. `tea-…`.
|
|
12
|
+
* Every ownerId chant fills in defaults to this, so a stack rarely names it.
|
|
13
|
+
* - Region → the target region (RENDER_REGION), default `oregon`.
|
|
14
|
+
*/
|
|
15
|
+
export const { OwnerId, Region } = createPseudoParameters({
|
|
16
|
+
OwnerId: "Render::OwnerId",
|
|
17
|
+
Region: "Render::Region",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Render namespace containing all pseudo-parameters.
|
|
22
|
+
*/
|
|
23
|
+
export const Render = {
|
|
24
|
+
OwnerId,
|
|
25
|
+
Region,
|
|
26
|
+
} as const;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import type { Declarable } from "@intentius/chant";
|
|
3
|
+
import { resolveAttrRefs } from "@intentius/chant/discovery/resolve";
|
|
4
|
+
import { renderSerializer, serializeRender, stampOwnership } from "./serializer";
|
|
5
|
+
import {
|
|
6
|
+
WebService,
|
|
7
|
+
WebServiceDetails,
|
|
8
|
+
StaticSite,
|
|
9
|
+
StaticSiteDetails,
|
|
10
|
+
CronJob,
|
|
11
|
+
CronJobDetails,
|
|
12
|
+
NativeEnvironmentDetails,
|
|
13
|
+
DockerDetails,
|
|
14
|
+
Postgres,
|
|
15
|
+
KeyValue,
|
|
16
|
+
EnvGroup,
|
|
17
|
+
Project,
|
|
18
|
+
ProjectEnvironment,
|
|
19
|
+
Environment,
|
|
20
|
+
Disk,
|
|
21
|
+
CustomDomain,
|
|
22
|
+
Image,
|
|
23
|
+
} from "./generated/index";
|
|
24
|
+
import { Render } from "./pseudo";
|
|
25
|
+
import { RENDER_ENV_OWNERSHIP_KEYS } from "./ownership";
|
|
26
|
+
|
|
27
|
+
/** Author a map of declarables keyed by logical name, in insertion order. */
|
|
28
|
+
function stack(...entries: Array<[string, unknown]>): Map<string, Declarable> {
|
|
29
|
+
return new Map(entries as Array<[string, Declarable]>);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parse = (entities: Map<string, Declarable>, ctx?: Parameters<typeof serializeRender>[2]) => {
|
|
33
|
+
// The build pipeline assigns each entity's logical name before serializing;
|
|
34
|
+
// do the same so attribute reads (`db.internalConnectionString`) resolve.
|
|
35
|
+
resolveAttrRefs(entities);
|
|
36
|
+
return JSON.parse(serializeRender(entities, undefined, ctx));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("render serializer", () => {
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
delete process.env.RENDER_OWNER_ID;
|
|
42
|
+
delete process.env.RENDER_REGION;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("serializes an empty map to valid JSON", () => {
|
|
46
|
+
expect(JSON.parse(serializeRender(new Map()))).toEqual({});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("has the correct name and rule prefix", () => {
|
|
50
|
+
expect(renderSerializer.name).toBe("render");
|
|
51
|
+
expect(renderSerializer.rulePrefix).toBe("REN");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("emits a WebService as POST /services with the fixed type, the marker, and the owner", () => {
|
|
55
|
+
process.env.RENDER_OWNER_ID = "tea-123";
|
|
56
|
+
const out = parse(
|
|
57
|
+
stack([
|
|
58
|
+
"web",
|
|
59
|
+
new WebService({
|
|
60
|
+
name: "my-web",
|
|
61
|
+
repo: "https://github.com/render-examples/express-hello-world",
|
|
62
|
+
serviceDetails: new WebServiceDetails({
|
|
63
|
+
runtime: "node",
|
|
64
|
+
plan: "starter",
|
|
65
|
+
region: "oregon",
|
|
66
|
+
envSpecificDetails: new NativeEnvironmentDetails({ buildCommand: "npm ci", startCommand: "npm start" }),
|
|
67
|
+
}),
|
|
68
|
+
envVars: [{ key: "LOG_LEVEL", value: "info" }],
|
|
69
|
+
}),
|
|
70
|
+
]),
|
|
71
|
+
);
|
|
72
|
+
expect(out.web.kind).toBe("WebService");
|
|
73
|
+
expect(out.web.entityType).toBe("Render::Services::WebService");
|
|
74
|
+
expect(out.web.endpoint).toBe("/services");
|
|
75
|
+
expect(out.web.method).toBe("POST");
|
|
76
|
+
expect(out.web.name).toBe("my-web");
|
|
77
|
+
expect(out.web.body.type).toBe("web_service");
|
|
78
|
+
expect(out.web.body.ownerId).toBe("tea-123");
|
|
79
|
+
expect(out.web.body.serviceDetails).toEqual({
|
|
80
|
+
runtime: "node",
|
|
81
|
+
plan: "starter",
|
|
82
|
+
region: "oregon",
|
|
83
|
+
envSpecificDetails: { buildCommand: "npm ci", startCommand: "npm start" },
|
|
84
|
+
});
|
|
85
|
+
expect(out.web.body.envVars).toEqual([
|
|
86
|
+
{ key: "LOG_LEVEL", value: "info" },
|
|
87
|
+
{ key: RENDER_ENV_OWNERSHIP_KEYS.managedBy, value: "chant" },
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("stamps stack/env identity from the serialize context", () => {
|
|
92
|
+
const out = parse(
|
|
93
|
+
stack(["web", new WebService({ name: "w", serviceDetails: new WebServiceDetails({ runtime: "docker" }) })]),
|
|
94
|
+
{ ownership: { stack: "shop", env: "prod" } },
|
|
95
|
+
);
|
|
96
|
+
expect(out.web.body.envVars).toEqual([
|
|
97
|
+
{ key: "CHANT_MANAGED_BY", value: "chant" },
|
|
98
|
+
{ key: "CHANT_STACK", value: "shop" },
|
|
99
|
+
{ key: "CHANT_ENV", value: "prod" },
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("the marker wins over a colliding user env var", () => {
|
|
104
|
+
expect(
|
|
105
|
+
stampOwnership([{ key: "CHANT_MANAGED_BY", value: "me" }, { key: "A", value: "1" }], { CHANT_MANAGED_BY: "chant" }),
|
|
106
|
+
).toEqual([
|
|
107
|
+
{ key: "A", value: "1" },
|
|
108
|
+
{ key: "CHANT_MANAGED_BY", value: "chant" },
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("leaves an $owner marker when no owner is known, and resolves Render.OwnerId from the env", () => {
|
|
113
|
+
const noEnv = parse(stack(["db", new Postgres({ name: "db", plan: "free", version: "16" })]));
|
|
114
|
+
expect(noEnv.db.body.ownerId).toEqual({ $owner: true });
|
|
115
|
+
|
|
116
|
+
process.env.RENDER_OWNER_ID = "tea-999";
|
|
117
|
+
const explicit = parse(stack(["db", new Postgres({ name: "db", plan: "free", version: "16", ownerId: Render.OwnerId })]));
|
|
118
|
+
expect(explicit.db.body.ownerId).toBe("tea-999");
|
|
119
|
+
expect(explicit.db.endpoint).toBe("/postgres");
|
|
120
|
+
expect(explicit.db.kind).toBe("Postgres");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("resolves Render.Region from RENDER_REGION with an oregon fallback", () => {
|
|
124
|
+
const fallback = parse(stack(["kv", new KeyValue({ name: "kv", plan: "free", region: Render.Region })]));
|
|
125
|
+
expect(fallback.kv.body.region).toBe("oregon");
|
|
126
|
+
process.env.RENDER_REGION = "frankfurt";
|
|
127
|
+
const set = parse(stack(["kv", new KeyValue({ name: "kv", plan: "free", region: Render.Region })]));
|
|
128
|
+
expect(set.kv.body.region).toBe("frankfurt");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("falls back to the entity name when no explicit name", () => {
|
|
132
|
+
const out = parse(stack(["billing", new Project({ name: "billing", environments: [new ProjectEnvironment({ name: "prod" })] })]));
|
|
133
|
+
expect(out.billing.name).toBe("billing");
|
|
134
|
+
expect(out.billing.body.environments).toEqual([{ name: "prod" }]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("turns a declared-resource reference into a $ref marker and an attribute read into $attr", () => {
|
|
138
|
+
const db = new Postgres({ name: "db", plan: "free", version: "16" });
|
|
139
|
+
const project = new Project({ name: "p", environments: [] });
|
|
140
|
+
const env = new Environment({ name: "prod", projectId: project });
|
|
141
|
+
const web = new WebService({
|
|
142
|
+
name: "web",
|
|
143
|
+
environmentId: env,
|
|
144
|
+
serviceDetails: new WebServiceDetails({ runtime: "docker" }),
|
|
145
|
+
envVars: [{ key: "DATABASE_URL", value: db.internalConnectionString }],
|
|
146
|
+
});
|
|
147
|
+
const out = parse(stack(["db", db], ["project", project], ["env", env], ["web", web]));
|
|
148
|
+
expect(out.env.body.projectId).toEqual({ $ref: "project" });
|
|
149
|
+
expect(out.web.body.environmentId).toEqual({ $ref: "env" });
|
|
150
|
+
expect(out.web.body.envVars[0]).toEqual({
|
|
151
|
+
key: "DATABASE_URL",
|
|
152
|
+
value: { $attr: { entity: "db", attribute: "internalConnectionString" } },
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("routes child collections through pathParams and fills literal ids into the endpoint", () => {
|
|
157
|
+
const web = new WebService({ name: "web", serviceDetails: new WebServiceDetails({ runtime: "docker" }) });
|
|
158
|
+
const out = parse(
|
|
159
|
+
stack(
|
|
160
|
+
["web", web],
|
|
161
|
+
["domain", new CustomDomain({ name: "example.com", serviceId: web })],
|
|
162
|
+
["literal", new CustomDomain({ name: "www.example.com", serviceId: "srv-abc" })],
|
|
163
|
+
["disk", new Disk({ name: "data", sizeGB: 10, mountPath: "/data", serviceId: web })],
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
expect(out.domain.endpoint).toBe("/services/{serviceId}/custom-domains");
|
|
167
|
+
expect(out.domain.pathParams).toEqual({ serviceId: { $ref: "web" } });
|
|
168
|
+
expect(out.domain.body).toEqual({ name: "example.com" });
|
|
169
|
+
expect(out.literal.endpoint).toBe("/services/srv-abc/custom-domains");
|
|
170
|
+
// Disk's serviceId is a body field, not a path segment.
|
|
171
|
+
expect(out.disk.endpoint).toBe("/disks");
|
|
172
|
+
expect(out.disk.body.serviceId).toEqual({ $ref: "web" });
|
|
173
|
+
expect(out.disk.pathParams).toBeUndefined();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("defaults image.ownerId to the service ownerId", () => {
|
|
177
|
+
process.env.RENDER_OWNER_ID = "tea-1";
|
|
178
|
+
const out = parse(
|
|
179
|
+
stack([
|
|
180
|
+
"web",
|
|
181
|
+
new WebService({
|
|
182
|
+
name: "web",
|
|
183
|
+
image: new Image({ imagePath: "docker.io/library/nginx:latest" }),
|
|
184
|
+
serviceDetails: new WebServiceDetails({ runtime: "image" }),
|
|
185
|
+
}),
|
|
186
|
+
]),
|
|
187
|
+
);
|
|
188
|
+
expect(out.web.body.image).toEqual({ imagePath: "docker.io/library/nginx:latest", ownerId: "tea-1" });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("emits every service type with its discriminator", () => {
|
|
192
|
+
const out = parse(
|
|
193
|
+
stack(
|
|
194
|
+
["site", new StaticSite({ name: "site", serviceDetails: new StaticSiteDetails({ publishPath: "dist" }) })],
|
|
195
|
+
[
|
|
196
|
+
"cron",
|
|
197
|
+
new CronJob({
|
|
198
|
+
name: "cron",
|
|
199
|
+
serviceDetails: new CronJobDetails({
|
|
200
|
+
runtime: "docker",
|
|
201
|
+
schedule: "0 * * * *",
|
|
202
|
+
envSpecificDetails: new DockerDetails({ dockerfilePath: "./Dockerfile" }),
|
|
203
|
+
}),
|
|
204
|
+
}),
|
|
205
|
+
],
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
expect(out.site.body.type).toBe("static_site");
|
|
209
|
+
expect(out.cron.body.type).toBe("cron_job");
|
|
210
|
+
expect(out.cron.body.serviceDetails.schedule).toBe("0 * * * *");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("stamps env groups too, and keeps their env vars", () => {
|
|
214
|
+
const out = parse(stack(["shared", new EnvGroup({ name: "shared", envVars: [{ key: "A", value: "1" }] })]));
|
|
215
|
+
expect(out.shared.endpoint).toBe("/env-groups");
|
|
216
|
+
expect(out.shared.body.envVars).toEqual([
|
|
217
|
+
{ key: "A", value: "1" },
|
|
218
|
+
{ key: "CHANT_MANAGED_BY", value: "chant" },
|
|
219
|
+
]);
|
|
220
|
+
});
|
|
221
|
+
});
|