openxiangda 1.0.176 → 1.0.178

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,79 @@
1
+ const FORM_QUERY_SYSTEM_FIELDS = new Set([
2
+ 'approvalResult',
3
+ 'createTime',
4
+ 'currentApprovalNodeName',
5
+ 'modifiedTime',
6
+ 'originator',
7
+ 'originatorCorp',
8
+ 'originatorName',
9
+ 'processInstanceId',
10
+ 'processInstanceStatus',
11
+ 'processInstanceTitle',
12
+ ]);
13
+
14
+ function collectFormSnapshotFieldIds(snapshot) {
15
+ const fields = new Set();
16
+ const formFields = snapshot?.form?.formFields || {};
17
+ for (const [key, value] of Object.entries(formFields)) {
18
+ if (key) fields.add(String(key));
19
+ for (const candidate of [value?.fieldId, value?.field_id]) {
20
+ if (candidate) fields.add(String(candidate));
21
+ }
22
+ }
23
+ const visit = value => {
24
+ if (!value || typeof value !== 'object') return;
25
+ if (Array.isArray(value)) {
26
+ value.forEach(visit);
27
+ return;
28
+ }
29
+ const fieldId = value.fieldId || value.props?.fieldId;
30
+ if (fieldId) fields.add(String(fieldId));
31
+ Object.values(value).forEach(visit);
32
+ };
33
+ visit(snapshot?.form?.schema);
34
+ return fields;
35
+ }
36
+
37
+ function validateFormFieldContracts(targets, snapshots) {
38
+ const references = Array.isArray(targets) ? targets : [];
39
+ const snapshotFields =
40
+ snapshots instanceof Map ? snapshots : new Map(Object.entries(snapshots || {}));
41
+ const errors = [];
42
+ for (const item of references) {
43
+ const { code: resourceCode, ...detail } = item;
44
+ if (!item.formUuid) {
45
+ errors.push({
46
+ code: 'FORM_FIELD_CONTRACT_BINDING_MISSING',
47
+ resourceCode,
48
+ ...detail,
49
+ message: `${item.kind}:${resourceCode} ${item.file}:${item.line} 查询 ${item.formCode}.${item.field},但无法解析该表单的 profile 绑定`,
50
+ });
51
+ continue;
52
+ }
53
+ if (
54
+ FORM_QUERY_SYSTEM_FIELDS.has(item.field) ||
55
+ snapshotFields.get(item.formUuid)?.has(item.field)
56
+ ) {
57
+ continue;
58
+ }
59
+ errors.push({
60
+ code: 'FORM_FIELD_CONTRACT_FIELD_MISSING',
61
+ resourceCode,
62
+ ...detail,
63
+ message: `${item.kind}:${resourceCode} ${item.file}:${item.line} 通过 ${item.api} 查询 ${item.formCode}.${item.field},但线上 Form ${item.formUuid} 不存在该字段`,
64
+ });
65
+ }
66
+ return {
67
+ contractVersion: 'form_field_contract_v1',
68
+ valid: errors.length === 0,
69
+ checkedForms: new Set(references.map(item => item.formUuid).filter(Boolean)).size,
70
+ checkedReferences: references.length,
71
+ errors,
72
+ };
73
+ }
74
+
75
+ module.exports = {
76
+ FORM_QUERY_SYSTEM_FIELDS,
77
+ collectFormSnapshotFieldIds,
78
+ validateFormFieldContracts,
79
+ };
@@ -2,7 +2,8 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { spawnSync } = require('child_process');
4
4
 
5
- const TASK_RESULT_SCHEMA_VERSION = 'openxiangda_task_result_v1';
5
+ const TASK_RESULT_SCHEMA_VERSION = 'openxiangda_task_result_v2';
6
+ const LEGACY_TASK_RESULT_SCHEMA_VERSION = 'openxiangda_task_result_v1';
6
7
  const INTEGRATION_BUNDLE_SCHEMA_VERSION = 'openxiangda_integration_bundle_v1';
7
8
 
