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.
- package/README.md +11 -3
- package/lib/cli.js +763 -140
- package/lib/form-field-contract.js +79 -0
- package/lib/integration-bundle.js +23 -2
- package/lib/publish-lease-heartbeat.js +1 -1
- package/lib/publish-lease.js +2 -2
- package/lib/release-error-classification.js +41 -0
- package/lib/sdd.js +131 -0
- package/lib/source-dependencies.js +190 -4
- package/lib/task-status.js +81 -18
- package/openxiangda-skills/SKILL.md +8 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +5 -1
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +2 -2
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +8 -0
- package/package.json +3 -1
- package/templates/openxiangda-react-spa/AGENTS.md +3 -2
- package/templates/sy-lowcode-app-workspace/AGENTS.md +3 -1
|
@@ -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 = '
|
|
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
|
|
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(),
|
package/lib/publish-lease.js
CHANGED
|
@@ -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 =
|
|
8
|
-
const PUBLISH_LEASE_RENEW_WINDOW_SECONDS =
|
|
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(
|
|
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
|
|
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)
|
|
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(
|
|
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
|
}
|