openxiangda 1.0.159 → 1.0.161
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 +39 -13
- package/lib/change-baseline.js +1077 -0
- package/lib/cli.js +12002 -6723
- package/lib/config.js +272 -7
- package/lib/http.js +2 -0
- package/lib/js-code-build.js +61 -0
- package/lib/publish-context.js +98 -0
- package/lib/publish-lease-heartbeat.js +182 -0
- package/lib/publish-lease.js +190 -0
- package/lib/release-telemetry.js +182 -0
- package/lib/runtime-lineage.js +297 -0
- package/lib/sdd.js +567 -35
- package/lib/source-dependencies.js +742 -0
- package/lib/workspace-init.js +6 -1
- package/lib/worktree-owner.js +219 -0
- package/openxiangda-skills/SKILL.md +103 -228
- package/openxiangda-skills/references/architecture-design.md +2 -1
- package/openxiangda-skills/references/openxiangda-api.md +27 -0
- package/openxiangda-skills/references/pages/page-sdk.md +11 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +12 -2
- package/openxiangda-skills/references/workspace-state.md +2 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +77 -243
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +8 -0
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +18 -0
- package/package.json +20 -1
- package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.mts → ProcessPreview-Dux73lY_.d.mts} +5 -0
- package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.ts → ProcessPreview-Dux73lY_.d.ts} +5 -0
- package/packages/sdk/dist/components/index.cjs +29 -1
- package/packages/sdk/dist/components/index.cjs.map +1 -1
- package/packages/sdk/dist/components/index.d.mts +4 -4
- package/packages/sdk/dist/components/index.d.ts +4 -4
- package/packages/sdk/dist/components/index.mjs +29 -1
- package/packages/sdk/dist/components/index.mjs.map +1 -1
- package/packages/sdk/dist/{dataManagementApi-C9O-Bb0j.d.ts → dataManagementApi-BNLH2o7f.d.ts} +2 -1
- package/packages/sdk/dist/{dataManagementApi-gpZkgRDM.d.mts → dataManagementApi-BOcIx4U-.d.mts} +2 -1
- package/packages/sdk/dist/runtime/index.cjs +150 -22
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +3 -3
- package/packages/sdk/dist/runtime/index.d.ts +3 -3
- package/packages/sdk/dist/runtime/index.mjs +150 -22
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +121 -21
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +15 -2
- package/packages/sdk/dist/runtime/react.d.ts +15 -2
- package/packages/sdk/dist/runtime/react.mjs +121 -21
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/packages/sdk/src/build-source/scripts/register.mjs +54 -23
- package/packages/sdk/src/build-source/scripts/sync-schema.mjs +59 -19
- package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +242 -2
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +7 -3
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +11 -4
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +7 -3
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +10 -4
- package/templates/openxiangda-react-spa/AGENTS.md +25 -12
- package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +344 -60
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +5 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +8 -2
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +8 -4
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +8 -2
- package/templates/sy-lowcode-app-workspace/AGENTS.md +14 -4
- package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +344 -60
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const ts = require('typescript');
|
|
4
|
+
|
|
5
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
6
|
+
'.ts',
|
|
7
|
+
'.tsx',
|
|
8
|
+
'.mts',
|
|
9
|
+
'.cts',
|
|
10
|
+
'.js',
|
|
11
|
+
'.jsx',
|
|
12
|
+
'.mjs',
|
|
13
|
+
'.cjs',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const LOCAL_MODULE_EXTENSIONS = [
|
|
17
|
+
'',
|
|
18
|
+
'.ts',
|
|
19
|
+
'.tsx',
|
|
20
|
+
'.mts',
|
|
21
|
+
'.cts',
|
|
22
|
+
'.js',
|
|
23
|
+
'.jsx',
|
|
24
|
+
'.mjs',
|
|
25
|
+
'.cjs',
|
|
26
|
+
'.json',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const RESOURCE_BUCKETS = [
|
|
30
|
+
'forms',
|
|
31
|
+
'dataViews',
|
|
32
|
+
'connectors',
|
|
33
|
+
'notifications',
|
|
34
|
+
'functions',
|
|
35
|
+
'storageConfigs',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const RESOURCE_BUCKET_ALIASES = new Map([
|
|
39
|
+
['forms', 'forms'],
|
|
40
|
+
['form', 'forms'],
|
|
41
|
+
['dataviews', 'dataViews'],
|
|
42
|
+
['data-views', 'dataViews'],
|
|
43
|
+
['dataview', 'dataViews'],
|
|
44
|
+
['connectors', 'connectors'],
|
|
45
|
+
['connector', 'connectors'],
|
|
46
|
+
['notifications', 'notifications'],
|
|
47
|
+
['notification', 'notifications'],
|
|
48
|
+
['notificationtypes', 'notifications'],
|
|
49
|
+
['notification-types', 'notifications'],
|
|
50
|
+
['functions', 'functions'],
|
|
51
|
+
['function', 'functions'],
|
|
52
|
+
['storageconfigs', 'storageConfigs'],
|
|
53
|
+
['storage-configs', 'storageConfigs'],
|
|
54
|
+
['storage', 'storageConfigs'],
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const TARGET_MANIFEST_KEYS = [
|
|
58
|
+
['functions', 'functions'],
|
|
59
|
+
['automations', 'automations'],
|
|
60
|
+
['workflows', 'workflows'],
|
|
61
|
+
['jsCodeNodes', 'jsCodeNodes'],
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function normalizeWorkspacePath(value) {
|
|
65
|
+
return String(value || '')
|
|
66
|
+
.split(path.sep)
|
|
67
|
+
.join('/')
|
|
68
|
+
.replace(/^\.\//, '');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isPathInside(root, target) {
|
|
72
|
+
const relative = path.relative(root, target);
|
|
73
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function workspaceRelativePath(workspaceRoot, filePath) {
|
|
77
|
+
return normalizeWorkspacePath(path.relative(workspaceRoot, filePath));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getScriptKind(filePath) {
|
|
81
|
+
switch (path.extname(filePath).toLowerCase()) {
|
|
82
|
+
case '.tsx':
|
|
83
|
+
return ts.ScriptKind.TSX;
|
|
84
|
+
case '.jsx':
|
|
85
|
+
return ts.ScriptKind.JSX;
|
|
86
|
+
case '.js':
|
|
87
|
+
case '.mjs':
|
|
88
|
+
case '.cjs':
|
|
89
|
+
return ts.ScriptKind.JS;
|
|
90
|
+
default:
|
|
91
|
+
return ts.ScriptKind.TS;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function loadCompilerOptions(workspaceRoot) {
|
|
96
|
+
const configPath = ts.findConfigFile(workspaceRoot, ts.sys.fileExists, 'tsconfig.json');
|
|
97
|
+
if (!configPath) {
|
|
98
|
+
return {
|
|
99
|
+
allowJs: true,
|
|
100
|
+
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
101
|
+
resolveJsonModule: true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const config = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
105
|
+
if (config.error) return { allowJs: true, resolveJsonModule: true };
|
|
106
|
+
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(configPath));
|
|
107
|
+
return {
|
|
108
|
+
...parsed.options,
|
|
109
|
+
allowJs: true,
|
|
110
|
+
resolveJsonModule: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveFileCandidate(basePath) {
|
|
115
|
+
for (const extension of LOCAL_MODULE_EXTENSIONS) {
|
|
116
|
+
const candidate = `${basePath}${extension}`;
|
|
117
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return path.resolve(candidate);
|
|
118
|
+
}
|
|
119
|
+
for (const extension of LOCAL_MODULE_EXTENSIONS.slice(1)) {
|
|
120
|
+
const candidate = path.join(basePath, `index${extension}`);
|
|
121
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return path.resolve(candidate);
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveLocalModule(workspaceRoot, containingFile, specifier, compilerOptions) {
|
|
127
|
+
const value = String(specifier || '').trim();
|
|
128
|
+
if (!value) return null;
|
|
129
|
+
const localLooking = value.startsWith('.') || value.startsWith('@/') || value.startsWith('~/');
|
|
130
|
+
const resolved = ts.resolveModuleName(value, containingFile, compilerOptions, ts.sys).resolvedModule;
|
|
131
|
+
if (resolved?.resolvedFileName) {
|
|
132
|
+
const filePath = path.resolve(resolved.resolvedFileName);
|
|
133
|
+
if (isPathInside(workspaceRoot, filePath) && !filePath.includes(`${path.sep}node_modules${path.sep}`)) {
|
|
134
|
+
return filePath;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!localLooking) return null;
|
|
138
|
+
const fallbackBase = value.startsWith('@/') || value.startsWith('~/')
|
|
139
|
+
? path.join(workspaceRoot, 'src', value.slice(2))
|
|
140
|
+
: path.resolve(path.dirname(containingFile), value);
|
|
141
|
+
return resolveFileCandidate(fallbackBase);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function unwrapExpression(expression) {
|
|
145
|
+
let current = expression;
|
|
146
|
+
while (
|
|
147
|
+
current &&
|
|
148
|
+
(ts.isParenthesizedExpression(current) ||
|
|
149
|
+
ts.isAsExpression(current) ||
|
|
150
|
+
ts.isTypeAssertionExpression(current) ||
|
|
151
|
+
ts.isNonNullExpression(current) ||
|
|
152
|
+
ts.isSatisfiesExpression?.(current))
|
|
153
|
+
) {
|
|
154
|
+
current = current.expression;
|
|
155
|
+
}
|
|
156
|
+
return current;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function propertyNameText(name) {
|
|
160
|
+
if (!name) return '';
|
|
161
|
+
if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) return name.text;
|
|
162
|
+
if (ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)) return String(name.text);
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function collectConstInitializers(sourceFile) {
|
|
167
|
+
const bindings = new Map();
|
|
168
|
+
const visit = node => {
|
|
169
|
+
if (
|
|
170
|
+
ts.isVariableStatement(node) &&
|
|
171
|
+
(node.declarationList.flags & ts.NodeFlags.Const) !== 0
|
|
172
|
+
) {
|
|
173
|
+
for (const declaration of node.declarationList.declarations) {
|
|
174
|
+
if (ts.isIdentifier(declaration.name) && declaration.initializer) {
|
|
175
|
+
bindings.set(declaration.name.text, declaration.initializer);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
ts.forEachChild(node, visit);
|
|
180
|
+
};
|
|
181
|
+
visit(sourceFile);
|
|
182
|
+
return bindings;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function readStaticString(expression, bindings, seen = new Set()) {
|
|
186
|
+
const value = unwrapExpression(expression);
|
|
187
|
+
if (!value) return null;
|
|
188
|
+
if (ts.isStringLiteralLike(value) || ts.isNoSubstitutionTemplateLiteral(value)) {
|
|
189
|
+
return value.text;
|
|
190
|
+
}
|
|
191
|
+
if (ts.isIdentifier(value)) {
|
|
192
|
+
if (seen.has(value.text)) return null;
|
|
193
|
+
const initializer = bindings.get(value.text);
|
|
194
|
+
if (!initializer) return null;
|
|
195
|
+
const nextSeen = new Set(seen);
|
|
196
|
+
nextSeen.add(value.text);
|
|
197
|
+
return readStaticString(initializer, bindings, nextSeen);
|
|
198
|
+
}
|
|
199
|
+
if (ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value)) {
|
|
200
|
+
const owner = unwrapExpression(value.expression);
|
|
201
|
+
const property = ts.isPropertyAccessExpression(value)
|
|
202
|
+
? value.name.text
|
|
203
|
+
: readStaticString(value.argumentExpression, bindings, seen);
|
|
204
|
+
if (!property) return null;
|
|
205
|
+
const ownerValue = ts.isIdentifier(owner) && bindings.has(owner.text)
|
|
206
|
+
? unwrapExpression(bindings.get(owner.text))
|
|
207
|
+
: owner;
|
|
208
|
+
if (!ownerValue || !ts.isObjectLiteralExpression(ownerValue)) return null;
|
|
209
|
+
const entry = ownerValue.properties.find(item =>
|
|
210
|
+
(ts.isPropertyAssignment(item) || ts.isShorthandPropertyAssignment(item)) &&
|
|
211
|
+
propertyNameText(item.name) === property
|
|
212
|
+
);
|
|
213
|
+
if (!entry) return null;
|
|
214
|
+
if (ts.isPropertyAssignment(entry)) return readStaticString(entry.initializer, bindings, seen);
|
|
215
|
+
return readStaticString(bindings.get(entry.name.text), bindings, seen);
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getPropertyPath(expression, bindings) {
|
|
221
|
+
const value = unwrapExpression(expression);
|
|
222
|
+
if (!value) return null;
|
|
223
|
+
if (ts.isIdentifier(value)) return [value.text];
|
|
224
|
+
if (ts.isPropertyAccessExpression(value)) {
|
|
225
|
+
const parent = getPropertyPath(value.expression, bindings);
|
|
226
|
+
return parent ? [...parent, value.name.text] : null;
|
|
227
|
+
}
|
|
228
|
+
if (ts.isElementAccessExpression(value)) {
|
|
229
|
+
const parent = getPropertyPath(value.expression, bindings);
|
|
230
|
+
const key = readStaticString(value.argumentExpression, bindings);
|
|
231
|
+
return parent && key ? [...parent, key] : null;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function readStaticObjectProperty(expression, propertyNames, bindings) {
|
|
237
|
+
const value = unwrapExpression(expression);
|
|
238
|
+
if (!value || !ts.isObjectLiteralExpression(value)) return null;
|
|
239
|
+
for (const property of value.properties) {
|
|
240
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
241
|
+
if (!propertyNames.includes(propertyNameText(property.name))) continue;
|
|
242
|
+
const result = readStaticString(property.initializer, bindings);
|
|
243
|
+
if (result) return result;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function sourceLocation(sourceFile, node, workspaceRoot) {
|
|
249
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
250
|
+
return {
|
|
251
|
+
file: workspaceRelativePath(workspaceRoot, sourceFile.fileName),
|
|
252
|
+
line: start.line + 1,
|
|
253
|
+
column: start.character + 1,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function addReference(references, sourceFile, node, workspaceRoot, bucket, code, api) {
|
|
258
|
+
const normalizedCode = String(code || '').trim();
|
|
259
|
+
if (!normalizedCode) return;
|
|
260
|
+
references.push({
|
|
261
|
+
bucket,
|
|
262
|
+
code: normalizedCode,
|
|
263
|
+
api,
|
|
264
|
+
...sourceLocation(sourceFile, node, workspaceRoot),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function addDynamicReferenceWarning(warnings, sourceFile, node, workspaceRoot, api, expected) {
|
|
269
|
+
warnings.push({
|
|
270
|
+
code: 'dynamic-resource-reference',
|
|
271
|
+
api,
|
|
272
|
+
message: `${api} 使用动态资源 code,无法静态核对 manifest;请确认 resources.${expected} 已完整声明`,
|
|
273
|
+
...sourceLocation(sourceFile, node, workspaceRoot),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function inspectResourceCall(node, sourceFile, workspaceRoot, bindings, references, warnings) {
|
|
278
|
+
if (!ts.isCallExpression(node)) return;
|
|
279
|
+
const callee = getPropertyPath(node.expression, bindings);
|
|
280
|
+
if (!callee || callee[0] !== 'ctx' || callee.length < 3) return;
|
|
281
|
+
const namespace = callee[1];
|
|
282
|
+
const method = callee[2];
|
|
283
|
+
const api = callee.join('.');
|
|
284
|
+
const firstArgument = node.arguments[0];
|
|
285
|
+
|
|
286
|
+
if (namespace === 'resources' && method.startsWith('resolve')) {
|
|
287
|
+
const bucket = {
|
|
288
|
+
resolveForm: 'forms',
|
|
289
|
+
resolveDataView: 'dataViews',
|
|
290
|
+
resolveConnector: 'connectors',
|
|
291
|
+
resolveNotification: 'notifications',
|
|
292
|
+
resolveNotificationType: 'notifications',
|
|
293
|
+
resolveTemplate: 'notifications',
|
|
294
|
+
resolveFunction: 'functions',
|
|
295
|
+
resolveStorage: 'storageConfigs',
|
|
296
|
+
}[method];
|
|
297
|
+
if (!bucket) return;
|
|
298
|
+
const code = readStaticString(firstArgument, bindings);
|
|
299
|
+
if (code) addReference(references, sourceFile, node, workspaceRoot, bucket, code, api);
|
|
300
|
+
else addDynamicReferenceWarning(warnings, sourceFile, node, workspaceRoot, api, bucket);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (namespace === 'form') {
|
|
305
|
+
const code = readStaticObjectProperty(firstArgument, ['formCode', 'code'], bindings) ||
|
|
306
|
+
readStaticString(firstArgument, bindings);
|
|
307
|
+
if (code) addReference(references, sourceFile, node, workspaceRoot, 'forms', code, api);
|
|
308
|
+
else if (firstArgument) addDynamicReferenceWarning(warnings, sourceFile, node, workspaceRoot, api, 'forms');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (namespace === 'dataView' && ['query', 'stats'].includes(method)) {
|
|
313
|
+
const code = readStaticString(firstArgument, bindings) ||
|
|
314
|
+
readStaticObjectProperty(firstArgument, ['dataViewCode', 'code'], bindings);
|
|
315
|
+
if (code) addReference(references, sourceFile, node, workspaceRoot, 'dataViews', code, api);
|
|
316
|
+
else if (firstArgument) addDynamicReferenceWarning(warnings, sourceFile, node, workspaceRoot, api, 'dataViews');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (namespace === 'connector' && ['invoke', 'call'].includes(method)) {
|
|
321
|
+
let code = readStaticObjectProperty(firstArgument, ['connector', 'connectorCode', 'code'], bindings);
|
|
322
|
+
if (!code && method === 'call') {
|
|
323
|
+
const callName = readStaticString(firstArgument, bindings);
|
|
324
|
+
code = callName ? callName.split('.')[0] : null;
|
|
325
|
+
}
|
|
326
|
+
if (code) addReference(references, sourceFile, node, workspaceRoot, 'connectors', code, api);
|
|
327
|
+
else if (firstArgument) addDynamicReferenceWarning(warnings, sourceFile, node, workspaceRoot, api, 'connectors');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (
|
|
332
|
+
namespace === 'notification' &&
|
|
333
|
+
[
|
|
334
|
+
'sendByType',
|
|
335
|
+
'batchSendByType',
|
|
336
|
+
'findConfig',
|
|
337
|
+
'previewTemplate',
|
|
338
|
+
'previewDingTalk',
|
|
339
|
+
'sendDingTalk',
|
|
340
|
+
'updateDingTalkCard',
|
|
341
|
+
].includes(method)
|
|
342
|
+
) {
|
|
343
|
+
const code = readStaticString(firstArgument, bindings) ||
|
|
344
|
+
readStaticObjectProperty(firstArgument, ['notificationType', 'templateCode', 'code'], bindings);
|
|
345
|
+
if (code) addReference(references, sourceFile, node, workspaceRoot, 'notifications', code, api);
|
|
346
|
+
else if (firstArgument) addDynamicReferenceWarning(warnings, sourceFile, node, workspaceRoot, api, 'notifications');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function collectModuleSpecifiers(sourceFile) {
|
|
351
|
+
const result = [];
|
|
352
|
+
const visit = node => {
|
|
353
|
+
if (
|
|
354
|
+
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
355
|
+
node.moduleSpecifier &&
|
|
356
|
+
ts.isStringLiteralLike(node.moduleSpecifier)
|
|
357
|
+
) {
|
|
358
|
+
result.push({ value: node.moduleSpecifier.text, node: node.moduleSpecifier });
|
|
359
|
+
} else if (
|
|
360
|
+
ts.isImportEqualsDeclaration(node) &&
|
|
361
|
+
ts.isExternalModuleReference(node.moduleReference) &&
|
|
362
|
+
node.moduleReference.expression &&
|
|
363
|
+
ts.isStringLiteralLike(node.moduleReference.expression)
|
|
364
|
+
) {
|
|
365
|
+
result.push({ value: node.moduleReference.expression.text, node: node.moduleReference.expression });
|
|
366
|
+
} else if (
|
|
367
|
+
ts.isCallExpression(node) &&
|
|
368
|
+
node.arguments.length > 0 &&
|
|
369
|
+
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
370
|
+
(ts.isIdentifier(node.expression) && node.expression.text === 'require')) &&
|
|
371
|
+
ts.isStringLiteralLike(node.arguments[0])
|
|
372
|
+
) {
|
|
373
|
+
result.push({ value: node.arguments[0].text, node: node.arguments[0] });
|
|
374
|
+
}
|
|
375
|
+
ts.forEachChild(node, visit);
|
|
376
|
+
};
|
|
377
|
+
visit(sourceFile);
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function dedupeDetails(values, keys) {
|
|
382
|
+
const seen = new Set();
|
|
383
|
+
return values.filter(value => {
|
|
384
|
+
const key = keys.map(name => value[name] || '').join('\0');
|
|
385
|
+
if (seen.has(key)) return false;
|
|
386
|
+
seen.add(key);
|
|
387
|
+
return true;
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function analyzeTypeScriptEntry(options = {}) {
|
|
392
|
+
const workspaceRoot = path.resolve(options.workspaceRoot || process.cwd());
|
|
393
|
+
const entryFile = path.resolve(workspaceRoot, options.entryFile || '');
|
|
394
|
+
const compilerOptions = options.compilerOptions || loadCompilerOptions(workspaceRoot);
|
|
395
|
+
const queue = [entryFile];
|
|
396
|
+
const visited = new Set();
|
|
397
|
+
const sourceDependencies = new Set();
|
|
398
|
+
const references = [];
|
|
399
|
+
const warnings = [];
|
|
400
|
+
|
|
401
|
+
while (queue.length > 0) {
|
|
402
|
+
const current = path.resolve(queue.shift());
|
|
403
|
+
if (visited.has(current)) continue;
|
|
404
|
+
visited.add(current);
|
|
405
|
+
if (!fs.existsSync(current) || !fs.statSync(current).isFile()) {
|
|
406
|
+
warnings.push({
|
|
407
|
+
code: 'missing-source-file',
|
|
408
|
+
file: workspaceRelativePath(workspaceRoot, current),
|
|
409
|
+
line: 1,
|
|
410
|
+
column: 1,
|
|
411
|
+
message: `本地 sourceFile 不存在: ${workspaceRelativePath(workspaceRoot, current)}`,
|
|
412
|
+
});
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (!isPathInside(workspaceRoot, current)) continue;
|
|
416
|
+
sourceDependencies.add(workspaceRelativePath(workspaceRoot, current));
|
|
417
|
+
if (!SOURCE_EXTENSIONS.has(path.extname(current).toLowerCase())) continue;
|
|
418
|
+
|
|
419
|
+
const content = fs.readFileSync(current, 'utf8');
|
|
420
|
+
const sourceFile = ts.createSourceFile(
|
|
421
|
+
current,
|
|
422
|
+
content,
|
|
423
|
+
ts.ScriptTarget.Latest,
|
|
424
|
+
true,
|
|
425
|
+
getScriptKind(current)
|
|
426
|
+
);
|
|
427
|
+
for (const diagnostic of sourceFile.parseDiagnostics || []) {
|
|
428
|
+
const position = diagnostic.start === undefined
|
|
429
|
+
? { line: 0, character: 0 }
|
|
430
|
+
: sourceFile.getLineAndCharacterOfPosition(diagnostic.start);
|
|
431
|
+
warnings.push({
|
|
432
|
+
code: 'source-parse-diagnostic',
|
|
433
|
+
file: workspaceRelativePath(workspaceRoot, current),
|
|
434
|
+
line: position.line + 1,
|
|
435
|
+
column: position.character + 1,
|
|
436
|
+
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const bindings = collectConstInitializers(sourceFile);
|
|
441
|
+
const inspect = node => {
|
|
442
|
+
inspectResourceCall(node, sourceFile, workspaceRoot, bindings, references, warnings);
|
|
443
|
+
ts.forEachChild(node, inspect);
|
|
444
|
+
};
|
|
445
|
+
inspect(sourceFile);
|
|
446
|
+
|
|
447
|
+
for (const imported of collectModuleSpecifiers(sourceFile)) {
|
|
448
|
+
const localLooking = imported.value.startsWith('.') || imported.value.startsWith('@/') || imported.value.startsWith('~/');
|
|
449
|
+
const resolved = resolveLocalModule(workspaceRoot, current, imported.value, compilerOptions);
|
|
450
|
+
if (resolved) {
|
|
451
|
+
queue.push(resolved);
|
|
452
|
+
} else if (localLooking) {
|
|
453
|
+
warnings.push({
|
|
454
|
+
code: 'unresolved-local-import',
|
|
455
|
+
specifier: imported.value,
|
|
456
|
+
message: `无法解析本地 import: ${imported.value}`,
|
|
457
|
+
...sourceLocation(sourceFile, imported.node, workspaceRoot),
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const resourceReferenceDetails = dedupeDetails(
|
|
464
|
+
references,
|
|
465
|
+
['bucket', 'code', 'file', 'line', 'column', 'api']
|
|
466
|
+
).sort((left, right) =>
|
|
467
|
+
`${left.bucket}\0${left.code}\0${left.file}\0${left.line}`.localeCompare(
|
|
468
|
+
`${right.bucket}\0${right.code}\0${right.file}\0${right.line}`
|
|
469
|
+
)
|
|
470
|
+
);
|
|
471
|
+
const resourceReferences = Object.fromEntries(
|
|
472
|
+
RESOURCE_BUCKETS.map(bucket => [
|
|
473
|
+
bucket,
|
|
474
|
+
Array.from(
|
|
475
|
+
new Set(
|
|
476
|
+
resourceReferenceDetails
|
|
477
|
+
.filter(reference => reference.bucket === bucket)
|
|
478
|
+
.map(reference => reference.code)
|
|
479
|
+
)
|
|
480
|
+
).sort(),
|
|
481
|
+
])
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
entryFile: workspaceRelativePath(workspaceRoot, entryFile),
|
|
486
|
+
sourceDependencies: Array.from(sourceDependencies).sort(),
|
|
487
|
+
resourceReferences,
|
|
488
|
+
resourceReferenceDetails,
|
|
489
|
+
warnings: dedupeDetails(warnings, ['code', 'file', 'line', 'column', 'api', 'message']),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function readJsonFile(filePath) {
|
|
494
|
+
try {
|
|
495
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
496
|
+
} catch {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function resolveManifestLocalPath(workspaceRoot, baseDir, localPath) {
|
|
502
|
+
const fromBase = path.resolve(baseDir || workspaceRoot, localPath);
|
|
503
|
+
if (fs.existsSync(fromBase)) return fromBase;
|
|
504
|
+
return path.resolve(workspaceRoot, localPath);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function collectManifestStructures(workspaceRoot, item) {
|
|
508
|
+
const sourceEntries = new Set();
|
|
509
|
+
const resourceContainers = [];
|
|
510
|
+
const visited = new Set();
|
|
511
|
+
const externalFiles = new Set();
|
|
512
|
+
|
|
513
|
+
const visit = (value, baseDir) => {
|
|
514
|
+
if (!value || typeof value !== 'object' || visited.has(value)) return;
|
|
515
|
+
visited.add(value);
|
|
516
|
+
if (Array.isArray(value)) {
|
|
517
|
+
for (const child of value) visit(child, baseDir);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (value.sourceFile?.localPath) {
|
|
521
|
+
sourceEntries.add(
|
|
522
|
+
resolveManifestLocalPath(workspaceRoot, baseDir, String(value.sourceFile.localPath))
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
if (value.resources && typeof value.resources === 'object') {
|
|
526
|
+
resourceContainers.push(value.resources);
|
|
527
|
+
}
|
|
528
|
+
if (value.resourceBindings && typeof value.resourceBindings === 'object') {
|
|
529
|
+
resourceContainers.push(value.resourceBindings);
|
|
530
|
+
}
|
|
531
|
+
for (const [key, child] of Object.entries(value)) {
|
|
532
|
+
if (key.startsWith('__')) continue;
|
|
533
|
+
if (key === 'definitionFile' && typeof child === 'string' && child.trim()) {
|
|
534
|
+
const definitionPath = path.resolve(baseDir || workspaceRoot, child);
|
|
535
|
+
if (!externalFiles.has(definitionPath) && fs.existsSync(definitionPath)) {
|
|
536
|
+
externalFiles.add(definitionPath);
|
|
537
|
+
const external = readJsonFile(definitionPath);
|
|
538
|
+
if (external) visit(external, path.dirname(definitionPath));
|
|
539
|
+
}
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (child && typeof child === 'object') visit(child, baseDir);
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
visit(item, item.__dir || workspaceRoot);
|
|
547
|
+
return {
|
|
548
|
+
sourceEntries: Array.from(sourceEntries).sort(),
|
|
549
|
+
resourceContainers,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function normalizeDeclaredCodes(value) {
|
|
554
|
+
if (value === undefined || value === null || value === false) return [];
|
|
555
|
+
if (Array.isArray(value)) {
|
|
556
|
+
return value.flatMap(normalizeDeclaredCodes);
|
|
557
|
+
}
|
|
558
|
+
if (typeof value === 'object') {
|
|
559
|
+
return Object.keys(value).map(key => String(key).trim()).filter(Boolean);
|
|
560
|
+
}
|
|
561
|
+
return String(value)
|
|
562
|
+
.split(',')
|
|
563
|
+
.map(item => item.trim())
|
|
564
|
+
.filter(Boolean);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function collectDeclaredResources(item, resourceContainers) {
|
|
568
|
+
const result = Object.fromEntries(RESOURCE_BUCKETS.map(bucket => [bucket, new Set()]));
|
|
569
|
+
for (const container of resourceContainers) {
|
|
570
|
+
for (const [rawBucket, value] of Object.entries(container || {})) {
|
|
571
|
+
const normalizedBucket = RESOURCE_BUCKET_ALIASES.get(
|
|
572
|
+
String(rawBucket).replace(/[_\s]+/g, '-').toLowerCase()
|
|
573
|
+
);
|
|
574
|
+
if (!normalizedBucket) continue;
|
|
575
|
+
for (const code of normalizeDeclaredCodes(value)) result[normalizedBucket].add(code);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (item.formCode) result.forms.add(String(item.formCode));
|
|
579
|
+
return Object.fromEntries(
|
|
580
|
+
RESOURCE_BUCKETS.map(bucket => [bucket, Array.from(result[bucket]).sort()])
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function getManifestItemCode(item) {
|
|
585
|
+
return String(
|
|
586
|
+
item?.code || item?.resourceCode || item?.functionCode || item?.scriptCode || item?.methodName || ''
|
|
587
|
+
).trim();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function mergeAnalyses(workspaceRoot, entries) {
|
|
591
|
+
const analyses = entries.map(entryFile =>
|
|
592
|
+
analyzeTypeScriptEntry({ workspaceRoot, entryFile })
|
|
593
|
+
);
|
|
594
|
+
const sourceDependencies = Array.from(
|
|
595
|
+
new Set(analyses.flatMap(analysis => analysis.sourceDependencies))
|
|
596
|
+
).sort();
|
|
597
|
+
const resourceReferenceDetails = dedupeDetails(
|
|
598
|
+
analyses.flatMap(analysis => analysis.resourceReferenceDetails),
|
|
599
|
+
['bucket', 'code', 'file', 'line', 'column', 'api']
|
|
600
|
+
);
|
|
601
|
+
const resourceReferences = Object.fromEntries(
|
|
602
|
+
RESOURCE_BUCKETS.map(bucket => [
|
|
603
|
+
bucket,
|
|
604
|
+
Array.from(
|
|
605
|
+
new Set(
|
|
606
|
+
resourceReferenceDetails
|
|
607
|
+
.filter(reference => reference.bucket === bucket)
|
|
608
|
+
.map(reference => reference.code)
|
|
609
|
+
)
|
|
610
|
+
).sort(),
|
|
611
|
+
])
|
|
612
|
+
);
|
|
613
|
+
return {
|
|
614
|
+
entryFiles: entries.map(entry => workspaceRelativePath(workspaceRoot, entry)).sort(),
|
|
615
|
+
sourceDependencies,
|
|
616
|
+
resourceReferences,
|
|
617
|
+
resourceReferenceDetails,
|
|
618
|
+
warnings: dedupeDetails(
|
|
619
|
+
analyses.flatMap(analysis => analysis.warnings),
|
|
620
|
+
['code', 'file', 'line', 'column', 'api', 'message']
|
|
621
|
+
),
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function analyzeManifestSourceDependencies(options = {}) {
|
|
626
|
+
const workspaceRoot = path.resolve(options.workspaceRoot || process.cwd());
|
|
627
|
+
const manifest = options.manifest || {};
|
|
628
|
+
const targets = [];
|
|
629
|
+
const byDependency = new Map();
|
|
630
|
+
|
|
631
|
+
for (const [manifestKey, targetKey] of TARGET_MANIFEST_KEYS) {
|
|
632
|
+
for (const item of manifest[manifestKey] || []) {
|
|
633
|
+
const code = getManifestItemCode(item);
|
|
634
|
+
if (!code) continue;
|
|
635
|
+
const structures = collectManifestStructures(workspaceRoot, item);
|
|
636
|
+
if (structures.sourceEntries.length === 0) continue;
|
|
637
|
+
const analysis = {
|
|
638
|
+
targetKey,
|
|
639
|
+
code,
|
|
640
|
+
declaredResources: collectDeclaredResources(item, structures.resourceContainers),
|
|
641
|
+
...mergeAnalyses(workspaceRoot, structures.sourceEntries),
|
|
642
|
+
};
|
|
643
|
+
Object.defineProperty(item, '__sourceAnalysis', {
|
|
644
|
+
value: analysis,
|
|
645
|
+
configurable: true,
|
|
646
|
+
enumerable: false,
|
|
647
|
+
writable: true,
|
|
648
|
+
});
|
|
649
|
+
targets.push(analysis);
|
|
650
|
+
for (const dependency of analysis.sourceDependencies) {
|
|
651
|
+
if (!byDependency.has(dependency)) byDependency.set(dependency, []);
|
|
652
|
+
byDependency.get(dependency).push({ targetKey, code });
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return { workspaceRoot, targets, byDependency };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function validateManifestSourceBindings(options = {}) {
|
|
661
|
+
const index = analyzeManifestSourceDependencies(options);
|
|
662
|
+
const errors = [];
|
|
663
|
+
const warnings = [];
|
|
664
|
+
|
|
665
|
+
for (const target of index.targets) {
|
|
666
|
+
const label = `${target.targetKey.replace(/s$/, '')} ${target.code}`;
|
|
667
|
+
const transitiveMissing = [];
|
|
668
|
+
for (const reference of target.resourceReferenceDetails) {
|
|
669
|
+
const declared = target.declaredResources[reference.bucket] || [];
|
|
670
|
+
if (declared.includes(reference.code)) continue;
|
|
671
|
+
if (reference.bucket === 'forms' && /^FORM[_-]/i.test(reference.code)) continue;
|
|
672
|
+
const message =
|
|
673
|
+
`${label}: ${reference.file}:${reference.line} ${reference.api} 引用 ${reference.bucket}.${reference.code},` +
|
|
674
|
+
`但 manifest 未声明。请将 "${reference.code}" 加入 resources.${reference.bucket}`;
|
|
675
|
+
if (target.entryFiles.includes(reference.file)) {
|
|
676
|
+
errors.push(message);
|
|
677
|
+
} else {
|
|
678
|
+
transitiveMissing.push(reference);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (transitiveMissing.length > 0) {
|
|
682
|
+
const codes = Array.from(
|
|
683
|
+
new Set(transitiveMissing.map(reference => `${reference.bucket}.${reference.code}`))
|
|
684
|
+
).sort();
|
|
685
|
+
const visible = codes.slice(0, 12);
|
|
686
|
+
warnings.push(
|
|
687
|
+
`${label}: 传递依赖中发现 manifest 未声明的资源引用: ${visible.join(', ')}` +
|
|
688
|
+
`${codes.length > visible.length ? ` 等 ${codes.length} 项` : ''}。` +
|
|
689
|
+
'静态分析无法证明相关导出一定被当前入口调用;若会执行,请补充对应 resources 声明'
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
for (const warning of target.warnings) {
|
|
693
|
+
if (
|
|
694
|
+
warning.code === 'dynamic-resource-reference' &&
|
|
695
|
+
!target.entryFiles.includes(warning.file)
|
|
696
|
+
) {
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
warnings.push(
|
|
700
|
+
`${label}: ${warning.file}:${warning.line} ${warning.message}`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
index,
|
|
707
|
+
errors: Array.from(new Set(errors)),
|
|
708
|
+
warnings: Array.from(new Set(warnings)),
|
|
709
|
+
sourceDependencies: index.targets.map(target => ({
|
|
710
|
+
kind: target.targetKey,
|
|
711
|
+
code: target.code,
|
|
712
|
+
entryFiles: target.entryFiles,
|
|
713
|
+
sourceDependencies: target.sourceDependencies,
|
|
714
|
+
resourceReferences: target.resourceReferences,
|
|
715
|
+
})),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function getImpactedResourceTargets(index, changedFiles = []) {
|
|
720
|
+
const result = {
|
|
721
|
+
functions: new Set(),
|
|
722
|
+
automations: new Set(),
|
|
723
|
+
workflows: new Set(),
|
|
724
|
+
jsCodeNodes: new Set(),
|
|
725
|
+
};
|
|
726
|
+
for (const rawFile of changedFiles) {
|
|
727
|
+
const file = normalizeWorkspacePath(rawFile);
|
|
728
|
+
for (const target of index?.byDependency?.get(file) || []) {
|
|
729
|
+
if (result[target.targetKey]) result[target.targetKey].add(target.code);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return Object.fromEntries(
|
|
733
|
+
Object.entries(result).map(([key, values]) => [key, Array.from(values).sort()])
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
module.exports = {
|
|
738
|
+
analyzeManifestSourceDependencies,
|
|
739
|
+
analyzeTypeScriptEntry,
|
|
740
|
+
getImpactedResourceTargets,
|
|
741
|
+
validateManifestSourceBindings,
|
|
742
|
+
};
|