openxiangda 1.0.172 → 1.0.173

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.
@@ -0,0 +1,166 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { builtinModules, createRequire } = require('module');
4
+ const esbuild = require('esbuild');
5
+
6
+ function resolveWorkspaceAlias(workspaceRoot, importPath) {
7
+ const raw = path.join(
8
+ workspaceRoot,
9
+ 'src',
10
+ String(importPath || '').replace(/^@\//, '')
11
+ );
12
+ const candidates = [
13
+ raw,
14
+ `${raw}.ts`,
15
+ `${raw}.tsx`,
16
+ `${raw}.js`,
17
+ `${raw}.mjs`,
18
+ `${raw}.cjs`,
19
+ path.join(raw, 'index.ts'),
20
+ path.join(raw, 'index.tsx'),
21
+ path.join(raw, 'index.js'),
22
+ ];
23
+ return candidates.find(candidate => fs.existsSync(candidate)) || raw;
24
+ }
25
+
26
+ async function loadLocalFormSchema(workspaceRoot, formCode) {
27
+ const schemaPath = path.join(
28
+ workspaceRoot,
29
+ 'src',
30
+ 'forms',
31
+ formCode,
32
+ 'schema.ts'
33
+ );
34
+ if (!fs.existsSync(schemaPath)) return null;
35
+ const external = Array.from(
36
+ new Set([...builtinModules, ...builtinModules.map(name => `node:${name}`)])
37
+ );
38
+ const built = await esbuild.build({
39
+ entryPoints: [schemaPath],
40
+ bundle: true,
41
+ format: 'cjs',
42
+ platform: 'node',
43
+ target: ['node20'],
44
+ write: false,
45
+ logLevel: 'silent',
46
+ external,
47
+ plugins: [
48
+ {
49
+ name: 'openxiangda-form-schema-workspace-alias',
50
+ setup(build) {
51
+ build.onResolve({ filter: /^@\// }, args => ({
52
+ path: resolveWorkspaceAlias(workspaceRoot, args.path),
53
+ }));
54
+ },
55
+ },
56
+ ],
57
+ });
58
+ const code = built.outputFiles?.[0]?.text;
59
+ if (!code) {
60
+ throw new Error(`FORM_SCHEMA_BUILD_FAILED: ${formCode} 未生成 schema bundle`);
61
+ }
62
+ const schemaModule = { exports: {} };
63
+ const localRequire = createRequire(schemaPath);
64
+ const evaluate = new Function(
65
+ 'module',
66
+ 'exports',
67
+ 'require',
68
+ '__filename',
69
+ '__dirname',
70
+ code
71
+ );
72
+ evaluate(
73
+ schemaModule,
74
+ schemaModule.exports,
75
+ localRequire,
76
+ schemaPath,
77
+ path.dirname(schemaPath)
78
+ );
79
+ const exported = schemaModule.exports?.default || schemaModule.exports;
80
+ return normalizeLocalFormSchema(exported, formCode);
81
+ }
82
+
83
+ function normalizeLocalFormSchema(schema, formCode) {
84
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
85
+ throw new Error(`FORM_SCHEMA_INVALID: ${formCode} 默认导出必须是对象`);
86
+ }
87
+ if (!schema.formMeta || typeof schema.formMeta !== 'object') {
88
+ throw new Error(`FORM_SCHEMA_INVALID: ${formCode}.formMeta 缺失`);
89
+ }
90
+ if (!Array.isArray(schema.fields) || schema.fields.length === 0) {
91
+ throw new Error(`FORM_SCHEMA_INVALID: ${formCode}.fields 不能为空`);
92
+ }
93
+ const seen = new Set();
94
+ const components = schema.fields.map((field, index) => {
95
+ const fieldId = String(field?.fieldId || '').trim();
96
+ const componentName = normalizeComponentName(field?.componentName);
97
+ if (!fieldId || !componentName) {
98
+ throw new Error(
99
+ `FORM_SCHEMA_INVALID: ${formCode}.fields[${index}] 缺少 fieldId/componentName`
100
+ );
101
+ }
102
+ if (seen.has(fieldId)) {
103
+ throw new Error(`FORM_SCHEMA_INVALID: ${formCode} fieldId 重复: ${fieldId}`);
104
+ }
105
+ seen.add(fieldId);
106
+ return {
107
+ componentName,
108
+ id: field.id || `${fieldId}_${index + 1}`,
109
+ title: field.label || fieldId,
110
+ hidden: false,
111
+ isLocked: false,
112
+ condition: true,
113
+ conditionGroup: '',
114
+ props: {
115
+ ...field,
116
+ componentName,
117
+ isFormComponent: true,
118
+ fieldId,
119
+ label: field.label || fieldId,
120
+ tips: field.tips || '',
121
+ value: field.value || '',
122
+ placeholder: field.placeholder || '',
123
+ },
124
+ };
125
+ });
126
+ const pageSchema = {
127
+ version: '2.0',
128
+ componentsTree: [
129
+ {
130
+ componentName: 'Page',
131
+ id: `${schema.formMeta.formUuid || formCode}_page`,
132
+ props: {},
133
+ children: components,
134
+ },
135
+ ],
136
+ };
137
+ return {
138
+ name: schema.formMeta.title || formCode,
139
+ schema: JSON.stringify(pageSchema),
140
+ packages: JSON.stringify({}),
141
+ formType: normalizeFormType(
142
+ schema.formMeta.formType || schema.template?.formType
143
+ ),
144
+ fieldCount: components.length,
145
+ sourcePath: `src/forms/${formCode}/schema.ts`,
146
+ };
147
+ }
148
+
149
+ function normalizeComponentName(value) {
150
+ const name = String(value || '').trim();
151
+ return name === 'TextAreaField' ? 'TextareaField' : name;
152
+ }
153
+
154
+ function normalizeFormType(value) {
155
+ const normalized = String(value || '').trim().toLowerCase();
156
+ return ['process', 'workflow', 'flow', 'flowform', 'processform'].includes(
157
+ normalized
158
+ )
159
+ ? 'process'
160
+ : 'receipt';
161
+ }
162
+
163
+ module.exports = {
164
+ loadLocalFormSchema,
165
+ normalizeLocalFormSchema,
166
+ };
@@ -0,0 +1,89 @@
1
+ const crypto = require('crypto');
2
+
3
+ function legacyRemoteIdentity(value) {
4
+ const remote = String(value || '').trim();
5
+ try {
6
+ const url = new URL(remote);
7
+ url.username = '';
8
+ url.password = '';
9
+ url.search = '';
10
+ url.hash = '';
11
+ return url.toString();
12
+ } catch {
13
+ return remote
14
+ .replace(/^[^@/]+@([^:]+):/, '$1:')
15
+ .replace(/[?#].*$/, '');
16
+ }
17
+ }
18
+
19
+ function toggleDotGit(value) {
20
+ const normalized = String(value || '').replace(/\/+$/, '');
21
+ return /\.git$/i.test(normalized)
22
+ ? normalized.replace(/\.git$/i, '')
23
+ : `${normalized}.git`;
24
+ }
25
+
26
+ function canonicalRemoteIdentity(value) {
27
+ const legacy = legacyRemoteIdentity(value).replace(/\/+$/, '');
28
+ try {
29
+ const url = new URL(legacy);
30
+ const host = url.hostname.toLowerCase();
31
+ const port = url.port ? `:${url.port}` : '';
32
+ const pathname = url.pathname
33
+ .replace(/^\/+|\/+$/g, '')
34
+ .replace(/\.git$/i, '');
35
+ return `${host}${port}/${pathname}`.replace(/\/+$/, '');
36
+ } catch {
37
+ return legacy
38
+ .replace(/^([^:]+):/, '$1/')
39
+ .replace(/\.git$/i, '')
40
+ .replace(/\/+$/, '');
41
+ }
42
+ }
43
+
44
+ function remoteIdentityVariants(value) {
45
+ const legacy = legacyRemoteIdentity(value).replace(/\/+$/, '');
46
+ return Array.from(
47
+ new Set(
48
+ [
49
+ legacy,
50
+ toggleDotGit(legacy),
51
+ canonicalRemoteIdentity(legacy),
52
+ ].filter(Boolean)
53
+ )
54
+ );
55
+ }
56
+
57
+ function sha256(value) {
58
+ return `sha256:${crypto
59
+ .createHash('sha256')
60
+ .update(String(value))
61
+ .digest('hex')}`;
62
+ }
63
+
64
+ function remoteRepositoryHashes(value) {
65
+ return remoteIdentityVariants(value).map(identity =>
66
+ sha256(`remote:${identity}`)
67
+ );
68
+ }
69
+
70
+ function remoteUrlHashes(value) {
71
+ return remoteIdentityVariants(value).map(sha256);
72
+ }
73
+
74
+ function identitiesIntersect(left, right) {
75
+ const leftSet = new Set(
76
+ (Array.isArray(left) ? left : [left]).filter(Boolean)
77
+ );
78
+ return (Array.isArray(right) ? right : [right]).some(value =>
79
+ leftSet.has(value)
80
+ );
81
+ }
82
+
83
+ module.exports = {
84
+ identitiesIntersect,
85
+ legacyRemoteIdentity,
86
+ remoteIdentityVariants,
87
+ remoteRepositoryHashes,
88
+ remoteUrlHashes,
89
+ };
@@ -1,8 +1,11 @@
1
- const crypto = require('crypto');
2
1
  const path = require('path');
3
2
  const { spawnSync } = require('child_process');
4
3
 
5
4
  const { readGitSourceBase } = require('./change-baseline');
5
+ const {
6
+ identitiesIntersect,
7
+ remoteUrlHashes,
8
+ } = require('./git-remote-identity');
6
9
  const { listRuntimeBuildInputChanges } = require('./runtime-lineage');
7
10
 
8
11
  const GIT_OUTPUT_LIMIT = 16 * 1024 * 1024;
@@ -68,6 +71,7 @@ function prepareReleaseSourceRevision(options = {}) {
68
71
  worktreeRoot: readGit(cwd, ['rev-parse', '--show-toplevel']).stdout,
69
72
  remoteName: mainline.remoteName,
70
73
  remoteUrlHash: mainline.remoteUrlHash,
74
+ remoteUrlHashAliases: mainline.remoteUrlHashAliases,
71
75
  mainBranch: mainline.branchName,
72
76
  mainTipCommit: mainline.tipCommit,
73
77
  mainlineSource: mainline.source,
@@ -82,7 +86,10 @@ function assertReleaseSourceRevisionStable(sourceRevision, options = {}) {
82
86
  const current = readGitSourceBase(cwd, 'HEAD');
83
87
  const dirtyPaths = listRuntimeBuildInputChanges(cwd);
84
88
  if (
85
- expected.repo !== current.repo ||
89
+ !identitiesIntersect(
90
+ [expected.repo, ...(expected.repoAliases || [])],
91
+ [current.repo, ...(current.repoAliases || [])]
92
+ ) ||
86
93
  expected.baseCommit !== current.baseCommit ||
87
94
  expected.treeHash !== current.treeHash ||
88
95
  dirtyPaths.length > 0
@@ -109,7 +116,12 @@ function inspectReleaseSourceIntegration(sourceRevision, options = {}) {
109
116
  const cwd = path.resolve(options.cwd || process.cwd());
110
117
  const source = normalizeSourceRevision(sourceRevision);
111
118
  const currentRepository = readGitSourceBase(cwd, 'HEAD');
112
- if (source.repo !== currentRepository.repo) {
119
+ if (
120
+ !identitiesIntersect(
121
+ [source.repo, ...(source.repoAliases || [])],
122
+ [currentRepository.repo, ...(currentRepository.repoAliases || [])]
123
+ )
124
+ ) {
113
125
  throw releaseMainlineError(
114
126
  'RELEASE_SOURCE_REPOSITORY_MISMATCH',
115
127
  '冻结的发布提交与当前 Git 仓库不一致,不能验证主分支回合状态'
@@ -120,7 +132,10 @@ function inspectReleaseSourceIntegration(sourceRevision, options = {}) {
120
132
  if (
121
133
  source.remoteName !== mainline.remoteName ||
122
134
  source.mainBranch !== mainline.branchName ||
123
- source.remoteUrlHash !== mainline.remoteUrlHash
135
+ !identitiesIntersect(
136
+ [source.remoteUrlHash, ...(source.remoteUrlHashAliases || [])],
137
+ [mainline.remoteUrlHash, ...(mainline.remoteUrlHashAliases || [])]
138
+ )
124
139
  ) {
125
140
  throw releaseMainlineError(
126
141
  'RELEASE_MAINLINE_TARGET_CHANGED',
@@ -204,7 +219,8 @@ function resolveAuthoritativeMainline(options = {}) {
204
219
  }
205
220
 
206
221
  function resolveRemoteMainline({ cwd, remoteName }) {
207
- const remoteUrlHash = readRemoteUrlHash(cwd, remoteName);
222
+ const remoteUrlHashAliases = readRemoteUrlHashes(cwd, remoteName);
223
+ const remoteUrlHash = remoteUrlHashAliases[0];
208
224
  const remoteHead = readGit(
209
225
  cwd,
210
226
  ['ls-remote', '--symref', remoteName, 'HEAD'],
@@ -217,6 +233,7 @@ function resolveRemoteMainline({ cwd, remoteName }) {
217
233
  return {
218
234
  remoteName,
219
235
  remoteUrlHash,
236
+ remoteUrlHashAliases,
220
237
  branchName: parsed.branchName,
221
238
  tipCommit: parsed.tipCommit,
222
239
  source: 'remote-head',
@@ -236,6 +253,7 @@ function resolveRemoteMainline({ cwd, remoteName }) {
236
253
  return {
237
254
  remoteName,
238
255
  remoteUrlHash,
256
+ remoteUrlHashAliases,
239
257
  ...candidates[0],
240
258
  source: 'remote-main-master-fallback',
241
259
  };
@@ -422,6 +440,25 @@ function normalizeSourceRevision(value) {
422
440
  const remoteName = normalizeOptionalName(value.remoteName, 'remote');
423
441
  const mainBranch = String(value.mainBranch || '').trim();
424
442
  const remoteUrlHash = String(value.remoteUrlHash || '').trim().toLowerCase();
443
+ const remoteUrlHashAliases = Array.from(
444
+ new Set(
445
+ [
446
+ remoteUrlHash,
447
+ ...(Array.isArray(value.remoteUrlHashAliases)
448
+ ? value.remoteUrlHashAliases
449
+ : []),
450
+ ]
451
+ .map(item => String(item || '').trim().toLowerCase())
452
+ .filter(item => /^sha256:[0-9a-f]{64}$/.test(item))
453
+ )
454
+ );
455
+ const repoAliases = Array.from(
456
+ new Set(
457
+ [repo, ...(Array.isArray(value.repoAliases) ? value.repoAliases : [])]
458
+ .map(item => String(item || '').trim().toLowerCase())
459
+ .filter(item => /^sha256:[0-9a-f]{64}$/.test(item))
460
+ )
461
+ );
425
462
  if (
426
463
  !/^sha256:[0-9a-f]{64}$/i.test(repo) ||
427
464
  !isObjectId(baseCommit) ||
@@ -439,15 +476,17 @@ function normalizeSourceRevision(value) {
439
476
  ...value,
440
477
  repo,
441
478
  repositoryId: repo,
479
+ repoAliases,
442
480
  baseCommit,
443
481
  treeHash,
444
482
  remoteName,
445
483
  remoteUrlHash,
484
+ remoteUrlHashAliases,
446
485
  mainBranch,
447
486
  };
448
487
  }
449
488
 
450
- function readRemoteUrlHash(cwd, remoteName) {
489
+ function readRemoteUrlHashes(cwd, remoteName) {
451
490
  const result = readGit(cwd, ['remote', 'get-url', remoteName]);
452
491
  if (!result.stdout) {
453
492
  throw releaseMainlineError(
@@ -455,24 +494,7 @@ function readRemoteUrlHash(cwd, remoteName) {
455
494
  `无法读取权威远端地址: ${remoteName}`
456
495
  );
457
496
  }
458
- const identity = normalizeRemoteUrlIdentity(result.stdout);
459
- return `sha256:${crypto.createHash('sha256').update(identity).digest('hex')}`;
460
- }
461
-
462
- function normalizeRemoteUrlIdentity(value) {
463
- const remote = String(value || '').trim();
464
- try {
465
- const url = new URL(remote);
466
- url.username = '';
467
- url.password = '';
468
- url.search = '';
469
- url.hash = '';
470
- return url.toString();
471
- } catch {
472
- return remote
473
- .replace(/^[^@/]+@([^:]+):/, '$1:')
474
- .replace(/[?#].*$/, '');
475
- }
497
+ return remoteUrlHashes(result.stdout);
476
498
  }
477
499
 
478
500
  function assertSafeDefaultBranch(remoteName, branchName) {
@@ -0,0 +1,294 @@
1
+ const crypto = require('crypto');
2
+
3
+ function uniqueSorted(values = []) {
4
+ return Array.from(
5
+ new Set(
6
+ values
7
+ .map(value => String(value || '').trim())
8
+ .filter(Boolean)
9
+ )
10
+ ).sort();
11
+ }
12
+
13
+ function normalizeReleaseTargets(targets = {}, runtimeMode = 'legacy') {
14
+ const logicalTargets = {
15
+ forms: uniqueSorted(targets.forms || []),
16
+ pages: uniqueSorted(targets.pages || []),
17
+ functions: uniqueSorted(targets.functions || []),
18
+ automations: uniqueSorted(targets.automations || []),
19
+ workflows: uniqueSorted(targets.workflows || []),
20
+ jsCodeNodes: uniqueSorted(targets.jsCodeNodes || []),
21
+ resources: Boolean(targets.resources),
22
+ runtime: Boolean(targets.runtime),
23
+ other: uniqueSorted(targets.other || []),
24
+ };
25
+ const activationTargets = {
26
+ ...logicalTargets,
27
+ pages:
28
+ runtimeMode === 'react-spa'
29
+ ? []
30
+ : logicalTargets.pages,
31
+ runtime:
32
+ runtimeMode === 'react-spa'
33
+ ? logicalTargets.runtime || logicalTargets.pages.length > 0
34
+ : logicalTargets.runtime,
35
+ };
36
+ return { logicalTargets, activationTargets };
37
+ }
38
+
39
+ function buildTargetedResourceReleaseCommands(
40
+ targets,
41
+ profileArg,
42
+ changeId
43
+ ) {
44
+ return buildTargetedResourceReleaseSteps(
45
+ targets,
46
+ profileArg,
47
+ changeId
48
+ ).map(step => step.command);
49
+ }
50
+
51
+ function commandFromArgs(args) {
52
+ return ['openxiangda', ...args]
53
+ .map(value => {
54
+ const text = String(value);
55
+ return /^[A-Za-z0-9_./,:@<>=+-]+$/.test(text)
56
+ ? text
57
+ : `'${text.replace(/'/g, `'\\''`)}'`;
58
+ })
59
+ .join(' ');
60
+ }
61
+
62
+ function createStep(id, args, options = {}) {
63
+ return {
64
+ id,
65
+ args,
66
+ command: commandFromArgs(args),
67
+ writes: options.writes !== false,
68
+ stagedKind: options.stagedKind || null,
69
+ };
70
+ }
71
+
72
+ function appendProfileAndChange(args, profileArg, changeId) {
73
+ args.push('--profile', profileArg);
74
+ if (changeId) args.push('--change', changeId);
75
+ return args;
76
+ }
77
+
78
+ function buildTargetedResourceReleaseSteps(
79
+ targets,
80
+ profileArg,
81
+ changeId
82
+ ) {
83
+ const steps = [];
84
+ const functions = targets.functions || [];
85
+ const automations = targets.automations || [];
86
+ if (functions.length > 0 || automations.length > 0) {
87
+ const resourceTypes = [
88
+ ...(functions.length > 0 ? ['function'] : []),
89
+ ...(automations.length > 0 ? ['automation'] : []),
90
+ ];
91
+ const qualify = resourceTypes.length > 1;
92
+ const selectors = [
93
+ ...functions.map(code => (qualify ? `function:${code}` : code)),
94
+ ...automations.map(code => (qualify ? `automation:${code}` : code)),
95
+ ];
96
+ steps.push(
97
+ createStep(
98
+ 'backend-stage',
99
+ appendProfileAndChange(
100
+ [
101
+ 'resource',
102
+ 'publish',
103
+ resourceTypes.join(','),
104
+ '--only',
105
+ selectors.join(','),
106
+ '--stage-only',
107
+ ],
108
+ profileArg,
109
+ changeId
110
+ ),
111
+ { stagedKind: 'BackendRelease' }
112
+ )
113
+ );
114
+ }
115
+ const workflows = targets.workflows || [];
116
+ if (workflows.length > 0) {
117
+ steps.push(
118
+ createStep(
119
+ 'workflow-publish',
120
+ appendProfileAndChange(
121
+ [
122
+ 'resource',
123
+ 'publish',
124
+ 'workflow',
125
+ '--only',
126
+ workflows.join(','),
127
+ ],
128
+ profileArg,
129
+ changeId
130
+ )
131
+ )
132
+ );
133
+ }
134
+ return steps;
135
+ }
136
+
137
+ function buildWorkspaceReleaseCommands(
138
+ targetsInput,
139
+ runtimeMode,
140
+ profile,
141
+ changeId
142
+ ) {
143
+ return buildWorkspaceReleaseSteps(
144
+ targetsInput,
145
+ runtimeMode,
146
+ profile,
147
+ changeId
148
+ ).map(step => step.command);
149
+ }
150
+
151
+ function buildWorkspaceReleaseSteps(
152
+ targetsInput,
153
+ runtimeMode,
154
+ profile,
155
+ changeId
156
+ ) {
157
+ const profileArg = profile || '<profile>';
158
+ const { activationTargets: targets } = normalizeReleaseTargets(
159
+ targetsInput,
160
+ runtimeMode
161
+ );
162
+ const steps = [];
163
+ const hasResourceTargets =
164
+ Boolean(targets.resources) ||
165
+ targets.functions.length > 0 ||
166
+ targets.automations.length > 0 ||
167
+ targets.workflows.length > 0 ||
168
+ targets.jsCodeNodes.length > 0;
169
+ if (runtimeMode === 'react-spa') {
170
+ if (targets.forms.length > 0) {
171
+ steps.push(
172
+ createStep(
173
+ 'form-stage',
174
+ appendProfileAndChange(
175
+ [
176
+ 'resource',
177
+ 'publish',
178
+ 'form-setting',
179
+ '--only',
180
+ targets.forms.join(','),
181
+ ],
182
+ profileArg,
183
+ changeId
184
+ ),
185
+ { stagedKind: 'FormRelease' }
186
+ )
187
+ );
188
+ }
189
+ if (hasResourceTargets) {
190
+ steps.push(
191
+ ...buildTargetedResourceReleaseSteps(
192
+ targets,
193
+ profileArg,
194
+ changeId
195
+ )
196
+ );
197
+ }
198
+ if (targets.runtime) {
199
+ steps.push(
200
+ createStep(
201
+ 'runtime-stage',
202
+ appendProfileAndChange(
203
+ ['runtime', 'deploy', '--no-activate'],
204
+ profileArg,
205
+ changeId
206
+ ),
207
+ { stagedKind: 'RuntimeRelease' }
208
+ )
209
+ );
210
+ }
211
+ if (
212
+ targets.forms.length > 0 ||
213
+ targets.functions.length > 0 ||
214
+ targets.automations.length > 0 ||
215
+ targets.runtime
216
+ ) {
217
+ steps.push(
218
+ createStep(
219
+ 'app-finalize',
220
+ appendProfileAndChange(
221
+ [
222
+ 'release',
223
+ 'app-finalize',
224
+ '--staged-resources-json',
225
+ `.openxiangda/releases/${changeId || 'change'}/staged-resources.json`,
226
+ ],
227
+ profileArg,
228
+ changeId
229
+ )
230
+ )
231
+ );
232
+ }
233
+ return steps;
234
+ }
235
+
236
+ const onlyTargets = [
237
+ ...targets.forms.map(code => `forms/${code}`),
238
+ ...targets.pages.map(code => `pages/${code}`),
239
+ ];
240
+ if (onlyTargets.length > 0) {
241
+ steps.push(
242
+ createStep(
243
+ 'workspace-publish',
244
+ appendProfileAndChange(
245
+ [
246
+ 'workspace',
247
+ 'publish',
248
+ '--only',
249
+ onlyTargets.join(','),
250
+ '--skip-resources',
251
+ ],
252
+ profileArg,
253
+ changeId
254
+ )
255
+ )
256
+ );
257
+ }
258
+ if (hasResourceTargets) {
259
+ steps.push(
260
+ ...buildTargetedResourceReleaseSteps(targets, profileArg, changeId)
261
+ );
262
+ }
263
+ return steps;
264
+ }
265
+
266
+ function releasePlanHash(value) {
267
+ return crypto
268
+ .createHash('sha256')
269
+ .update(canonicalJson(value))
270
+ .digest('hex');
271
+ }
272
+
273
+ function canonicalJson(value) {
274
+ if (Array.isArray(value)) {
275
+ return `[${value.map(item => canonicalJson(item)).join(',')}]`;
276
+ }
277
+ if (value && typeof value === 'object') {
278
+ return `{${Object.keys(value)
279
+ .sort()
280
+ .map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
281
+ .join(',')}}`;
282
+ }
283
+ return JSON.stringify(value);
284
+ }
285
+
286
+ module.exports = {
287
+ buildTargetedResourceReleaseCommands,
288
+ buildTargetedResourceReleaseSteps,
289
+ buildWorkspaceReleaseCommands,
290
+ buildWorkspaceReleaseSteps,
291
+ commandFromArgs,
292
+ normalizeReleaseTargets,
293
+ releasePlanHash,
294
+ };