@intentius/chant 0.0.12 → 0.0.14
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/package.json +1 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +3 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/getting-started.mdx +6 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/lint-rules.mdx +6 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/serialization.mdx +6 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/package.json +9 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/src/infra.ts +12 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/composites/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/post-synth/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +37 -42
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +37 -42
- package/src/cli/commands/build.ts +12 -7
- package/src/cli/commands/check-lexicon.ts +385 -0
- package/src/cli/commands/import.ts +6 -3
- package/src/cli/commands/init-lexicon.test.ts +22 -1
- package/src/cli/commands/init-lexicon.ts +194 -43
- package/src/cli/commands/init.ts +3 -3
- package/src/cli/commands/onboard.test.ts +295 -0
- package/src/cli/commands/onboard.ts +313 -0
- package/src/cli/handlers/dev.ts +26 -1
- package/src/cli/main.ts +5 -1
- package/src/codegen/docs.ts +11 -5
- package/src/codegen/generate-registry.ts +3 -2
- package/src/codegen/typecheck.ts +44 -2
- package/src/detectLexicon.test.ts +24 -0
- package/src/detectLexicon.ts +4 -2
- package/src/runtime.ts +4 -0
- package/src/serializer-walker.test.ts +8 -0
- package/src/toml.test.ts +388 -0
- package/src/toml.ts +606 -0
- /package/src/cli/commands/__fixtures__/init-lexicon-output/{examples/getting-started → src/actions}/.gitkeep +0 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { formatSuccess, formatError, formatWarning } from "../format";
|
|
5
|
+
|
|
6
|
+
export interface OnboardOptions {
|
|
7
|
+
name: string;
|
|
8
|
+
verbose?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface OnboardResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
patched: string[];
|
|
14
|
+
skipped: string[];
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Resolve the monorepo root (5 dirs up from packages/core/src/cli/commands/). */
|
|
19
|
+
function findRepoRoot(): string {
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url)); // commands/
|
|
21
|
+
return dirname(dirname(dirname(dirname(dirname(here))))); // -> root
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Patch root package.json to add a workspace dependency for the lexicon.
|
|
26
|
+
*/
|
|
27
|
+
function patchRootPackageJson(root: string, name: string): { patched: boolean; reason?: string } {
|
|
28
|
+
const pkgPath = join(root, "package.json");
|
|
29
|
+
if (!existsSync(pkgPath)) return { patched: false, reason: "root package.json not found" };
|
|
30
|
+
|
|
31
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
32
|
+
const depKey = `@intentius/chant-lexicon-${name}`;
|
|
33
|
+
|
|
34
|
+
if (pkg.dependencies?.[depKey]) {
|
|
35
|
+
return { patched: false, reason: `${depKey} already in dependencies` };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pkg.dependencies = pkg.dependencies ?? {};
|
|
39
|
+
pkg.dependencies[depKey] = "workspace:*";
|
|
40
|
+
|
|
41
|
+
// Sort dependencies for consistency
|
|
42
|
+
const sorted: Record<string, string> = {};
|
|
43
|
+
for (const k of Object.keys(pkg.dependencies).sort()) {
|
|
44
|
+
sorted[k] = pkg.dependencies[k];
|
|
45
|
+
}
|
|
46
|
+
pkg.dependencies = sorted;
|
|
47
|
+
|
|
48
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
49
|
+
return { patched: true };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Insert a new prepack line after the last prepack line in each contiguous group
|
|
54
|
+
* of 2+ lines. Single standalone lines (like YAML `run:` values) are ignored.
|
|
55
|
+
* Used for multi-line `run: |` blocks in workflows and Dockerfile RUN sequences.
|
|
56
|
+
*/
|
|
57
|
+
function insertPrepackInContiguousGroups(lines: string[], name: string): boolean {
|
|
58
|
+
const newFragment = `lexicons/${name} prepack`;
|
|
59
|
+
if (lines.some((l) => l.includes(newFragment))) return false;
|
|
60
|
+
|
|
61
|
+
// Identify contiguous groups of prepack lines
|
|
62
|
+
const groups: { start: number; end: number }[] = [];
|
|
63
|
+
let groupStart = -1;
|
|
64
|
+
for (let i = 0; i <= lines.length; i++) {
|
|
65
|
+
const isPrepack =
|
|
66
|
+
i < lines.length &&
|
|
67
|
+
lines[i].includes("bun run --cwd lexicons/") &&
|
|
68
|
+
lines[i].includes("prepack");
|
|
69
|
+
if (isPrepack && groupStart === -1) {
|
|
70
|
+
groupStart = i;
|
|
71
|
+
} else if (!isPrepack && groupStart !== -1) {
|
|
72
|
+
groups.push({ start: groupStart, end: i - 1 });
|
|
73
|
+
groupStart = -1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Only insert into groups of 2+ lines (multi-line blocks, not standalone steps)
|
|
78
|
+
const insertAfter = groups.filter((g) => g.end > g.start).map((g) => g.end);
|
|
79
|
+
|
|
80
|
+
if (insertAfter.length === 0) return false;
|
|
81
|
+
|
|
82
|
+
for (const idx of insertAfter.reverse()) {
|
|
83
|
+
const newLine = lines[idx].replace(/lexicons\/[a-z0-9-]+/i, `lexicons/${name}`);
|
|
84
|
+
lines.splice(idx + 1, 0, newLine);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Insert a new prepack line after every last-in-group prepack line (groups of 1+).
|
|
92
|
+
* Used for Dockerfiles and publish.yml where all occurrences should get a new line.
|
|
93
|
+
*/
|
|
94
|
+
function insertPrepackAfterEach(lines: string[], name: string): boolean {
|
|
95
|
+
const newFragment = `lexicons/${name} prepack`;
|
|
96
|
+
if (lines.some((l) => l.includes(newFragment))) return false;
|
|
97
|
+
|
|
98
|
+
const insertAfter: number[] = [];
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
if (!lines[i].includes("bun run --cwd lexicons/") || !lines[i].includes("prepack")) continue;
|
|
101
|
+
const nextIsAlsoPrepack =
|
|
102
|
+
i + 1 < lines.length &&
|
|
103
|
+
lines[i + 1].includes("bun run --cwd lexicons/") &&
|
|
104
|
+
lines[i + 1].includes("prepack");
|
|
105
|
+
if (!nextIsAlsoPrepack) {
|
|
106
|
+
insertAfter.push(i);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (insertAfter.length === 0) return false;
|
|
111
|
+
|
|
112
|
+
for (const idx of insertAfter.reverse()) {
|
|
113
|
+
const newLine = lines[idx].replace(/lexicons\/[a-z0-9-]+/i, `lexicons/${name}`);
|
|
114
|
+
lines.splice(idx + 1, 0, newLine);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Patch chant.yml: add prepack lines in check/test multi-line `run: |` blocks,
|
|
122
|
+
* and add a new validate step.
|
|
123
|
+
*
|
|
124
|
+
* The file has two patterns:
|
|
125
|
+
* 1. Multi-line blocks (check + test jobs): `run: |\n bun run --cwd lexicons/aws prepack\n ...`
|
|
126
|
+
* 2. Standalone steps (validate job): `- name: Generate and validate ...\n run: bun run --cwd ...`
|
|
127
|
+
*
|
|
128
|
+
* We only insert into pattern 1 (contiguous groups) and separately add a new pattern 2 step.
|
|
129
|
+
*/
|
|
130
|
+
function patchCiWorkflow(root: string, name: string): { patched: boolean; reason?: string } {
|
|
131
|
+
const filePath = join(root, ".github/workflows/chant.yml");
|
|
132
|
+
if (!existsSync(filePath)) return { patched: false, reason: "chant.yml not found" };
|
|
133
|
+
|
|
134
|
+
const content = readFileSync(filePath, "utf-8");
|
|
135
|
+
if (content.includes(`lexicons/${name} prepack`)) {
|
|
136
|
+
return { patched: false, reason: `${name} already in chant.yml` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const lines = content.split("\n");
|
|
140
|
+
|
|
141
|
+
// 1. Insert into multi-line `run: |` blocks only (contiguous prepack groups of 3+).
|
|
142
|
+
// The validate job has standalone steps that are NOT contiguous, so they won't match.
|
|
143
|
+
insertPrepackInContiguousGroups(lines, name);
|
|
144
|
+
|
|
145
|
+
// 2. Add a named validate step after the last "Generate and validate" step
|
|
146
|
+
const displayName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
147
|
+
const validateStepName = `Generate and validate ${displayName} lexicon`;
|
|
148
|
+
|
|
149
|
+
if (!lines.some((l) => l.includes(validateStepName))) {
|
|
150
|
+
let lastValidateRunIdx = -1;
|
|
151
|
+
for (let i = 0; i < lines.length; i++) {
|
|
152
|
+
if (lines[i].includes("Generate and validate")) {
|
|
153
|
+
if (i + 1 < lines.length && lines[i + 1].trimStart().startsWith("run:")) {
|
|
154
|
+
lastValidateRunIdx = i + 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (lastValidateRunIdx > 0) {
|
|
160
|
+
const block = [
|
|
161
|
+
"",
|
|
162
|
+
` - name: ${validateStepName}`,
|
|
163
|
+
` run: bun run --cwd lexicons/${name} prepack`,
|
|
164
|
+
];
|
|
165
|
+
lines.splice(lastValidateRunIdx + 1, 0, ...block);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
writeFileSync(filePath, lines.join("\n"));
|
|
170
|
+
return { patched: true };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Patch publish.yml: add prepack line in test job + publish step.
|
|
175
|
+
*/
|
|
176
|
+
function patchPublishWorkflow(root: string, name: string): { patched: boolean; reason?: string } {
|
|
177
|
+
const filePath = join(root, ".github/workflows/publish.yml");
|
|
178
|
+
if (!existsSync(filePath)) return { patched: false, reason: "publish.yml not found" };
|
|
179
|
+
|
|
180
|
+
const content = readFileSync(filePath, "utf-8");
|
|
181
|
+
if (content.includes(`working-directory: lexicons/${name}`)) {
|
|
182
|
+
return { patched: false, reason: `publish step for ${name} already present` };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const lines = content.split("\n");
|
|
186
|
+
|
|
187
|
+
// Insert prepack line in test job
|
|
188
|
+
insertPrepackAfterEach(lines, name);
|
|
189
|
+
|
|
190
|
+
// Add publish step after the last existing publish step
|
|
191
|
+
let lastPublishRunIdx = -1;
|
|
192
|
+
for (let i = 0; i < lines.length; i++) {
|
|
193
|
+
if (lines[i].includes("bun publish --access public --tolerate-republish")) {
|
|
194
|
+
lastPublishRunIdx = i;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (lastPublishRunIdx > 0) {
|
|
199
|
+
const block = [
|
|
200
|
+
"",
|
|
201
|
+
` - name: Publish @intentius/chant-lexicon-${name}`,
|
|
202
|
+
` working-directory: lexicons/${name}`,
|
|
203
|
+
" run: bun publish --access public --tolerate-republish",
|
|
204
|
+
];
|
|
205
|
+
lines.splice(lastPublishRunIdx + 1, 0, ...block);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
writeFileSync(filePath, lines.join("\n"));
|
|
209
|
+
return { patched: true };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Patch a Dockerfile to add a prepack RUN line.
|
|
214
|
+
*/
|
|
215
|
+
function patchDockerfile(filePath: string, name: string): { patched: boolean; reason?: string } {
|
|
216
|
+
if (!existsSync(filePath)) return { patched: false, reason: `${filePath} not found` };
|
|
217
|
+
|
|
218
|
+
const content = readFileSync(filePath, "utf-8");
|
|
219
|
+
if (content.includes(`lexicons/${name} prepack`)) {
|
|
220
|
+
return { patched: false, reason: `prepack for ${name} already in Dockerfile` };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const lines = content.split("\n");
|
|
224
|
+
insertPrepackAfterEach(lines, name);
|
|
225
|
+
writeFileSync(filePath, lines.join("\n"));
|
|
226
|
+
return { patched: true };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Execute the onboard command — patches monorepo infrastructure for a new lexicon.
|
|
231
|
+
*/
|
|
232
|
+
export function onboardCommand(options: OnboardOptions): OnboardResult {
|
|
233
|
+
const root = findRepoRoot();
|
|
234
|
+
const patched: string[] = [];
|
|
235
|
+
const skipped: string[] = [];
|
|
236
|
+
|
|
237
|
+
// 1. Root package.json
|
|
238
|
+
const pkgResult = patchRootPackageJson(root, options.name);
|
|
239
|
+
if (pkgResult.patched) patched.push("package.json (root dependency)");
|
|
240
|
+
else skipped.push(`package.json: ${pkgResult.reason}`);
|
|
241
|
+
|
|
242
|
+
// 2. CI workflow
|
|
243
|
+
const ciResult = patchCiWorkflow(root, options.name);
|
|
244
|
+
if (ciResult.patched) patched.push("chant.yml (prepack + validate)");
|
|
245
|
+
else skipped.push(`chant.yml: ${ciResult.reason}`);
|
|
246
|
+
|
|
247
|
+
// 3. Publish workflow
|
|
248
|
+
const pubResult = patchPublishWorkflow(root, options.name);
|
|
249
|
+
if (pubResult.patched) patched.push("publish.yml (prepack + publish step)");
|
|
250
|
+
else skipped.push(`publish.yml: ${pubResult.reason}`);
|
|
251
|
+
|
|
252
|
+
// 4. Dockerfiles
|
|
253
|
+
const dockerBun = join(root, "test/Dockerfile.smoke");
|
|
254
|
+
const dockerNode = join(root, "test/Dockerfile.smoke-node");
|
|
255
|
+
|
|
256
|
+
const db = patchDockerfile(dockerBun, options.name);
|
|
257
|
+
if (db.patched) patched.push("Dockerfile.smoke (prepack)");
|
|
258
|
+
else skipped.push(`Dockerfile.smoke: ${db.reason}`);
|
|
259
|
+
|
|
260
|
+
const dn = patchDockerfile(dockerNode, options.name);
|
|
261
|
+
if (dn.patched) patched.push("Dockerfile.smoke-node (prepack)");
|
|
262
|
+
else skipped.push(`Dockerfile.smoke-node: ${dn.reason}`);
|
|
263
|
+
|
|
264
|
+
return { success: true, patched, skipped };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Print onboard results and remaining manual steps.
|
|
269
|
+
*/
|
|
270
|
+
export async function printOnboardResult(result: OnboardResult, name: string): Promise<void> {
|
|
271
|
+
if (!result.success) {
|
|
272
|
+
console.error(formatError({ message: result.error ?? "onboard failed" }));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (result.patched.length > 0) {
|
|
277
|
+
console.log(formatSuccess("Patched:"));
|
|
278
|
+
for (const f of result.patched) {
|
|
279
|
+
console.log(` ${f}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (result.skipped.length > 0) {
|
|
284
|
+
console.log("");
|
|
285
|
+
console.log("Skipped (already configured):");
|
|
286
|
+
for (const s of result.skipped) {
|
|
287
|
+
console.log(` ${s}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.log("");
|
|
292
|
+
console.log("Remaining manual steps:");
|
|
293
|
+
console.log(` 1. Create an example: lexicons/${name}/examples/<example-name>/`);
|
|
294
|
+
console.log(` (must depend on @intentius/chant-lexicon-${name} for workspace resolution)`);
|
|
295
|
+
console.log(` 2. Add smoke tests to test/integration.sh`);
|
|
296
|
+
console.log(` 3. Run: bun install (to update workspace links)`);
|
|
297
|
+
console.log(` 4. First npm publish: tag with v<version> and push`);
|
|
298
|
+
console.log(` 5. Run: chant dev check-lexicon lexicons/${name} (to see completeness status)`);
|
|
299
|
+
console.log(formatWarning({
|
|
300
|
+
message: "First-time scoped packages may publish as private despite --access public",
|
|
301
|
+
}));
|
|
302
|
+
console.log(` Check https://www.npmjs.com/org/intentius and toggle visibility if needed`);
|
|
303
|
+
|
|
304
|
+
// Run check-lexicon if the lexicon directory exists
|
|
305
|
+
const lexiconDir = join(findRepoRoot(), "lexicons", name);
|
|
306
|
+
if (existsSync(lexiconDir)) {
|
|
307
|
+
console.log("");
|
|
308
|
+
console.log("Lexicon completeness:");
|
|
309
|
+
const { checkLexicon, printCheckResult } = await import("./check-lexicon");
|
|
310
|
+
const checkResult = checkLexicon(lexiconDir);
|
|
311
|
+
printCheckResult(checkResult, false);
|
|
312
|
+
}
|
|
313
|
+
}
|
package/src/cli/handlers/dev.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import { formatError, formatSuccess } from "../format";
|
|
2
3
|
import type { CommandContext } from "../registry";
|
|
3
4
|
|
|
@@ -21,10 +22,34 @@ export async function runDevPublish(ctx: CommandContext): Promise<number> {
|
|
|
21
22
|
return 0;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
export async function runDevOnboard(ctx: CommandContext): Promise<number> {
|
|
26
|
+
const name = ctx.args.extraPositional;
|
|
27
|
+
if (!name) {
|
|
28
|
+
console.error(formatError({
|
|
29
|
+
message: "Missing lexicon name",
|
|
30
|
+
hint: "Usage: chant dev onboard <name>",
|
|
31
|
+
}));
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { onboardCommand, printOnboardResult } = await import("../commands/onboard");
|
|
36
|
+
const result = onboardCommand({ name, verbose: ctx.args.verbose });
|
|
37
|
+
await printOnboardResult(result, name);
|
|
38
|
+
return result.success ? 0 : 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runDevCheckLexicon(ctx: CommandContext): Promise<number> {
|
|
42
|
+
const dir = ctx.args.extraPositional ?? ".";
|
|
43
|
+
const { checkLexicon, printCheckResult } = await import("../commands/check-lexicon");
|
|
44
|
+
const result = checkLexicon(resolve(dir));
|
|
45
|
+
printCheckResult(result, ctx.args.format === "json");
|
|
46
|
+
return result.tier1Pass ? 0 : 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
export async function runDevUnknown(ctx: CommandContext): Promise<number> {
|
|
25
50
|
console.error(formatError({
|
|
26
51
|
message: `Unknown dev subcommand: ${ctx.args.path}`,
|
|
27
|
-
hint: "Available: chant dev generate, chant dev publish",
|
|
52
|
+
hint: "Available: chant dev generate, chant dev publish, chant dev onboard, chant dev check-lexicon",
|
|
28
53
|
}));
|
|
29
54
|
return 1;
|
|
30
55
|
}
|
package/src/cli/main.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { loadChantConfig } from "../config";
|
|
|
8
8
|
import { initRuntime } from "../runtime-adapter";
|
|
9
9
|
import { runBuild } from "./handlers/build";
|
|
10
10
|
import { runLint } from "./handlers/lint";
|
|
11
|
-
import { runDevGenerate, runDevPublish, runDevUnknown } from "./handlers/dev";
|
|
11
|
+
import { runDevGenerate, runDevPublish, runDevOnboard, runDevCheckLexicon, runDevUnknown } from "./handlers/dev";
|
|
12
12
|
import { runServeLsp, runServeMcp, runServeUnknown } from "./handlers/serve";
|
|
13
13
|
import { runInit, runInitLexicon } from "./handlers/init";
|
|
14
14
|
import { runList, runImport, runUpdate, runDoctor } from "./handlers/misc";
|
|
@@ -94,6 +94,8 @@ Commands:
|
|
|
94
94
|
Lexicon development:
|
|
95
95
|
dev generate Generate lexicon artifacts (+ validate + coverage)
|
|
96
96
|
dev publish Package lexicon for distribution
|
|
97
|
+
dev onboard <name> Patch CI, Dockerfiles, and workflows for a new lexicon
|
|
98
|
+
dev check-lexicon <dir> Check lexicon completeness (tier 1/2/3)
|
|
97
99
|
|
|
98
100
|
Servers:
|
|
99
101
|
serve lsp Start the LSP server (stdio)
|
|
@@ -172,6 +174,8 @@ const registry: CommandDef[] = [
|
|
|
172
174
|
// Dev subcommands
|
|
173
175
|
{ name: "dev generate", requiresPlugins: true, handler: runDevGenerate },
|
|
174
176
|
{ name: "dev publish", requiresPlugins: true, handler: runDevPublish },
|
|
177
|
+
{ name: "dev onboard", handler: runDevOnboard },
|
|
178
|
+
{ name: "dev check-lexicon", handler: runDevCheckLexicon },
|
|
175
179
|
|
|
176
180
|
// Serve subcommands
|
|
177
181
|
{ name: "serve lsp", requiresPlugins: true, handler: runServeLsp },
|
package/src/codegen/docs.ts
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
import { readFileSync, readdirSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
11
11
|
import { join } from "path";
|
|
12
12
|
|
|
13
|
+
/** Escape curly braces so MDX doesn't treat them as JSX expressions. */
|
|
14
|
+
function escapeMdx(text: string): string {
|
|
15
|
+
return text.replace(/\{/g, "\\{").replace(/\}/g, "\\}");
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
// ── Types ──────────────────────────────────────────────────────────
|
|
14
19
|
|
|
15
20
|
export interface DocsConfig {
|
|
@@ -398,7 +403,7 @@ function buildSidebar(
|
|
|
398
403
|
items.push({ label: "Pseudo-Parameters", slug: "pseudo-parameters" });
|
|
399
404
|
}
|
|
400
405
|
|
|
401
|
-
if (!suppress.has("rules") && !extraSlugs.has("rules") && result.pages.has("rules.mdx")) {
|
|
406
|
+
if (!suppress.has("rules") && !extraSlugs.has("rules") && !extraSlugs.has("lint-rules") && result.pages.has("rules.mdx")) {
|
|
402
407
|
items.push({ label: "Lint Rules", slug: "rules" });
|
|
403
408
|
}
|
|
404
409
|
|
|
@@ -467,7 +472,8 @@ function generateOverview(
|
|
|
467
472
|
`- [Pseudo-Parameters](./pseudo-parameters) — ${Object.keys(manifest.pseudoParameters).length} pseudo-parameters`,
|
|
468
473
|
);
|
|
469
474
|
}
|
|
470
|
-
|
|
475
|
+
const overviewExtraSlugs = new Set((config.extraPages ?? []).map((p) => p.slug));
|
|
476
|
+
if (!suppress.has("rules") && !overviewExtraSlugs.has("lint-rules") && rules.length > 0) {
|
|
471
477
|
lines.push(`- [Lint Rules](./rules) — ${rules.length} rules`);
|
|
472
478
|
}
|
|
473
479
|
if (!suppress.has("serialization")) {
|
|
@@ -504,7 +510,7 @@ function generateIntrinsics(
|
|
|
504
510
|
for (const fn of intrinsics) {
|
|
505
511
|
const tag = fn.isTag ? "Yes" : "No";
|
|
506
512
|
lines.push(
|
|
507
|
-
`| \`${fn.name}\` | ${fn.description ?? "—"} | \`${fn.outputKey ?? fn.name}\` | ${tag} |`,
|
|
513
|
+
`| \`${fn.name}\` | ${escapeMdx(fn.description ?? "—")} | \`${fn.outputKey ?? fn.name}\` | ${tag} |`,
|
|
508
514
|
);
|
|
509
515
|
}
|
|
510
516
|
|
|
@@ -561,7 +567,7 @@ function generateRules(config: DocsConfig, rules: RuleMeta[]): string {
|
|
|
561
567
|
);
|
|
562
568
|
for (const rule of lintRules.sort((a, b) => a.id.localeCompare(b.id))) {
|
|
563
569
|
lines.push(
|
|
564
|
-
`| \`${rule.id}\` | ${rule.severity} | ${rule.category} | ${rule.description} |`,
|
|
570
|
+
`| \`${rule.id}\` | ${rule.severity} | ${rule.category} | ${escapeMdx(rule.description)} |`,
|
|
565
571
|
);
|
|
566
572
|
}
|
|
567
573
|
lines.push("");
|
|
@@ -579,7 +585,7 @@ function generateRules(config: DocsConfig, rules: RuleMeta[]): string {
|
|
|
579
585
|
for (const rule of postSynthRules.sort((a, b) =>
|
|
580
586
|
a.id.localeCompare(b.id),
|
|
581
587
|
)) {
|
|
582
|
-
lines.push(`| \`${rule.id}\` | ${rule.description} |`);
|
|
588
|
+
lines.push(`| \`${rule.id}\` | ${escapeMdx(rule.description)} |`);
|
|
583
589
|
}
|
|
584
590
|
lines.push("");
|
|
585
591
|
}
|
|
@@ -46,12 +46,13 @@ export function buildRegistry<E>(
|
|
|
46
46
|
const tsName = naming.resolve(typeName);
|
|
47
47
|
if (!tsName) continue;
|
|
48
48
|
|
|
49
|
-
// Build attrs map:
|
|
49
|
+
// Build attrs map: TS key (underscores) → CF attr name (dots)
|
|
50
50
|
let attrs: Record<string, string> | undefined;
|
|
51
51
|
if (r.attributes.length > 0) {
|
|
52
52
|
attrs = {};
|
|
53
53
|
for (const a of r.attributes) {
|
|
54
|
-
|
|
54
|
+
const tsKey = a.name.replace(/\./g, "_");
|
|
55
|
+
attrs[tsKey] = a.name;
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
package/src/codegen/typecheck.ts
CHANGED
|
@@ -6,6 +6,39 @@ import { join } from "path";
|
|
|
6
6
|
import { tmpdir } from "os";
|
|
7
7
|
import { getRuntime } from "../runtime-adapter";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Minimal TypeScript lib stub — declares just enough built-in types for
|
|
11
|
+
* validating generated .d.ts files without needing the full standard library.
|
|
12
|
+
* This avoids transient failures from corrupted bunx TypeScript caches.
|
|
13
|
+
*/
|
|
14
|
+
const MINIMAL_LIB = `
|
|
15
|
+
interface Array<T> { length: number; [n: number]: T; }
|
|
16
|
+
interface ReadonlyArray<T> { length: number; readonly [n: number]: T; }
|
|
17
|
+
interface String { length: number; }
|
|
18
|
+
interface Boolean {}
|
|
19
|
+
interface Number {}
|
|
20
|
+
interface Function {}
|
|
21
|
+
interface CallableFunction extends Function {}
|
|
22
|
+
interface NewableFunction extends Function {}
|
|
23
|
+
interface Object {}
|
|
24
|
+
interface RegExp {}
|
|
25
|
+
interface IArguments {}
|
|
26
|
+
interface Symbol {}
|
|
27
|
+
type Record<K extends string | number | symbol, V> = { [P in K]: V; };
|
|
28
|
+
type Partial<T> = { [P in keyof T]?: T[P]; };
|
|
29
|
+
type Required<T> = { [P in keyof T]-?: T[P]; };
|
|
30
|
+
type Readonly<T> = { readonly [P in keyof T]: T[P]; };
|
|
31
|
+
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
|
|
32
|
+
type Omit<T, K extends string | number | symbol> = Pick<T, Exclude<keyof T, K>>;
|
|
33
|
+
type Exclude<T, U> = T extends U ? never : T;
|
|
34
|
+
type Extract<T, U> = T extends U ? T : never;
|
|
35
|
+
type NonNullable<T> = T extends null | undefined ? never : T;
|
|
36
|
+
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
|
|
37
|
+
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
|
|
38
|
+
type Promise<T> = { then<TResult>(onfulfilled?: (value: T) => TResult): Promise<TResult>; };
|
|
39
|
+
interface TemplateStringsArray extends ReadonlyArray<string> { readonly raw: readonly string[]; }
|
|
40
|
+
`;
|
|
41
|
+
|
|
9
42
|
export interface TypeCheckResult {
|
|
10
43
|
ok: boolean;
|
|
11
44
|
diagnostics: string[];
|
|
@@ -25,17 +58,26 @@ export async function typecheckDTS(content: string): Promise<TypeCheckResult> {
|
|
|
25
58
|
const dtsPath = join(dir, "index.d.ts");
|
|
26
59
|
writeFileSync(dtsPath, content);
|
|
27
60
|
|
|
28
|
-
// Write a minimal
|
|
61
|
+
// Write a minimal lib stub so tsc doesn't depend on a full TypeScript
|
|
62
|
+
// standard library installation (avoids bunx cache corruption issues).
|
|
63
|
+
writeFileSync(
|
|
64
|
+
join(dir, "lib.d.ts"),
|
|
65
|
+
MINIMAL_LIB,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Write a minimal tsconfig — noLib: true prevents tsc from looking for
|
|
69
|
+
// standard lib files; our lib.d.ts is included via the include array.
|
|
29
70
|
const tsconfig = {
|
|
30
71
|
compilerOptions: {
|
|
31
72
|
strict: true,
|
|
32
73
|
noEmit: true,
|
|
33
74
|
skipLibCheck: false,
|
|
75
|
+
noLib: true,
|
|
34
76
|
target: "ES2022",
|
|
35
77
|
module: "ES2022",
|
|
36
78
|
moduleResolution: "bundler",
|
|
37
79
|
},
|
|
38
|
-
include: ["index.d.ts"],
|
|
80
|
+
include: ["lib.d.ts", "index.d.ts"],
|
|
39
81
|
};
|
|
40
82
|
writeFileSync(join(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
|
|
41
83
|
|
|
@@ -223,6 +223,30 @@ describe("detectLexicons", () => {
|
|
|
223
223
|
expect(result).toEqual(["testdom"]);
|
|
224
224
|
});
|
|
225
225
|
|
|
226
|
+
test("detects lexicon from multiline import", async () => {
|
|
227
|
+
const file = join(testDir, "infra.ts");
|
|
228
|
+
await writeFile(
|
|
229
|
+
file,
|
|
230
|
+
`import {\n Namespace,\n StatefulSet,\n Service,\n} from "@intentius/chant-lexicon-k8s";\n\nexport const ns = new Namespace({});`
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const result = await detectLexicons([file]);
|
|
234
|
+
expect(result).toEqual(["k8s"]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("detects multiple lexicons from multiline imports", async () => {
|
|
238
|
+
const file = join(testDir, "infra.ts");
|
|
239
|
+
await writeFile(
|
|
240
|
+
file,
|
|
241
|
+
`import {\n Namespace,\n Service,\n} from "@intentius/chant-lexicon-k8s";\nimport {\n FlywayProject,\n Environment,\n} from "@intentius/chant-lexicon-flyway";\n\nexport const ns = new Namespace({});`
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const result = await detectLexicons([file]);
|
|
245
|
+
expect(result).toContain("k8s");
|
|
246
|
+
expect(result).toContain("flyway");
|
|
247
|
+
expect(result).toHaveLength(2);
|
|
248
|
+
});
|
|
249
|
+
|
|
226
250
|
test("detects lexicon from export with curly braces", async () => {
|
|
227
251
|
const file = join(testDir, "reexport.ts");
|
|
228
252
|
await writeFile(
|
package/src/detectLexicon.ts
CHANGED
|
@@ -20,8 +20,10 @@ export async function detectLexicons(files: string[]): Promise<string[]> {
|
|
|
20
20
|
continue;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// Match @intentius/chant-lexicon-<name> in import/export statements
|
|
24
|
-
|
|
23
|
+
// Match @intentius/chant-lexicon-<name> in import/export statements.
|
|
24
|
+
// Uses [\s\S] instead of . to handle multiline imports like:
|
|
25
|
+
// import { Foo, Bar } from "@intentius/chant-lexicon-k8s";
|
|
26
|
+
const regex = /(?:import|export)\s+[\s\S]*?\s+from\s+['"]@intentius\/chant-lexicon-([a-z][\w-]*)['"]/g;
|
|
25
27
|
|
|
26
28
|
for (const match of content.matchAll(regex)) {
|
|
27
29
|
detectedLexicons.add(match[1]);
|
package/src/runtime.ts
CHANGED
|
@@ -31,6 +31,10 @@ export function createResource(
|
|
|
31
31
|
Object.defineProperty(this, "props", { value: props ?? {}, enumerable: false, configurable: true });
|
|
32
32
|
Object.defineProperty(this, "attributes", { value: attributes ?? {}, enumerable: false, configurable: true });
|
|
33
33
|
|
|
34
|
+
// Ref returns the resource instance itself — the serializer walker
|
|
35
|
+
// detects Declarable objects and emits { Ref: logicalName }
|
|
36
|
+
Object.defineProperty(this, "Ref", { value: this, enumerable: false });
|
|
37
|
+
|
|
34
38
|
// Create AttrRef instances for each attribute
|
|
35
39
|
// Must be enumerable so getAttributes() can discover them for resolveAttrRefs()
|
|
36
40
|
for (const [camelName, attrName] of Object.entries(attrMap)) {
|
|
@@ -3,6 +3,7 @@ import { walkValue, type SerializerVisitor } from "./serializer-walker";
|
|
|
3
3
|
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
4
4
|
import { INTRINSIC_MARKER } from "./intrinsic";
|
|
5
5
|
import { AttrRef } from "./attrref";
|
|
6
|
+
import { createResource } from "./runtime";
|
|
6
7
|
|
|
7
8
|
function makeDeclarable(type: string, kind: "resource" | "property" = "resource", props?: Record<string, unknown>): Declarable & { props?: Record<string, unknown> } {
|
|
8
9
|
const d: Declarable & { props?: Record<string, unknown> } = {
|
|
@@ -93,6 +94,13 @@ describe("walkValue", () => {
|
|
|
93
94
|
expect(walkValue({ a: 1, b: { c: 2 } }, names, mockVisitor)).toEqual({ a: 1, b: { c: 2 } });
|
|
94
95
|
});
|
|
95
96
|
|
|
97
|
+
test("resource.Ref resolves via resourceRef", () => {
|
|
98
|
+
const TestTable = createResource("Test::Table", "test", {});
|
|
99
|
+
const resource = new TestTable({}) as unknown as Declarable;
|
|
100
|
+
const names = new Map<Declarable, string>([[resource, "MyTable"]]);
|
|
101
|
+
expect(walkValue((resource as any).Ref, names, mockVisitor)).toEqual({ __ref: "MyTable" });
|
|
102
|
+
});
|
|
103
|
+
|
|
96
104
|
test("complex nested structure", () => {
|
|
97
105
|
const resource = makeDeclarable("Test::Role");
|
|
98
106
|
const ref = new AttrRef(resource, "arn");
|