openxiangda 1.0.231 → 1.0.232
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 +27 -2
- package/lib/cli.js +219 -29
- package/lib/delivery-v2-cli.js +216 -0
- package/lib/delivery-v2-executor.js +955 -0
- package/lib/delivery-v2-package.js +681 -0
- package/lib/policy.js +4 -0
- package/lib/workspace-bootstrap.js +1 -0
- package/lib/workspace-init.js +4 -20
- package/openxiangda-skills/SKILL.md +21 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +17 -0
- package/package.json +4 -2
- package/packages/sdk/dist/build/index.cjs.map +1 -1
- package/packages/sdk/dist/build/index.d.mts +1 -0
- package/packages/sdk/dist/build/index.d.ts +1 -0
- package/packages/sdk/dist/build/index.mjs.map +1 -1
- package/policy/openxiangda-policy.json +8 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +6 -5
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +6 -6
- package/templates/openxiangda-react-spa/AGENTS.md +5 -1
- package/templates/openxiangda-react-spa/DELIVERY.md +33 -0
- package/templates/openxiangda-react-spa/app-workspace.config.ts +3 -3
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +4 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +4 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +7 -4
- package/templates/sy-lowcode-app-workspace/DELIVERY.md +33 -0
- package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +3 -3
- package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +1 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const PACKAGE_SCHEMA_VERSION = 'openxiangda-app-package-v2';
|
|
7
|
+
const COMPILER_VERSION = 'delivery-v2.0';
|
|
8
|
+
const MAX_RUNTIME_BYTES = 25 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
const ROOT_SOURCE_FILES = new Set([
|
|
11
|
+
'app-workspace.config.ts',
|
|
12
|
+
'index.html',
|
|
13
|
+
'package.json',
|
|
14
|
+
'package-lock.json',
|
|
15
|
+
'pnpm-lock.yaml',
|
|
16
|
+
'yarn.lock',
|
|
17
|
+
'postcss.config.cjs',
|
|
18
|
+
'postcss.config.js',
|
|
19
|
+
'tailwind.config.cjs',
|
|
20
|
+
'tailwind.config.js',
|
|
21
|
+
'tsconfig.app.json',
|
|
22
|
+
'tsconfig.json',
|
|
23
|
+
'tsconfig.node.json',
|
|
24
|
+
'vite.config.js',
|
|
25
|
+
'vite.config.mjs',
|
|
26
|
+
'vite.config.ts',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const IGNORED_SEGMENTS = new Set([
|
|
30
|
+
'.git',
|
|
31
|
+
'.openxiangda',
|
|
32
|
+
'.turbo',
|
|
33
|
+
'coverage',
|
|
34
|
+
'dist',
|
|
35
|
+
'node_modules',
|
|
36
|
+
'openspec',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const LAYER_ORDER = [
|
|
40
|
+
'source',
|
|
41
|
+
'form',
|
|
42
|
+
'configuration',
|
|
43
|
+
'backend',
|
|
44
|
+
'workflow',
|
|
45
|
+
'runtime',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const CONFIG_RESOURCE_TYPES = Object.freeze({
|
|
49
|
+
'src/resources/roles/': 'role',
|
|
50
|
+
'src/resources/connectors/': 'connector',
|
|
51
|
+
'src/resources/notifications/': 'notification',
|
|
52
|
+
'src/resources/menus/': 'menu',
|
|
53
|
+
'src/resources/data-views/': 'data-view',
|
|
54
|
+
'src/resources/storage/': 'storage',
|
|
55
|
+
'src/resources/auth/': 'auth-config',
|
|
56
|
+
'src/resources/routes/': 'route',
|
|
57
|
+
'src/resources/public-access/': 'public-access',
|
|
58
|
+
'src/resources/permissions/page-groups/': 'page-permission-group',
|
|
59
|
+
'src/resources/permissions/form-groups/': 'form-permission-group',
|
|
60
|
+
'src/resources/permissions/scope-dimensions/': 'scope-dimension',
|
|
61
|
+
'src/resources/permissions/scope-grant-sources/': 'scope-grant-source',
|
|
62
|
+
'src/resources/permissions/data-scope-policies/': 'data-scope-policy',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function canonicalize(value) {
|
|
66
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
67
|
+
if (value && typeof value === 'object') {
|
|
68
|
+
return Object.keys(value)
|
|
69
|
+
.sort()
|
|
70
|
+
.reduce((result, key) => {
|
|
71
|
+
if (value[key] !== undefined) result[key] = canonicalize(value[key]);
|
|
72
|
+
return result;
|
|
73
|
+
}, {});
|
|
74
|
+
}
|
|
75
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
76
|
+
return String(value);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function canonicalJson(value) {
|
|
82
|
+
return JSON.stringify(canonicalize(value));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sha256(buffer) {
|
|
86
|
+
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sha256Canonical(value) {
|
|
90
|
+
return sha256(Buffer.from(canonicalJson(value)));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizePath(value) {
|
|
94
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isSecretFile(relativePath) {
|
|
98
|
+
const normalized = normalizePath(relativePath);
|
|
99
|
+
const base = path.posix.basename(normalized);
|
|
100
|
+
if (base === '.env.example') return false;
|
|
101
|
+
return (
|
|
102
|
+
base === '.env' ||
|
|
103
|
+
base.startsWith('.env.') ||
|
|
104
|
+
/\.(pem|p12|pfx|key)$/i.test(base)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function shouldIncludeSourceFile(relativePath) {
|
|
109
|
+
const normalized = normalizePath(relativePath);
|
|
110
|
+
if (!normalized || isSecretFile(normalized)) return false;
|
|
111
|
+
const segments = normalized.split('/');
|
|
112
|
+
if (segments.some(segment => IGNORED_SEGMENTS.has(segment))) return false;
|
|
113
|
+
if (ROOT_SOURCE_FILES.has(normalized)) return true;
|
|
114
|
+
return (
|
|
115
|
+
normalized.startsWith('src/') ||
|
|
116
|
+
normalized.startsWith('public/') ||
|
|
117
|
+
normalized === 'scripts/build-js-code.mjs'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function walkFiles(rootDir, currentDir = rootDir, result = []) {
|
|
122
|
+
const entries = fs
|
|
123
|
+
.readdirSync(currentDir, { withFileTypes: true })
|
|
124
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
const absolute = path.join(currentDir, entry.name);
|
|
127
|
+
const relative = normalizePath(path.relative(rootDir, absolute));
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
if (IGNORED_SEGMENTS.has(entry.name)) continue;
|
|
130
|
+
walkFiles(rootDir, absolute, result);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!entry.isFile() || !shouldIncludeSourceFile(relative)) continue;
|
|
134
|
+
result.push(relative);
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function layerForSourceFile(relativePath) {
|
|
140
|
+
const file = normalizePath(relativePath);
|
|
141
|
+
if (
|
|
142
|
+
file.startsWith('src/forms/') ||
|
|
143
|
+
file.startsWith('src/resources/forms/')
|
|
144
|
+
) {
|
|
145
|
+
return 'form';
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
file.startsWith('src/functions/') ||
|
|
149
|
+
file.startsWith('src/automations/') ||
|
|
150
|
+
file.startsWith('src/js-code-nodes/') ||
|
|
151
|
+
file.startsWith('src/resources/functions/') ||
|
|
152
|
+
file.startsWith('src/resources/automations/')
|
|
153
|
+
) {
|
|
154
|
+
return 'backend';
|
|
155
|
+
}
|
|
156
|
+
if (
|
|
157
|
+
file.startsWith('src/workflows/') ||
|
|
158
|
+
file.startsWith('src/resources/workflows/')
|
|
159
|
+
) {
|
|
160
|
+
return 'workflow';
|
|
161
|
+
}
|
|
162
|
+
if (file.startsWith('src/resources/')) return 'configuration';
|
|
163
|
+
return 'runtime';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function artifactFromBuffer(kind, buffer, metadata = {}) {
|
|
167
|
+
return {
|
|
168
|
+
digest: sha256(buffer),
|
|
169
|
+
kind,
|
|
170
|
+
contentType: metadata.contentType || 'application/octet-stream',
|
|
171
|
+
size: buffer.length,
|
|
172
|
+
buffer,
|
|
173
|
+
metadata: {
|
|
174
|
+
...metadata,
|
|
175
|
+
contentType: undefined,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function fileDescriptor(artifact, relativePath) {
|
|
181
|
+
return {
|
|
182
|
+
path: normalizePath(relativePath),
|
|
183
|
+
digest: artifact.digest,
|
|
184
|
+
size: artifact.size,
|
|
185
|
+
contentType: artifact.contentType,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function sourceArtifacts(workspaceRoot) {
|
|
190
|
+
return walkFiles(workspaceRoot).map(relativePath => {
|
|
191
|
+
const buffer = fs.readFileSync(path.join(workspaceRoot, relativePath));
|
|
192
|
+
const artifact = artifactFromBuffer('source', buffer, {
|
|
193
|
+
path: relativePath,
|
|
194
|
+
contentType: inferContentType(relativePath),
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
relativePath,
|
|
198
|
+
category: layerForSourceFile(relativePath),
|
|
199
|
+
artifact,
|
|
200
|
+
descriptor: fileDescriptor(artifact, relativePath),
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function sourceHashForCategory(files, category) {
|
|
206
|
+
return sha256Canonical(
|
|
207
|
+
files
|
|
208
|
+
.filter(file => category === 'source' || file.category === category)
|
|
209
|
+
.map(file => ({
|
|
210
|
+
path: file.relativePath,
|
|
211
|
+
digest: file.artifact.digest,
|
|
212
|
+
size: file.artifact.size,
|
|
213
|
+
}))
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function discoverResourceInventory(workspaceRoot, files) {
|
|
218
|
+
const inventory = {
|
|
219
|
+
forms: new Set(),
|
|
220
|
+
functions: new Set(),
|
|
221
|
+
automations: new Set(),
|
|
222
|
+
workflows: new Set(),
|
|
223
|
+
configuration: new Map(),
|
|
224
|
+
};
|
|
225
|
+
const addConfiguration = (type, code) => {
|
|
226
|
+
if (!type || !code) return;
|
|
227
|
+
if (!inventory.configuration.has(type)) {
|
|
228
|
+
inventory.configuration.set(type, new Set());
|
|
229
|
+
}
|
|
230
|
+
inventory.configuration.get(type).add(code);
|
|
231
|
+
};
|
|
232
|
+
for (const file of files) {
|
|
233
|
+
const relative = file.relativePath;
|
|
234
|
+
for (const [prefix, bucket] of [
|
|
235
|
+
['src/forms/', inventory.forms],
|
|
236
|
+
['src/functions/', inventory.functions],
|
|
237
|
+
['src/automations/', inventory.automations],
|
|
238
|
+
['src/workflows/', inventory.workflows],
|
|
239
|
+
]) {
|
|
240
|
+
if (relative.startsWith(prefix)) {
|
|
241
|
+
const code = relative.slice(prefix.length).split('/')[0];
|
|
242
|
+
if (code && !code.startsWith('.')) bucket.add(code);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!relative.endsWith('.json')) continue;
|
|
246
|
+
const absolute = path.join(workspaceRoot, relative);
|
|
247
|
+
let value;
|
|
248
|
+
try {
|
|
249
|
+
value = JSON.parse(fs.readFileSync(absolute, 'utf8'));
|
|
250
|
+
} catch {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const codes = collectManifestCodes(value);
|
|
254
|
+
if (relative.startsWith('src/resources/forms/')) {
|
|
255
|
+
for (const code of codes) inventory.forms.add(code);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (relative.startsWith('src/resources/functions/')) {
|
|
259
|
+
for (const code of codes) inventory.functions.add(code);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (relative.startsWith('src/resources/automations/')) {
|
|
263
|
+
for (const code of codes) inventory.automations.add(code);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (relative.startsWith('src/resources/workflows/')) {
|
|
267
|
+
for (const code of codes) inventory.workflows.add(code);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const matched = Object.entries(CONFIG_RESOURCE_TYPES).find(([prefix]) =>
|
|
271
|
+
relative.startsWith(prefix)
|
|
272
|
+
);
|
|
273
|
+
if (matched) {
|
|
274
|
+
for (const code of codes) addConfiguration(matched[1], code);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
forms: [...inventory.forms].sort(),
|
|
279
|
+
functions: [...inventory.functions].sort(),
|
|
280
|
+
automations: [...inventory.automations].sort(),
|
|
281
|
+
workflows: [...inventory.workflows].sort(),
|
|
282
|
+
configuration: Object.fromEntries(
|
|
283
|
+
[...inventory.configuration.entries()]
|
|
284
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
285
|
+
.map(([type, codes]) => [type, [...codes].sort()])
|
|
286
|
+
),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function collectManifestCodes(value, result = new Set()) {
|
|
291
|
+
if (Array.isArray(value)) {
|
|
292
|
+
for (const item of value) collectManifestCodes(item, result);
|
|
293
|
+
return [...result];
|
|
294
|
+
}
|
|
295
|
+
if (!value || typeof value !== 'object') return [...result];
|
|
296
|
+
const code = String(
|
|
297
|
+
value.code ||
|
|
298
|
+
value.resourceCode ||
|
|
299
|
+
value.formCode ||
|
|
300
|
+
value.notificationType ||
|
|
301
|
+
''
|
|
302
|
+
).trim();
|
|
303
|
+
if (code) result.add(code);
|
|
304
|
+
for (const [key, child] of Object.entries(value)) {
|
|
305
|
+
if (
|
|
306
|
+
[
|
|
307
|
+
'definitionJson',
|
|
308
|
+
'schema',
|
|
309
|
+
'components',
|
|
310
|
+
'configJson',
|
|
311
|
+
'settings',
|
|
312
|
+
'grants',
|
|
313
|
+
].includes(key)
|
|
314
|
+
) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (Array.isArray(child)) collectManifestCodes(child, result);
|
|
318
|
+
}
|
|
319
|
+
return [...result];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function buildAuthoredLayer(kind, files, previousLayer) {
|
|
323
|
+
const selected =
|
|
324
|
+
kind === 'source'
|
|
325
|
+
? files
|
|
326
|
+
: files.filter(file => file.category === kind);
|
|
327
|
+
if (selected.length === 0 && kind !== 'source') return null;
|
|
328
|
+
const sourceHash = sourceHashForCategory(files, kind);
|
|
329
|
+
if (
|
|
330
|
+
previousLayer &&
|
|
331
|
+
previousLayer.sourceHash === sourceHash &&
|
|
332
|
+
previousLayer.digest
|
|
333
|
+
) {
|
|
334
|
+
return {
|
|
335
|
+
descriptor: previousLayer,
|
|
336
|
+
artifacts: [],
|
|
337
|
+
reused: true,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const manifest = {
|
|
341
|
+
schemaVersion: 'openxiangda-app-layer-v2',
|
|
342
|
+
kind,
|
|
343
|
+
sourceHash,
|
|
344
|
+
files: selected.map(file => file.descriptor),
|
|
345
|
+
};
|
|
346
|
+
const manifestBuffer = Buffer.from(canonicalJson(manifest));
|
|
347
|
+
const layerArtifact = artifactFromBuffer(kind, manifestBuffer, {
|
|
348
|
+
layer: kind,
|
|
349
|
+
contentType: 'application/json',
|
|
350
|
+
});
|
|
351
|
+
return {
|
|
352
|
+
descriptor: {
|
|
353
|
+
kind,
|
|
354
|
+
digest: layerArtifact.digest,
|
|
355
|
+
sourceHash,
|
|
356
|
+
fileCount: selected.length,
|
|
357
|
+
size: selected.reduce((sum, file) => sum + file.artifact.size, 0),
|
|
358
|
+
files: manifest.files,
|
|
359
|
+
},
|
|
360
|
+
artifacts: [
|
|
361
|
+
...selected.map(file => file.artifact),
|
|
362
|
+
layerArtifact,
|
|
363
|
+
],
|
|
364
|
+
reused: false,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function buildRuntimeLayer(options, files, previousLayer) {
|
|
369
|
+
const sourceHash = sourceHashForCategory(files, 'runtime');
|
|
370
|
+
if (
|
|
371
|
+
previousLayer &&
|
|
372
|
+
previousLayer.sourceHash === sourceHash &&
|
|
373
|
+
previousLayer.digest
|
|
374
|
+
) {
|
|
375
|
+
return {
|
|
376
|
+
descriptor: previousLayer,
|
|
377
|
+
artifacts: [],
|
|
378
|
+
reused: true,
|
|
379
|
+
built: false,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (!options.skipBuild) {
|
|
383
|
+
await runBuild({
|
|
384
|
+
workspaceRoot: options.workspaceRoot,
|
|
385
|
+
command: options.buildCommand || 'npm run build',
|
|
386
|
+
env: {
|
|
387
|
+
OPENXIANGDA_APP_TYPE: '__OPENXIANGDA_APP_TYPE__',
|
|
388
|
+
APP_TYPE: '__OPENXIANGDA_APP_TYPE__',
|
|
389
|
+
OPENXIANGDA_BUILD_ID: `pkg-${sourceHash.slice(0, 20)}`,
|
|
390
|
+
OPENXIANGDA_RUNTIME_ASSET_BASE: './',
|
|
391
|
+
},
|
|
392
|
+
onOutput: options.onBuildOutput,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const distDir = path.resolve(
|
|
396
|
+
options.workspaceRoot,
|
|
397
|
+
options.distDir || 'dist'
|
|
398
|
+
);
|
|
399
|
+
const runtimeFiles = collectRuntimeFiles(distDir);
|
|
400
|
+
const runtimeArtifacts = runtimeFiles.map(file =>
|
|
401
|
+
artifactFromBuffer('runtime', file.buffer, {
|
|
402
|
+
path: file.path,
|
|
403
|
+
contentType: file.contentType,
|
|
404
|
+
})
|
|
405
|
+
);
|
|
406
|
+
const descriptors = runtimeArtifacts.map((artifact, index) =>
|
|
407
|
+
fileDescriptor(artifact, runtimeFiles[index].path)
|
|
408
|
+
);
|
|
409
|
+
const manifest = {
|
|
410
|
+
schemaVersion: 'openxiangda-app-layer-v2',
|
|
411
|
+
kind: 'runtime',
|
|
412
|
+
sourceHash,
|
|
413
|
+
assetBaseMode: 'relative',
|
|
414
|
+
files: descriptors,
|
|
415
|
+
};
|
|
416
|
+
const layerArtifact = artifactFromBuffer(
|
|
417
|
+
'runtime',
|
|
418
|
+
Buffer.from(canonicalJson(manifest)),
|
|
419
|
+
{
|
|
420
|
+
layer: 'runtime',
|
|
421
|
+
contentType: 'application/json',
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
return {
|
|
425
|
+
descriptor: {
|
|
426
|
+
kind: 'runtime',
|
|
427
|
+
digest: layerArtifact.digest,
|
|
428
|
+
sourceHash,
|
|
429
|
+
assetBaseMode: 'relative',
|
|
430
|
+
fileCount: descriptors.length,
|
|
431
|
+
size: runtimeArtifacts.reduce((sum, artifact) => sum + artifact.size, 0),
|
|
432
|
+
files: descriptors,
|
|
433
|
+
},
|
|
434
|
+
artifacts: [...runtimeArtifacts, layerArtifact],
|
|
435
|
+
reused: false,
|
|
436
|
+
built: !options.skipBuild,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function compileAppPackage(options = {}) {
|
|
441
|
+
const workspaceRoot = path.resolve(options.workspaceRoot || process.cwd());
|
|
442
|
+
const configFile = path.join(workspaceRoot, 'app-workspace.config.ts');
|
|
443
|
+
if (!fs.existsSync(configFile)) {
|
|
444
|
+
throw createCompilerError(
|
|
445
|
+
'DELIVERY_WORKSPACE_CONFIG_MISSING',
|
|
446
|
+
`未发现 ${configFile}`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
const configText = fs.readFileSync(configFile, 'utf8');
|
|
450
|
+
if (!/deliveryVersion\s*:\s*2\b/.test(configText)) {
|
|
451
|
+
throw createCompilerError(
|
|
452
|
+
'DELIVERY_V2_NOT_ENABLED',
|
|
453
|
+
'app-workspace.config.ts 必须显式声明 deliveryVersion: 2'
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
const runtimeEnabled =
|
|
457
|
+
/runtimeMode\s*:\s*['"]react-spa['"]/.test(configText);
|
|
458
|
+
const files = sourceArtifacts(workspaceRoot);
|
|
459
|
+
const previousManifest = options.previousManifest || null;
|
|
460
|
+
const previousLayers = new Map(
|
|
461
|
+
(previousManifest?.layers || []).map(layer => [layer.kind, layer])
|
|
462
|
+
);
|
|
463
|
+
const artifacts = [];
|
|
464
|
+
const layerResults = [];
|
|
465
|
+
const resources = discoverResourceInventory(workspaceRoot, files);
|
|
466
|
+
|
|
467
|
+
for (const kind of ['source', 'form', 'configuration', 'backend', 'workflow']) {
|
|
468
|
+
const result = buildAuthoredLayer(
|
|
469
|
+
kind,
|
|
470
|
+
files,
|
|
471
|
+
previousLayers.get(kind)
|
|
472
|
+
);
|
|
473
|
+
if (!result) continue;
|
|
474
|
+
artifacts.push(...result.artifacts);
|
|
475
|
+
layerResults.push(result);
|
|
476
|
+
}
|
|
477
|
+
if (runtimeEnabled) {
|
|
478
|
+
const runtimeResult = await buildRuntimeLayer(
|
|
479
|
+
{ ...options, workspaceRoot },
|
|
480
|
+
files,
|
|
481
|
+
previousLayers.get('runtime')
|
|
482
|
+
);
|
|
483
|
+
artifacts.push(...runtimeResult.artifacts);
|
|
484
|
+
layerResults.push(runtimeResult);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const layers = LAYER_ORDER.map(kind =>
|
|
488
|
+
layerResults.find(result => result.descriptor.kind === kind)
|
|
489
|
+
)
|
|
490
|
+
.filter(Boolean)
|
|
491
|
+
.map(result => result.descriptor);
|
|
492
|
+
const packageManifest = {
|
|
493
|
+
schemaVersion: PACKAGE_SCHEMA_VERSION,
|
|
494
|
+
compilerVersion: COMPILER_VERSION,
|
|
495
|
+
mode: 'trusted-developer',
|
|
496
|
+
runtimeMode: runtimeEnabled ? 'react-spa' : 'legacy',
|
|
497
|
+
resources,
|
|
498
|
+
layers,
|
|
499
|
+
};
|
|
500
|
+
const packageBuffer = Buffer.from(canonicalJson(packageManifest));
|
|
501
|
+
const packageArtifact = artifactFromBuffer(
|
|
502
|
+
'package-manifest',
|
|
503
|
+
packageBuffer,
|
|
504
|
+
{
|
|
505
|
+
contentType: 'application/json',
|
|
506
|
+
schemaVersion: PACKAGE_SCHEMA_VERSION,
|
|
507
|
+
}
|
|
508
|
+
);
|
|
509
|
+
artifacts.push(packageArtifact);
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
workspaceRoot,
|
|
513
|
+
packageDigest: packageArtifact.digest,
|
|
514
|
+
packageManifest,
|
|
515
|
+
artifacts: deduplicateArtifacts(artifacts),
|
|
516
|
+
summary: {
|
|
517
|
+
sourceFileCount: files.length,
|
|
518
|
+
artifactUploadCount: deduplicateArtifacts(artifacts).length,
|
|
519
|
+
layers: layerResults.map(result => ({
|
|
520
|
+
kind: result.descriptor.kind,
|
|
521
|
+
digest: result.descriptor.digest,
|
|
522
|
+
sourceHash: result.descriptor.sourceHash,
|
|
523
|
+
reused: result.reused,
|
|
524
|
+
built: Boolean(result.built),
|
|
525
|
+
fileCount: result.descriptor.fileCount,
|
|
526
|
+
size: result.descriptor.size,
|
|
527
|
+
})),
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function deduplicateArtifacts(artifacts) {
|
|
533
|
+
const byDigest = new Map();
|
|
534
|
+
for (const artifact of artifacts) {
|
|
535
|
+
if (!byDigest.has(artifact.digest)) byDigest.set(artifact.digest, artifact);
|
|
536
|
+
}
|
|
537
|
+
return [...byDigest.values()];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function collectRuntimeFiles(distDir) {
|
|
541
|
+
if (!fs.existsSync(distDir) || !fs.statSync(distDir).isDirectory()) {
|
|
542
|
+
throw createCompilerError(
|
|
543
|
+
'DELIVERY_RUNTIME_DIST_MISSING',
|
|
544
|
+
`Runtime dist 目录不存在: ${distDir}`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
const files = [];
|
|
548
|
+
const visit = currentDir => {
|
|
549
|
+
const entries = fs
|
|
550
|
+
.readdirSync(currentDir, { withFileTypes: true })
|
|
551
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
552
|
+
for (const entry of entries) {
|
|
553
|
+
const absolute = path.join(currentDir, entry.name);
|
|
554
|
+
if (entry.isDirectory()) {
|
|
555
|
+
visit(absolute);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (!entry.isFile() || entry.name.endsWith('.map')) continue;
|
|
559
|
+
const relative = normalizePath(path.relative(distDir, absolute));
|
|
560
|
+
const buffer = fs.readFileSync(absolute);
|
|
561
|
+
files.push({
|
|
562
|
+
path: relative,
|
|
563
|
+
buffer,
|
|
564
|
+
contentType: inferContentType(relative),
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
visit(distDir);
|
|
569
|
+
if (!files.some(file => file.path === 'index.html')) {
|
|
570
|
+
throw createCompilerError(
|
|
571
|
+
'DELIVERY_RUNTIME_INDEX_MISSING',
|
|
572
|
+
`Runtime dist 缺少 index.html: ${distDir}`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
const totalBytes = files.reduce((sum, file) => sum + file.buffer.length, 0);
|
|
576
|
+
if (totalBytes > MAX_RUNTIME_BYTES) {
|
|
577
|
+
throw createCompilerError(
|
|
578
|
+
'DELIVERY_RUNTIME_TOO_LARGE',
|
|
579
|
+
`Runtime dist 超过 ${MAX_RUNTIME_BYTES} bytes`
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
return files;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function runBuild(options) {
|
|
586
|
+
await new Promise((resolve, reject) => {
|
|
587
|
+
let child;
|
|
588
|
+
try {
|
|
589
|
+
child = spawn(options.command, [], {
|
|
590
|
+
cwd: options.workspaceRoot,
|
|
591
|
+
shell: true,
|
|
592
|
+
stdio: options.onOutput ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
593
|
+
env: {
|
|
594
|
+
...process.env,
|
|
595
|
+
...options.env,
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
} catch (error) {
|
|
599
|
+
reject(
|
|
600
|
+
createCompilerError(
|
|
601
|
+
'DELIVERY_RUNTIME_BUILD_START_FAILED',
|
|
602
|
+
error?.message || String(error)
|
|
603
|
+
)
|
|
604
|
+
);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (options.onOutput) {
|
|
608
|
+
child.stdout?.on('data', chunk =>
|
|
609
|
+
options.onOutput('stdout', String(chunk))
|
|
610
|
+
);
|
|
611
|
+
child.stderr?.on('data', chunk =>
|
|
612
|
+
options.onOutput('stderr', String(chunk))
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
child.once('error', error => {
|
|
616
|
+
reject(
|
|
617
|
+
createCompilerError(
|
|
618
|
+
'DELIVERY_RUNTIME_BUILD_START_FAILED',
|
|
619
|
+
error.message
|
|
620
|
+
)
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
child.once('close', (code, signal) => {
|
|
624
|
+
if (code === 0) {
|
|
625
|
+
resolve();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
reject(
|
|
629
|
+
createCompilerError(
|
|
630
|
+
'DELIVERY_RUNTIME_BUILD_FAILED',
|
|
631
|
+
Number.isInteger(code)
|
|
632
|
+
? `exit ${code}`
|
|
633
|
+
: `signal ${signal || 'unknown'}`
|
|
634
|
+
)
|
|
635
|
+
);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function inferContentType(file) {
|
|
641
|
+
const extension = path.extname(file).toLowerCase();
|
|
642
|
+
return (
|
|
643
|
+
{
|
|
644
|
+
'.css': 'text/css; charset=utf-8',
|
|
645
|
+
'.html': 'text/html; charset=utf-8',
|
|
646
|
+
'.ico': 'image/x-icon',
|
|
647
|
+
'.jpeg': 'image/jpeg',
|
|
648
|
+
'.jpg': 'image/jpeg',
|
|
649
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
650
|
+
'.json': 'application/json',
|
|
651
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
652
|
+
'.png': 'image/png',
|
|
653
|
+
'.svg': 'image/svg+xml',
|
|
654
|
+
'.ts': 'text/plain; charset=utf-8',
|
|
655
|
+
'.tsx': 'text/plain; charset=utf-8',
|
|
656
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
657
|
+
'.webp': 'image/webp',
|
|
658
|
+
'.woff': 'font/woff',
|
|
659
|
+
'.woff2': 'font/woff2',
|
|
660
|
+
}[extension] || 'application/octet-stream'
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function createCompilerError(code, message) {
|
|
665
|
+
const error = new Error(`${code}: ${message}`);
|
|
666
|
+
error.code = code;
|
|
667
|
+
error.retryable = false;
|
|
668
|
+
return error;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
module.exports = {
|
|
672
|
+
COMPILER_VERSION,
|
|
673
|
+
PACKAGE_SCHEMA_VERSION,
|
|
674
|
+
canonicalJson,
|
|
675
|
+
compileAppPackage,
|
|
676
|
+
discoverResourceInventory,
|
|
677
|
+
layerForSourceFile,
|
|
678
|
+
sha256,
|
|
679
|
+
sha256Canonical,
|
|
680
|
+
shouldIncludeSourceFile,
|
|
681
|
+
};
|
package/lib/policy.js
CHANGED
|
@@ -95,6 +95,10 @@ function renderEngineeringPolicyMarkdown(policy = loadEngineeringPolicy()) {
|
|
|
95
95
|
'',
|
|
96
96
|
`- 风险按 ${policy.risk.classifyBy.join('、')} 分级;目录只作信号。`,
|
|
97
97
|
'- SDD verify 只读;Markdown 是按需派生视图。',
|
|
98
|
+
`- Delivery V${policy.release.normalDeliveryVersion} 是正常发布入口:${policy.release.normalCommands.join(
|
|
99
|
+
' / '
|
|
100
|
+
)};生产只消费预发验证过的封存包,ReleaseRun 与检查点由平台持久化。`,
|
|
101
|
+
'- Delivery V2 信任平台开发者,不要求 SDD、Git 主线、`--change` 或 `--only`;下列 V1 规则只保留兼容性。',
|
|
98
102
|
'- 平台仓库直接开发权威 master;应用任务可用独立 worktree,但只从干净、已推送的权威主线发布。',
|
|
99
103
|
'- 应用任务在主线发布和 release end 后从 canonical 主工作区执行 workspace cleanup;只删除已合并、干净、无 owner 和发布私有状态的 SAFE worktree/本地分支。',
|
|
100
104
|
'- 环境托管应用冻结不可变 candidate,先部署预发并提交有效测试证据,再将同一 candidate 晋级生产;禁止生产直发。',
|
|
@@ -18,6 +18,7 @@ const TEMPLATE_DIR = LEGACY_TEMPLATE_DIR;
|
|
|
18
18
|
// 这些路径相对于 workspace 根;源模板必须与现有 workspace runtime 匹配。
|
|
19
19
|
const BOOTSTRAP_FILES = [
|
|
20
20
|
'AGENTS.md',
|
|
21
|
+
'DELIVERY.md',
|
|
21
22
|
'scripts/guard-publish.mjs',
|
|
22
23
|
'.qoder/rules/openxiangda.md',
|
|
23
24
|
'.qoder/rules/openxiangda-form.md',
|