@intentius/chant-lexicon-gitlab 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/integrity.json +3 -3
- package/dist/manifest.json +1 -1
- package/dist/rules/yaml-helpers.ts +1 -8
- package/package.json +3 -2
- package/src/codegen/typecheck.test.ts +24 -0
- package/src/codegen/typecheck.ts +4 -0
- package/src/import/parser.ts +1 -167
- package/src/lint/post-synth/yaml-helpers.ts +1 -8
- package/src/serializer.ts +1 -105
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
GitLab CI lexicon for [chant](https://intentius.io/chant/) — declare CI/CD pipelines as typed TypeScript that serializes to `.gitlab-ci.yml`.
|
|
4
4
|
|
|
5
|
-
This package provides typed constructors for all GitLab CI keywords (Jobs, Workflows,
|
|
5
|
+
This package provides typed constructors for all GitLab CI keywords (Jobs, Workflows, Default, and property types like Artifacts, Cache, Image, Rule, Environment, and Trigger), the `CI` pseudo-parameter object for predefined variables, the `reference()` intrinsic for YAML `!reference` tags, and GitLab-specific lint rules. It also includes LSP and MCP server support for editor completions and hover.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install --save-dev @intentius/chant @intentius/chant-lexicon-gitlab
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "xxhash64",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "2a9ab90e8dd88933",
|
|
5
5
|
"meta.json": "c663c6c63748a9d0",
|
|
6
6
|
"types/index.d.ts": "64e65524615be023",
|
|
7
7
|
"rules/missing-stage.ts": "6d5379e74209a735",
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
"rules/wgl011.ts": "b6b97e5104d91267",
|
|
12
12
|
"rules/wgl015.ts": "d7e9e080994f985",
|
|
13
13
|
"rules/wgl012.ts": "3d188d13fb2236c0",
|
|
14
|
-
"rules/yaml-helpers.ts": "
|
|
14
|
+
"rules/yaml-helpers.ts": "b5416b80369484f2",
|
|
15
15
|
"rules/wgl010.ts": "1548cad287cdf286",
|
|
16
16
|
"rules/wgl014.ts": "6248a852888e8028",
|
|
17
17
|
"rules/wgl013.ts": "3519c933e23fc605",
|
|
18
18
|
"skills/chant-gitlab.md": "4393eb63e0b84b7f"
|
|
19
19
|
},
|
|
20
|
-
"composite": "
|
|
20
|
+
"composite": "9af2adcc06f56cf1"
|
|
21
21
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -7,14 +7,7 @@
|
|
|
7
7
|
* without a full YAML parser dependency.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Extract the primary output string from a serializer result.
|
|
14
|
-
*/
|
|
15
|
-
export function getPrimaryOutput(output: string | SerializerResult): string {
|
|
16
|
-
return typeof output === "string" ? output : output.primary;
|
|
17
|
-
}
|
|
10
|
+
export { getPrimaryOutput } from "@intentius/chant/lint/post-synth";
|
|
18
11
|
|
|
19
12
|
/**
|
|
20
13
|
* Parse a serialized GitLab CI YAML into a structured object.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-gitlab",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": ["src/", "dist/"],
|
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
"generate": "bun run src/codegen/generate-cli.ts",
|
|
19
19
|
"bundle": "bun run src/package-cli.ts",
|
|
20
20
|
"validate": "bun run src/validate-cli.ts",
|
|
21
|
+
"docs": "bun run src/codegen/docs-cli.ts",
|
|
21
22
|
"prepack": "bun run bundle && bun run validate"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@intentius/chant": "0.0.
|
|
25
|
+
"@intentius/chant": "0.0.12"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"typescript": "^5.9.3"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { typecheckDTS } from "./typecheck";
|
|
3
|
+
|
|
4
|
+
describe("typecheckDTS", () => {
|
|
5
|
+
test("passes valid .d.ts content", async () => {
|
|
6
|
+
const result = await typecheckDTS("export declare class Foo { bar: string; }");
|
|
7
|
+
expect(result.ok).toBe(true);
|
|
8
|
+
expect(result.diagnostics).toHaveLength(0);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("fails on syntax errors", async () => {
|
|
12
|
+
const result = await typecheckDTS("export declare class { }"); // missing class name
|
|
13
|
+
expect(result.ok).toBe(false);
|
|
14
|
+
expect(result.diagnostics.length).toBeGreaterThan(0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("fails on undefined type references", async () => {
|
|
18
|
+
const result = await typecheckDTS(
|
|
19
|
+
"export declare class Foo { bar: UndefinedType; }",
|
|
20
|
+
);
|
|
21
|
+
expect(result.ok).toBe(false);
|
|
22
|
+
expect(result.diagnostics.length).toBeGreaterThan(0);
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/import/parser.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { TemplateParser, TemplateIR, ResourceIR } from "@intentius/chant/import/parser";
|
|
9
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Reserved top-level keys in .gitlab-ci.yml that are NOT job definitions.
|
|
@@ -33,173 +34,6 @@ const RESERVED_KEYS = new Set([
|
|
|
33
34
|
]);
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
/**
|
|
37
|
-
* Parse a YAML document into a plain object.
|
|
38
|
-
* Uses a simple YAML parser approach — GitLab CI YAML is straightforward
|
|
39
|
-
* enough that we can parse it without a full YAML library by parsing JSON
|
|
40
|
-
* or using Bun's built-in YAML support if available.
|
|
41
|
-
*/
|
|
42
|
-
function parseYAML(content: string): Record<string, unknown> {
|
|
43
|
-
// Try JSON first (some CI files may be JSON)
|
|
44
|
-
try {
|
|
45
|
-
return JSON.parse(content);
|
|
46
|
-
} catch {
|
|
47
|
-
// Fall through to YAML parsing
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Simple YAML parser for GitLab CI files
|
|
51
|
-
// This handles the common cases: scalars, arrays, objects, block scalars
|
|
52
|
-
const lines = content.split("\n");
|
|
53
|
-
return parseYAMLLines(lines, 0, 0).value as Record<string, unknown>;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface ParseResult {
|
|
57
|
-
value: unknown;
|
|
58
|
-
endIndex: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function parseYAMLLines(lines: string[], startIndex: number, baseIndent: number): ParseResult {
|
|
62
|
-
const result: Record<string, unknown> = {};
|
|
63
|
-
let i = startIndex;
|
|
64
|
-
|
|
65
|
-
while (i < lines.length) {
|
|
66
|
-
const line = lines[i];
|
|
67
|
-
// Skip empty lines and comments
|
|
68
|
-
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
69
|
-
i++;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const indent = line.search(/\S/);
|
|
74
|
-
if (indent < baseIndent) break; // Dedented — done with this block
|
|
75
|
-
if (indent > baseIndent && startIndex > 0) break; // Unexpected indent
|
|
76
|
-
|
|
77
|
-
const keyMatch = line.match(/^(\s*)([^\s:][^:]*?):\s*(.*)$/);
|
|
78
|
-
if (keyMatch) {
|
|
79
|
-
const key = keyMatch[2].trim();
|
|
80
|
-
const inlineValue = keyMatch[3].trim();
|
|
81
|
-
|
|
82
|
-
if (inlineValue === "" || inlineValue.startsWith("#")) {
|
|
83
|
-
// Check next line for array or nested object
|
|
84
|
-
if (i + 1 < lines.length) {
|
|
85
|
-
const nextLine = lines[i + 1];
|
|
86
|
-
const nextIndent = nextLine.search(/\S/);
|
|
87
|
-
if (nextIndent > indent && nextLine.trimStart().startsWith("- ")) {
|
|
88
|
-
// Array
|
|
89
|
-
const arr = parseYAMLArray(lines, i + 1, nextIndent);
|
|
90
|
-
result[key] = arr.value;
|
|
91
|
-
i = arr.endIndex;
|
|
92
|
-
continue;
|
|
93
|
-
} else if (nextIndent > indent) {
|
|
94
|
-
// Nested object
|
|
95
|
-
const nested = parseYAMLLines(lines, i + 1, nextIndent);
|
|
96
|
-
result[key] = nested.value;
|
|
97
|
-
i = nested.endIndex;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
result[key] = null;
|
|
102
|
-
i++;
|
|
103
|
-
} else if (inlineValue.startsWith("[")) {
|
|
104
|
-
// Inline array
|
|
105
|
-
try {
|
|
106
|
-
result[key] = JSON.parse(inlineValue);
|
|
107
|
-
} catch {
|
|
108
|
-
result[key] = inlineValue;
|
|
109
|
-
}
|
|
110
|
-
i++;
|
|
111
|
-
} else if (inlineValue.startsWith("{")) {
|
|
112
|
-
// Inline object
|
|
113
|
-
try {
|
|
114
|
-
result[key] = JSON.parse(inlineValue);
|
|
115
|
-
} catch {
|
|
116
|
-
result[key] = inlineValue;
|
|
117
|
-
}
|
|
118
|
-
i++;
|
|
119
|
-
} else {
|
|
120
|
-
result[key] = parseScalar(inlineValue);
|
|
121
|
-
i++;
|
|
122
|
-
}
|
|
123
|
-
} else if (line.trimStart().startsWith("- ")) {
|
|
124
|
-
// We hit an array at the top level — shouldn't happen normally
|
|
125
|
-
break;
|
|
126
|
-
} else {
|
|
127
|
-
i++;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return { value: result, endIndex: i };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function parseYAMLArray(lines: string[], startIndex: number, baseIndent: number): ParseResult {
|
|
135
|
-
const result: unknown[] = [];
|
|
136
|
-
let i = startIndex;
|
|
137
|
-
|
|
138
|
-
while (i < lines.length) {
|
|
139
|
-
const line = lines[i];
|
|
140
|
-
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
141
|
-
i++;
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const indent = line.search(/\S/);
|
|
146
|
-
if (indent < baseIndent) break;
|
|
147
|
-
|
|
148
|
-
const itemMatch = line.match(/^(\s*)- (.*)$/);
|
|
149
|
-
if (itemMatch && indent === baseIndent) {
|
|
150
|
-
const itemValue = itemMatch[2].trim();
|
|
151
|
-
// Check if it's a key-value pair (object item in array)
|
|
152
|
-
const kvMatch = itemValue.match(/^([^\s:][^:]*?):\s*(.*)$/);
|
|
153
|
-
if (kvMatch) {
|
|
154
|
-
const obj: Record<string, unknown> = {};
|
|
155
|
-
obj[kvMatch[1].trim()] = parseScalar(kvMatch[2].trim());
|
|
156
|
-
// Check for more keys at indent+2
|
|
157
|
-
const nextIndent = indent + 2;
|
|
158
|
-
let j = i + 1;
|
|
159
|
-
while (j < lines.length) {
|
|
160
|
-
const nextLine = lines[j];
|
|
161
|
-
if (nextLine.trim() === "" || nextLine.trim().startsWith("#")) {
|
|
162
|
-
j++;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
const ni = nextLine.search(/\S/);
|
|
166
|
-
if (ni !== nextIndent) break;
|
|
167
|
-
const nextKV = nextLine.match(/^(\s*)([^\s:][^:]*?):\s*(.*)$/);
|
|
168
|
-
if (nextKV) {
|
|
169
|
-
obj[nextKV[2].trim()] = parseScalar(nextKV[3].trim());
|
|
170
|
-
j++;
|
|
171
|
-
} else {
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
result.push(obj);
|
|
176
|
-
i = j;
|
|
177
|
-
} else {
|
|
178
|
-
result.push(parseScalar(itemValue));
|
|
179
|
-
i++;
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return { value: result, endIndex: i };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function parseScalar(value: string): unknown {
|
|
190
|
-
if (value === "" || value === "~" || value === "null") return null;
|
|
191
|
-
if (value === "true" || value === "yes") return true;
|
|
192
|
-
if (value === "false" || value === "no") return false;
|
|
193
|
-
// Strip quotes
|
|
194
|
-
if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))) {
|
|
195
|
-
return value.slice(1, -1);
|
|
196
|
-
}
|
|
197
|
-
// Number
|
|
198
|
-
const num = Number(value);
|
|
199
|
-
if (!isNaN(num) && value !== "") return num;
|
|
200
|
-
return value;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
37
|
/**
|
|
204
38
|
* GitLab CI YAML parser implementation.
|
|
205
39
|
*/
|
|
@@ -7,14 +7,7 @@
|
|
|
7
7
|
* without a full YAML parser dependency.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Extract the primary output string from a serializer result.
|
|
14
|
-
*/
|
|
15
|
-
export function getPrimaryOutput(output: string | SerializerResult): string {
|
|
16
|
-
return typeof output === "string" ? output : output.primary;
|
|
17
|
-
}
|
|
10
|
+
export { getPrimaryOutput } from "@intentius/chant/lint/post-synth";
|
|
18
11
|
|
|
19
12
|
/**
|
|
20
13
|
* Parse a serialized GitLab CI YAML into a structured object.
|
package/src/serializer.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { Serializer } from "@intentius/chant/serializer";
|
|
|
13
13
|
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
14
14
|
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
15
15
|
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
16
|
+
import { emitYAML } from "@intentius/chant/yaml";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* GitLab CI visitor for the generic serializer walker.
|
|
@@ -82,111 +83,6 @@ function toYAMLValue(value: unknown, entityNames: Map<Declarable, string>): unkn
|
|
|
82
83
|
return walkValue(preprocessed, entityNames, gitlabVisitor(entityNames));
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
/**
|
|
86
|
-
* Emit a YAML value with proper indentation.
|
|
87
|
-
*/
|
|
88
|
-
function emitYAML(value: unknown, indent: number): string {
|
|
89
|
-
const prefix = " ".repeat(indent);
|
|
90
|
-
|
|
91
|
-
if (value === null || value === undefined) {
|
|
92
|
-
return "null";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (typeof value === "boolean") {
|
|
96
|
-
return value ? "true" : "false";
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (typeof value === "number") {
|
|
100
|
-
return String(value);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (typeof value === "string") {
|
|
104
|
-
// Quote strings that could be misinterpreted
|
|
105
|
-
if (
|
|
106
|
-
value === "" ||
|
|
107
|
-
value === "true" ||
|
|
108
|
-
value === "false" ||
|
|
109
|
-
value === "null" ||
|
|
110
|
-
value === "yes" ||
|
|
111
|
-
value === "no" ||
|
|
112
|
-
value.includes(": ") ||
|
|
113
|
-
value.includes("#") ||
|
|
114
|
-
value.startsWith("*") ||
|
|
115
|
-
value.startsWith("&") ||
|
|
116
|
-
value.startsWith("!") ||
|
|
117
|
-
value.startsWith("{") ||
|
|
118
|
-
value.startsWith("[") ||
|
|
119
|
-
value.startsWith("'") ||
|
|
120
|
-
value.startsWith('"') ||
|
|
121
|
-
value.startsWith("$") ||
|
|
122
|
-
/^\d/.test(value)
|
|
123
|
-
) {
|
|
124
|
-
// Use single quotes, escaping internal single quotes
|
|
125
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
126
|
-
}
|
|
127
|
-
return value;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (Array.isArray(value)) {
|
|
131
|
-
if (value.length === 0) return "[]";
|
|
132
|
-
const lines: string[] = [];
|
|
133
|
-
for (const item of value) {
|
|
134
|
-
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
135
|
-
// Object items in arrays
|
|
136
|
-
const entries = Object.entries(item as Record<string, unknown>);
|
|
137
|
-
if (entries.length > 0) {
|
|
138
|
-
const [firstKey, firstVal] = entries[0];
|
|
139
|
-
const firstEmitted = emitYAML(firstVal, indent + 2);
|
|
140
|
-
if (firstEmitted.startsWith("\n")) {
|
|
141
|
-
// Multi-line value: put on next line, indented under the key
|
|
142
|
-
lines.push(`${prefix}- ${firstKey}:${firstEmitted}`);
|
|
143
|
-
} else {
|
|
144
|
-
lines.push(`${prefix}- ${firstKey}: ${firstEmitted}`);
|
|
145
|
-
}
|
|
146
|
-
for (let i = 1; i < entries.length; i++) {
|
|
147
|
-
const [key, val] = entries[i];
|
|
148
|
-
const emitted = emitYAML(val, indent + 2);
|
|
149
|
-
if (emitted.startsWith("\n")) {
|
|
150
|
-
lines.push(`${prefix} ${key}:${emitted}`);
|
|
151
|
-
} else {
|
|
152
|
-
lines.push(`${prefix} ${key}: ${emitted}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
} else {
|
|
157
|
-
lines.push(`${prefix}- ${emitYAML(item, indent + 1).trimStart()}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return "\n" + lines.join("\n");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (typeof value === "object") {
|
|
164
|
-
const obj = value as Record<string, unknown>;
|
|
165
|
-
|
|
166
|
-
// Handle tagged values (intrinsics like !reference)
|
|
167
|
-
if ("tag" in obj && "value" in obj && typeof obj.tag === "string") {
|
|
168
|
-
if (obj.tag === "!reference" && Array.isArray(obj.value)) {
|
|
169
|
-
return `!reference [${(obj.value as string[]).join(", ")}]`;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const entries = Object.entries(obj);
|
|
174
|
-
if (entries.length === 0) return "{}";
|
|
175
|
-
const lines: string[] = [];
|
|
176
|
-
for (const [key, val] of entries) {
|
|
177
|
-
const emitted = emitYAML(val, indent + 1);
|
|
178
|
-
if (emitted.startsWith("\n")) {
|
|
179
|
-
lines.push(`${prefix}${key}:${emitted}`);
|
|
180
|
-
} else {
|
|
181
|
-
lines.push(`${prefix}${key}: ${emitted}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return "\n" + lines.join("\n");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return String(value);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
86
|
/**
|
|
191
87
|
* GitLab CI YAML serializer implementation.
|
|
192
88
|
*/
|