openxiangda 1.0.161 → 1.0.163
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 +2 -0
- package/lib/cli.js +191 -25
- package/lib/js-code-build.js +120 -8
- package/openxiangda-skills/SKILL.md +2 -0
- package/openxiangda-skills/references/automation-v3.md +1 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +1 -1
- package/openxiangda-skills/references/workflow-v3.md +1 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +2 -0
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -2
- package/package.json +4 -2
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +1 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/openxiangda-react-spa/AGENTS.md +1 -1
- package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +172 -20
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +2 -0
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +2 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +1 -1
- package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +172 -20
package/README.md
CHANGED
|
@@ -119,6 +119,8 @@ openxiangda resource publish function --code customer_get --change <change> --pr
|
|
|
119
119
|
--replace-manifest --reason "reviewed manifest is the complete desired definition"
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
Exact `--only` / `--code` selectors are applied before unrelated manifests, source dependencies, and JS_CODE targets are read or built. Shared/transitive dependencies of the selected targets remain in scope; omitting a selector intentionally preserves full-workspace validation and planning. Resource commands use the packaged canonical scoped builder for standard workspaces, so an older checked-in `scripts/build-js-code.mjs` does not need to be upgraded before the installed CLI gains this optimization; refresh the workspace template only when developers also need the same behavior from a manual `pnpm build-js-code` command.
|
|
123
|
+
|
|
122
124
|
For source-only Function/Automation changes, the final command does not reconstruct whole definitions with client-side GET+PUT. `release begin` captures the Git/change baseline, one preflight covers every selected code, and Backend Release performs one `prepare -> verify -> activate` sequence for all eligible updates. Inspect history with `openxiangda release backend-head|backend-list|backend-detail`, compare it with `backend-diff`, or create an audited immutable rollback with `backend-rollback <releaseId> --change <change> --reason "..."`. Always run `release end` when promotion finishes or is abandoned.
|
|
123
125
|
|
|
124
126
|
Page repair publishing is staged by default. It first freezes `pages/snapshot`, sends the active Page Release parent plus every page revision, and uses revision `0` only for a genuinely new page. Review with `openxiangda page head|releases|detail|diff`; activate an immutable complete release explicitly with `page activate <releaseId> --change <change>`. Historical activation requires `page rollback <releaseId> --rollback --change <change> --reason "..."`. Parent or revision conflicts are never refreshed or retried automatically.
|
package/lib/cli.js
CHANGED
|
@@ -6931,7 +6931,7 @@ async function resource(args) {
|
|
|
6931
6931
|
' - publish --dry-run 等价于发布前计划,不会写平台资源。',
|
|
6932
6932
|
' - 在线 publish 必须用 --only/--code 精确选择;真正全量发布必须显式 --all --reason,SDD bypass 不能绕过此范围门。',
|
|
6933
6933
|
' - 源码依赖触发的 Function/Automation 更新默认只替换 source snapshot,保留线上 bindings/schema/metadata;显式替换 manifest 必须加 --replace-manifest --reason。',
|
|
6934
|
-
' - --only/--code
|
|
6934
|
+
' - --only/--code 会在读取/分析 manifest 与构建源码前按逻辑资源 code 早期收窄;也支持 function:foo 这类带类型 selector。',
|
|
6935
6935
|
' - type 可选: workflow, notification, route, menu, role, permission, data-view, function, connector 等。',
|
|
6936
6936
|
' - plan/publish 会在 stderr 输出阶段进度,超过 15 秒持续心跳;JSON 结果包含 timings,不污染 stdout。',
|
|
6937
6937
|
' - plan/publish --dry-run 严格只允许 GET/HEAD;READ_ONLY_AUTH_REQUIRED 时先执行 auth refresh 或重新登录。',
|
|
@@ -9539,6 +9539,12 @@ function resourceCodeSelectorMatches(selector, resourceKey, item) {
|
|
|
9539
9539
|
return getResourceItemCode(item) === selector.code;
|
|
9540
9540
|
}
|
|
9541
9541
|
|
|
9542
|
+
function resourceCodeSelectorAppliesToKey(selector, resourceKey) {
|
|
9543
|
+
if (selector.key) return selector.key === resourceKey;
|
|
9544
|
+
if (selector.keys) return selector.keys.includes(resourceKey);
|
|
9545
|
+
return true;
|
|
9546
|
+
}
|
|
9547
|
+
|
|
9542
9548
|
function normalizeResourceTypeAlias(value) {
|
|
9543
9549
|
return String(value || '')
|
|
9544
9550
|
.trim()
|
|
@@ -9569,21 +9575,40 @@ function loadWorkspaceResources(options = {}) {
|
|
|
9569
9575
|
}
|
|
9570
9576
|
: {}),
|
|
9571
9577
|
};
|
|
9578
|
+
if (codeFilters) {
|
|
9579
|
+
Object.defineProperty(manifest, '__resourceCodeSelectors', {
|
|
9580
|
+
value: codeFilters,
|
|
9581
|
+
configurable: true,
|
|
9582
|
+
enumerable: false,
|
|
9583
|
+
writable: false,
|
|
9584
|
+
});
|
|
9585
|
+
}
|
|
9572
9586
|
for (const spec of RESOURCE_SPECS) {
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
+
if (typeFilters && !typeFilters.has(spec.key)) {
|
|
9588
|
+
manifest[spec.key] = [];
|
|
9589
|
+
continue;
|
|
9590
|
+
}
|
|
9591
|
+
if (!codeFilters) {
|
|
9592
|
+
manifest[spec.key] = readResourceItems(baseDir, spec);
|
|
9593
|
+
continue;
|
|
9594
|
+
}
|
|
9595
|
+
|
|
9596
|
+
const selectorEntries = codeFilters
|
|
9597
|
+
.map((selector, index) => ({ selector, index }))
|
|
9598
|
+
.filter(({ selector }) => resourceCodeSelectorAppliesToKey(selector, spec.key));
|
|
9599
|
+
if (selectorEntries.length === 0) {
|
|
9600
|
+
manifest[spec.key] = [];
|
|
9601
|
+
continue;
|
|
9602
|
+
}
|
|
9603
|
+
manifest[spec.key] = readResourceItems(baseDir, spec, {
|
|
9604
|
+
selectorEntries,
|
|
9605
|
+
matchedCodeFilters,
|
|
9606
|
+
// Composite menu/notification files may expose several logical codes
|
|
9607
|
+
// whose children cannot be located from the filename alone. They still
|
|
9608
|
+
// filter before validation/source analysis; exact Function/Automation
|
|
9609
|
+
// manifests use the fail-safe early file path below.
|
|
9610
|
+
selectorFileScope: !['menus', 'notifications'].includes(spec.key),
|
|
9611
|
+
});
|
|
9587
9612
|
}
|
|
9588
9613
|
if (codeFilters) {
|
|
9589
9614
|
manifest.unmatchedResourceCodeFilters = codeFilters
|
|
@@ -9593,19 +9618,63 @@ function loadWorkspaceResources(options = {}) {
|
|
|
9593
9618
|
return manifest;
|
|
9594
9619
|
}
|
|
9595
9620
|
|
|
9596
|
-
function readResourceItems(baseDir, spec) {
|
|
9621
|
+
function readResourceItems(baseDir, spec, options = {}) {
|
|
9597
9622
|
const items = [];
|
|
9623
|
+
const selectorEntries = options.selectorEntries || null;
|
|
9624
|
+
const matchedSelectorIndexes = new Set();
|
|
9625
|
+
const visitedFiles = new Set();
|
|
9626
|
+
const appendFile = filePath => {
|
|
9627
|
+
if (visitedFiles.has(filePath)) return;
|
|
9628
|
+
visitedFiles.add(filePath);
|
|
9629
|
+
const fileItems = readResourceItemsFromFile(filePath, spec);
|
|
9630
|
+
if (!selectorEntries) {
|
|
9631
|
+
items.push(...fileItems);
|
|
9632
|
+
return;
|
|
9633
|
+
}
|
|
9634
|
+
for (const item of fileItems) {
|
|
9635
|
+
let matched = false;
|
|
9636
|
+
for (const { selector, index } of selectorEntries) {
|
|
9637
|
+
if (!resourceCodeSelectorMatches(selector, spec.key, item)) continue;
|
|
9638
|
+
options.matchedCodeFilters?.add(index);
|
|
9639
|
+
matchedSelectorIndexes.add(index);
|
|
9640
|
+
matched = true;
|
|
9641
|
+
}
|
|
9642
|
+
if (matched) items.push(item);
|
|
9643
|
+
}
|
|
9644
|
+
};
|
|
9598
9645
|
for (const relativeFile of spec.topFiles || []) {
|
|
9599
9646
|
const filePath = path.join(baseDir, relativeFile);
|
|
9600
9647
|
if (fs.existsSync(filePath)) {
|
|
9601
|
-
|
|
9648
|
+
appendFile(filePath);
|
|
9602
9649
|
}
|
|
9603
9650
|
}
|
|
9604
9651
|
|
|
9605
9652
|
const dirPath = path.join(baseDir, spec.dir);
|
|
9606
9653
|
if (fs.existsSync(dirPath)) {
|
|
9607
|
-
|
|
9608
|
-
|
|
9654
|
+
if (selectorEntries && options.selectorFileScope !== false) {
|
|
9655
|
+
const remaining = () => selectorEntries.filter(
|
|
9656
|
+
({ index }) => !matchedSelectorIndexes.has(index)
|
|
9657
|
+
);
|
|
9658
|
+
for (const filePath of listSelectedResourceJsonFiles(
|
|
9659
|
+
dirPath,
|
|
9660
|
+
spec,
|
|
9661
|
+
remaining().map(({ selector }) => selector.code)
|
|
9662
|
+
)) {
|
|
9663
|
+
appendFile(filePath);
|
|
9664
|
+
}
|
|
9665
|
+
if (remaining().length > 0) {
|
|
9666
|
+
// A direct path is only a hint. The file may declare another explicit
|
|
9667
|
+
// code, while the requested logical resource lives in an arbitrarily
|
|
9668
|
+
// named legacy bundle. Fall back only when no selected item actually
|
|
9669
|
+
// matched, preserving the standard-layout fast path.
|
|
9670
|
+
for (const filePath of listResourceJsonFiles(dirPath, spec)) {
|
|
9671
|
+
appendFile(filePath);
|
|
9672
|
+
}
|
|
9673
|
+
}
|
|
9674
|
+
} else {
|
|
9675
|
+
for (const filePath of listResourceJsonFiles(dirPath, spec)) {
|
|
9676
|
+
appendFile(filePath);
|
|
9677
|
+
}
|
|
9609
9678
|
}
|
|
9610
9679
|
}
|
|
9611
9680
|
return items;
|
|
@@ -9754,6 +9823,36 @@ function listResourceJsonFiles(dirPath, spec) {
|
|
|
9754
9823
|
);
|
|
9755
9824
|
}
|
|
9756
9825
|
|
|
9826
|
+
function listSelectedResourceJsonFiles(dirPath, spec, selectedCodes = []) {
|
|
9827
|
+
const codeSet = new Set(selectedCodes.map(code => String(code || '').trim()).filter(Boolean));
|
|
9828
|
+
if (codeSet.size === 0) return [];
|
|
9829
|
+
|
|
9830
|
+
const directCandidates = new Set();
|
|
9831
|
+
for (const code of codeSet) {
|
|
9832
|
+
if (
|
|
9833
|
+
code === '.' ||
|
|
9834
|
+
code === '..' ||
|
|
9835
|
+
code.includes('/') ||
|
|
9836
|
+
code.includes('\\')
|
|
9837
|
+
) {
|
|
9838
|
+
continue;
|
|
9839
|
+
}
|
|
9840
|
+
const directFile = path.join(dirPath, `${code}.json`);
|
|
9841
|
+
if (fs.existsSync(directFile) && fs.statSync(directFile).isFile()) {
|
|
9842
|
+
directCandidates.add(directFile);
|
|
9843
|
+
}
|
|
9844
|
+
const directDir = path.join(dirPath, code);
|
|
9845
|
+
if (fs.existsSync(directDir) && fs.statSync(directDir).isDirectory()) {
|
|
9846
|
+
for (const filePath of listJsonFiles(directDir)) {
|
|
9847
|
+
if (shouldReadResourceJsonFile(filePath, dirPath, spec)) {
|
|
9848
|
+
directCandidates.add(filePath);
|
|
9849
|
+
}
|
|
9850
|
+
}
|
|
9851
|
+
}
|
|
9852
|
+
}
|
|
9853
|
+
return Array.from(directCandidates).sort();
|
|
9854
|
+
}
|
|
9855
|
+
|
|
9757
9856
|
function shouldReadResourceJsonFile(filePath, dirPath, spec) {
|
|
9758
9857
|
if (spec.key !== 'workflows') return true;
|
|
9759
9858
|
const relativeParts = path.relative(dirPath, filePath).split(path.sep);
|
|
@@ -10987,7 +11086,7 @@ async function publishResourcesForWorkspace(config, profileName, options = {}) {
|
|
|
10987
11086
|
|
|
10988
11087
|
async function buildResourcePlan(config, target, manifest) {
|
|
10989
11088
|
return await withReadOnlyHttpGuard('resource plan', async () => {
|
|
10990
|
-
prepareManifestJsCodeBundlesForPlan(manifest);
|
|
11089
|
+
await prepareManifestJsCodeBundlesForPlan(manifest);
|
|
10991
11090
|
const existing = await fetchExistingResourceMaps(config, target, manifest);
|
|
10992
11091
|
const actions = [];
|
|
10993
11092
|
addPlanActions(actions, 'role', manifest.roles, existing.roles, roleEquals);
|
|
@@ -11024,7 +11123,7 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
11024
11123
|
});
|
|
11025
11124
|
}
|
|
11026
11125
|
|
|
11027
|
-
function prepareManifestJsCodeBundlesForPlan(manifest) {
|
|
11126
|
+
async function prepareManifestJsCodeBundlesForPlan(manifest) {
|
|
11028
11127
|
const targetsByWorkspace = new Map();
|
|
11029
11128
|
const seenObjects = new Set();
|
|
11030
11129
|
|
|
@@ -11079,21 +11178,45 @@ function prepareManifestJsCodeBundlesForPlan(manifest) {
|
|
|
11079
11178
|
};
|
|
11080
11179
|
|
|
11081
11180
|
for (const spec of RESOURCE_SPECS) {
|
|
11082
|
-
|
|
11181
|
+
const selectors = manifest.__resourceCodeSelectors || null;
|
|
11182
|
+
const items = selectors
|
|
11183
|
+
? (manifest[spec.key] || []).filter(item =>
|
|
11184
|
+
selectors.some(selector =>
|
|
11185
|
+
resourceCodeSelectorMatches(selector, spec.key, item)
|
|
11186
|
+
)
|
|
11187
|
+
)
|
|
11188
|
+
: manifest[spec.key] || [];
|
|
11189
|
+
visit(items, manifest.baseDir);
|
|
11083
11190
|
}
|
|
11084
11191
|
|
|
11085
11192
|
for (const [workspaceRoot, sourceMap] of targetsByWorkspace.entries()) {
|
|
11086
11193
|
const sourceInfos = Array.from(sourceMap.values());
|
|
11087
11194
|
if (sourceInfos.length === 0) continue;
|
|
11088
|
-
|
|
11195
|
+
let result = runWorkspaceJsCodeBuildBatch(
|
|
11089
11196
|
workspaceRoot,
|
|
11090
11197
|
sourceInfos.map(item => ({
|
|
11091
11198
|
sourceKind: item.sourceKind,
|
|
11092
11199
|
scriptCode: item.scriptCode,
|
|
11093
11200
|
}))
|
|
11094
11201
|
);
|
|
11095
|
-
if (result.
|
|
11096
|
-
|
|
11202
|
+
if (result.status !== 0) {
|
|
11203
|
+
const legacyFallback = result.openxiangdaBuildMode === 'workspace-script'
|
|
11204
|
+
? await tryBuildFunctionSourcesWithLegacyWorkspaceBuilder(
|
|
11205
|
+
workspaceRoot,
|
|
11206
|
+
sourceInfos
|
|
11207
|
+
)
|
|
11208
|
+
: { ok: false, result: null };
|
|
11209
|
+
if (legacyFallback.ok) {
|
|
11210
|
+
result = { status: 0, stdout: '', stderr: '' };
|
|
11211
|
+
} else {
|
|
11212
|
+
const failure = legacyFallback.result || result;
|
|
11213
|
+
if (failure.stdout) process.stderr.write(failure.stdout);
|
|
11214
|
+
if (failure.stderr) process.stderr.write(failure.stderr);
|
|
11215
|
+
}
|
|
11216
|
+
} else {
|
|
11217
|
+
if (result.stdout) process.stderr.write(result.stdout);
|
|
11218
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
11219
|
+
}
|
|
11097
11220
|
if (result.status !== 0) {
|
|
11098
11221
|
fail(
|
|
11099
11222
|
`JS_CODE batch bundle构建失败,无法生成资源计划: ${sourceInfos
|
|
@@ -11112,6 +11235,48 @@ function prepareManifestJsCodeBundlesForPlan(manifest) {
|
|
|
11112
11235
|
}
|
|
11113
11236
|
}
|
|
11114
11237
|
|
|
11238
|
+
async function tryBuildFunctionSourcesWithLegacyWorkspaceBuilder(
|
|
11239
|
+
workspaceRoot,
|
|
11240
|
+
sourceInfos
|
|
11241
|
+
) {
|
|
11242
|
+
if (
|
|
11243
|
+
sourceInfos.length === 0 ||
|
|
11244
|
+
sourceInfos.some(item => item.sourceKind !== 'functions')
|
|
11245
|
+
) {
|
|
11246
|
+
return { ok: false, result: null };
|
|
11247
|
+
}
|
|
11248
|
+
|
|
11249
|
+
let usedLegacyBuilder = false;
|
|
11250
|
+
for (const sourceInfo of sourceInfos) {
|
|
11251
|
+
const result = runWorkspaceJsCodeBuild(
|
|
11252
|
+
workspaceRoot,
|
|
11253
|
+
sourceInfo.scriptCode,
|
|
11254
|
+
sourceInfo.sourceKind
|
|
11255
|
+
);
|
|
11256
|
+
if (result.status === 0) {
|
|
11257
|
+
usedLegacyBuilder = true;
|
|
11258
|
+
if (result.stdout) process.stderr.write(result.stdout);
|
|
11259
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
11260
|
+
continue;
|
|
11261
|
+
}
|
|
11262
|
+
if (!isUnsupportedFunctionsBuildScript(result)) {
|
|
11263
|
+
return { ok: false, result };
|
|
11264
|
+
}
|
|
11265
|
+
warn(
|
|
11266
|
+
`当前工作区 scripts/build-js-code.mjs 不支持 App Function source=functions,已使用 openxiangda 内置构建器兼容构建 ${sourceInfo.scriptCode}`
|
|
11267
|
+
);
|
|
11268
|
+
await buildFunctionSourceWithBundledEsbuild(
|
|
11269
|
+
sourceInfo.entryPath,
|
|
11270
|
+
workspaceRoot,
|
|
11271
|
+
sourceInfo.scriptCode
|
|
11272
|
+
);
|
|
11273
|
+
}
|
|
11274
|
+
if (usedLegacyBuilder) {
|
|
11275
|
+
warn('当前工作区 build-js-code 不支持批量 --scripts,已兼容使用逐 Function 构建');
|
|
11276
|
+
}
|
|
11277
|
+
return { ok: true, result: null };
|
|
11278
|
+
}
|
|
11279
|
+
|
|
11115
11280
|
function buildResourcePlanActionIndex(actions = []) {
|
|
11116
11281
|
const index = new Map();
|
|
11117
11282
|
for (const action of actions || []) {
|
|
@@ -17034,6 +17199,7 @@ function getJsCodeSourceInfoForPlan(localPath) {
|
|
|
17034
17199
|
workspaceRoot,
|
|
17035
17200
|
sourceKind,
|
|
17036
17201
|
scriptCode,
|
|
17202
|
+
entryPath: localPath,
|
|
17037
17203
|
bundlePath: path.join(workspaceRoot, 'dist', sourceKind, scriptCode, 'index.cjs'),
|
|
17038
17204
|
};
|
|
17039
17205
|
}
|
package/lib/js-code-build.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
1
3
|
const { spawnSync } = require('child_process');
|
|
2
4
|
|
|
3
5
|
const JS_CODE_SOURCE_KINDS = new Set(['js-code-nodes', 'automations', 'functions']);
|
|
6
|
+
const CANONICAL_BUILD_SCRIPT = path.join(
|
|
7
|
+
__dirname,
|
|
8
|
+
'..',
|
|
9
|
+
'templates',
|
|
10
|
+
'openxiangda-react-spa',
|
|
11
|
+
'scripts',
|
|
12
|
+
'build-js-code.mjs'
|
|
13
|
+
);
|
|
4
14
|
|
|
5
15
|
function normalizeJsCodeBuildTargets(targets = []) {
|
|
6
16
|
const normalized = [];
|
|
@@ -42,20 +52,122 @@ function buildJsCodeBatchArgs(targets, options = {}) {
|
|
|
42
52
|
];
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
function readWorkspacePackage(workspaceRoot) {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(
|
|
58
|
+
fs.readFileSync(path.join(workspaceRoot, 'package.json'), 'utf8')
|
|
59
|
+
);
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isStandardWorkspaceJsCodeBuilder(workspaceRoot) {
|
|
66
|
+
const command = String(
|
|
67
|
+
readWorkspacePackage(workspaceRoot)?.scripts?.['build-js-code'] || ''
|
|
68
|
+
).trim().replace(/\\/g, '/');
|
|
69
|
+
if (!command) return true;
|
|
70
|
+
// Only bypass the exact template-shaped command. Extra flags, command
|
|
71
|
+
// chaining, or wrappers may be meaningful custom preprocessing and must keep
|
|
72
|
+
// using the workspace builder.
|
|
73
|
+
const matched = command.match(
|
|
74
|
+
/^(?:node\s+)?((?:\.\/)?scripts\/build-js-code\.(?:mjs|cjs|js))$/
|
|
75
|
+
);
|
|
76
|
+
if (!matched) return false;
|
|
77
|
+
try {
|
|
78
|
+
const source = fs.readFileSync(path.resolve(workspaceRoot, matched[1]), 'utf8');
|
|
79
|
+
return [
|
|
80
|
+
'tsconfig.js-code-nodes.json',
|
|
81
|
+
'sourceKinds',
|
|
82
|
+
'buildScript',
|
|
83
|
+
].every(marker => source.includes(marker)) &&
|
|
84
|
+
(source.includes('resolveBuildTargets') ||
|
|
85
|
+
source.includes('resolveBuildSelection'));
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findWorkspaceTargetEntry(workspaceRoot, target) {
|
|
92
|
+
const base = path.join(
|
|
93
|
+
workspaceRoot,
|
|
94
|
+
'src',
|
|
95
|
+
target.sourceKind,
|
|
96
|
+
target.scriptCode,
|
|
97
|
+
'index'
|
|
98
|
+
);
|
|
99
|
+
return ['.ts', '.tsx']
|
|
100
|
+
.map(extension => `${base}${extension}`)
|
|
101
|
+
.find(filePath => fs.existsSync(filePath));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function canUseCanonicalJsCodeBuilder(workspaceRoot, targets, options = {}) {
|
|
105
|
+
if (
|
|
106
|
+
options.preferWorkspaceBuilder ||
|
|
107
|
+
process.env.OPENXIANGDA_USE_WORKSPACE_JS_CODE_BUILDER === '1'
|
|
108
|
+
) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (
|
|
112
|
+
!fs.existsSync(CANONICAL_BUILD_SCRIPT) ||
|
|
113
|
+
!fs.existsSync(path.join(workspaceRoot, 'tsconfig.js-code-nodes.json')) ||
|
|
114
|
+
!isStandardWorkspaceJsCodeBuilder(workspaceRoot)
|
|
115
|
+
) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return targets.every(target => findWorkspaceTargetEntry(workspaceRoot, target));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveWorkspaceJsCodeBuildCommand(workspaceRoot, targets, options = {}) {
|
|
122
|
+
const normalized = normalizeJsCodeBuildTargets(targets);
|
|
123
|
+
const packageArgs = buildJsCodeBatchArgs(normalized, options);
|
|
124
|
+
if (canUseCanonicalJsCodeBuilder(workspaceRoot, normalized, options)) {
|
|
125
|
+
return {
|
|
126
|
+
mode: 'canonical-scoped',
|
|
127
|
+
command: options.nodeBinary || process.execPath,
|
|
128
|
+
args: [CANONICAL_BUILD_SCRIPT, ...packageArgs.slice(1)],
|
|
129
|
+
env: {
|
|
130
|
+
OPENXIANGDA_WORKSPACE_ROOT: path.resolve(workspaceRoot),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
mode: 'workspace-script',
|
|
136
|
+
command: options.packageManager || 'pnpm',
|
|
137
|
+
args: packageArgs,
|
|
138
|
+
env: {},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
45
142
|
function runWorkspaceJsCodeBuildBatch(workspaceRoot, targets, options = {}) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
cwd: workspaceRoot,
|
|
51
|
-
encoding: 'utf8',
|
|
52
|
-
...options.spawnOptions,
|
|
53
|
-
}
|
|
143
|
+
const resolved = resolveWorkspaceJsCodeBuildCommand(
|
|
144
|
+
workspaceRoot,
|
|
145
|
+
targets,
|
|
146
|
+
options
|
|
54
147
|
);
|
|
148
|
+
const spawnOptions = {
|
|
149
|
+
cwd: workspaceRoot,
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
...options.spawnOptions,
|
|
152
|
+
};
|
|
153
|
+
if (resolved.mode === 'canonical-scoped') {
|
|
154
|
+
spawnOptions.env = {
|
|
155
|
+
...process.env,
|
|
156
|
+
...(options.spawnOptions?.env || {}),
|
|
157
|
+
...resolved.env,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const result = spawnSync(resolved.command, resolved.args, spawnOptions);
|
|
161
|
+
Object.defineProperty(result, 'openxiangdaBuildMode', {
|
|
162
|
+
value: resolved.mode,
|
|
163
|
+
enumerable: false,
|
|
164
|
+
});
|
|
165
|
+
return result;
|
|
55
166
|
}
|
|
56
167
|
|
|
57
168
|
module.exports = {
|
|
58
169
|
buildJsCodeBatchArgs,
|
|
59
170
|
normalizeJsCodeBuildTargets,
|
|
171
|
+
resolveWorkspaceJsCodeBuildCommand,
|
|
60
172
|
runWorkspaceJsCodeBuildBatch,
|
|
61
173
|
};
|
|
@@ -31,6 +31,8 @@ This file is a router and safety card. Read only the one or two subskills select
|
|
|
31
31
|
4. Plan and publish exact resource codes with `--only <codes>` or `--code <code>`; a type-only or full-resource publish is allowed only when the dependency closure intentionally contains the whole type/application.
|
|
32
32
|
5. A React SPA page change rebuilds one application runtime. Commit the complete build input first, upload/preview with `runtime deploy --no-activate`, and promote only from a clean merged release head that descends from the online Runtime source revision.
|
|
33
33
|
|
|
34
|
+
An exact Function/Automation selector is an early scope boundary: unrelated manifests, source dependency graphs, and JS_CODE targets must not be read, typechecked, or built. Transitive/shared/ambient dependencies of the selected entry remain included. No selector intentionally keeps full-workspace behavior.
|
|
35
|
+
|
|
34
36
|
Never infer a release scope from a globally dirty `git status` when multiple changes are present. Never publish a temporary source tree that differs from the reviewed patch.
|
|
35
37
|
|
|
36
38
|
## Risk and approval
|
|
@@ -287,7 +287,7 @@ Use JS_CODE V2 when the script is local to one automation graph node.
|
|
|
287
287
|
}
|
|
288
288
|
```
|
|
289
289
|
|
|
290
|
-
Author source in `src/js-code-nodes/<scriptCode>/index.ts`. AI-authored source must be TypeScript. Build with `pnpm build-js-code --script <scriptCode>`; the
|
|
290
|
+
Author source in `src/js-code-nodes/<scriptCode>/index.ts`. AI-authored source must be TypeScript. Build with `pnpm build-js-code --script <scriptCode>`; the explicit selector typechecks only that entry plus its transitive/shared/ambient dependencies before bundling, while an unscoped build validates the whole workspace. During validate/create, the CLI uploads the generated bundle, replaces `sourceFile.localPath` with `{ bucketName, objectName, sha256, ... }`, and the backend verifies sha256 before execution.
|
|
291
291
|
|
|
292
292
|
The backend runs the snapshot in the trusted Node runtime, applies the node timeout (`30000` ms by default), stores execution logs, and writes the returned value to the node output and `variables.node_<nodeId>`. Scripts may use `export default async function (ctx) {}`, `module.exports = async (ctx) => {}`, `require`, `process`, `Buffer`, arbitrary HTTP, and `platform.api` for `/openxiangda-api/v1`.
|
|
293
293
|
|
|
@@ -754,7 +754,7 @@ export default async function (ctx) {
|
|
|
754
754
|
}
|
|
755
755
|
```
|
|
756
756
|
|
|
757
|
-
构建:`pnpm build-js-code --script sync_customer
|
|
757
|
+
构建:`pnpm build-js-code --script sync_customer`(只校验该入口及其传递/shared/ambient 依赖,再打包到 `dist/js-code-nodes/<code>/index.cjs`;仅无 selector 时全量校验)。CLI validate/create/publish 时会上传快照、用 `{ bucketName, objectName, sha256 }` 替换 `sourceFile.localPath`。
|
|
758
758
|
|
|
759
759
|
`ctx.methods.*` 仍可用于兼容旧脚本;新脚本优先使用 `ctx.resources`、`ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.platform.roles` 和 `ctx.platform.api`。
|
|
760
760
|
|
|
@@ -371,7 +371,7 @@ File snapshot:
|
|
|
371
371
|
}
|
|
372
372
|
```
|
|
373
373
|
|
|
374
|
-
AI-authored JS_CODE source must be TypeScript under `src/js-code-nodes/<scriptCode>/index.ts`. When validating or creating, the CLI runs `pnpm build-js-code --script <scriptCode>`, which
|
|
374
|
+
AI-authored JS_CODE source must be TypeScript under `src/js-code-nodes/<scriptCode>/index.ts`. When validating or creating, the CLI runs `pnpm build-js-code --script <scriptCode>`, which typechecks only the selected entry plus its transitive/shared/ambient dependencies, bundles to `dist/js-code-nodes/<scriptCode>/index.cjs`, uploads the bundle, and replaces `sourceFile.localPath` with immutable snapshot metadata. An intentionally unscoped build retains full-workspace TypeScript validation. The backend verifies snapshot `sha256`, runs it in the trusted Node runtime, applies the node timeout (`30000` ms by default), stores console/runtime logs in the execution record, and writes the returned value to the node output and `variables.node_<nodeId>`.
|
|
375
375
|
|
|
376
376
|
For reusable backend logic that should be shared by pages, automations, and workflows, prefer an App Function under `src/functions/<functionCode>/index.ts` plus `src/resources/functions/<functionCode>.json`. Workflow graphs that support App Function nodes can call it with:
|
|
377
377
|
|
|
@@ -53,6 +53,8 @@ openxiangda resource publish function --only function_a,function_b --profile <na
|
|
|
53
53
|
|
|
54
54
|
`--code <code>` is the single-resource equivalent of `--only <codes>`. Type-only planning is appropriate for an intentional shared dependency closure. Unscoped full-resource planning/publishing is app-wide and requires an app-wide approved change.
|
|
55
55
|
|
|
56
|
+
Selectors apply before manifest parsing, source dependency analysis, and JS_CODE typecheck/build. A scoped Function/Automation plan must touch only the selected targets plus their transitive/shared/ambient dependencies; an unscoped plan intentionally retains full-workspace behavior. Resource commands use the canonical scoped builder packaged with the installed CLI for standard workspaces, so old checked-in builders still receive the optimization; refresh/bootstrap the workspace script only for equivalent manual `pnpm build-js-code` behavior. Nonstandard custom builders remain an explicit compatibility fallback.
|
|
57
|
+
|
|
56
58
|
Source-triggered Function/Automation targets use one immutable Backend Release by default. The CLI uploads every eligible existing source-only target, freezes the current Backend Release parent plus Git/change baseline, then runs `prepare -> verify -> activate`; activation first CAS-checks the entire set and applies all updates in one transaction. Never fall back to sequential PATCH after prepare/verify/activate has started. Compatibility fallback is allowed only when the Backend Release `head` feature probe is an explicit HTTP 404, and must warn. Existing online bindings, contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged; noops do not advance resource versions/timestamps. A deliberate whole-definition replacement requires exact `--only/--code` and `--replace-manifest --reason "<why>"`; SDD bypass does not imply replacement authority.
|
|
57
59
|
|
|
58
60
|
Use `openxiangda release backend-head|backend-list|backend-detail|backend-diff` to inspect immutable history. `backend-rollback <releaseId> --change <change> --reason "..."` prepares, verifies, and activates a new release from the historical snapshot; it never mutates or directly reactivates the old row. `backend-abort` stops prepared/verified work and `backend-retry` retries only recorded post-commit side effects. Do not fetch newer revisions to replay an older payload.
|
|
@@ -29,7 +29,7 @@ Create a workflow only when the scenario has **real approval semantics**: approv
|
|
|
29
29
|
- ✅ Delayed approval start: save the process-form record first with `StandardFormPage submitBehavior="save-draft"` or `sdk.form.create({ formUuid, data, saveAsDraft: true, startProcess: false })`, then start approval in place with `sdk.process.startFromExistingInstance({ formUuid, formInstId })` or `StandardFormPage submitBehavior="start-existing-process"`.
|
|
30
30
|
- ✅ Return-to-initiator flows: use `returnPolicy: { scopeType: "initiator", resubmitMode: "resume_current" | "replay" }` or `flow.action.returnToInitiator()`. The runtime capabilities protocol exposes `resubmit` only for pending `originator_return` tasks; return to a previous approval node continues through the normal `approve` action.
|
|
31
31
|
- ✅ Reusable backend business logic: prefer **App Function** (`src/functions/<functionCode>/index.ts` + `src/resources/functions/<functionCode>.json`), then call it from page `sdk.function.invoke` or automation/workflow `function_call`.
|
|
32
|
-
- ✅ JS_CODE V2 trusted_node: source in TypeScript under `src/js-code-nodes/<scriptCode>/index.ts`, `src/automations/<resourceCode>/index.ts`, or `src/functions/<functionCode>/index.ts`. Run `pnpm build-js-code --script <code
|
|
32
|
+
- ✅ JS_CODE V2 trusted_node: source in TypeScript under `src/js-code-nodes/<scriptCode>/index.ts`, `src/automations/<resourceCode>/index.ts`, or `src/functions/<functionCode>/index.ts`. Run `pnpm build-js-code --script <code>`; an explicit selector validates/builds only that entry plus its transitive/shared/ambient dependencies, while an unscoped command validates the whole workspace.
|
|
33
33
|
- ✅ Use `trigger_v2` for new automation triggers; CLI fills root `appType` / `formUuid` from active profile when `--form-code` is provided.
|
|
34
34
|
- ✅ Use logical `workflowCode` / `automationCode` locally; live IDs are profile-isolated under `.openxiangda/state.json`.
|
|
35
35
|
- ✅ `ctx.logger.debug/info/warn/error(message, data?)` at every important step — inspect via `automation executions` / `automation logs` / `automation diagnose`.
|
|
@@ -112,7 +112,7 @@ For new AI-authored workflows where users do not need canvas editing, prefer com
|
|
|
112
112
|
Do not use JS_CODE for simple UI interactions, ordinary form validation, display-only page behavior, or logic that belongs in a normal React code page. For non-trivial backend logic, prefer JS_CODE V2 trusted Node scripts over large inline snippets. AI-authored JS_CODE source must be TypeScript:
|
|
113
113
|
|
|
114
114
|
1. Put source in `src/js-code-nodes/<scriptCode>/index.ts`.
|
|
115
|
-
2. Run `pnpm build-js-code --script <scriptCode>`.
|
|
115
|
+
2. Run `pnpm build-js-code --script <scriptCode>`. In a refreshed workspace this command typechecks only the selected entry and its transitive/shared/ambient dependencies, then bundles after validation passes; omit the selector only for an intentional full-workspace validation. Installed CLI resource commands independently use their packaged canonical scoped builder, including against standard older workspaces.
|
|
116
116
|
3. In workflow or automation JSON, use:
|
|
117
117
|
```json
|
|
118
118
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openxiangda",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.163",
|
|
4
4
|
"description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"bin": {
|
|
@@ -91,6 +91,8 @@
|
|
|
91
91
|
"test:app-release-cli": "node scripts/app-release-cli-smoke.mjs",
|
|
92
92
|
"test:form-release-cas": "node scripts/form-release-cas-smoke.mjs",
|
|
93
93
|
"test:source-dependencies": "node scripts/source-dependencies-smoke.mjs",
|
|
94
|
+
"test:package-runtime-dependencies": "node scripts/package-runtime-dependencies-smoke.mjs",
|
|
95
|
+
"test:packed-cli": "node scripts/packed-cli-smoke.mjs",
|
|
94
96
|
"test:runtime-deploy": "node scripts/runtime-deploy-smoke.mjs",
|
|
95
97
|
"test:sdd": "node scripts/sdd-smoke.mjs",
|
|
96
98
|
"test:sdd-stages": "node scripts/sdd-stages-smoke.mjs",
|
|
@@ -145,6 +147,7 @@
|
|
|
145
147
|
"qrcode-terminal": "^0.12.0",
|
|
146
148
|
"tailwindcss": "^3.4.17",
|
|
147
149
|
"tsx": "^4.20.0",
|
|
150
|
+
"typescript": "^5.7.0",
|
|
148
151
|
"undici": "^6.27.0",
|
|
149
152
|
"vite": "^6.0.0",
|
|
150
153
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
|
@@ -164,7 +167,6 @@
|
|
|
164
167
|
"react": "18.3.1",
|
|
165
168
|
"react-dom": "18.3.1",
|
|
166
169
|
"tsup": "^8.3.0",
|
|
167
|
-
"typescript": "^5.7.0",
|
|
168
170
|
"vitest": "^4.1.7"
|
|
169
171
|
},
|
|
170
172
|
"engines": {
|
|
@@ -24,6 +24,7 @@ openxiangda resource typegen --profile <name>
|
|
|
24
24
|
- Guest access to forms, dataViews, functions, and connectors requires explicit policy `grants`.
|
|
25
25
|
- Connector secrets and third-party credentials belong in the platform backend, never in manifests or page source.
|
|
26
26
|
- Formal changes should keep Git as the source of truth: edit manifests, validate, plan, then publish.
|
|
27
|
+
- Exact selectors apply before manifest/source analysis and JS_CODE build: touch only selected targets plus transitive/shared/ambient dependencies; omit selectors only for intentional full-workspace work.
|
|
27
28
|
- `resource plan` and publish dry-runs are GET/HEAD-only. On `READ_ONLY_AUTH_REQUIRED`, run `openxiangda auth refresh --profile <name>` or log in again before retrying; never refresh inside the plan.
|
|
28
29
|
- Source-triggered Function/Automation publishing patches only source fields on the server and preserves online bindings/contracts/metadata/state. Whole-manifest replacement requires `--replace-manifest --reason "..."`.
|
|
29
30
|
- Formal promotion runs `release begin --change` (freeze Git + remote baseline, preflight the complete set) through `release end`. Reconcile `SOURCE_BASE_DIVERGED` / `RESOURCE_FIELD_CONFLICT`; never retry an old full payload with a fresh revision.
|
|
@@ -24,6 +24,7 @@ openxiangda resource typegen --profile <name>
|
|
|
24
24
|
- Guest access to forms, dataViews, functions, and connectors requires explicit policy `grants`.
|
|
25
25
|
- Connector secrets and third-party credentials belong in the platform backend, never in manifests or page source.
|
|
26
26
|
- Formal changes should keep Git as the source of truth: edit manifests, validate, plan, then publish.
|
|
27
|
+
- Exact selectors apply before manifest/source analysis and JS_CODE build: touch only selected targets plus transitive/shared/ambient dependencies; omit selectors only for intentional full-workspace work.
|
|
27
28
|
- `resource plan` and publish dry-runs are GET/HEAD-only. On `READ_ONLY_AUTH_REQUIRED`, run `openxiangda auth refresh --profile <name>` or log in again before retrying; never refresh inside the plan.
|
|
28
29
|
- Source-triggered Function/Automation publishing patches only source fields on the server and preserves online bindings/contracts/metadata/state. Whole-manifest replacement requires `--replace-manifest --reason "..."`.
|
|
29
30
|
- Formal promotion runs `release begin --change` (freeze Git + remote baseline, preflight the complete set) through `release end`. Reconcile `SOURCE_BASE_DIVERGED` / `RESOURCE_FIELD_CONFLICT`; never retry an old full payload with a fresh revision.
|
|
@@ -64,7 +64,7 @@ openxiangda runtime activate <reviewedReleaseId> --change <change> --profile <na
|
|
|
64
64
|
openxiangda release end --profile <name>
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
`pnpm build-js-code` 会检查并打包 `src/js-code-nodes/<code>/index.ts`、`src/automations/<code>/index.ts`、`src/functions/<code>/index.ts`,供 JS_CODE V2、代码自动化和 App Function 资源发布使用。批量目标使用 `pnpm build-js-code --scripts functions:a,functions:b,automations:c`,也兼容重复的 `--script a --script b --source functions
|
|
67
|
+
`pnpm build-js-code` 会检查并打包 `src/js-code-nodes/<code>/index.ts`、`src/automations/<code>/index.ts`、`src/functions/<code>/index.ts`,供 JS_CODE V2、代码自动化和 App Function 资源发布使用。批量目标使用 `pnpm build-js-code --scripts functions:a,functions:b,automations:c`,也兼容重复的 `--script a --script b --source functions`。显式 selector 只校验和构建选中入口及其传递/shared/ambient 依赖;不带 selector 才执行全工作区 TypeScript 校验。构建缓存写入 `.openxiangda/build-cache.json` 并按 scoped target、实际共享依赖、构建配置、Node/Vite/TypeScript 版本及产物校验和判断命中;需要强制重建时追加 `--force`。
|
|
68
68
|
|
|
69
69
|
`openxiangda runtime deploy --no-activate` 会构建并上传不可变预览版本;发布前先提交所有可能进入构建的源码/配置。所有 Runtime deploy(包括 `--no-activate`)都会先获取应用发布 lease,并在任何构建和上传前冻结 clean `HEAD` 与当前 active Runtime 父血缘;旧分支返回 `RUNTIME_SOURCE_BASE_DIVERGED`,不能先上传旧 preview 再激活。`openspec/` SDD 证据和生成/状态目录不算源码 dirty。仅审批的回退可使用 `--allow-runtime-rollback --reason "至少 8 个字符"`;`--no-build` 不会跳过守卫。不要手工修改 `dist/index.html`。
|
|
70
70
|
|
|
@@ -19,13 +19,29 @@ import path from "node:path";
|
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { build } from "vite";
|
|
21
21
|
|
|
22
|
-
const rootDir = fileURLToPath(new URL("..", import.meta.url));
|
|
23
22
|
const scriptFile = fileURLToPath(import.meta.url);
|
|
23
|
+
const defaultRootDir = fileURLToPath(new URL("..", import.meta.url));
|
|
24
|
+
const rootDir = path.resolve(process.env.OPENXIANGDA_WORKSPACE_ROOT || defaultRootDir);
|
|
24
25
|
const args = process.argv.slice(2);
|
|
25
26
|
const require = createRequire(import.meta.url);
|
|
26
|
-
const
|
|
27
|
-
const
|
|
27
|
+
const ts = require("typescript");
|
|
28
|
+
const CACHE_VERSION = 3;
|
|
29
|
+
const builderFingerprint = crypto
|
|
30
|
+
.createHash("sha256")
|
|
31
|
+
.update(readFileSync(scriptFile))
|
|
32
|
+
.digest("hex");
|
|
33
|
+
const workspaceBuilderFile = path.join(rootDir, "scripts", "build-js-code.mjs");
|
|
34
|
+
const workspaceBuilderMatches =
|
|
35
|
+
existsSync(workspaceBuilderFile) &&
|
|
36
|
+
crypto.createHash("sha256").update(readFileSync(workspaceBuilderFile)).digest("hex") ===
|
|
37
|
+
builderFingerprint;
|
|
38
|
+
const cacheFileName =
|
|
39
|
+
path.resolve(defaultRootDir) !== rootDir && !workspaceBuilderMatches
|
|
40
|
+
? "build-cache.cli-v3.json"
|
|
41
|
+
: "build-cache.json";
|
|
42
|
+
const CACHE_FILE = path.join(rootDir, ".openxiangda", cacheFileName);
|
|
28
43
|
const CACHE_LOCK_FILE = `${CACHE_FILE}.lock`;
|
|
44
|
+
const TSCONFIG_FILE = path.join(rootDir, "tsconfig.js-code-nodes.json");
|
|
29
45
|
const forceBuild = args.includes("--force") || args.includes("--no-cache");
|
|
30
46
|
|
|
31
47
|
const sourceKinds = {
|
|
@@ -84,11 +100,24 @@ function assertScriptCode(scriptCode) {
|
|
|
84
100
|
function listScriptCodes(kind) {
|
|
85
101
|
if (!existsSync(kind.sourceRoot)) return [];
|
|
86
102
|
return readdirSync(kind.sourceRoot, { withFileTypes: true })
|
|
87
|
-
.filter(
|
|
103
|
+
.filter(
|
|
104
|
+
(entry) =>
|
|
105
|
+
entry.isDirectory() &&
|
|
106
|
+
["index.ts", "index.tsx"].some((name) =>
|
|
107
|
+
existsSync(path.join(kind.sourceRoot, entry.name, name)),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
88
110
|
.map((entry) => entry.name)
|
|
89
111
|
.sort();
|
|
90
112
|
}
|
|
91
113
|
|
|
114
|
+
function targetEntryFile(target) {
|
|
115
|
+
const base = path.join(target.kind.sourceRoot, target.scriptCode);
|
|
116
|
+
return ["index.ts", "index.tsx"]
|
|
117
|
+
.map((name) => path.join(base, name))
|
|
118
|
+
.find((file) => existsSync(file)) || path.join(base, "index.ts");
|
|
119
|
+
}
|
|
120
|
+
|
|
92
121
|
function parseScriptSpec(rawSpec, defaultKind) {
|
|
93
122
|
const prefixed = String(rawSpec).match(/^(js-code-nodes|automations|functions)[:/](.+)$/);
|
|
94
123
|
const requestedKind = prefixed?.[1] || defaultKind?.name;
|
|
@@ -101,7 +130,9 @@ function parseScriptSpec(rawSpec, defaultKind) {
|
|
|
101
130
|
if (requestedKind) return { kind: sourceKinds[requestedKind], scriptCode };
|
|
102
131
|
|
|
103
132
|
const matches = Object.values(sourceKinds).filter((kind) =>
|
|
104
|
-
|
|
133
|
+
["index.ts", "index.tsx"].some((name) =>
|
|
134
|
+
existsSync(path.join(kind.sourceRoot, scriptCode, name)),
|
|
135
|
+
),
|
|
105
136
|
);
|
|
106
137
|
if (matches.length > 1) {
|
|
107
138
|
console.warn(
|
|
@@ -111,7 +142,7 @@ function parseScriptSpec(rawSpec, defaultKind) {
|
|
|
111
142
|
return { kind: matches[0] || sourceKinds["js-code-nodes"], scriptCode };
|
|
112
143
|
}
|
|
113
144
|
|
|
114
|
-
function
|
|
145
|
+
function resolveBuildSelection() {
|
|
115
146
|
const sourceArgs = readArgs("source");
|
|
116
147
|
const sourceNames = [...new Set(sourceArgs)];
|
|
117
148
|
if (sourceNames.length > 1) throw new Error("--source may only select one source kind");
|
|
@@ -128,7 +159,10 @@ function resolveBuildTargets() {
|
|
|
128
159
|
: (selectedKind ? [selectedKind] : Object.values(sourceKinds)).flatMap((kind) =>
|
|
129
160
|
listScriptCodes(kind).map((scriptCode) => ({ kind, scriptCode })),
|
|
130
161
|
);
|
|
131
|
-
return
|
|
162
|
+
return {
|
|
163
|
+
scoped: specs.length > 0,
|
|
164
|
+
targets: [...new Map(targets.map((target) => [`${target.kind.name}/${target.scriptCode}`, target])).values()],
|
|
165
|
+
};
|
|
132
166
|
}
|
|
133
167
|
|
|
134
168
|
function packageVersion(name) {
|
|
@@ -144,6 +178,7 @@ const toolFingerprint = crypto
|
|
|
144
178
|
.update(
|
|
145
179
|
JSON.stringify({
|
|
146
180
|
cacheVersion: CACHE_VERSION,
|
|
181
|
+
builder: builderFingerprint,
|
|
147
182
|
node: process.version,
|
|
148
183
|
platform: process.platform,
|
|
149
184
|
arch: process.arch,
|
|
@@ -157,14 +192,25 @@ const toolFingerprint = crypto
|
|
|
157
192
|
.digest("hex");
|
|
158
193
|
|
|
159
194
|
function emptyCache() {
|
|
160
|
-
return {
|
|
195
|
+
return {
|
|
196
|
+
version: CACHE_VERSION,
|
|
197
|
+
toolFingerprint,
|
|
198
|
+
typecheck: null,
|
|
199
|
+
typechecks: {},
|
|
200
|
+
targets: {},
|
|
201
|
+
};
|
|
161
202
|
}
|
|
162
203
|
|
|
163
204
|
function readCache(file = CACHE_FILE) {
|
|
164
205
|
try {
|
|
165
206
|
const cache = JSON.parse(readFileSync(file, "utf8"));
|
|
166
207
|
if (cache.version !== CACHE_VERSION) return emptyCache();
|
|
167
|
-
return {
|
|
208
|
+
return {
|
|
209
|
+
...emptyCache(),
|
|
210
|
+
...cache,
|
|
211
|
+
typechecks: cache.typechecks || {},
|
|
212
|
+
targets: cache.targets || {},
|
|
213
|
+
};
|
|
168
214
|
} catch {
|
|
169
215
|
return emptyCache();
|
|
170
216
|
}
|
|
@@ -231,7 +277,7 @@ function typecheckInputHash() {
|
|
|
231
277
|
}
|
|
232
278
|
|
|
233
279
|
function targetInputHash(target, dependencies) {
|
|
234
|
-
const entry = relativeWorkspaceFile(
|
|
280
|
+
const entry = relativeWorkspaceFile(targetEntryFile(target));
|
|
235
281
|
return hashFileSet(
|
|
236
282
|
[...configInputFiles(), entry, ...(dependencies || [])].filter(Boolean),
|
|
237
283
|
`target:${target.kind.name}/${target.scriptCode}`,
|
|
@@ -258,11 +304,94 @@ function isTargetCacheHit(cache, target) {
|
|
|
258
304
|
);
|
|
259
305
|
}
|
|
260
306
|
|
|
261
|
-
function
|
|
307
|
+
function formatTypeScriptDiagnostics(diagnostics) {
|
|
308
|
+
return ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
309
|
+
getCanonicalFileName: (file) => file,
|
|
310
|
+
getCurrentDirectory: () => rootDir,
|
|
311
|
+
getNewLine: () => "\n",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function scopedTypecheckKey(targets) {
|
|
316
|
+
return targets
|
|
317
|
+
.map((target) => `${target.kind.name}/${target.scriptCode}`)
|
|
318
|
+
.sort()
|
|
319
|
+
.join(",");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function scopedAmbientDeclarationFiles() {
|
|
323
|
+
return walkFiles(path.join(rootDir, "src"))
|
|
324
|
+
.filter((file) => file.endsWith(".d.ts"))
|
|
325
|
+
.map(relativeWorkspaceFile)
|
|
326
|
+
.filter(Boolean);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function createScopedTypecheckProgram(targets) {
|
|
330
|
+
const loaded = ts.readConfigFile(TSCONFIG_FILE, ts.sys.readFile);
|
|
331
|
+
if (loaded.error) {
|
|
332
|
+
process.stderr.write(formatTypeScriptDiagnostics([loaded.error]));
|
|
333
|
+
throw new Error("JS_CODE scoped TypeScript config validation failed");
|
|
334
|
+
}
|
|
335
|
+
const files = [
|
|
336
|
+
...targets.map((target) =>
|
|
337
|
+
relativeWorkspaceFile(targetEntryFile(target)),
|
|
338
|
+
),
|
|
339
|
+
...scopedAmbientDeclarationFiles(),
|
|
340
|
+
].filter(Boolean);
|
|
341
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
342
|
+
{ ...loaded.config, files, include: [] },
|
|
343
|
+
ts.sys,
|
|
344
|
+
rootDir,
|
|
345
|
+
{ noEmit: true },
|
|
346
|
+
TSCONFIG_FILE,
|
|
347
|
+
);
|
|
348
|
+
if (parsed.errors.length > 0) {
|
|
349
|
+
process.stderr.write(formatTypeScriptDiagnostics(parsed.errors));
|
|
350
|
+
throw new Error("JS_CODE scoped TypeScript config validation failed");
|
|
351
|
+
}
|
|
352
|
+
return ts.createProgram({
|
|
353
|
+
rootNames: parsed.fileNames,
|
|
354
|
+
options: parsed.options,
|
|
355
|
+
projectReferences: parsed.projectReferences,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function scopedTypecheckInputHash(program, cacheKey) {
|
|
360
|
+
const sourceFiles = program
|
|
361
|
+
.getSourceFiles()
|
|
362
|
+
.map((sourceFile) => relativeWorkspaceFile(sourceFile.fileName))
|
|
363
|
+
.filter(Boolean);
|
|
364
|
+
return hashFileSet([...configInputFiles(), ...sourceFiles], `typecheck:${cacheKey}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function typecheckSelectedScripts(cache, targets) {
|
|
368
|
+
const cacheKey = scopedTypecheckKey(targets);
|
|
369
|
+
const program = createScopedTypecheckProgram(targets);
|
|
370
|
+
const inputHash = scopedTypecheckInputHash(program, cacheKey);
|
|
371
|
+
if (
|
|
372
|
+
!forceBuild &&
|
|
373
|
+
cache.toolFingerprint === toolFingerprint &&
|
|
374
|
+
cache.typechecks?.[cacheKey]?.inputHash === inputHash
|
|
375
|
+
) {
|
|
376
|
+
console.log(`[build-js-code] scoped TypeScript validation cache hit (${targets.length} selected)`);
|
|
377
|
+
return { cacheHit: true, cacheKey, inputHash, mode: "scoped" };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
console.log(`[build-js-code] validating TypeScript for ${targets.length} selected target(s)...`);
|
|
381
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
382
|
+
if (diagnostics.length > 0) {
|
|
383
|
+
process.stderr.write(formatTypeScriptDiagnostics(diagnostics));
|
|
384
|
+
throw new Error("JS_CODE scoped TypeScript validation failed");
|
|
385
|
+
}
|
|
386
|
+
console.log("[build-js-code] scoped TypeScript validation passed");
|
|
387
|
+
return { cacheHit: false, cacheKey, inputHash, mode: "scoped" };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function typecheckAllScripts(cache) {
|
|
262
391
|
const inputHash = typecheckInputHash();
|
|
263
392
|
if (!forceBuild && cache.toolFingerprint === toolFingerprint && cache.typecheck?.inputHash === inputHash) {
|
|
264
393
|
console.log("[build-js-code] TypeScript validation cache hit");
|
|
265
|
-
return { cacheHit: true, inputHash };
|
|
394
|
+
return { cacheHit: true, inputHash, mode: "full" };
|
|
266
395
|
}
|
|
267
396
|
|
|
268
397
|
console.log("[build-js-code] validating TypeScript once for this batch...");
|
|
@@ -275,7 +404,13 @@ function typecheckScripts(cache) {
|
|
|
275
404
|
console.log("[build-js-code] TypeScript validation passed");
|
|
276
405
|
// pnpm may materialize/update a lockfile on the first invocation. Record the
|
|
277
406
|
// post-validation inputs so the next process can reuse this successful tsc.
|
|
278
|
-
return { cacheHit: false, inputHash: typecheckInputHash() };
|
|
407
|
+
return { cacheHit: false, inputHash: typecheckInputHash(), mode: "full" };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function typecheckScripts(cache, targets, scoped) {
|
|
411
|
+
return scoped
|
|
412
|
+
? typecheckSelectedScripts(cache, targets)
|
|
413
|
+
: typecheckAllScripts(cache);
|
|
279
414
|
}
|
|
280
415
|
|
|
281
416
|
function collectBuildDependencies(buildResult, entry) {
|
|
@@ -296,7 +431,7 @@ function collectBuildDependencies(buildResult, entry) {
|
|
|
296
431
|
|
|
297
432
|
async function buildScript(target) {
|
|
298
433
|
const { kind, scriptCode } = target;
|
|
299
|
-
const entry =
|
|
434
|
+
const entry = targetEntryFile(target);
|
|
300
435
|
if (!existsSync(entry)) throw new Error(`${kind.label} script not found: ${entry}`);
|
|
301
436
|
const outDir = path.join(kind.outputRoot, scriptCode);
|
|
302
437
|
const external = Array.from(
|
|
@@ -361,14 +496,31 @@ function saveCache(typecheck, updatedTargets) {
|
|
|
361
496
|
if (typecheck.cacheHit && updatedTargets.size === 0) return;
|
|
362
497
|
const lockFd = acquireCacheLock();
|
|
363
498
|
try {
|
|
364
|
-
const
|
|
499
|
+
const stored = readCache();
|
|
500
|
+
// A scoped build updates only part of the cache. Never carry metadata from
|
|
501
|
+
// a different compiler/builder fingerprint into the new cache, otherwise
|
|
502
|
+
// untouched targets could become false hits after this process stamps the
|
|
503
|
+
// cache with the current fingerprint.
|
|
504
|
+
const latest = stored.toolFingerprint === toolFingerprint
|
|
505
|
+
? stored
|
|
506
|
+
: emptyCache();
|
|
507
|
+
const validatedAt = new Date().toISOString();
|
|
508
|
+
const mergedTypechecks = { ...latest.typechecks };
|
|
509
|
+
if (!typecheck.cacheHit && typecheck.mode === "scoped") {
|
|
510
|
+
mergedTypechecks[typecheck.cacheKey] = {
|
|
511
|
+
inputHash: typecheck.inputHash,
|
|
512
|
+
validatedAt,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
365
515
|
const merged = {
|
|
366
516
|
...latest,
|
|
367
517
|
version: CACHE_VERSION,
|
|
368
518
|
toolFingerprint,
|
|
369
|
-
typecheck:
|
|
370
|
-
|
|
371
|
-
|
|
519
|
+
typecheck:
|
|
520
|
+
!typecheck.cacheHit && typecheck.mode === "full"
|
|
521
|
+
? { inputHash: typecheck.inputHash, validatedAt }
|
|
522
|
+
: latest.typecheck,
|
|
523
|
+
typechecks: mergedTypechecks,
|
|
372
524
|
targets: { ...latest.targets, ...Object.fromEntries(updatedTargets) },
|
|
373
525
|
};
|
|
374
526
|
const tempFile = `${CACHE_FILE}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
@@ -398,7 +550,7 @@ function saveCache(typecheck, updatedTargets) {
|
|
|
398
550
|
}
|
|
399
551
|
}
|
|
400
552
|
|
|
401
|
-
const targets =
|
|
553
|
+
const { scoped, targets } = resolveBuildSelection();
|
|
402
554
|
if (targets.length === 0) {
|
|
403
555
|
console.log(
|
|
404
556
|
"no JS_CODE scripts found under src/js-code-nodes/<scriptCode>/index.ts, src/automations/<scriptCode>/index.ts, or src/functions/<functionCode>/index.ts",
|
|
@@ -407,7 +559,7 @@ if (targets.length === 0) {
|
|
|
407
559
|
}
|
|
408
560
|
|
|
409
561
|
const cache = readCache();
|
|
410
|
-
const typecheck = typecheckScripts(cache);
|
|
562
|
+
const typecheck = typecheckScripts(cache, targets, scoped);
|
|
411
563
|
const updatedTargets = new Map();
|
|
412
564
|
let builtCount = 0;
|
|
413
565
|
let cachedCount = 0;
|
|
@@ -27,6 +27,8 @@ Commands: `openxiangda resource validate|plan|publish <type> --only <codes> --pr
|
|
|
27
27
|
|
|
28
28
|
`resource plan` and publish dry-runs are strictly GET/HEAD-only. On `READ_ONLY_AUTH_REQUIRED`, run `openxiangda auth refresh --profile <name>` or log in again before retrying; never refresh inside the plan.
|
|
29
29
|
|
|
30
|
+
Exact `--only/--code` selectors apply before manifest/source analysis and JS_CODE build. Touch only selected targets plus transitive/shared/ambient dependencies; omit selectors only for intentional full-workspace work.
|
|
31
|
+
|
|
30
32
|
When a Function/Automation enters scope only through source changes, publishing uses a server-side source-field PATCH and preserves online bindings, contracts, metadata, trigger/view configuration, and enabled/published state. Whole-manifest replacement requires `--replace-manifest --reason "..."`. Formal promotion runs `release begin --change` (Git + remote baseline, whole-set preflight) through `release end`; `SOURCE_BASE_DIVERGED` and `RESOURCE_FIELD_CONFLICT` require reconciliation and a new plan.
|
|
31
33
|
|
|
32
34
|
Before editing `roles`, `permissions/page-groups`, or `permissions/form-groups` for account/role/data-scope/RBAC/query-param authorization work, run `openxiangda design gates --topic permissions --json`, choose the permission mode, and write the permission matrix.
|
|
@@ -45,6 +45,8 @@ Function/Automation 仅因源码变化进入 scope 时,默认通过服务端
|
|
|
45
45
|
|
|
46
46
|
`resource plan` 与 publish dry-run 严格只允许 GET/HEAD。遇到 `READ_ONLY_AUTH_REQUIRED` 时,先执行 `openxiangda auth refresh --profile <name>` 或重新登录再重试;不得在 plan 内自动 POST 刷新 token。
|
|
47
47
|
|
|
48
|
+
精确 `--only/--code` 会在 manifest/source 分析与 JS_CODE 构建前收窄,只触碰目标及其传递/shared/ambient 依赖;只有明确的全工作区任务才省略 selector。
|
|
49
|
+
|
|
48
50
|
## 严禁
|
|
49
51
|
|
|
50
52
|
- ❌ 把 `formUuid` / `pageId` / `workflowId` 等平台 ID 直接写进 manifest(CLI 解析逻辑 code)。
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
|
|
87
87
|
## JS_CODE 批量构建
|
|
88
88
|
|
|
89
|
-
`pnpm build-js-code --scripts functions:a,functions:b,automations:c` 可在一次进程中构建多个 App Function / Automation / JS_CODE 目标;也兼容 `--script a --script b --source functions
|
|
89
|
+
`pnpm build-js-code --scripts functions:a,functions:b,automations:c` 可在一次进程中构建多个 App Function / Automation / JS_CODE 目标;也兼容 `--script a --script b --source functions`。显式 selector 只校验和构建选中入口及其传递/shared/ambient 依赖;不带 selector 才保持全工作区 TypeScript 校验。`.openxiangda/build-cache.json` 分 scoped target 记录源码、实际共享依赖、构建配置、Node/Vite/TypeScript 版本及产物校验和,不能只因 `dist/**/index.cjs` 存在就视为有效;强制重建使用 `--force`。
|
|
90
90
|
|
|
91
91
|
## 工作区结构速查
|
|
92
92
|
|
|
@@ -19,13 +19,29 @@ import path from "node:path";
|
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { build } from "vite";
|
|
21
21
|
|
|
22
|
-
const rootDir = fileURLToPath(new URL("..", import.meta.url));
|
|
23
22
|
const scriptFile = fileURLToPath(import.meta.url);
|
|
23
|
+
const defaultRootDir = fileURLToPath(new URL("..", import.meta.url));
|
|
24
|
+
const rootDir = path.resolve(process.env.OPENXIANGDA_WORKSPACE_ROOT || defaultRootDir);
|
|
24
25
|
const args = process.argv.slice(2);
|
|
25
26
|
const require = createRequire(import.meta.url);
|
|
26
|
-
const
|
|
27
|
-
const
|
|
27
|
+
const ts = require("typescript");
|
|
28
|
+
const CACHE_VERSION = 3;
|
|
29
|
+
const builderFingerprint = crypto
|
|
30
|
+
.createHash("sha256")
|
|
31
|
+
.update(readFileSync(scriptFile))
|
|
32
|
+
.digest("hex");
|
|
33
|
+
const workspaceBuilderFile = path.join(rootDir, "scripts", "build-js-code.mjs");
|
|
34
|
+
const workspaceBuilderMatches =
|
|
35
|
+
existsSync(workspaceBuilderFile) &&
|
|
36
|
+
crypto.createHash("sha256").update(readFileSync(workspaceBuilderFile)).digest("hex") ===
|
|
37
|
+
builderFingerprint;
|
|
38
|
+
const cacheFileName =
|
|
39
|
+
path.resolve(defaultRootDir) !== rootDir && !workspaceBuilderMatches
|
|
40
|
+
? "build-cache.cli-v3.json"
|
|
41
|
+
: "build-cache.json";
|
|
42
|
+
const CACHE_FILE = path.join(rootDir, ".openxiangda", cacheFileName);
|
|
28
43
|
const CACHE_LOCK_FILE = `${CACHE_FILE}.lock`;
|
|
44
|
+
const TSCONFIG_FILE = path.join(rootDir, "tsconfig.js-code-nodes.json");
|
|
29
45
|
const forceBuild = args.includes("--force") || args.includes("--no-cache");
|
|
30
46
|
|
|
31
47
|
const sourceKinds = {
|
|
@@ -84,11 +100,24 @@ function assertScriptCode(scriptCode) {
|
|
|
84
100
|
function listScriptCodes(kind) {
|
|
85
101
|
if (!existsSync(kind.sourceRoot)) return [];
|
|
86
102
|
return readdirSync(kind.sourceRoot, { withFileTypes: true })
|
|
87
|
-
.filter(
|
|
103
|
+
.filter(
|
|
104
|
+
(entry) =>
|
|
105
|
+
entry.isDirectory() &&
|
|
106
|
+
["index.ts", "index.tsx"].some((name) =>
|
|
107
|
+
existsSync(path.join(kind.sourceRoot, entry.name, name)),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
88
110
|
.map((entry) => entry.name)
|
|
89
111
|
.sort();
|
|
90
112
|
}
|
|
91
113
|
|
|
114
|
+
function targetEntryFile(target) {
|
|
115
|
+
const base = path.join(target.kind.sourceRoot, target.scriptCode);
|
|
116
|
+
return ["index.ts", "index.tsx"]
|
|
117
|
+
.map((name) => path.join(base, name))
|
|
118
|
+
.find((file) => existsSync(file)) || path.join(base, "index.ts");
|
|
119
|
+
}
|
|
120
|
+
|
|
92
121
|
function parseScriptSpec(rawSpec, defaultKind) {
|
|
93
122
|
const prefixed = String(rawSpec).match(/^(js-code-nodes|automations|functions)[:/](.+)$/);
|
|
94
123
|
const requestedKind = prefixed?.[1] || defaultKind?.name;
|
|
@@ -101,7 +130,9 @@ function parseScriptSpec(rawSpec, defaultKind) {
|
|
|
101
130
|
if (requestedKind) return { kind: sourceKinds[requestedKind], scriptCode };
|
|
102
131
|
|
|
103
132
|
const matches = Object.values(sourceKinds).filter((kind) =>
|
|
104
|
-
|
|
133
|
+
["index.ts", "index.tsx"].some((name) =>
|
|
134
|
+
existsSync(path.join(kind.sourceRoot, scriptCode, name)),
|
|
135
|
+
),
|
|
105
136
|
);
|
|
106
137
|
if (matches.length > 1) {
|
|
107
138
|
console.warn(
|
|
@@ -111,7 +142,7 @@ function parseScriptSpec(rawSpec, defaultKind) {
|
|
|
111
142
|
return { kind: matches[0] || sourceKinds["js-code-nodes"], scriptCode };
|
|
112
143
|
}
|
|
113
144
|
|
|
114
|
-
function
|
|
145
|
+
function resolveBuildSelection() {
|
|
115
146
|
const sourceArgs = readArgs("source");
|
|
116
147
|
const sourceNames = [...new Set(sourceArgs)];
|
|
117
148
|
if (sourceNames.length > 1) throw new Error("--source may only select one source kind");
|
|
@@ -128,7 +159,10 @@ function resolveBuildTargets() {
|
|
|
128
159
|
: (selectedKind ? [selectedKind] : Object.values(sourceKinds)).flatMap((kind) =>
|
|
129
160
|
listScriptCodes(kind).map((scriptCode) => ({ kind, scriptCode })),
|
|
130
161
|
);
|
|
131
|
-
return
|
|
162
|
+
return {
|
|
163
|
+
scoped: specs.length > 0,
|
|
164
|
+
targets: [...new Map(targets.map((target) => [`${target.kind.name}/${target.scriptCode}`, target])).values()],
|
|
165
|
+
};
|
|
132
166
|
}
|
|
133
167
|
|
|
134
168
|
function packageVersion(name) {
|
|
@@ -144,6 +178,7 @@ const toolFingerprint = crypto
|
|
|
144
178
|
.update(
|
|
145
179
|
JSON.stringify({
|
|
146
180
|
cacheVersion: CACHE_VERSION,
|
|
181
|
+
builder: builderFingerprint,
|
|
147
182
|
node: process.version,
|
|
148
183
|
platform: process.platform,
|
|
149
184
|
arch: process.arch,
|
|
@@ -157,14 +192,25 @@ const toolFingerprint = crypto
|
|
|
157
192
|
.digest("hex");
|
|
158
193
|
|
|
159
194
|
function emptyCache() {
|
|
160
|
-
return {
|
|
195
|
+
return {
|
|
196
|
+
version: CACHE_VERSION,
|
|
197
|
+
toolFingerprint,
|
|
198
|
+
typecheck: null,
|
|
199
|
+
typechecks: {},
|
|
200
|
+
targets: {},
|
|
201
|
+
};
|
|
161
202
|
}
|
|
162
203
|
|
|
163
204
|
function readCache(file = CACHE_FILE) {
|
|
164
205
|
try {
|
|
165
206
|
const cache = JSON.parse(readFileSync(file, "utf8"));
|
|
166
207
|
if (cache.version !== CACHE_VERSION) return emptyCache();
|
|
167
|
-
return {
|
|
208
|
+
return {
|
|
209
|
+
...emptyCache(),
|
|
210
|
+
...cache,
|
|
211
|
+
typechecks: cache.typechecks || {},
|
|
212
|
+
targets: cache.targets || {},
|
|
213
|
+
};
|
|
168
214
|
} catch {
|
|
169
215
|
return emptyCache();
|
|
170
216
|
}
|
|
@@ -231,7 +277,7 @@ function typecheckInputHash() {
|
|
|
231
277
|
}
|
|
232
278
|
|
|
233
279
|
function targetInputHash(target, dependencies) {
|
|
234
|
-
const entry = relativeWorkspaceFile(
|
|
280
|
+
const entry = relativeWorkspaceFile(targetEntryFile(target));
|
|
235
281
|
return hashFileSet(
|
|
236
282
|
[...configInputFiles(), entry, ...(dependencies || [])].filter(Boolean),
|
|
237
283
|
`target:${target.kind.name}/${target.scriptCode}`,
|
|
@@ -258,11 +304,94 @@ function isTargetCacheHit(cache, target) {
|
|
|
258
304
|
);
|
|
259
305
|
}
|
|
260
306
|
|
|
261
|
-
function
|
|
307
|
+
function formatTypeScriptDiagnostics(diagnostics) {
|
|
308
|
+
return ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
309
|
+
getCanonicalFileName: (file) => file,
|
|
310
|
+
getCurrentDirectory: () => rootDir,
|
|
311
|
+
getNewLine: () => "\n",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function scopedTypecheckKey(targets) {
|
|
316
|
+
return targets
|
|
317
|
+
.map((target) => `${target.kind.name}/${target.scriptCode}`)
|
|
318
|
+
.sort()
|
|
319
|
+
.join(",");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function scopedAmbientDeclarationFiles() {
|
|
323
|
+
return walkFiles(path.join(rootDir, "src"))
|
|
324
|
+
.filter((file) => file.endsWith(".d.ts"))
|
|
325
|
+
.map(relativeWorkspaceFile)
|
|
326
|
+
.filter(Boolean);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function createScopedTypecheckProgram(targets) {
|
|
330
|
+
const loaded = ts.readConfigFile(TSCONFIG_FILE, ts.sys.readFile);
|
|
331
|
+
if (loaded.error) {
|
|
332
|
+
process.stderr.write(formatTypeScriptDiagnostics([loaded.error]));
|
|
333
|
+
throw new Error("JS_CODE scoped TypeScript config validation failed");
|
|
334
|
+
}
|
|
335
|
+
const files = [
|
|
336
|
+
...targets.map((target) =>
|
|
337
|
+
relativeWorkspaceFile(targetEntryFile(target)),
|
|
338
|
+
),
|
|
339
|
+
...scopedAmbientDeclarationFiles(),
|
|
340
|
+
].filter(Boolean);
|
|
341
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
342
|
+
{ ...loaded.config, files, include: [] },
|
|
343
|
+
ts.sys,
|
|
344
|
+
rootDir,
|
|
345
|
+
{ noEmit: true },
|
|
346
|
+
TSCONFIG_FILE,
|
|
347
|
+
);
|
|
348
|
+
if (parsed.errors.length > 0) {
|
|
349
|
+
process.stderr.write(formatTypeScriptDiagnostics(parsed.errors));
|
|
350
|
+
throw new Error("JS_CODE scoped TypeScript config validation failed");
|
|
351
|
+
}
|
|
352
|
+
return ts.createProgram({
|
|
353
|
+
rootNames: parsed.fileNames,
|
|
354
|
+
options: parsed.options,
|
|
355
|
+
projectReferences: parsed.projectReferences,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function scopedTypecheckInputHash(program, cacheKey) {
|
|
360
|
+
const sourceFiles = program
|
|
361
|
+
.getSourceFiles()
|
|
362
|
+
.map((sourceFile) => relativeWorkspaceFile(sourceFile.fileName))
|
|
363
|
+
.filter(Boolean);
|
|
364
|
+
return hashFileSet([...configInputFiles(), ...sourceFiles], `typecheck:${cacheKey}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function typecheckSelectedScripts(cache, targets) {
|
|
368
|
+
const cacheKey = scopedTypecheckKey(targets);
|
|
369
|
+
const program = createScopedTypecheckProgram(targets);
|
|
370
|
+
const inputHash = scopedTypecheckInputHash(program, cacheKey);
|
|
371
|
+
if (
|
|
372
|
+
!forceBuild &&
|
|
373
|
+
cache.toolFingerprint === toolFingerprint &&
|
|
374
|
+
cache.typechecks?.[cacheKey]?.inputHash === inputHash
|
|
375
|
+
) {
|
|
376
|
+
console.log(`[build-js-code] scoped TypeScript validation cache hit (${targets.length} selected)`);
|
|
377
|
+
return { cacheHit: true, cacheKey, inputHash, mode: "scoped" };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
console.log(`[build-js-code] validating TypeScript for ${targets.length} selected target(s)...`);
|
|
381
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
382
|
+
if (diagnostics.length > 0) {
|
|
383
|
+
process.stderr.write(formatTypeScriptDiagnostics(diagnostics));
|
|
384
|
+
throw new Error("JS_CODE scoped TypeScript validation failed");
|
|
385
|
+
}
|
|
386
|
+
console.log("[build-js-code] scoped TypeScript validation passed");
|
|
387
|
+
return { cacheHit: false, cacheKey, inputHash, mode: "scoped" };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function typecheckAllScripts(cache) {
|
|
262
391
|
const inputHash = typecheckInputHash();
|
|
263
392
|
if (!forceBuild && cache.toolFingerprint === toolFingerprint && cache.typecheck?.inputHash === inputHash) {
|
|
264
393
|
console.log("[build-js-code] TypeScript validation cache hit");
|
|
265
|
-
return { cacheHit: true, inputHash };
|
|
394
|
+
return { cacheHit: true, inputHash, mode: "full" };
|
|
266
395
|
}
|
|
267
396
|
|
|
268
397
|
console.log("[build-js-code] validating TypeScript once for this batch...");
|
|
@@ -275,7 +404,13 @@ function typecheckScripts(cache) {
|
|
|
275
404
|
console.log("[build-js-code] TypeScript validation passed");
|
|
276
405
|
// pnpm may materialize/update a lockfile on the first invocation. Record the
|
|
277
406
|
// post-validation inputs so the next process can reuse this successful tsc.
|
|
278
|
-
return { cacheHit: false, inputHash: typecheckInputHash() };
|
|
407
|
+
return { cacheHit: false, inputHash: typecheckInputHash(), mode: "full" };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function typecheckScripts(cache, targets, scoped) {
|
|
411
|
+
return scoped
|
|
412
|
+
? typecheckSelectedScripts(cache, targets)
|
|
413
|
+
: typecheckAllScripts(cache);
|
|
279
414
|
}
|
|
280
415
|
|
|
281
416
|
function collectBuildDependencies(buildResult, entry) {
|
|
@@ -296,7 +431,7 @@ function collectBuildDependencies(buildResult, entry) {
|
|
|
296
431
|
|
|
297
432
|
async function buildScript(target) {
|
|
298
433
|
const { kind, scriptCode } = target;
|
|
299
|
-
const entry =
|
|
434
|
+
const entry = targetEntryFile(target);
|
|
300
435
|
if (!existsSync(entry)) throw new Error(`${kind.label} script not found: ${entry}`);
|
|
301
436
|
const outDir = path.join(kind.outputRoot, scriptCode);
|
|
302
437
|
const external = Array.from(
|
|
@@ -361,14 +496,31 @@ function saveCache(typecheck, updatedTargets) {
|
|
|
361
496
|
if (typecheck.cacheHit && updatedTargets.size === 0) return;
|
|
362
497
|
const lockFd = acquireCacheLock();
|
|
363
498
|
try {
|
|
364
|
-
const
|
|
499
|
+
const stored = readCache();
|
|
500
|
+
// A scoped build updates only part of the cache. Never carry metadata from
|
|
501
|
+
// a different compiler/builder fingerprint into the new cache, otherwise
|
|
502
|
+
// untouched targets could become false hits after this process stamps the
|
|
503
|
+
// cache with the current fingerprint.
|
|
504
|
+
const latest = stored.toolFingerprint === toolFingerprint
|
|
505
|
+
? stored
|
|
506
|
+
: emptyCache();
|
|
507
|
+
const validatedAt = new Date().toISOString();
|
|
508
|
+
const mergedTypechecks = { ...latest.typechecks };
|
|
509
|
+
if (!typecheck.cacheHit && typecheck.mode === "scoped") {
|
|
510
|
+
mergedTypechecks[typecheck.cacheKey] = {
|
|
511
|
+
inputHash: typecheck.inputHash,
|
|
512
|
+
validatedAt,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
365
515
|
const merged = {
|
|
366
516
|
...latest,
|
|
367
517
|
version: CACHE_VERSION,
|
|
368
518
|
toolFingerprint,
|
|
369
|
-
typecheck:
|
|
370
|
-
|
|
371
|
-
|
|
519
|
+
typecheck:
|
|
520
|
+
!typecheck.cacheHit && typecheck.mode === "full"
|
|
521
|
+
? { inputHash: typecheck.inputHash, validatedAt }
|
|
522
|
+
: latest.typecheck,
|
|
523
|
+
typechecks: mergedTypechecks,
|
|
372
524
|
targets: { ...latest.targets, ...Object.fromEntries(updatedTargets) },
|
|
373
525
|
};
|
|
374
526
|
const tempFile = `${CACHE_FILE}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
@@ -398,7 +550,7 @@ function saveCache(typecheck, updatedTargets) {
|
|
|
398
550
|
}
|
|
399
551
|
}
|
|
400
552
|
|
|
401
|
-
const targets =
|
|
553
|
+
const { scoped, targets } = resolveBuildSelection();
|
|
402
554
|
if (targets.length === 0) {
|
|
403
555
|
console.log(
|
|
404
556
|
"no JS_CODE scripts found under src/js-code-nodes/<scriptCode>/index.ts, src/automations/<scriptCode>/index.ts, or src/functions/<functionCode>/index.ts",
|
|
@@ -407,7 +559,7 @@ if (targets.length === 0) {
|
|
|
407
559
|
}
|
|
408
560
|
|
|
409
561
|
const cache = readCache();
|
|
410
|
-
const typecheck = typecheckScripts(cache);
|
|
562
|
+
const typecheck = typecheckScripts(cache, targets, scoped);
|
|
411
563
|
const updatedTargets = new Map();
|
|
412
564
|
let builtCount = 0;
|
|
413
565
|
let cachedCount = 0;
|