8
9
  function runGit(cwd, args, options = {}) {
@@ -77,6 +78,16 @@ function recordIntegrationTaskResult(options = {}) {
77
78
  commit,
78
79
  treeHash,
79
80
  branch,
81
+ ...(options.sourceBase?.baseCommit && options.sourceBase?.treeHash
82
+ ? {
83
+ sourceBase: {
84
+ repo: options.sourceBase.repo,
85
+ baseCommit: options.sourceBase.baseCommit,
86
+ treeHash: options.sourceBase.treeHash,
87
+ },
88
+ baseRevision: options.sourceBase.baseCommit,
89
+ }
90
+ : {}),
80
91
  targets: options.targets || {},
81
92
  changedFiles: Array.from(
82
93
  new Set((options.changedFiles || []).map(String).filter(Boolean))
@@ -96,7 +107,10 @@ function readIntegrationTaskResult(cwd, changeId) {
96
107
  if (!fs.existsSync(file)) return null;
97
108
  try {
98
109
  const value = JSON.parse(fs.readFileSync(file, 'utf8'));
99
- return value?.schemaVersion === TASK_RESULT_SCHEMA_VERSION
110
+ return [
111
+ TASK_RESULT_SCHEMA_VERSION,
112
+ LEGACY_TASK_RESULT_SCHEMA_VERSION,
113
+ ].includes(value?.schemaVersion)
100
114
  ? { ...value, file }
101
115
  : null;
102
116
  } catch {
@@ -146,6 +160,13 @@ function collectIntegrationTaskResults(options = {}) {
146
160
  commit: result.commit,
147
161
  treeHash: result.treeHash,
148
162
  sourceBranch: result.branch,
163
+ ...(result.sourceBase?.baseCommit && result.sourceBase?.treeHash
164
+ ? {
165
+ sourceBase: result.sourceBase,
166
+ baseRevision:
167
+ result.baseRevision || result.sourceBase.baseCommit,
168
+ }
169
+ : {}),
149
170
  })),
150
171
  verifiedAgainst: runGit(cwd, ['rev-parse', 'HEAD']).stdout,
151
172
  createdAt: new Date().toISOString(),
@@ -1,5 +1,5 @@
1
1
  const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
2
- const DEFAULT_RENEW_WINDOW_MS = 180_000;
2
+ const DEFAULT_RENEW_WINDOW_MS = 60_000;
3
3
 
4
4
  class PublishLeaseLostError extends Error {
5
5
  constructor(message, cause) {
@@ -4,8 +4,8 @@ const path = require('path');
4
4
 
5
5
  const { saveProjectState } = require('./config');
6
6
 
7
- const DEFAULT_CLI_PUBLISH_LEASE_TTL_SECONDS = 1800;
8
- const PUBLISH_LEASE_RENEW_WINDOW_SECONDS = 180;
7
+ const DEFAULT_CLI_PUBLISH_LEASE_TTL_SECONDS = 120;
8
+ const PUBLISH_LEASE_RENEW_WINDOW_SECONDS = 60;
9
9
 
10
10
  function getStoredPublishLease(target) {
11
11
  const lease = target?.bound?.promotion?.publishLease;
@@ -0,0 +1,41 @@
1
+ const DEFINITELY_PRE_WRITE_CODES = new Set([
2
+ 'APP_RELEASE_STAGED_SCOPE_MISMATCH',
3
+ 'APP_RELEASE_STAGED_SCOPE_REQUIRED',
4
+ 'BASELINE_SCOPE_MISSING',
5
+ 'FORM_FIELD_CONTRACT_INVALID',
6
+ 'PUBLISH_CONTEXT_REQUIRED',
7
+ 'RELEASE_GIT_AUTH_REQUIRED',
8
+ 'RELEASE_MAIN_BRANCH_UNRESOLVED',
9
+ 'RELEASE_MAIN_REF_UNVERIFIED',
10
+ 'RELEASE_PUBLISH_REVISION_CHANGED',
11
+ 'RELEASE_SOURCE_BEHIND_MAIN',
12
+ 'RELEASE_SOURCE_BRANCH_REQUIRED',
13
+ 'RELEASE_SOURCE_DIRTY',
14
+ 'RELEASE_SOURCE_MAINLINE_NOT_PUSHED',
15
+ 'RELEASE_SOURCE_MAINLINE_REQUIRED',
16
+ 'RESOURCE_FIELD_CONFLICT',
17
+ 'SDD_PREPUBLISH_FAILED',
18
+ 'SOURCE_BASE_DIVERGED',
19
+ ]);
20
+
21
+ function isDefinitelyPreWriteReleaseError(error) {
22
+ const code = String(error?.code || '').trim();
23
+ if (DEFINITELY_PRE_WRITE_CODES.has(code)) return true;
24
+ return /^(?:SDD_|RELEASE_(?:SOURCE|GIT|MAINLINE|MAIN_BRANCH|MAIN_REF)|RUNTIME_(?:SOURCE|BUILD|PACKAGE|DEPENDENCY))/.test(
25
+ code
26
+ );
27
+ }
28
+
29
+ function classifyReleaseExecutionFailure({
30
+ stagedWriteOccurred = false,
31
+ writeAttempted = false,
32
+ } = {}) {
33
+ if (stagedWriteOccurred) return 'staged-resumable';
34
+ if (writeAttempted) return 'write-review-required';
35
+ return 'failed';
36
+ }
37
+
38
+ module.exports = {
39
+ classifyReleaseExecutionFailure,
40
+ isDefinitelyPreWriteReleaseError,
41
+ };
package/lib/sdd.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
+ const { spawnSync } = require('child_process');
4
5
  const { readGitSourceBase } = require('./change-baseline');
5
6
  const { assertReleaseSourceIntegrated } = require('./release-mainline');
6
7
  const {
@@ -604,6 +605,7 @@ function createChangeMetadata(changeId, options = {}) {
604
605
  baseCommit: sourceBase.baseCommit,
605
606
  treeHash: sourceBase.treeHash,
606
607
  };
608
+ metadata.baseRevision = sourceBase.baseCommit;
607
609
  }
608
610
  } catch {
609
611
  // SDD can still be used before a workspace is initialized as a Git repo.
@@ -1013,6 +1015,30 @@ function createMainlineSddBundle(options = {}) {
1013
1015
  if (nested) {
1014
1016
  throw new Error(`mainline bundle 不允许嵌套: ${nested.changeId}`);
1015
1017
  }
1018
+ const bundleSourceBase = resolveMainlineBundleSourceBase(
1019
+ sourceChanges,
1020
+ options
1021
+ );
1022
+ const integratedChanges = sourceChanges.map(item => {
1023
+ const sourceBase =
1024
+ normalizeSddSourceBase(item.meta.sourceBase) || bundleSourceBase;
1025
+ const task = options.integration?.requiredCommits?.find(
1026
+ candidate => candidate.changeId === item.changeId
1027
+ );
1028
+ return {
1029
+ changeId: item.changeId,
1030
+ sourceBase,
1031
+ baseRevision:
1032
+ item.meta.baseRevision || sourceBase.baseCommit,
1033
+ ...(task
1034
+ ? {
1035
+ commit: task.commit,
1036
+ treeHash: task.treeHash,
1037
+ sourceBranch: task.sourceBranch,
1038
+ }
1039
+ : {}),
1040
+ };
1041
+ });
1016
1042
 
1017
1043
  const affected = mergeAffectedScopes(
1018
1044
  ...sourceChanges.flatMap(item => [
@@ -1041,16 +1067,19 @@ function createMainlineSddBundle(options = {}) {
1041
1067
  const createdAt = nowIso();
1042
1068
  const meta = {
1043
1069
  ...createChangeMetadata(changeId, {
1070
+ cwd: options.cwd,
1044
1071
  title: options.title || `Mainline release: ${sourceIds.join(', ')}`,
1045
1072
  affected,
1046
1073
  changeMode: 'mainline-bundle',
1047
1074
  riskLevel: 'medium',
1075
+ sourceBase: bundleSourceBase,
1048
1076
  }),
1049
1077
  status: 'approved',
1050
1078
  approvedAt: createdAt,
1051
1079
  approvedBy: 'mainline-release-coordinator',
1052
1080
  approvalSummary: `Aggregate approved changes on authoritative mainline: ${sourceIds.join(', ')}`,
1053
1081
  bundledChanges: sourceIds,
1082
+ integratedChanges,
1054
1083
  createdAt,
1055
1084
  updatedAt: createdAt,
1056
1085
  };
@@ -1083,6 +1112,9 @@ function createMainlineSddBundle(options = {}) {
1083
1112
  changeId,
1084
1113
  mainlinePolicy: 'publish-from-main-v1',
1085
1114
  bundledChanges: sourceIds,
1115
+ sourceBase: bundleSourceBase,
1116
+ baseRevision: bundleSourceBase.baseCommit,
1117
+ integratedChanges,
1086
1118
  ...(options.integration ? { integration: options.integration } : {}),
1087
1119
  targets: normalizedTargets.activationTargets,
1088
1120
  logicalTargets: normalizedTargets.logicalTargets,
@@ -1092,6 +1124,8 @@ function createMainlineSddBundle(options = {}) {
1092
1124
  `openxiangda release publish --change ${changeId} --profile ${profile}`,
1093
1125
  planHash: releasePlanHash({
1094
1126
  changeId,
1127
+ baseRevision: bundleSourceBase.baseCommit,
1128
+ integratedChanges,
1095
1129
  runtimeMode,
1096
1130
  targets: normalizedTargets.activationTargets,
1097
1131
  commands,
@@ -1137,6 +1171,103 @@ function createMainlineSddBundle(options = {}) {
1137
1171
  };
1138
1172
  }
1139
1173
 
1174
+ function normalizeSddSourceBase(value) {
1175
+ if (!value?.baseCommit || !value?.treeHash) return null;
1176
+ return {
1177
+ repo: value.repo,
1178
+ baseCommit: String(value.baseCommit),
1179
+ treeHash: String(value.treeHash),
1180
+ };
1181
+ }
1182
+
1183
+ function runSddGit(cwd, args, options = {}) {
1184
+ const result = spawnSync('git', args, {
1185
+ cwd: cwd || process.cwd(),
1186
+ encoding: 'utf8',
1187
+ stdio: ['ignore', 'pipe', 'pipe'],
1188
+ });
1189
+ if (result.status !== 0 && !options.allowFailure) {
1190
+ const error = new Error(
1191
+ String(result.stderr || result.stdout || `git ${args.join(' ')} failed`).trim()
1192
+ );
1193
+ error.code = 'SDD_SOURCE_BASE_GIT_FAILED';
1194
+ throw error;
1195
+ }
1196
+ return {
1197
+ ok: result.status === 0,
1198
+ stdout: String(result.stdout || '').trim(),
1199
+ };
1200
+ }
1201
+
1202
+ function resolveMainlineBundleSourceBase(sourceChanges, options = {}) {
1203
+ const cwd = options.cwd || process.cwd();
1204
+ if (options.sourceBaseRef) {
1205
+ return readGitSourceBase(cwd, options.sourceBaseRef);
1206
+ }
1207
+ const missing = sourceChanges.filter(
1208
+ item => !normalizeSddSourceBase(item.meta.sourceBase)
1209
+ );
1210
+ if (missing.length > 0) {
1211
+ const error = new Error(
1212
+ `SDD_SOURCE_BASE_REQUIRED: ${missing
1213
+ .map(item => item.changeId)
1214
+ .join(', ')} 缺少创建变更时冻结的 sourceBase;请用 --source-base-ref <commit> 明确指定共同基线`
1215
+ );
1216
+ error.code = 'SDD_SOURCE_BASE_REQUIRED';
1217
+ error.changes = missing.map(item => item.changeId);
1218
+ throw error;
1219
+ }
1220
+ const sourceBases = sourceChanges.map(item =>
1221
+ normalizeSddSourceBase(item.meta.sourceBase)
1222
+ );
1223
+ const repos = Array.from(
1224
+ new Set(sourceBases.map(item => item.repo).filter(Boolean))
1225
+ );
1226
+ if (repos.length > 1) {
1227
+ const error = new Error(
1228
+ 'SDD_SOURCE_BASE_REPOSITORY_MISMATCH: mainline bundle 的来源变更不属于同一 Git 仓库'
1229
+ );
1230
+ error.code = 'SDD_SOURCE_BASE_REPOSITORY_MISMATCH';
1231
+ throw error;
1232
+ }
1233
+ for (const item of sourceChanges) {
1234
+ const task = options.integration?.requiredCommits?.find(
1235
+ candidate => candidate.changeId === item.changeId
1236
+ );
1237
+ if (
1238
+ task?.commit &&
1239
+ !runSddGit(cwd, [
1240
+ 'merge-base',
1241
+ '--is-ancestor',
1242
+ item.meta.sourceBase.baseCommit,
1243
+ task.commit,
1244
+ ], { allowFailure: true }).ok
1245
+ ) {
1246
+ const error = new Error(
1247
+ `SDD_SOURCE_BASE_NOT_ANCESTOR: ${item.changeId} 的 sourceBase 不是任务提交 ${String(task.commit).slice(0, 12)} 的祖先`
1248
+ );
1249
+ error.code = 'SDD_SOURCE_BASE_NOT_ANCESTOR';
1250
+ error.changeId = item.changeId;
1251
+ throw error;
1252
+ }
1253
+ }
1254
+ const commits = Array.from(
1255
+ new Set(sourceBases.map(item => item.baseCommit))
1256
+ );
1257
+ const commonCommit =
1258
+ commits.length === 1
1259
+ ? commits[0]
1260
+ : runSddGit(cwd, ['merge-base', '--octopus', ...commits]).stdout;
1261
+ if (!commonCommit) {
1262
+ const error = new Error(
1263
+ 'SDD_SOURCE_BASE_COMMON_ANCESTOR_REQUIRED: 来源变更没有可验证的共同 Git 基线'
1264
+ );
1265
+ error.code = 'SDD_SOURCE_BASE_COMMON_ANCESTOR_REQUIRED';
1266
+ throw error;
1267
+ }
1268
+ return readGitSourceBase(cwd, commonCommit);
1269
+ }
1270
+
1140
1271
  function loadSddChange(options = {}) {
1141
1272
  const governance = getSddGovernanceConfig(options);
1142
1273
  const changeId = normalizeChangeId(options.changeId);
@@ -245,6 +245,122 @@ function readStaticObjectProperty(expression, propertyNames, bindings) {
245
245
  return null;
246
246
  }
247
247
 
248
+ function resolveStaticExpression(expression, bindings, seen = new Set()) {
249
+ const value = unwrapExpression(expression);
250
+ if (!value || !ts.isIdentifier(value)) return value;
251
+ if (seen.has(value.text)) return value;
252
+ const initializer = bindings.get(value.text);
253
+ if (!initializer) return value;
254
+ const nextSeen = new Set(seen);
255
+ nextSeen.add(value.text);
256
+ return resolveStaticExpression(initializer, bindings, nextSeen);
257
+ }
258
+
259
+ function readStaticObjectPropertyExpression(
260
+ expression,
261
+ propertyNames,
262
+ bindings
263
+ ) {
264
+ const value = resolveStaticExpression(expression, bindings);
265
+ if (!value || !ts.isObjectLiteralExpression(value)) return null;
266
+ for (const property of value.properties) {
267
+ if (!ts.isPropertyAssignment(property)) continue;
268
+ if (!propertyNames.includes(propertyNameText(property.name))) continue;
269
+ return property.initializer;
270
+ }
271
+ return null;
272
+ }
273
+
274
+ function collectStaticQueryFieldNodes(
275
+ expression,
276
+ bindings,
277
+ result,
278
+ fieldPropertyNames,
279
+ seen = new Set()
280
+ ) {
281
+ const value = resolveStaticExpression(expression, bindings, seen);
282
+ if (!value || seen.has(value)) return;
283
+ seen.add(value);
284
+ if (ts.isArrayLiteralExpression(value)) {
285
+ for (const element of value.elements) {
286
+ collectStaticQueryFieldNodes(
287
+ element,
288
+ bindings,
289
+ result,
290
+ fieldPropertyNames,
291
+ seen
292
+ );
293
+ }
294
+ return;
295
+ }
296
+ if (!ts.isObjectLiteralExpression(value)) return;
297
+ for (const property of value.properties) {
298
+ if (ts.isSpreadAssignment(property)) {
299
+ collectStaticQueryFieldNodes(
300
+ property.expression,
301
+ bindings,
302
+ result,
303
+ fieldPropertyNames,
304
+ seen
305
+ );
306
+ continue;
307
+ }
308
+ if (!ts.isPropertyAssignment(property)) continue;
309
+ const name = propertyNameText(property.name);
310
+ if (fieldPropertyNames.has(name)) {
311
+ const field = readStaticString(property.initializer, bindings);
312
+ if (field) result.push({ field, node: property.initializer });
313
+ }
314
+ collectStaticQueryFieldNodes(
315
+ property.initializer,
316
+ bindings,
317
+ result,
318
+ fieldPropertyNames,
319
+ seen
320
+ );
321
+ }
322
+ }
323
+
324
+ function addFormFieldReferences(
325
+ formFieldReferences,
326
+ sourceFile,
327
+ workspaceRoot,
328
+ bindings,
329
+ formCode,
330
+ inputExpression,
331
+ api
332
+ ) {
333
+ if (!formCode || !inputExpression) return;
334
+ const fields = [];
335
+ for (const propertyName of ['filters', 'order']) {
336
+ const expression = readStaticObjectPropertyExpression(
337
+ inputExpression,
338
+ [propertyName],
339
+ bindings
340
+ );
341
+ if (expression) {
342
+ collectStaticQueryFieldNodes(
343
+ expression,
344
+ bindings,
345
+ fields,
346
+ new Set(
347
+ propertyName === 'order'
348
+ ? ['id', 'key', 'field', 'fieldId']
349
+ : ['key', 'field', 'fieldId']
350
+ )
351
+ );
352
+ }
353
+ }
354
+ for (const item of fields) {
355
+ formFieldReferences.push({
356
+ formCode,
357
+ field: item.field,
358
+ api,
359
+ ...sourceLocation(sourceFile, item.node, workspaceRoot),
360
+ });
361
+ }
362
+ }
363
+
248
364
  function sourceLocation(sourceFile, node, workspaceRoot) {
249
365
  const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
250
366
  return {
@@ -274,10 +390,46 @@ function addDynamicReferenceWarning(warnings, sourceFile, node, workspaceRoot, a
274
390
  });
275
391
  }
276
392
 
277
- function inspectResourceCall(node, sourceFile, workspaceRoot, bindings, references, warnings) {
393
+ function inspectResourceCall(
394
+ node,
395
+ sourceFile,
396
+ workspaceRoot,
397
+ bindings,
398
+ references,
399
+ formFieldReferences,
400
+ warnings
401
+ ) {
278
402
  if (!ts.isCallExpression(node)) return;
279
403
  const callee = getPropertyPath(node.expression, bindings);
280
- if (!callee || callee[0] !== 'ctx' || callee.length < 3) return;
404
+ if (!callee) return;
405
+ if (
406
+ callee.length === 1 &&
407
+ callee[0] === 'queryAdvancedFormPage'
408
+ ) {
409
+ const formCode = readStaticString(node.arguments[1], bindings);
410
+ if (formCode) {
411
+ addReference(
412
+ references,
413
+ sourceFile,
414
+ node,
415
+ workspaceRoot,
416
+ 'forms',
417
+ formCode,
418
+ 'queryAdvancedFormPage'
419
+ );
420
+ addFormFieldReferences(
421
+ formFieldReferences,
422
+ sourceFile,
423
+ workspaceRoot,
424
+ bindings,
425
+ formCode,
426
+ node.arguments[2],
427
+ 'queryAdvancedFormPage'
428
+ );
429
+ }
430
+ return;
431
+ }
432
+ if (callee[0] !== 'ctx' || callee.length < 3) return;
281
433
  const namespace = callee[1];
282
434
  const method = callee[2];
283
435
  const api = callee.join('.');
@@ -304,7 +456,22 @@ function inspectResourceCall(node, sourceFile, workspaceRoot, bindings, referenc
304
456
  if (namespace === 'form') {
305
457
  const code = readStaticObjectProperty(firstArgument, ['formCode', 'code'], bindings) ||
306
458
  readStaticString(firstArgument, bindings);
307
- if (code) addReference(references, sourceFile, node, workspaceRoot, 'forms', code, api);
459
+ if (code) {
460
+ addReference(references, sourceFile, node, workspaceRoot, 'forms', code, api);
461
+ addFormFieldReferences(
462
+ formFieldReferences,
463
+ sourceFile,
464
+ workspaceRoot,
465
+ bindings,
466
+ code,
467
+ ts.isObjectLiteralExpression(
468
+ resolveStaticExpression(firstArgument, bindings)
469
+ )
470
+ ? firstArgument
471
+ : node.arguments[1],
472
+ api
473
+ );
474
+ }
308
475
  else if (firstArgument) addDynamicReferenceWarning(warnings, sourceFile, node, workspaceRoot, api, 'forms');
309
476
  return;
310
477
  }
@@ -396,6 +563,7 @@ function analyzeTypeScriptEntry(options = {}) {
396
563
  const visited = new Set();
397
564
  const sourceDependencies = new Set();
398
565
  const references = [];
566
+ const formFieldReferences = [];
399
567
  const warnings = [];
400
568
 
401
569
  while (queue.length > 0) {
@@ -439,7 +607,15 @@ function analyzeTypeScriptEntry(options = {}) {
439
607
 
440
608
  const bindings = collectConstInitializers(sourceFile);
441
609
  const inspect = node => {
442
- inspectResourceCall(node, sourceFile, workspaceRoot, bindings, references, warnings);
610
+ inspectResourceCall(
611
+ node,
612
+ sourceFile,
613
+ workspaceRoot,
614
+ bindings,
615
+ references,
616
+ formFieldReferences,
617
+ warnings
618
+ );
443
619
  ts.forEachChild(node, inspect);
444
620
  };
445
621
  inspect(sourceFile);
@@ -486,6 +662,10 @@ function analyzeTypeScriptEntry(options = {}) {
486
662
  sourceDependencies: Array.from(sourceDependencies).sort(),
487
663
  resourceReferences,
488
664
  resourceReferenceDetails,
665
+ formFieldReferences: dedupeDetails(
666
+ formFieldReferences,
667
+ ['formCode', 'field', 'file', 'line', 'column', 'api']
668
+ ),
489
669
  warnings: dedupeDetails(warnings, ['code', 'file', 'line', 'column', 'api', 'message']),
490
670
  };
491
671
  }
@@ -610,11 +790,16 @@ function mergeAnalyses(workspaceRoot, entries) {
610
790
  ).sort(),
611
791
  ])
612
792
  );
793
+ const formFieldReferences = dedupeDetails(
794
+ analyses.flatMap(analysis => analysis.formFieldReferences || []),
795
+ ['formCode', 'field', 'file', 'line', 'column', 'api']
796
+ );
613
797
  return {
614
798
  entryFiles: entries.map(entry => workspaceRelativePath(workspaceRoot, entry)).sort(),
615
799
  sourceDependencies,
616
800
  resourceReferences,
617
801
  resourceReferenceDetails,
802
+ formFieldReferences,
618
803
  warnings: dedupeDetails(
619
804
  analyses.flatMap(analysis => analysis.warnings),
620
805
  ['code', 'file', 'line', 'column', 'api', 'message']
@@ -712,6 +897,7 @@ function validateManifestSourceBindings(options = {}) {
712
897
  entryFiles: target.entryFiles,
713
898
  sourceDependencies: target.sourceDependencies,
714
899
  resourceReferences: target.resourceReferences,
900
+ formFieldReferences: target.formFieldReferences,
715
901
  })),
716
902
  };
717
903
  }