@shapeshift-labs/frontier-lang-parser 0.3.13 → 0.3.15
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 +19 -0
- package/dist/conversion.js +173 -1
- package/dist/metadata.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -220,6 +220,25 @@ npm install @shapeshift-labs/frontier-lang-parser
|
|
|
220
220
|
|
|
221
221
|
The parser projects text into `@shapeshift-labs/frontier-lang-kernel` documents. The syntax is intentionally small and experimental.
|
|
222
222
|
|
|
223
|
+
## Authored conversion syntax
|
|
224
|
+
|
|
225
|
+
`.frontier` files can carry universal conversion evidence directly in `conversion` or `universalConversionPlan` blocks. The parser projects these records into `metadata.universalConversionPlan` for the compiler facade and downstream semantic merge tooling.
|
|
226
|
+
|
|
227
|
+
```frontier
|
|
228
|
+
conversion TodoJavascriptToRust @id("conversion_todo_js_rust") {
|
|
229
|
+
sourceLanguage javascript
|
|
230
|
+
target rust
|
|
231
|
+
sourceRuntime javascript node
|
|
232
|
+
targetRuntime rust cli
|
|
233
|
+
runtimeRequirement fetchRuntime @id("runtime_requirement_fetch") capability fetch sourceRuntime node targetRuntime cli evidence artifact_todo_title_probe
|
|
234
|
+
dialect nodeProcess @id("dialect_node_process") language javascript dialect node.runtime kind runtime target rust disposition unsupported readiness blocked loss loss_node_process_projection
|
|
235
|
+
extern viteRoutes @id("extern_vite_routes") language javascript dialect vite.plugin.virtual-module externKind generatorArtifact target rust disposition runtime-required evidence evidence_vite_routes_manifest bindingSymbol virtual:routes
|
|
236
|
+
constraint type publicApi @id("type_constraint_public_api") role source kind public-function symbol symbol:addTodo signatureHash sig_add_todo evidence artifact_todo_title_probe
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`sourceRuntime` and `targetRuntime` become runtime maps. `runtimeRequirement` rows become proof obligations for host/runtime capabilities. `dialect` and `extern` rows preserve dialect-specific constructs, projection readiness, loss/evidence ids, and binding metadata without requiring the authored Frontier file to drop down to raw JSON.
|
|
241
|
+
|
|
223
242
|
## Benchmarks
|
|
224
243
|
|
|
225
244
|
Run the package-local benchmark with:
|
package/dist/conversion.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
const FAMILIES = {
|
|
2
2
|
type: { field: 'typeConstraints', sourceKey: 'sourceTypes', targetKey: 'targetTypes' },
|
|
3
3
|
typeConstraint: { field: 'typeConstraints', sourceKey: 'sourceTypes', targetKey: 'targetTypes' },
|
|
4
|
+
resourceTransfer: { field: 'resourceTransfers', sourceKey: 'sourceGraphs', targetKey: 'targetGraphs', graph: true },
|
|
5
|
+
resourceTransferConstraint: { field: 'resourceTransfers', sourceKey: 'sourceGraphs', targetKey: 'targetGraphs', graph: true },
|
|
6
|
+
'resource-transfer': { field: 'resourceTransfers', sourceKey: 'sourceGraphs', targetKey: 'targetGraphs', graph: true },
|
|
7
|
+
ownership: { field: 'resourceTransfers', sourceKey: 'sourceGraphs', targetKey: 'targetGraphs', graph: true },
|
|
4
8
|
controlFlow: { field: 'controlFlowConstraints', sourceKey: 'sourceControlFlows', targetKey: 'targetControlFlows' },
|
|
5
9
|
controlFlowConstraint: { field: 'controlFlowConstraints', sourceKey: 'sourceControlFlows', targetKey: 'targetControlFlows' },
|
|
6
10
|
lifetime: { field: 'lifetimeConstraints', sourceKey: 'sourceLifetimeConstraints', targetKey: 'targetLifetimeConstraints' },
|
|
7
11
|
lifetimeConstraint: { field: 'lifetimeConstraints', sourceKey: 'sourceLifetimeConstraints', targetKey: 'targetLifetimeConstraints' },
|
|
12
|
+
borrowScope: { field: 'borrowScopeConstraints', sourceKey: 'sourceBorrowScopes', targetKey: 'targetBorrowScopes' },
|
|
13
|
+
borrowScopeConstraint: { field: 'borrowScopeConstraints', sourceKey: 'sourceBorrowScopes', targetKey: 'targetBorrowScopes' },
|
|
14
|
+
'borrow-scope': { field: 'borrowScopeConstraints', sourceKey: 'sourceBorrowScopes', targetKey: 'targetBorrowScopes' },
|
|
15
|
+
'borrow-scope-constraint': { field: 'borrowScopeConstraints', sourceKey: 'sourceBorrowScopes', targetKey: 'targetBorrowScopes' },
|
|
16
|
+
borrowChecker: { field: 'borrowCheckerConstraints', sourceKey: 'sourceBorrowScopes', targetKey: 'targetBorrowScopes' },
|
|
17
|
+
borrowCheckerConstraint: { field: 'borrowCheckerConstraints', sourceKey: 'sourceBorrowScopes', targetKey: 'targetBorrowScopes' },
|
|
18
|
+
'borrow-checker': { field: 'borrowCheckerConstraints', sourceKey: 'sourceBorrowScopes', targetKey: 'targetBorrowScopes' },
|
|
19
|
+
'borrow-checker-constraint': { field: 'borrowCheckerConstraints', sourceKey: 'sourceBorrowScopes', targetKey: 'targetBorrowScopes' },
|
|
8
20
|
callableBoundary: { field: 'callableBoundaryConstraints', sourceKey: 'sourceCallables', targetKey: 'targetCallables' },
|
|
9
21
|
adtPattern: { field: 'adtPatternConstraints', sourceKey: 'sourcePatterns', targetKey: 'targetPatterns' },
|
|
10
22
|
dataLayout: { field: 'dataLayoutConstraints', sourceKey: 'sourceLayouts', targetKey: 'targetLayouts' },
|
|
@@ -34,14 +46,75 @@ export function parseConversionBlock(block) {
|
|
|
34
46
|
if (!line || line.startsWith('#')) continue;
|
|
35
47
|
const target = /^target\s+([^\s,]+)/.exec(line)?.[1];
|
|
36
48
|
const sourceLanguage = /^sourceLanguage\s+([^\s,]+)/.exec(line)?.[1] ?? /^source\s+([^\s,]+)/.exec(line)?.[1];
|
|
49
|
+
const sourceRuntime = /^sourceRuntime\s+([^\s,]+)\s+([^\s,]+)/.exec(line);
|
|
50
|
+
const targetRuntime = /^targetRuntime\s+([^\s,]+)\s+([^\s,]+)/.exec(line);
|
|
51
|
+
const runtimeRequirement = /^(?:runtimeRequirement|requiredRuntime|requiresRuntime)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
|
|
52
|
+
const dialect = /^dialect\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
|
|
53
|
+
const extern = /^extern\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
|
|
37
54
|
const constraint = /^constraint\s+([A-Za-z_$][\w$-]*)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
|
|
38
55
|
if (target) plan.targets.push(target);
|
|
39
56
|
else if (sourceLanguage) plan.sourceLanguage = sourceLanguage;
|
|
57
|
+
else if (sourceRuntime) plan.sourceRuntimes = { ...(plan.sourceRuntimes ?? {}), [sourceRuntime[1]]: sourceRuntime[2] };
|
|
58
|
+
else if (targetRuntime) plan.targetRuntimes = { ...(plan.targetRuntimes ?? {}), [targetRuntime[1]]: targetRuntime[2] };
|
|
59
|
+
else if (runtimeRequirement) addRuntimeRequirement(plan, runtimeRequirement[1], runtimeRequirement[2]);
|
|
60
|
+
else if (dialect) addDialectRecord(plan, dialect[1], dialect[2], false);
|
|
61
|
+
else if (extern) addDialectRecord(plan, extern[1], extern[2], true);
|
|
40
62
|
else if (constraint) addConstraint(plan, constraint[1], constraint[2], constraint[3]);
|
|
41
63
|
}
|
|
42
64
|
return cleanRecord({ ...plan, targets: unique(plan.targets) });
|
|
43
65
|
}
|
|
44
66
|
|
|
67
|
+
function addRuntimeRequirement(plan, name, text) {
|
|
68
|
+
plan.runtimeRequirements = [...(plan.runtimeRequirements ?? []), cleanRecord({
|
|
69
|
+
id: idFrom(text, `runtime_requirement_${name}`),
|
|
70
|
+
name,
|
|
71
|
+
sourceLanguage: readInlineWord('sourceLanguage', text) ?? plan.sourceLanguage,
|
|
72
|
+
target: readInlineWord('target', text) ?? readInlineWord('targetLanguage', text) ?? plan.targets[0],
|
|
73
|
+
capability: readInlineWord('capability', text) ?? readInlineWord('kind', text) ?? name,
|
|
74
|
+
kind: readInlineWord('kind', text),
|
|
75
|
+
sourceRuntime: readInlineWord('sourceRuntime', text) ?? readInlineWord('runtime', text),
|
|
76
|
+
targetRuntime: readInlineWord('targetRuntime', text),
|
|
77
|
+
sourceHost: readInlineWord('sourceHost', text),
|
|
78
|
+
targetHost: readInlineWord('targetHost', text),
|
|
79
|
+
reason: readInlineQuoted('reason', text) ?? readInlineWord('reason', text),
|
|
80
|
+
evidenceIds: readInlineList(text, 'evidence', 'evidenceIds')
|
|
81
|
+
})];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function addDialectRecord(plan, name, text, isExtern) {
|
|
85
|
+
const projection = cleanRecord({
|
|
86
|
+
disposition: readInlineWord('disposition', text),
|
|
87
|
+
readiness: readInlineWord('readiness', text),
|
|
88
|
+
targets: readInlineList(text, 'target', 'targets', 'targetLanguage') ?? plan.targets,
|
|
89
|
+
evidenceIds: readInlineList(text, 'projectionEvidence', 'projectionEvidenceIds', 'evidence', 'evidenceIds'),
|
|
90
|
+
lossIds: readInlineList(text, 'projectionLoss', 'projectionLossIds', 'loss', 'lossId', 'lossIds')
|
|
91
|
+
});
|
|
92
|
+
const record = cleanRecord({
|
|
93
|
+
id: idFrom(text, `${isExtern ? 'extern' : 'dialect'}_${name}`),
|
|
94
|
+
language: readInlineWord('language', text) ?? plan.sourceLanguage,
|
|
95
|
+
dialect: readInlineWord('dialect', text) ?? name,
|
|
96
|
+
name: readInlineWord('name', text) ?? name,
|
|
97
|
+
nativeKind: readInlineWord('nativeKind', text),
|
|
98
|
+
sourcePath: readInlineWord('sourcePath', text) ?? readInlineWord('path', text),
|
|
99
|
+
nativeSourceId: readInlineWord('nativeSource', text) ?? readInlineWord('nativeSourceId', text),
|
|
100
|
+
nativeAstId: readInlineWord('nativeAst', text) ?? readInlineWord('nativeAstId', text),
|
|
101
|
+
nativeAstNodeId: readInlineWord('nativeAstNode', text) ?? readInlineWord('nativeAstNodeId', text),
|
|
102
|
+
semanticNodeId: readInlineWord('semanticNode', text) ?? readInlineWord('semanticNodeId', text),
|
|
103
|
+
semanticSymbolId: readInlineWord('symbol', text) ?? readInlineWord('semanticSymbolId', text),
|
|
104
|
+
sourceMapId: readInlineWord('sourceMap', text) ?? readInlineWord('sourceMapId', text),
|
|
105
|
+
sourceMapMappingId: readInlineWord('sourceMapMapping', text) ?? readInlineWord('sourceMapMappingId', text),
|
|
106
|
+
lossIds: readInlineList(text, 'loss', 'lossId', 'lossIds'),
|
|
107
|
+
evidenceIds: readInlineList(text, 'evidence', 'evidenceIds'),
|
|
108
|
+
projection
|
|
109
|
+
});
|
|
110
|
+
if (isExtern) {
|
|
111
|
+
const binding = cleanRecord({ module: readInlineWord('module', text), path: readInlineWord('bindingPath', text), symbol: readInlineWord('bindingSymbol', text) ?? readInlineWord('symbol', text), abi: readInlineWord('abi', text), version: readInlineWord('version', text) });
|
|
112
|
+
plan.externs = [...(plan.externs ?? []), cleanRecord({ ...record, externKind: readInlineWord('externKind', text) ?? readInlineWord('kind', text) ?? 'extern', ...(Object.keys(binding).length ? { binding } : {}) })];
|
|
113
|
+
} else {
|
|
114
|
+
plan.dialects = [...(plan.dialects ?? []), cleanRecord({ ...record, constructKind: readInlineWord('constructKind', text) ?? readInlineWord('kind', text) ?? 'runtime', externIds: readInlineList(text, 'extern', 'externIds') })];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
45
118
|
function addConstraint(plan, family, name, text) {
|
|
46
119
|
const config = FAMILIES[family] ?? { field: family.endsWith('s') ? family : `${family}Constraints`, sourceKey: 'sourceRecords', targetKey: 'targetRecords' };
|
|
47
120
|
const role = readInlineWord('role', text) ?? 'source';
|
|
@@ -58,7 +131,7 @@ function addConstraint(plan, family, name, text) {
|
|
|
58
131
|
metadata: { name, family, authoredConversionBlockId: plan.id }
|
|
59
132
|
});
|
|
60
133
|
const recordKey = role === 'target' ? config.targetKey : config.sourceKey;
|
|
61
|
-
entry[recordKey] = [record];
|
|
134
|
+
entry[recordKey] = [config.graph ? resourceGraphFromRecord(record, entry) : record];
|
|
62
135
|
plan[config.field] = [...(plan[config.field] ?? []), entry];
|
|
63
136
|
}
|
|
64
137
|
|
|
@@ -75,17 +148,28 @@ function parseConstraintRecord(name, text, role) {
|
|
|
75
148
|
symbolId: readInlineWord('symbol', text) ?? readInlineWord('symbolId', text),
|
|
76
149
|
symbolName: readInlineWord('symbolName', text),
|
|
77
150
|
localName: readInlineWord('localName', text),
|
|
151
|
+
ownerId: readInlineWord('owner', text) ?? readInlineWord('ownerId', text),
|
|
152
|
+
ownerKind: readInlineWord('ownerKind', text),
|
|
153
|
+
mode: readInlineWord('mode', text) ?? readInlineWord('loanMode', text),
|
|
154
|
+
aliasKind: readInlineWord('aliasKind', text),
|
|
155
|
+
moveKind: readInlineWord('moveKind', text),
|
|
156
|
+
dropKind: readInlineWord('dropKind', text),
|
|
157
|
+
resourceKind: readInlineWord('resourceKind', text),
|
|
158
|
+
scopeKind: readInlineWord('scopeKind', text),
|
|
78
159
|
typeKind: readInlineWord('typeKind', text),
|
|
79
160
|
signatureHash: readInlineWord('signatureHash', text),
|
|
80
161
|
contractHash: readInlineWord('contractHash', text),
|
|
81
162
|
typeHash: readInlineWord('typeHash', text),
|
|
82
163
|
flowKind: readInlineWord('flowKind', text),
|
|
164
|
+
controlFlowKind: readInlineWord('controlFlowKind', text),
|
|
165
|
+
sourceControlFlowId: readInlineWord('sourceControlFlowId', text),
|
|
83
166
|
sourceId: readInlineWord('from', text) ?? readInlineWord('sourceId', text),
|
|
84
167
|
targetId: readInlineWord('to', text) ?? readInlineWord('targetId', text),
|
|
85
168
|
label: readInlineWord('label', text),
|
|
86
169
|
conditionHash: readInlineWord('conditionHash', text),
|
|
87
170
|
orderingKey: readInlineWord('orderingKey', text) ?? readInlineWord('orderKey', text),
|
|
88
171
|
lifetimeKind: readInlineWord('lifetimeKind', text),
|
|
172
|
+
lifetimeRegionId: readInlineWord('lifetimeRegion', text) ?? readInlineWord('lifetimeRegionId', text),
|
|
89
173
|
regionKind: readInlineWord('regionKind', text),
|
|
90
174
|
predicate: readInlineQuoted('predicate', text) ?? readInlineWord('predicate', text),
|
|
91
175
|
resourceId: readInlineWord('resource', text) ?? readInlineWord('resourceId', text),
|
|
@@ -103,6 +187,94 @@ function parseConstraintRecord(name, text, role) {
|
|
|
103
187
|
});
|
|
104
188
|
}
|
|
105
189
|
|
|
190
|
+
function resourceGraphFromRecord(record, entry) {
|
|
191
|
+
const tokens = lowerTokens(record);
|
|
192
|
+
const resourceId = record.resourceId ?? record.symbolId ?? `${record.id}_resource`;
|
|
193
|
+
const evidenceIds = record.evidenceIds ?? entry.evidenceIds;
|
|
194
|
+
return cleanRecord({
|
|
195
|
+
id: `${record.id}_resource_graph`,
|
|
196
|
+
sourceLanguage: entry.sourceLanguage,
|
|
197
|
+
target: entry.target,
|
|
198
|
+
sourcePath: record.sourcePath,
|
|
199
|
+
sourceHash: record.sourceHash,
|
|
200
|
+
evidenceIds,
|
|
201
|
+
resources: [{
|
|
202
|
+
id: resourceId,
|
|
203
|
+
resourceKind: record.resourceKind ?? record.kind ?? record.constraintKind,
|
|
204
|
+
sourcePath: record.sourcePath,
|
|
205
|
+
sourceHash: record.sourceHash,
|
|
206
|
+
evidenceIds,
|
|
207
|
+
metadata: { factKinds: record.factKinds, constraintKinds: record.constraintKinds }
|
|
208
|
+
}],
|
|
209
|
+
owners: needsOwner(tokens, record) ? [{
|
|
210
|
+
id: `${record.id}_owner`,
|
|
211
|
+
resourceId,
|
|
212
|
+
ownerId: record.ownerId ?? record.symbolId ?? `${resourceId}:owner`,
|
|
213
|
+
ownerKind: record.ownerKind ?? tokenKind(tokens, /owner|single-owner/) ?? 'owner',
|
|
214
|
+
evidenceIds
|
|
215
|
+
}] : undefined,
|
|
216
|
+
loans: needsLoan(tokens, record) ? [{
|
|
217
|
+
id: `${record.id}_loan`,
|
|
218
|
+
resourceId,
|
|
219
|
+
mode: record.mode ?? loanMode(tokens),
|
|
220
|
+
lifetimeRegionId: record.lifetimeRegionId,
|
|
221
|
+
evidenceIds
|
|
222
|
+
}] : undefined,
|
|
223
|
+
aliases: tokenMatches(tokens, /alias/) ? [{
|
|
224
|
+
id: `${record.id}_alias`,
|
|
225
|
+
resourceId,
|
|
226
|
+
aliasKind: record.aliasKind ?? tokenKind(tokens, /alias/) ?? 'alias',
|
|
227
|
+
evidenceIds
|
|
228
|
+
}] : undefined,
|
|
229
|
+
moves: tokenMatches(tokens, /move|transfer/) ? [{
|
|
230
|
+
id: `${record.id}_move`,
|
|
231
|
+
resourceId,
|
|
232
|
+
moveKind: record.moveKind ?? tokenKind(tokens, /move|transfer/) ?? 'move',
|
|
233
|
+
evidenceIds
|
|
234
|
+
}] : undefined,
|
|
235
|
+
drops: tokenMatches(tokens, /drop/) ? [{
|
|
236
|
+
id: `${record.id}_drop`,
|
|
237
|
+
resourceId,
|
|
238
|
+
dropKind: record.dropKind ?? tokenKind(tokens, /drop/) ?? 'drop',
|
|
239
|
+
evidenceIds
|
|
240
|
+
}] : undefined,
|
|
241
|
+
lifetimeRegions: record.lifetimeKind || record.lifetimeRegionId || tokenMatches(tokens, /lifetime/) ? [{
|
|
242
|
+
id: record.lifetimeRegionId ?? `${record.id}_lifetime`,
|
|
243
|
+
resourceId,
|
|
244
|
+
lifetimeKind: record.lifetimeKind ?? tokenKind(tokens, /lifetime/) ?? record.regionKind,
|
|
245
|
+
evidenceIds
|
|
246
|
+
}] : undefined,
|
|
247
|
+
borrowScopes: needsLoan(tokens, record) || record.scopeKind ? [{
|
|
248
|
+
id: `${record.id}_borrow_scope`,
|
|
249
|
+
resourceId,
|
|
250
|
+
scopeKind: record.scopeKind ?? record.kind,
|
|
251
|
+
lifetimeRegionId: record.lifetimeRegionId,
|
|
252
|
+
constraintKinds: record.constraintKinds,
|
|
253
|
+
evidenceIds
|
|
254
|
+
}] : undefined,
|
|
255
|
+
unsafeBoundaries: tokenMatches(tokens, /unsafe|raw/) ? [{
|
|
256
|
+
id: `${record.id}_unsafe`,
|
|
257
|
+
resourceId,
|
|
258
|
+
kind: tokenKind(tokens, /unsafe|raw/) ?? 'unsafe-boundary',
|
|
259
|
+
evidenceIds
|
|
260
|
+
}] : undefined
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function lowerTokens(record) {
|
|
265
|
+
return unique([record.kind, record.constraintKind, record.scopeKind, record.resourceKind, record.ownerKind, record.mode, record.aliasKind, record.moveKind, record.dropKind, record.lifetimeKind, record.regionKind, record.flowKind, record.controlFlowKind, ...(record.constraintKinds ?? []), ...(record.factKinds ?? [])].filter(Boolean).map((value) => String(value).toLowerCase()));
|
|
266
|
+
}
|
|
267
|
+
function needsOwner(tokens, record) { return record.ownerId || record.ownerKind || tokenMatches(tokens, /owner|single-owner/); }
|
|
268
|
+
function needsLoan(tokens, record) { return record.mode || tokenMatches(tokens, /borrow|loan|raw/); }
|
|
269
|
+
function loanMode(tokens) {
|
|
270
|
+
if (tokens.includes('shared-borrow') || tokens.includes('shared')) return 'shared';
|
|
271
|
+
if (tokens.includes('exclusive-borrow') || tokens.includes('mutable') || tokens.includes('exclusive')) return 'exclusive';
|
|
272
|
+
if (tokens.includes('raw-access-boundary') || tokens.includes('raw')) return 'raw';
|
|
273
|
+
if (tokens.includes('move')) return 'move';
|
|
274
|
+
return 'unknown';
|
|
275
|
+
}
|
|
276
|
+
function tokenMatches(tokens, pattern) { return tokens.some((token) => pattern.test(token)); }
|
|
277
|
+
function tokenKind(tokens, pattern) { return tokens.find((token) => pattern.test(token)); }
|
|
106
278
|
function idFrom(text, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(text)?.[1] ?? fallback; }
|
|
107
279
|
function nameFrom(header) { return /^([A-Za-z_$][\w$]*)/.exec(header)?.[1] ?? 'Conversion'; }
|
|
108
280
|
function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
|
package/dist/metadata.js
CHANGED
|
@@ -66,6 +66,9 @@ function mergeConversionBlocks(blocks) {
|
|
|
66
66
|
if (block.sourceLanguage && !plan.sourceLanguage) plan.sourceLanguage = block.sourceLanguage;
|
|
67
67
|
for (const [key, value] of Object.entries(block)) {
|
|
68
68
|
if (Array.isArray(value) && key !== 'targets') plan[key] = [...(plan[key] ?? []), ...value];
|
|
69
|
+
else if ((key === 'sourceRuntimes' || key === 'targetRuntimes') && value && typeof value === 'object') {
|
|
70
|
+
plan[key] = { ...(plan[key] ?? {}), ...value };
|
|
71
|
+
}
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
return plan;
|