@intentius/chant-lexicon-gitlab 0.0.1
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 +27 -0
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +33 -0
- package/src/codegen/docs-cli.ts +3 -0
- package/src/codegen/docs.ts +962 -0
- package/src/codegen/fetch.ts +73 -0
- package/src/codegen/generate-cli.ts +41 -0
- package/src/codegen/generate-lexicon.ts +53 -0
- package/src/codegen/generate-typescript.ts +144 -0
- package/src/codegen/generate.ts +166 -0
- package/src/codegen/naming.ts +52 -0
- package/src/codegen/package.ts +64 -0
- package/src/codegen/parse.test.ts +195 -0
- package/src/codegen/parse.ts +531 -0
- package/src/codegen/patches.test.ts +99 -0
- package/src/codegen/patches.ts +100 -0
- package/src/codegen/rollback.ts +26 -0
- package/src/codegen/snapshot.test.ts +109 -0
- package/src/coverage.test.ts +39 -0
- package/src/coverage.ts +52 -0
- package/src/generated/index.d.ts +248 -0
- package/src/generated/index.ts +23 -0
- package/src/generated/lexicon-gitlab.json +77 -0
- package/src/generated/runtime.ts +4 -0
- package/src/import/generator.test.ts +151 -0
- package/src/import/generator.ts +173 -0
- package/src/import/parser.test.ts +160 -0
- package/src/import/parser.ts +282 -0
- package/src/import/roundtrip.test.ts +89 -0
- package/src/index.ts +25 -0
- package/src/intrinsics.test.ts +42 -0
- package/src/intrinsics.ts +40 -0
- package/src/lint/post-synth/post-synth.test.ts +155 -0
- package/src/lint/post-synth/wgl010.ts +41 -0
- package/src/lint/post-synth/wgl011.ts +54 -0
- package/src/lint/post-synth/yaml-helpers.ts +88 -0
- package/src/lint/rules/artifact-no-expiry.ts +62 -0
- package/src/lint/rules/deprecated-only-except.ts +53 -0
- package/src/lint/rules/index.ts +8 -0
- package/src/lint/rules/missing-script.ts +65 -0
- package/src/lint/rules/missing-stage.ts +62 -0
- package/src/lint/rules/rules.test.ts +146 -0
- package/src/lsp/completions.test.ts +85 -0
- package/src/lsp/completions.ts +18 -0
- package/src/lsp/hover.test.ts +60 -0
- package/src/lsp/hover.ts +36 -0
- package/src/plugin.test.ts +228 -0
- package/src/plugin.ts +380 -0
- package/src/serializer.test.ts +309 -0
- package/src/serializer.ts +226 -0
- package/src/testdata/ci-schema-fixture.json +2184 -0
- package/src/testdata/create-fixture.ts +46 -0
- package/src/testdata/load-fixtures.ts +23 -0
- package/src/validate-cli.ts +19 -0
- package/src/validate.test.ts +43 -0
- package/src/validate.ts +125 -0
- package/src/variables.ts +27 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitLab CI YAML serializer.
|
|
3
|
+
*
|
|
4
|
+
* Converts Chant declarables to .gitlab-ci.yml YAML output.
|
|
5
|
+
* Uses snake_case keys (GitLab CI convention) and produces
|
|
6
|
+
* valid YAML without a library dependency — the CI schema is
|
|
7
|
+
* simple enough for a direct emitter.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
11
|
+
import { isPropertyDeclarable } from "@intentius/chant/declarable";
|
|
12
|
+
import type { Serializer } from "@intentius/chant/serializer";
|
|
13
|
+
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
14
|
+
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
15
|
+
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Convert camelCase or PascalCase to snake_case.
|
|
19
|
+
*/
|
|
20
|
+
function toSnakeCase(name: string): string {
|
|
21
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* GitLab CI visitor for the generic serializer walker.
|
|
26
|
+
*/
|
|
27
|
+
function gitlabVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
|
|
28
|
+
return {
|
|
29
|
+
attrRef: (name, _attr) => name,
|
|
30
|
+
resourceRef: (name) => name,
|
|
31
|
+
propertyDeclarable: (entity, walk) => {
|
|
32
|
+
if (!("props" in entity) || typeof entity.props !== "object" || entity.props === null) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const props = entity.props as Record<string, unknown>;
|
|
36
|
+
const result: Record<string, unknown> = {};
|
|
37
|
+
for (const [key, value] of Object.entries(props)) {
|
|
38
|
+
if (value !== undefined) {
|
|
39
|
+
result[toSnakeCase(key)] = walk(value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
43
|
+
},
|
|
44
|
+
transformKey: toSnakeCase,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert a value to YAML-compatible form using the walker.
|
|
50
|
+
*/
|
|
51
|
+
function toYAMLValue(value: unknown, entityNames: Map<Declarable, string>): unknown {
|
|
52
|
+
return walkValue(value, entityNames, gitlabVisitor(entityNames));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Emit a YAML value with proper indentation.
|
|
57
|
+
*/
|
|
58
|
+
function emitYAML(value: unknown, indent: number): string {
|
|
59
|
+
const prefix = " ".repeat(indent);
|
|
60
|
+
|
|
61
|
+
if (value === null || value === undefined) {
|
|
62
|
+
return "null";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof value === "boolean") {
|
|
66
|
+
return value ? "true" : "false";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof value === "number") {
|
|
70
|
+
return String(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof value === "string") {
|
|
74
|
+
// Quote strings that could be misinterpreted
|
|
75
|
+
if (
|
|
76
|
+
value === "" ||
|
|
77
|
+
value === "true" ||
|
|
78
|
+
value === "false" ||
|
|
79
|
+
value === "null" ||
|
|
80
|
+
value === "yes" ||
|
|
81
|
+
value === "no" ||
|
|
82
|
+
value.includes(": ") ||
|
|
83
|
+
value.includes("#") ||
|
|
84
|
+
value.startsWith("*") ||
|
|
85
|
+
value.startsWith("&") ||
|
|
86
|
+
value.startsWith("!") ||
|
|
87
|
+
value.startsWith("{") ||
|
|
88
|
+
value.startsWith("[") ||
|
|
89
|
+
value.startsWith("'") ||
|
|
90
|
+
value.startsWith('"') ||
|
|
91
|
+
value.startsWith("$") ||
|
|
92
|
+
/^\d/.test(value)
|
|
93
|
+
) {
|
|
94
|
+
// Use single quotes, escaping internal single quotes
|
|
95
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
if (value.length === 0) return "[]";
|
|
102
|
+
const lines: string[] = [];
|
|
103
|
+
for (const item of value) {
|
|
104
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
105
|
+
// Object items in arrays
|
|
106
|
+
const entries = Object.entries(item as Record<string, unknown>);
|
|
107
|
+
if (entries.length > 0) {
|
|
108
|
+
const [firstKey, firstVal] = entries[0];
|
|
109
|
+
lines.push(`${prefix}- ${firstKey}: ${emitYAML(firstVal, indent + 2).trimStart()}`);
|
|
110
|
+
for (let i = 1; i < entries.length; i++) {
|
|
111
|
+
const [key, val] = entries[i];
|
|
112
|
+
lines.push(`${prefix} ${key}: ${emitYAML(val, indent + 2).trimStart()}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
lines.push(`${prefix}- ${emitYAML(item, indent + 1).trimStart()}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return "\n" + lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (typeof value === "object") {
|
|
123
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
124
|
+
if (entries.length === 0) return "{}";
|
|
125
|
+
const lines: string[] = [];
|
|
126
|
+
for (const [key, val] of entries) {
|
|
127
|
+
const emitted = emitYAML(val, indent + 1);
|
|
128
|
+
if (emitted.startsWith("\n")) {
|
|
129
|
+
lines.push(`${prefix}${key}:${emitted}`);
|
|
130
|
+
} else {
|
|
131
|
+
lines.push(`${prefix}${key}: ${emitted}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return "\n" + lines.join("\n");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return String(value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* GitLab CI YAML serializer implementation.
|
|
142
|
+
*/
|
|
143
|
+
export const gitlabSerializer: Serializer = {
|
|
144
|
+
name: "gitlab",
|
|
145
|
+
rulePrefix: "WGL",
|
|
146
|
+
|
|
147
|
+
serialize(entities: Map<string, Declarable>, _outputs?: LexiconOutput[]): string {
|
|
148
|
+
// Build reverse map: entity → name
|
|
149
|
+
const entityNames = new Map<Declarable, string>();
|
|
150
|
+
for (const [name, entity] of entities) {
|
|
151
|
+
entityNames.set(entity, name);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const sections: string[] = [];
|
|
155
|
+
|
|
156
|
+
// Separate entities by type
|
|
157
|
+
const jobs: Array<[string, Declarable]> = [];
|
|
158
|
+
const defaults: Array<[string, Declarable]> = [];
|
|
159
|
+
const workflows: Array<[string, Declarable]> = [];
|
|
160
|
+
const others: Array<[string, Declarable]> = [];
|
|
161
|
+
|
|
162
|
+
for (const [name, entity] of entities) {
|
|
163
|
+
if (isPropertyDeclarable(entity)) continue; // Skip property-only entities
|
|
164
|
+
|
|
165
|
+
const entityType = (entity as Record<string, unknown>).entityType as string;
|
|
166
|
+
if (entityType === "GitLab::CI::Job") {
|
|
167
|
+
jobs.push([name, entity]);
|
|
168
|
+
} else if (entityType === "GitLab::CI::Default") {
|
|
169
|
+
defaults.push([name, entity]);
|
|
170
|
+
} else if (entityType === "GitLab::CI::Workflow") {
|
|
171
|
+
workflows.push([name, entity]);
|
|
172
|
+
} else {
|
|
173
|
+
others.push([name, entity]);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Emit stages (collect from jobs)
|
|
178
|
+
const stages = new Set<string>();
|
|
179
|
+
for (const [, entity] of jobs) {
|
|
180
|
+
const props = (entity as Record<string, unknown>).props as Record<string, unknown> | undefined;
|
|
181
|
+
if (props?.stage && typeof props.stage === "string") {
|
|
182
|
+
stages.add(props.stage);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (stages.size > 0) {
|
|
186
|
+
sections.push("stages:" + emitYAML([...stages], 1));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Emit defaults
|
|
190
|
+
for (const [, entity] of defaults) {
|
|
191
|
+
const converted = toYAMLValue(
|
|
192
|
+
(entity as Record<string, unknown>).props,
|
|
193
|
+
entityNames,
|
|
194
|
+
) as Record<string, unknown> | undefined;
|
|
195
|
+
if (converted) {
|
|
196
|
+
sections.push("default:" + emitYAML(converted, 1));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Emit workflow
|
|
201
|
+
for (const [, entity] of workflows) {
|
|
202
|
+
const converted = toYAMLValue(
|
|
203
|
+
(entity as Record<string, unknown>).props,
|
|
204
|
+
entityNames,
|
|
205
|
+
) as Record<string, unknown> | undefined;
|
|
206
|
+
if (converted) {
|
|
207
|
+
sections.push("workflow:" + emitYAML(converted, 1));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Emit jobs
|
|
212
|
+
for (const [name, entity] of jobs) {
|
|
213
|
+
const converted = toYAMLValue(
|
|
214
|
+
(entity as Record<string, unknown>).props,
|
|
215
|
+
entityNames,
|
|
216
|
+
) as Record<string, unknown> | undefined;
|
|
217
|
+
if (converted) {
|
|
218
|
+
// Convert job name from camelCase to kebab-case for YAML
|
|
219
|
+
const yamlName = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
220
|
+
sections.push(`${yamlName}:` + emitYAML(converted, 1));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return sections.join("\n\n") + "\n";
|
|
225
|
+
},
|
|
226
|
+
};
|