genesis-compiler 1.2.11 → 1.2.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.
@@ -1,464 +0,0 @@
1
- import { GenesisError } from './errors.js';
2
- import {
3
- isCanonicalProjectWorkdir,
4
- isSafeProcessExecutable,
5
- parseBacktickedArguments,
6
- } from './stack-process.js';
7
-
8
- const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
9
- const PLACEHOLDER = /\{([a-z][a-z0-9-]*)\}/gu;
10
- const PREVIEW_IDENTITY_ENVIRONMENT_NAME = /^[A-Z_][A-Z0-9_]*$/u;
11
- const PREVIEW_IDENTITY_PROTOCOL = 'genesis.preview-identity.command.v1';
12
- const PREVIEW_IDENTITY_RESERVED_ENVIRONMENT_NAMES = new Set([
13
- 'HOME',
14
- 'LOGNAME',
15
- 'NODE_OPTIONS',
16
- 'PATH',
17
- 'TMPDIR',
18
- 'USER',
19
- ]);
20
- const PREVIEW_IDENTITY_TYPES = new Set(['email', 'login', 'user-id']);
21
- const PREVIEW_FIELDS = [
22
- ['- Command: ', 'command', 'Command'],
23
- ['- Protocol: ', 'protocol', 'Protocol'],
24
- ['- Identity types: ', 'identityTypes', 'Identity types'],
25
- ['- Enabled environment: ', 'enabledEnvironment', 'Enabled environment'],
26
- ['- Secret environment: ', 'secretEnvironment', 'Secret environment'],
27
- ['- Runtimes: ', 'runtimes', 'Runtimes'],
28
- ['- Timeout ms: ', 'timeoutMs', 'Timeout ms'],
29
- ];
30
- const TARGET_HEADING = /^### Target `([^`\r\n]+)`:[ \t]+(.+)$/u;
31
-
32
- function invalid(launchPath, message, line, details = {}) {
33
- throw new GenesisError('STACK_LAUNCH_INVALID', message, {
34
- path: launchPath,
35
- ...(line === undefined ? {} : { line }),
36
- ...details,
37
- });
38
- }
39
-
40
- function text(value, launchPath, label, line) {
41
- if (typeof value !== 'string' || !value.trim() || /[\0\r\n]/u.test(value)) {
42
- invalid(launchPath, `${label} must be one non-empty line.`, line);
43
- }
44
- return value.trim();
45
- }
46
-
47
- function id(value, launchPath, label, line) {
48
- const normalized = text(value, launchPath, label, line);
49
- if (!ID_PATTERN.test(normalized)) {
50
- invalid(launchPath, `${label} must use lowercase letters, digits, and single hyphens.`, line);
51
- }
52
- return normalized;
53
- }
54
-
55
- function tokenValues(source, prefix, launchPath, label, line) {
56
- const values = parseBacktickedArguments(source.slice(prefix.length));
57
- if (!values) {
58
- invalid(launchPath, `${label} must contain separate non-empty backticked values.`, line);
59
- }
60
- return values;
61
- }
62
-
63
- function oneToken(source, prefix, launchPath, label, line) {
64
- const values = tokenValues(source, prefix, launchPath, label, line);
65
- if (values.length !== 1) invalid(launchPath, `${label} accepts exactly one value.`, line);
66
- return values[0];
67
- }
68
-
69
- function runtimeRequirements(values, launchPath, label, line, { deduplicate = false } = {}) {
70
- const requirements = values.map((entry) => id(entry, launchPath, label, line));
71
- if (!deduplicate && new Set(requirements).size !== requirements.length) {
72
- invalid(launchPath, `${label} contains a duplicate technology id.`, line);
73
- }
74
- return deduplicate ? [...new Set(requirements)] : requirements;
75
- }
76
-
77
- function normalizeLaunchArgv(values, launchPath, line) {
78
- if (!values || values.some((entry) => entry.includes('\0'))) {
79
- invalid(
80
- launchPath,
81
- 'Every launch command and argument must be one separate non-empty backticked value.',
82
- line,
83
- );
84
- }
85
- for (const entry of values) {
86
- if ([...entry.matchAll(PLACEHOLDER)].some((match) => !['host', 'port'].includes(match[1]))) {
87
- invalid(launchPath, 'Launch argv supports only {host} and {port} placeholders.', line);
88
- }
89
- }
90
- if (!isSafeProcessExecutable(values[0])) {
91
- invalid(
92
- launchPath,
93
- 'A launch executable must be a PATH name or project-relative path without traversal.',
94
- line,
95
- );
96
- }
97
- return values;
98
- }
99
-
100
- function launchArgv(source, launchPath, line) {
101
- return normalizeLaunchArgv(parseBacktickedArguments(source), launchPath, line);
102
- }
103
-
104
- function setOnce(draft, field, value, launchPath, line, label) {
105
- if (draft.seen.has(field)) invalid(launchPath, `Duplicate ${label}.`, line);
106
- draft.seen.add(field);
107
- draft[field] = value;
108
- }
109
-
110
- function targetDraft(match, launchPath, line) {
111
- return {
112
- id: id(match[1], launchPath, 'Launch target id', line),
113
- label: text(match[2], launchPath, 'Launch target label', line),
114
- default: false,
115
- workdir: '.',
116
- preferredPort: null,
117
- urlPath: '/',
118
- readiness: null,
119
- runtimeRequirements: [],
120
- steps: [],
121
- previewIdentity: null,
122
- seen: new Set(),
123
- line,
124
- };
125
- }
126
-
127
- function parseReadiness(source, launchPath, line) {
128
- const match = source.match(/^- Ready when: `GET` `([^`\r\n]+)` returns `([0-9]{3})`$/u);
129
- if (!match) {
130
- invalid(
131
- launchPath,
132
- 'Launch target readiness must use `- Ready when: `GET` `/path` returns `200``.',
133
- line,
134
- );
135
- }
136
- const status = Number(match[2]);
137
- if (!Number.isInteger(status) || status < 200 || status > 399) {
138
- invalid(launchPath, 'Launch target readiness status must be from 200 through 399.', line);
139
- }
140
- return {
141
- kind: 'http',
142
- method: 'GET',
143
- path: parseUrlPath(match[1], launchPath, line),
144
- status,
145
- };
146
- }
147
-
148
- function parsePreferredPort(value, launchPath, line) {
149
- if (!/^[0-9]+$/u.test(value)) {
150
- invalid(launchPath, 'Launch target preferred port must be an integer from 1024 through 65535.', line);
151
- }
152
- const port = Number(value);
153
- if (!Number.isSafeInteger(port) || port < 1024 || port > 65_535) {
154
- invalid(launchPath, 'Launch target preferred port must be an integer from 1024 through 65535.', line);
155
- }
156
- return port;
157
- }
158
-
159
- function parseUrlPath(value, launchPath, line) {
160
- const normalized = text(value, launchPath, 'Launch target URL path', line);
161
- if (!normalized.startsWith('/') || normalized.startsWith('//') || /[\\?#]/u.test(normalized)) {
162
- invalid(
163
- launchPath,
164
- 'Launch target URL path must be an application path beginning with one slash.',
165
- line,
166
- );
167
- }
168
- return normalized;
169
- }
170
-
171
- function parseTargetLine(draft, source, launchPath, line) {
172
- if (source === '- Default.') {
173
- setOnce(draft, 'default', true, launchPath, line, 'Launch target Default entry');
174
- return;
175
- }
176
- if (source.startsWith('- Workdir: ')) {
177
- const value = oneToken(source, '- Workdir: ', launchPath, 'Launch target Workdir', line);
178
- if (!isCanonicalProjectWorkdir(value)) {
179
- invalid(launchPath, 'Launch target workdir must be a canonical project-relative directory.', line);
180
- }
181
- setOnce(draft, 'workdir', value, launchPath, line, 'Launch target Workdir entry');
182
- return;
183
- }
184
- if (source.startsWith('- Preferred port: ')) {
185
- const value = oneToken(
186
- source,
187
- '- Preferred port: ',
188
- launchPath,
189
- 'Launch target Preferred port',
190
- line,
191
- );
192
- setOnce(
193
- draft,
194
- 'preferredPort',
195
- parsePreferredPort(value, launchPath, line),
196
- launchPath,
197
- line,
198
- 'Launch target Preferred port entry',
199
- );
200
- return;
201
- }
202
- if (source.startsWith('- URL path: ')) {
203
- const value = oneToken(source, '- URL path: ', launchPath, 'Launch target URL path', line);
204
- setOnce(
205
- draft,
206
- 'urlPath',
207
- parseUrlPath(value, launchPath, line),
208
- launchPath,
209
- line,
210
- 'Launch target URL path entry',
211
- );
212
- return;
213
- }
214
- if (source.startsWith('- Runtimes: ')) {
215
- const values = tokenValues(source, '- Runtimes: ', launchPath, 'Launch target Runtimes', line);
216
- setOnce(
217
- draft,
218
- 'runtimeRequirements',
219
- runtimeRequirements(values, launchPath, 'Launch target Runtimes', line),
220
- launchPath,
221
- line,
222
- 'Launch target Runtimes entry',
223
- );
224
- return;
225
- }
226
- if (source.startsWith('- Ready when: ')) {
227
- setOnce(
228
- draft,
229
- 'readiness',
230
- parseReadiness(source, launchPath, line),
231
- launchPath,
232
- line,
233
- 'Launch target Ready when entry',
234
- );
235
- return;
236
- }
237
- const step = source.match(/^- (Prepare|Serve) `([^`\r\n]+)`:[ \t]+(.+)$/u);
238
- if (step) {
239
- draft.steps.push({
240
- label: text(step[2], launchPath, 'Launch step label', line),
241
- argv: launchArgv(step[3], launchPath, line),
242
- role: step[1] === 'Serve' ? 'server' : 'prepare',
243
- });
244
- return;
245
- }
246
- invalid(launchPath, `Unknown Launch target entry: ${source}.`, line);
247
- }
248
-
249
- function previewIdentityEnvironmentName(value, launchPath, label, line) {
250
- if (value === undefined) return '';
251
- const name = text(value, launchPath, `Preview identity ${label} environment name`, line);
252
- if (
253
- !PREVIEW_IDENTITY_ENVIRONMENT_NAME.test(name)
254
- || PREVIEW_IDENTITY_RESERVED_ENVIRONMENT_NAMES.has(name)
255
- || name.startsWith('XDG_')
256
- ) {
257
- invalid(launchPath, `Preview identity ${label} environment name is invalid.`, line);
258
- }
259
- return name;
260
- }
261
-
262
- function previewDraft(line) {
263
- return { seen: new Set(), line };
264
- }
265
-
266
- function parsePreviewLine(draft, source, launchPath, line) {
267
- const field = PREVIEW_FIELDS.find(([prefix]) => source.startsWith(prefix));
268
- if (!field) invalid(launchPath, `Unknown Preview identity entry: ${source}.`, line);
269
- const [prefix, key, label] = field;
270
- const values = tokenValues(source, prefix, launchPath, `Preview identity ${label}`, line);
271
- if (!['command', 'identityTypes', 'runtimes'].includes(key) && values.length !== 1) {
272
- invalid(launchPath, `Preview identity ${label} accepts exactly one value.`, line);
273
- }
274
- setOnce(draft, key, values, launchPath, line, `Preview identity ${label} entry`);
275
- }
276
-
277
- function normalizePreviewIdentity(draft, launchPath, targetId) {
278
- if (!draft.command) {
279
- invalid(launchPath, `Launch target ${targetId} Preview identity needs Command.`, draft.line);
280
- }
281
- if (!draft.protocol) {
282
- invalid(launchPath, `Launch target ${targetId} Preview identity needs Protocol.`, draft.line);
283
- }
284
- if (!draft.identityTypes) {
285
- invalid(launchPath, `Launch target ${targetId} Preview identity needs Identity types.`, draft.line);
286
- }
287
- const command = normalizeLaunchArgv(draft.command, launchPath, draft.line);
288
- if (
289
- command.length > 64
290
- || command.some((entry) => !entry.trim() || entry.length > 4096 || /[\r\n]/u.test(entry))
291
- || !command[0].includes('/')
292
- || command[0].includes('\\')
293
- || command[0].split('/').some((part) => !part || ['.', '..'].includes(part))
294
- || command.some((entry) => /\{(?:host|port)\}/u.test(entry))
295
- ) {
296
- invalid(
297
- launchPath,
298
- 'Preview identity command must use a committed app-owned project-relative executable and contain at most 64 literal arguments.',
299
- draft.line,
300
- );
301
- }
302
- const protocol = text(draft.protocol[0], launchPath, 'Preview identity protocol', draft.line);
303
- if (protocol !== PREVIEW_IDENTITY_PROTOCOL) {
304
- invalid(launchPath, `Preview identity protocol must be ${PREVIEW_IDENTITY_PROTOCOL}.`, draft.line);
305
- }
306
- const identityTypes = [...new Set(draft.identityTypes.map((entry) => {
307
- const type = text(entry, launchPath, 'Preview identity type', draft.line);
308
- if (!PREVIEW_IDENTITY_TYPES.has(type)) {
309
- invalid(launchPath, `Unsupported preview identity type: ${type}.`, draft.line);
310
- }
311
- return type;
312
- }))];
313
- const environment = {
314
- enabled: previewIdentityEnvironmentName(
315
- draft.enabledEnvironment?.[0],
316
- launchPath,
317
- 'enabled',
318
- draft.line,
319
- ),
320
- secret: previewIdentityEnvironmentName(
321
- draft.secretEnvironment?.[0],
322
- launchPath,
323
- 'secret',
324
- draft.line,
325
- ),
326
- };
327
- if (environment.enabled && environment.enabled === environment.secret) {
328
- invalid(
329
- launchPath,
330
- 'Preview identity enabled and secret environment names must differ.',
331
- draft.line,
332
- );
333
- }
334
- let timeoutMs = 10_000;
335
- if (draft.timeoutMs) {
336
- const value = draft.timeoutMs[0];
337
- if (!/^[0-9]+$/u.test(value)) {
338
- invalid(launchPath, 'Preview identity Timeout ms must be an integer from 1 through 30000.', draft.line);
339
- }
340
- timeoutMs = Number(value);
341
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30_000) {
342
- invalid(launchPath, 'Preview identity Timeout ms must be an integer from 1 through 30000.', draft.line);
343
- }
344
- }
345
- return {
346
- command,
347
- environment,
348
- identityTypes,
349
- protocol,
350
- runtimes: draft.runtimes
351
- ? runtimeRequirements(
352
- draft.runtimes,
353
- launchPath,
354
- 'Preview identity Runtimes',
355
- draft.line,
356
- { deduplicate: true },
357
- )
358
- : [],
359
- timeoutMs,
360
- };
361
- }
362
-
363
- function normalizeTarget(draft, launchPath) {
364
- if (draft.steps.length === 0) {
365
- invalid(launchPath, `Launch target ${draft.id} needs at least one step.`, draft.line);
366
- }
367
- const serverSteps = draft.steps.filter(({ role }) => role === 'server');
368
- if (serverSteps.length !== 1 || draft.steps.at(-1).role !== 'server') {
369
- invalid(launchPath, `Launch target ${draft.id} needs exactly one final Serve step.`, draft.line);
370
- }
371
- if (!draft.readiness) {
372
- invalid(launchPath, `Launch target ${draft.id} needs one Ready when entry.`, draft.line);
373
- }
374
- return {
375
- id: draft.id,
376
- label: draft.label,
377
- default: draft.default,
378
- workdir: draft.workdir,
379
- preferredPort: draft.preferredPort,
380
- urlPath: draft.urlPath,
381
- readiness: draft.readiness,
382
- runtimeRequirements: draft.runtimeRequirements,
383
- steps: draft.steps,
384
- ...(draft.previewIdentity
385
- ? { previewIdentity: normalizePreviewIdentity(draft.previewIdentity, launchPath, draft.id) }
386
- : {}),
387
- };
388
- }
389
-
390
- export function parseStackLaunchLines(lines, {
391
- path: launchPath = 'stack launch',
392
- } = {}) {
393
- if (lines === undefined) return null;
394
- const entries = lines.map((line) => line.trim()).filter(Boolean);
395
- if (entries.length === 1 && entries[0] === '- Nothing.') return { version: 1, targets: [] };
396
-
397
- const targets = [];
398
- let target = null;
399
- let inPreviewIdentity = false;
400
- for (let index = 0; index < lines.length; index += 1) {
401
- const source = lines[index].trim();
402
- if (!source) continue;
403
- const line = index + 1;
404
- const heading = source.match(TARGET_HEADING);
405
- if (heading) {
406
- if (target) targets.push(normalizeTarget(target, launchPath));
407
- target = targetDraft(heading, launchPath, line);
408
- inPreviewIdentity = false;
409
- continue;
410
- }
411
- if (source === '#### Preview identity') {
412
- if (!target || target.previewIdentity) {
413
- invalid(launchPath, 'Preview identity must appear once below a Launch target.', line);
414
- }
415
- target.previewIdentity = previewDraft(line);
416
- inPreviewIdentity = true;
417
- continue;
418
- }
419
- if (!target) {
420
- invalid(launchPath, '## Launch must begin with a Target heading.', line);
421
- }
422
- if (inPreviewIdentity) parsePreviewLine(target.previewIdentity, source, launchPath, line);
423
- else parseTargetLine(target, source, launchPath, line);
424
- }
425
- if (target) targets.push(normalizeTarget(target, launchPath));
426
- if (targets.length === 0) {
427
- invalid(launchPath, '## Launch needs at least one Target or exactly `- Nothing.`.');
428
- }
429
- const ids = targets.map(({ id: targetId }) => targetId);
430
- if (new Set(ids).size !== ids.length) invalid(launchPath, 'Launch contains a duplicate target id.');
431
- if (targets.filter(({ default: isDefault }) => isDefault).length > 1) {
432
- invalid(launchPath, 'Launch may declare at most one default target.');
433
- }
434
- return { version: 1, targets };
435
- }
436
-
437
- export function composeStackLaunchTargets(components, projectLaunch) {
438
- if (projectLaunch) {
439
- return projectLaunch.targets.map((target) => ({ ...target, source: 'project' }));
440
- }
441
- const targets = components.flatMap((component) => (
442
- (component.launchTargets || []).map((target) => ({
443
- ...target,
444
- source: `component:${component.id}`,
445
- }))
446
- ));
447
- const sourcesById = new Map();
448
- for (const target of targets) {
449
- const previous = sourcesById.get(target.id);
450
- if (previous) {
451
- invalid(
452
- 'selected Stack components',
453
- `Selected Stack components provide duplicate launch target ${target.id}.`,
454
- undefined,
455
- { sources: [previous, target.source], target: target.id },
456
- );
457
- }
458
- sourcesById.set(target.id, target.source);
459
- }
460
- if (targets.filter((target) => target.default).length > 1) {
461
- invalid('selected Stack components', 'Selected Stack components provide more than one default launch target.');
462
- }
463
- return targets;
464
- }
@@ -1,133 +0,0 @@
1
- import { GenesisError } from './errors.js';
2
- import {
3
- isCanonicalProjectWorkdir,
4
- isSafeProcessExecutable,
5
- parseBacktickedArguments,
6
- } from './stack-process.js';
7
-
8
- const PREPARE_LINE = /^- Prepare `([^`\r\n]+)` with (.+?):[ \t]+(.+)$/u;
9
- const RUNTIME_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
10
- const READY_WHEN_SUFFIX = /^(.*?)[ \t]+when `([^`\r\n]+)` exists$/u;
11
- const OPTIONAL_WHEN_SUFFIX = /^(.*?)[ \t]+if `([^`\r\n]+)` exists$/u;
12
- const WORKDIR_SUFFIX = /^(.*?)[ \t]+in `([^`\r\n]+)`$/u;
13
-
14
- function invalid(setupPath, message, line, details = {}) {
15
- throw new GenesisError('STACK_WORKSPACE_SETUP_INVALID', message, {
16
- path: setupPath,
17
- ...(line === undefined ? {} : { line }),
18
- ...details,
19
- });
20
- }
21
-
22
- function parseRuntimeRequirements(source, setupPath, line) {
23
- const runtimes = parseBacktickedArguments(source);
24
- if (!runtimes) {
25
- invalid(
26
- setupPath,
27
- 'Workspace setup runtimes must be separate backticked technology ids.',
28
- line,
29
- );
30
- }
31
- const normalized = runtimes.map((runtime) => {
32
- if (!RUNTIME_ID.test(runtime)) {
33
- invalid(
34
- setupPath,
35
- 'Workspace setup runtimes must use lowercase letters, digits, and single hyphens.',
36
- line,
37
- { runtime },
38
- );
39
- }
40
- return runtime;
41
- });
42
- if (new Set(normalized).size !== normalized.length) {
43
- invalid(setupPath, 'A Workspace setup step contains a duplicate runtime.', line);
44
- }
45
- return normalized;
46
- }
47
-
48
- export function parseStackWorkspaceSetupLines(lines, {
49
- path: setupPath = 'stack workspace setup',
50
- } = {}) {
51
- if (lines === undefined) return null;
52
- const entries = lines.map((line) => line.trim()).filter(Boolean);
53
- if (entries.length === 1 && entries[0] === '- Nothing.') return [];
54
- const steps = [];
55
- for (let index = 0; index < lines.length; index += 1) {
56
- const source = lines[index].trim();
57
- if (!source) continue;
58
- const line = index + 1;
59
- const entry = source.match(PREPARE_LINE);
60
- if (!entry) {
61
- invalid(
62
- setupPath,
63
- 'Every Workspace setup entry must use `- Prepare `label` with `runtime` [in `workdir`] [when|if `path` exists]: `command` `argument`...`.',
64
- line,
65
- { observed: source },
66
- );
67
- }
68
- const label = entry[1].trim();
69
- if (!label || /[\0\r\n]/u.test(label)) {
70
- invalid(setupPath, 'Workspace setup labels must be non-empty single lines.', line);
71
- }
72
- const readyEntry = entry[2].match(READY_WHEN_SUFFIX);
73
- const optionalEntry = readyEntry ? null : entry[2].match(OPTIONAL_WHEN_SUFFIX);
74
- const setupSource = readyEntry ? readyEntry[1] : optionalEntry ? optionalEntry[1] : entry[2];
75
- const readyWhen = readyEntry ? readyEntry[2] : '';
76
- const runWhen = optionalEntry ? optionalEntry[2] : '';
77
- if ((readyWhen || runWhen) && ((readyWhen || runWhen) === '.' || !isCanonicalProjectWorkdir(readyWhen || runWhen))) {
78
- invalid(
79
- setupPath,
80
- 'Workspace setup readiness paths must be canonical project-relative paths.',
81
- line,
82
- );
83
- }
84
- const workdirEntry = setupSource.match(WORKDIR_SUFFIX);
85
- const runtimeSource = workdirEntry ? workdirEntry[1] : setupSource;
86
- const workdir = workdirEntry ? workdirEntry[2] : '.';
87
- if (!isCanonicalProjectWorkdir(workdir)) {
88
- invalid(setupPath, 'Workspace setup workdir must be a canonical project-relative directory.', line);
89
- }
90
- const argv = parseBacktickedArguments(entry[3]);
91
- if (!argv || argv.some((value) => value.includes('\0')) || !isSafeProcessExecutable(argv[0])) {
92
- invalid(
93
- setupPath,
94
- 'Workspace setup command and arguments must be separate non-empty backticked values with a safe executable.',
95
- line,
96
- );
97
- }
98
- steps.push({
99
- label,
100
- argv,
101
- runtimeRequirements: parseRuntimeRequirements(runtimeSource, setupPath, line),
102
- workdir,
103
- ...(readyWhen ? { readyWhen } : {}),
104
- ...(runWhen ? { runWhen } : {}),
105
- });
106
- }
107
- if (steps.length === 0) {
108
- invalid(setupPath, '## Workspace setup needs at least one Prepare entry.');
109
- }
110
- return steps;
111
- }
112
-
113
- export function composeStackWorkspaceSetup(components, projectSteps) {
114
- if (projectSteps !== null) return { source: 'project', steps: projectSteps, diagnostics: [] };
115
- const declarations = components
116
- .filter((component) => component.workspaceSetupSteps.length > 0)
117
- .map((component) => ({
118
- source: `component:${component.id}`,
119
- steps: component.workspaceSetupSteps,
120
- }));
121
- if (declarations.length === 0) return { source: null, steps: [], diagnostics: [] };
122
- if (declarations.length === 1) return { ...declarations[0], diagnostics: [] };
123
- const sources = declarations.map(({ source }) => source).sort();
124
- return {
125
- source: null,
126
- steps: [],
127
- diagnostics: [{
128
- code: 'STACK_WORKSPACE_SETUP_AMBIGUOUS',
129
- message: `Selected Stack components provide competing Workspace setup recipes: ${sources.join(', ')}. Add one project ## Workspace setup section to choose the exact recipe.`,
130
- details: { sources },
131
- }],
132
- };
133
- }