genesis-compiler 1.2.15 → 1.2.16
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
CHANGED
package/src/index/contracts.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const GENESIS_CONTRACTS = Object.freeze({
|
|
2
2
|
derivedArtifacts: 'genesis.derived-artifacts.v1',
|
|
3
|
-
environment: 'genesis.environment.
|
|
3
|
+
environment: 'genesis.environment.v2',
|
|
4
4
|
stackSection: 'genesis.stack-section.v1',
|
|
5
5
|
verification: 'genesis.verification.v1',
|
|
6
6
|
});
|
package/src/index/stack-piece.js
CHANGED
|
@@ -7,6 +7,8 @@ import { parseStackVerificationLines } from './stack-verification.js';
|
|
|
7
7
|
import { normalizeSource } from './utils.js';
|
|
8
8
|
|
|
9
9
|
const STACK_PIECE_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
10
|
+
const RESOURCE_SEMANTIC_PATTERN = /^[a-z][A-Za-z0-9]*$/u;
|
|
11
|
+
const ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
10
12
|
const STACK_CUSTOMIZATION_TITLE = /^# Stack customization: ([a-z0-9]+(?:-[a-z0-9]+)*)\s*$/u;
|
|
11
13
|
const STACK_CUSTOMIZATION_FIELDS = new Map([
|
|
12
14
|
['Description', 'description'],
|
|
@@ -190,6 +192,14 @@ export function parseStackResourceLines(lines, {
|
|
|
190
192
|
}
|
|
191
193
|
if (
|
|
192
194
|
!resource
|
|
195
|
+
|| typeof resource !== 'object'
|
|
196
|
+
|| Array.isArray(resource)
|
|
197
|
+
|| Object.keys(resource).some((name) => ![
|
|
198
|
+
'environmentAlternatives',
|
|
199
|
+
'id',
|
|
200
|
+
'kind',
|
|
201
|
+
'optionalBindings',
|
|
202
|
+
].includes(name))
|
|
193
203
|
|| typeof resource.id !== 'string'
|
|
194
204
|
|| !STACK_PIECE_ID_PATTERN.test(resource.id)
|
|
195
205
|
|| typeof resource.kind !== 'string'
|
|
@@ -199,26 +209,70 @@ export function parseStackResourceLines(lines, {
|
|
|
199
209
|
) {
|
|
200
210
|
invalid(resourcePath, 'A Stack resource needs an id, kind, and at least one environment alternative.');
|
|
201
211
|
}
|
|
212
|
+
const optionalBindings = normalizeResourceBindings(
|
|
213
|
+
resource.optionalBindings === undefined ? {} : resource.optionalBindings,
|
|
214
|
+
resourcePath,
|
|
215
|
+
resource.id,
|
|
216
|
+
{ allowEmpty: true },
|
|
217
|
+
);
|
|
218
|
+
const optionalSemantics = new Set(Object.keys(optionalBindings));
|
|
202
219
|
const environmentAlternatives = resource.environmentAlternatives.map((alternative) => {
|
|
203
|
-
|
|
204
|
-
|
|
220
|
+
if (!alternative || typeof alternative !== 'object' || Array.isArray(alternative)) {
|
|
221
|
+
invalid(resourcePath, `Stack resource ${resource.id} has an invalid environment alternative.`);
|
|
222
|
+
}
|
|
223
|
+
const unknown = Object.keys(alternative).filter((name) => (
|
|
224
|
+
!['allowEmpty', 'bindings', 'preferred'].includes(name)
|
|
225
|
+
));
|
|
226
|
+
const allowEmpty = alternative.allowEmpty === undefined ? [] : alternative.allowEmpty;
|
|
205
227
|
if (
|
|
206
|
-
|
|
228
|
+
unknown.length > 0
|
|
207
229
|
|| !Array.isArray(allowEmpty)
|
|
230
|
+
|| (alternative.preferred !== undefined && typeof alternative.preferred !== 'boolean')
|
|
208
231
|
) {
|
|
209
232
|
invalid(resourcePath, `Stack resource ${resource.id} has an invalid environment alternative.`);
|
|
210
233
|
}
|
|
211
|
-
const
|
|
234
|
+
const bindings = normalizeResourceBindings(
|
|
235
|
+
alternative.bindings,
|
|
236
|
+
resourcePath,
|
|
237
|
+
resource.id,
|
|
238
|
+
);
|
|
239
|
+
const semantics = Object.keys(bindings);
|
|
212
240
|
if (
|
|
213
|
-
|
|
214
|
-
||
|
|
215
|
-
|| allowEmpty.some((name) => !required.includes(name))
|
|
216
|
-
|| new Set(required).size !== required.length
|
|
241
|
+
allowEmpty.some((semantic) => !semantics.includes(semantic))
|
|
242
|
+
|| allowEmpty.some((semantic) => typeof semantic !== 'string')
|
|
217
243
|
|| new Set(allowEmpty).size !== allowEmpty.length
|
|
218
|
-
|
|
219
|
-
|
|
244
|
+
|| semantics.some((semantic) => optionalSemantics.has(semantic))
|
|
245
|
+
|| Object.values(bindings).some((name) => Object.values(optionalBindings).includes(name))
|
|
246
|
+
) {
|
|
247
|
+
invalid(resourcePath, `Stack resource ${resource.id} has an invalid environment alternative.`);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
allowEmpty: [...allowEmpty].sort(),
|
|
251
|
+
bindings,
|
|
252
|
+
preferred: alternative.preferred === true,
|
|
253
|
+
};
|
|
220
254
|
});
|
|
221
|
-
|
|
255
|
+
if (environmentAlternatives.filter(({ preferred }) => preferred).length !== 1) {
|
|
256
|
+
invalid(resourcePath, `Stack resource ${resource.id} needs exactly one preferred environment alternative.`);
|
|
257
|
+
}
|
|
258
|
+
const semanticByEnvironment = new Map(Object.entries(optionalBindings).map(([semantic, name]) => [
|
|
259
|
+
name,
|
|
260
|
+
semantic,
|
|
261
|
+
]));
|
|
262
|
+
for (const alternative of environmentAlternatives) {
|
|
263
|
+
for (const [semantic, name] of Object.entries(alternative.bindings)) {
|
|
264
|
+
if (semanticByEnvironment.has(name) && semanticByEnvironment.get(name) !== semantic) {
|
|
265
|
+
invalid(resourcePath, `Stack resource ${resource.id} assigns conflicting meanings to ${name}.`);
|
|
266
|
+
}
|
|
267
|
+
semanticByEnvironment.set(name, semantic);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
id: resource.id,
|
|
272
|
+
kind: resource.kind,
|
|
273
|
+
environmentAlternatives,
|
|
274
|
+
optionalBindings,
|
|
275
|
+
};
|
|
222
276
|
});
|
|
223
277
|
if (new Set(parsed.map(({ id }) => id)).size !== parsed.length) {
|
|
224
278
|
invalid(resourcePath, '## Resources contains a duplicate resource id.');
|
|
@@ -226,6 +280,29 @@ export function parseStackResourceLines(lines, {
|
|
|
226
280
|
return parsed;
|
|
227
281
|
}
|
|
228
282
|
|
|
283
|
+
function normalizeResourceBindings(value, resourcePath, resourceId, {
|
|
284
|
+
allowEmpty = false,
|
|
285
|
+
} = {}) {
|
|
286
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
287
|
+
invalid(resourcePath, `Stack resource ${resourceId} has invalid semantic environment bindings.`);
|
|
288
|
+
}
|
|
289
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
290
|
+
if (!allowEmpty && entries.length === 0) {
|
|
291
|
+
invalid(resourcePath, `Stack resource ${resourceId} has an empty environment alternative.`);
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
entries.some(([semantic, name]) => (
|
|
295
|
+
!RESOURCE_SEMANTIC_PATTERN.test(semantic)
|
|
296
|
+
|| typeof name !== 'string'
|
|
297
|
+
|| !ENVIRONMENT_NAME_PATTERN.test(name)
|
|
298
|
+
))
|
|
299
|
+
|| new Set(entries.map(([, name]) => name)).size !== entries.length
|
|
300
|
+
) {
|
|
301
|
+
invalid(resourcePath, `Stack resource ${resourceId} has invalid semantic environment bindings.`);
|
|
302
|
+
}
|
|
303
|
+
return Object.fromEntries(entries);
|
|
304
|
+
}
|
|
305
|
+
|
|
229
306
|
function resources(all, piecePath) {
|
|
230
307
|
return parseStackResourceLines(all.has('Resources') ? all.get('Resources') : undefined, {
|
|
231
308
|
path: piecePath,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
function present(environment, name, allowEmpty) {
|
|
1
|
+
function present(environment, name, allowEmpty = false) {
|
|
2
2
|
if (!Object.hasOwn(environment, name) || typeof environment[name] !== 'string') return false;
|
|
3
3
|
const value = environment[name].trim();
|
|
4
4
|
if (value === `$${name}` || value === `\${${name}}`) return false;
|
|
5
|
-
return value.length > 0 || allowEmpty
|
|
5
|
+
return value.length > 0 || allowEmpty;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
/** Core reports declarations; each Stack component defines their meaning. */
|
|
@@ -11,11 +11,15 @@ export function missingStackResources({ environment = process.env, resources = [
|
|
|
11
11
|
throw new TypeError('Stack resource inspection requires an environment object and resource declarations.');
|
|
12
12
|
}
|
|
13
13
|
return resources.flatMap(({ component, resource }) => {
|
|
14
|
-
const satisfied = resource.environmentAlternatives.some(({
|
|
15
|
-
|
|
14
|
+
const satisfied = resource.environmentAlternatives.some(({ bindings, allowEmpty }) => (
|
|
15
|
+
Object.entries(bindings).every(([semantic, name]) => (
|
|
16
|
+
present(environment, name, allowEmpty.includes(semantic))
|
|
17
|
+
))
|
|
16
18
|
));
|
|
17
19
|
if (satisfied) return [];
|
|
18
|
-
const alternatives = resource.environmentAlternatives.map(({
|
|
20
|
+
const alternatives = resource.environmentAlternatives.map(({ bindings }) => (
|
|
21
|
+
Object.values(bindings).join(' + ')
|
|
22
|
+
));
|
|
19
23
|
return [{
|
|
20
24
|
code: 'STACK_RESOURCE_MISSING',
|
|
21
25
|
message: `Stack component ${component} requires ${resource.id}: ${alternatives.join(' OR ')}.`,
|