openxiangda-cli 2.0.0-alpha.100
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 +13 -0
- package/bin/run.js +92 -0
- package/dist/base.d.ts +44 -0
- package/dist/base.d.ts.map +1 -0
- package/dist/base.js +119 -0
- package/dist/base.js.map +1 -0
- package/dist/commands/accept.d.ts +23 -0
- package/dist/commands/accept.d.ts.map +1 -0
- package/dist/commands/accept.js +21 -0
- package/dist/commands/accept.js.map +1 -0
- package/dist/commands/check.d.ts +22 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +7 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/create.d.ts +23 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +101 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/deploy.d.ts +26 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +42 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/dev.d.ts +24 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +28 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/login.d.ts +24 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +73 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logs.d.ts +25 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +9 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/rollback.d.ts +24 -0
- package/dist/commands/rollback.d.ts.map +1 -0
- package/dist/commands/rollback.js +16 -0
- package/dist/commands/rollback.js.map +1 -0
- package/dist/commands/skill.d.ts +26 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +43 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/status.d.ts +25 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +9 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/create-workspace.d.ts +31 -0
- package/dist/create-workspace.d.ts.map +1 -0
- package/dist/create-workspace.js +273 -0
- package/dist/create-workspace.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/toolchain-capsule.d.ts +4 -0
- package/dist/toolchain-capsule.d.ts.map +1 -0
- package/dist/toolchain-capsule.js +116 -0
- package/dist/toolchain-capsule.js.map +1 -0
- package/package.json +49 -0
- package/template/.dockerignore +23 -0
- package/template/.env.example +3 -0
- package/template/AGENTS.md +22 -0
- package/template/README.md +26 -0
- package/template/_gitignore +14 -0
- package/template/apps/server/Dockerfile +20 -0
- package/template/apps/server/package.json +27 -0
- package/template/apps/server/src/app.module.ts +43 -0
- package/template/apps/server/src/main.ts +5 -0
- package/template/apps/server/test/smoke.test.ts +16 -0
- package/template/apps/server/tsconfig.json +9 -0
- package/template/apps/web/data-api-live.e2e.html +15 -0
- package/template/apps/web/e2e/client-fixture.ts +5 -0
- package/template/apps/web/e2e/data-api-live-fixture.tsx +146 -0
- package/template/apps/web/e2e/data-api-live.spec.ts +138 -0
- package/template/apps/web/e2e/field-protocol-fixture.tsx +347 -0
- package/template/apps/web/e2e/field-protocol.spec.ts +186 -0
- package/template/apps/web/e2e/resources.spec.ts +285 -0
- package/template/apps/web/e2e/workflow.spec.ts +329 -0
- package/template/apps/web/field-protocol.e2e.html +15 -0
- package/template/apps/web/index.html +12 -0
- package/template/apps/web/package.json +32 -0
- package/template/apps/web/playwright.config.ts +44 -0
- package/template/apps/web/scripts/check.mjs +43 -0
- package/template/apps/web/scripts/verify-build.mjs +6 -0
- package/template/apps/web/src/main.tsx +21 -0
- package/template/apps/web/test/contracts.test.ts +99 -0
- package/template/apps/web/test/loopback.test.ts +62 -0
- package/template/apps/web/tsconfig.json +12 -0
- package/template/apps/web/vite.config.ts +31 -0
- package/template/openxiangda.config.ts +30 -0
- package/template/package.json +28 -0
- package/template/packages/contracts/package.json +23 -0
- package/template/packages/contracts/src/generated.ts +124 -0
- package/template/packages/contracts/src/index.ts +1 -0
- package/template/packages/contracts/tsconfig.json +9 -0
- package/template/pnpm-workspace.yaml +7 -0
- package/template/scripts/verify-template-budget.mjs +49 -0
- package/template/tsconfig.base.json +13 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineConfig } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const e2ePort = resolveE2ePort();
|
|
4
|
+
const baseURL =
|
|
5
|
+
process.env.OPENXIANGDA_E2E_BASE_URL || `http://127.0.0.1:${e2ePort}`;
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
testDir: './e2e',
|
|
9
|
+
workers: 2,
|
|
10
|
+
use: { baseURL, trace: 'retain-on-failure' },
|
|
11
|
+
webServer: process.env.OPENXIANGDA_E2E_BASE_URL
|
|
12
|
+
? undefined
|
|
13
|
+
: {
|
|
14
|
+
command: 'pnpm dev',
|
|
15
|
+
url: baseURL,
|
|
16
|
+
reuseExistingServer: false,
|
|
17
|
+
env: {
|
|
18
|
+
OPENXIANGDA_WEB_PORT: String(e2ePort),
|
|
19
|
+
OPENXIANGDA_ENVIRONMENT_KEY: 'production',
|
|
20
|
+
OPENXIANGDA_DEV_PROXY:
|
|
21
|
+
process.env.OPENXIANGDA_E2E_PLATFORM_URL ||
|
|
22
|
+
'http://127.0.0.1:7001',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function resolveE2ePort() {
|
|
28
|
+
const configured = process.env.OPENXIANGDA_E2E_PORT;
|
|
29
|
+
if (configured !== undefined) {
|
|
30
|
+
const port = Number(configured);
|
|
31
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'OPENXIANGDA_E2E_PORT must be an integer between 1024 and 65535'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return port;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let hash = 2166136261;
|
|
40
|
+
for (const character of process.cwd()) {
|
|
41
|
+
hash = Math.imul(hash ^ character.charCodeAt(0), 16777619);
|
|
42
|
+
}
|
|
43
|
+
return 30_000 + ((hash >>> 0) % 30_000);
|
|
44
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const root = resolve(import.meta.dirname, '..');
|
|
5
|
+
const files = [];
|
|
6
|
+
function visit(directory) {
|
|
7
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
8
|
+
const path = join(directory, entry.name);
|
|
9
|
+
if (entry.isDirectory() && !['.umi', '.umi-production', '.turbopack'].includes(entry.name)) visit(path);
|
|
10
|
+
else if (/\.(?:ts|tsx)$/.test(entry.name)) files.push(path);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
visit(join(root, 'src'));
|
|
14
|
+
const lines = files.reduce((total, file) => total + readFileSync(file, 'utf8').split(/\r?\n/).length, 0);
|
|
15
|
+
// The shared Surface renderer is deliberately counted as application source;
|
|
16
|
+
// keep its small budget explicit instead of hiding it in generated output.
|
|
17
|
+
// Field Kit families are separate modules so a semantic field does not add a
|
|
18
|
+
// branch to one universal renderer. Keep explicit ceilings while allowing the
|
|
19
|
+
// complete 30-field standard kit to remain decomposed.
|
|
20
|
+
if (files.length > 36) throw new Error(`WEB_SOURCE_FILE_BUDGET_EXCEEDED:${files.length}>36`);
|
|
21
|
+
if (lines > 10_000) throw new Error(`WEB_BUSINESS_LOC_BUDGET_EXCEEDED:${lines}>10000`);
|
|
22
|
+
const source = files.map((file) => readFileSync(file, 'utf8')).join('\n');
|
|
23
|
+
for (const marker of [
|
|
24
|
+
'@umijs/',
|
|
25
|
+
'openxiangda-' + 'admin',
|
|
26
|
+
'openxiangda-' + 'user',
|
|
27
|
+
'instrument_query',
|
|
28
|
+
'instrument_save',
|
|
29
|
+
'function.invoke',
|
|
30
|
+
'authorization' + '-mode',
|
|
31
|
+
'focused' + '_role',
|
|
32
|
+
'/native/current' + '-user',
|
|
33
|
+
'college-' + 'am',
|
|
34
|
+
'user-' + 'wang',
|
|
35
|
+
'dept-' + 'am-test',
|
|
36
|
+
]) {
|
|
37
|
+
if (source.includes(marker)) throw new Error(`WEB_FORBIDDEN_MARKER:${marker}`);
|
|
38
|
+
}
|
|
39
|
+
const playwright = readFileSync(join(root, 'playwright.config.ts'), 'utf8');
|
|
40
|
+
if (!playwright.includes('reuseExistingServer: false')) {
|
|
41
|
+
throw new Error('WEB_E2E_MUST_NOT_REUSE_UNKNOWN_SERVER');
|
|
42
|
+
}
|
|
43
|
+
process.stdout.write(`Verified Vite/Refine web source: ${files.length} files, ${lines} LOC.\n`);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { verifyOpenXiangdaWebBuild } from 'openxiangda/testing';
|
|
2
|
+
|
|
3
|
+
const result = verifyOpenXiangdaWebBuild(new URL('../dist', import.meta.url));
|
|
4
|
+
process.stdout.write(
|
|
5
|
+
`Verified Vite build: ${result.totalBytes} bytes, ${result.totalJavaScriptGzipBytes} total gzip bytes, ${result.initialJavaScriptGzipBytes} initial gzip bytes.\n`
|
|
6
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import { OpenXiangdaApplication } from 'openxiangda/react';
|
|
4
|
+
import {
|
|
5
|
+
appCode,
|
|
6
|
+
appName,
|
|
7
|
+
resourceDefinitions,
|
|
8
|
+
workflowCodes,
|
|
9
|
+
} from '../../../packages/contracts/src/generated.js';
|
|
10
|
+
import 'openxiangda/react/styles.css';
|
|
11
|
+
|
|
12
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
13
|
+
<React.StrictMode>
|
|
14
|
+
<OpenXiangdaApplication
|
|
15
|
+
appCode={appCode}
|
|
16
|
+
appName={appName}
|
|
17
|
+
resourceDefinitions={resourceDefinitions}
|
|
18
|
+
workflows={workflowCodes}
|
|
19
|
+
/>
|
|
20
|
+
</React.StrictMode>,
|
|
21
|
+
);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
import { readRuntimeMount, resolveApplicationBasename } from 'openxiangda/core';
|
|
5
|
+
import {
|
|
6
|
+
resourceCodes,
|
|
7
|
+
resourceDefinitions,
|
|
8
|
+
} from '../../../packages/contracts/src/generated.js';
|
|
9
|
+
|
|
10
|
+
test('clean template contains only application declarations and one React entry', () => {
|
|
11
|
+
const config = readFileSync(
|
|
12
|
+
new URL('../../../openxiangda.config.ts', import.meta.url),
|
|
13
|
+
'utf8'
|
|
14
|
+
);
|
|
15
|
+
const source = readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8');
|
|
16
|
+
const productionSources = readdirSync(new URL('../src', import.meta.url), {
|
|
17
|
+
withFileTypes: true,
|
|
18
|
+
})
|
|
19
|
+
.filter(entry => entry.isFile())
|
|
20
|
+
.map(entry => entry.name)
|
|
21
|
+
.sort();
|
|
22
|
+
assert.deepEqual(productionSources, ['main.tsx']);
|
|
23
|
+
assert.deepEqual(Object.keys(resourceDefinitions).sort(), [...resourceCodes].sort());
|
|
24
|
+
assert.match(config, /data:\s*\{\s*resources:/);
|
|
25
|
+
assert.match(source, /OpenXiangdaApplication/);
|
|
26
|
+
assert.match(source, /from 'openxiangda\/react'/);
|
|
27
|
+
assert.match(source, /openxiangda\/react\/styles\.css/);
|
|
28
|
+
const legacyMarkers = [
|
|
29
|
+
'instruments',
|
|
30
|
+
'colleges',
|
|
31
|
+
'仪器' + '资源管理',
|
|
32
|
+
'学院' + '字典',
|
|
33
|
+
'HGY',
|
|
34
|
+
'Instrument',
|
|
35
|
+
];
|
|
36
|
+
assert.doesNotMatch(`${config}\n${source}`, new RegExp(legacyMarkers.join('|')));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('application manifests expose one direct OpenXiangda dependency', () => {
|
|
40
|
+
for (const relative of [
|
|
41
|
+
'../../../package.json',
|
|
42
|
+
'../package.json',
|
|
43
|
+
'../../server/package.json',
|
|
44
|
+
]) {
|
|
45
|
+
const manifest = JSON.parse(
|
|
46
|
+
readFileSync(new URL(relative, import.meta.url), 'utf8')
|
|
47
|
+
) as Record<string, Record<string, string>>;
|
|
48
|
+
const dependencies = Object.entries({
|
|
49
|
+
...(manifest.dependencies || {}),
|
|
50
|
+
...(manifest.devDependencies || {}),
|
|
51
|
+
...(manifest.peerDependencies || {}),
|
|
52
|
+
}).filter(([name]) =>
|
|
53
|
+
name === 'openxiangda' || name.startsWith('openxiangda-')
|
|
54
|
+
);
|
|
55
|
+
assert.deepEqual(
|
|
56
|
+
dependencies.map(([name]) => name),
|
|
57
|
+
dependencies.length ? ['openxiangda'] : []
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('runtime metadata stays mount-scoped', () => {
|
|
63
|
+
const mount = readRuntimeMount(
|
|
64
|
+
name =>
|
|
65
|
+
({
|
|
66
|
+
'openxiangda-runtime-base': '/view/example-app/',
|
|
67
|
+
'openxiangda-app-code': 'example-app',
|
|
68
|
+
'openxiangda-environment': 'preproduction',
|
|
69
|
+
})[name]
|
|
70
|
+
);
|
|
71
|
+
assert.equal(resolveApplicationBasename(mount), '/view/example-app');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('resource E2E harness owns the standard export download response', () => {
|
|
75
|
+
const source = readFileSync(
|
|
76
|
+
new URL('../e2e/resources.spec.ts', import.meta.url),
|
|
77
|
+
'utf8'
|
|
78
|
+
);
|
|
79
|
+
assert.match(source, /\/native\\\/data\\\/\(\[\^\/\]\+\)\\\/export\$/);
|
|
80
|
+
assert.match(source, /content-disposition/);
|
|
81
|
+
assert.match(source, /text\/csv/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('published E2E harnesses do not write evidence into the platform source repository', () => {
|
|
85
|
+
const workflowSource = readFileSync(
|
|
86
|
+
new URL('../e2e/workflow.spec.ts', import.meta.url),
|
|
87
|
+
'utf8'
|
|
88
|
+
);
|
|
89
|
+
assert.doesNotMatch(workflowSource, /docs\/architecture|evidenceDirectory/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('the platform package owns Web build verification policy', () => {
|
|
93
|
+
const verifier = readFileSync(
|
|
94
|
+
new URL('../scripts/verify-build.mjs', import.meta.url),
|
|
95
|
+
'utf8'
|
|
96
|
+
);
|
|
97
|
+
assert.match(verifier, /verifyOpenXiangdaWebBuild/);
|
|
98
|
+
assert.doesNotMatch(verifier, /2_650_000|800_000|600_000/);
|
|
99
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
4
|
+
import { networkInterfaces } from 'node:os';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
|
|
8
|
+
test('Vite is reachable on loopback and not on a LAN interface', async t => {
|
|
9
|
+
const port = await freePort();
|
|
10
|
+
const root = resolve(import.meta.dirname, '..');
|
|
11
|
+
const child = spawn('pnpm', ['exec', 'vite', '--port', String(port), '--strictPort'], {
|
|
12
|
+
cwd: root,
|
|
13
|
+
stdio: 'ignore',
|
|
14
|
+
env: {
|
|
15
|
+
...process.env,
|
|
16
|
+
OPENXIANGDA_WEB_PORT: String(port),
|
|
17
|
+
OPENXIANGDA_DEV_PROXY: 'http://127.0.0.1:7001',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
await waitFor(`http://127.0.0.1:${port}`);
|
|
22
|
+
const lan = Object.values(networkInterfaces())
|
|
23
|
+
.flat()
|
|
24
|
+
.find(address => address?.family === 'IPv4' && !address.internal)?.address;
|
|
25
|
+
if (!lan) {
|
|
26
|
+
t.diagnostic('No non-loopback IPv4 interface; loopback reachability verified.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await assert.rejects(
|
|
30
|
+
fetch(`http://${lan}:${port}`, {
|
|
31
|
+
signal: AbortSignal.timeout(500),
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
} finally {
|
|
35
|
+
child.kill('SIGTERM');
|
|
36
|
+
await new Promise(resolve => child.once('exit', resolve));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
async function freePort() {
|
|
41
|
+
return await new Promise<number>((resolve, reject) => {
|
|
42
|
+
const server = createServer();
|
|
43
|
+
server.once('error', reject);
|
|
44
|
+
server.listen({ host: '127.0.0.1', port: 0 }, () => {
|
|
45
|
+
const address = server.address();
|
|
46
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
47
|
+
server.close(error => (error ? reject(error) : resolve(port)));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function waitFor(url: string) {
|
|
53
|
+
const deadline = Date.now() + 10_000;
|
|
54
|
+
while (Date.now() < deadline) {
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(url);
|
|
57
|
+
if (response.ok) return;
|
|
58
|
+
} catch {}
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`VITE_READINESS_TIMEOUT:${url}`);
|
|
62
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"exactOptionalPropertyTypes": false,
|
|
9
|
+
"types": ["vite/client", "node"]
|
|
10
|
+
},
|
|
11
|
+
"include": ["src", "test", "vite.config.ts", "playwright.config.ts"]
|
|
12
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import react from '@vitejs/plugin-react';
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
|
|
4
|
+
const connectedProxy =
|
|
5
|
+
process.env.OPENXIANGDA_DEV_PROXY?.trim() || 'http://127.0.0.1:7001';
|
|
6
|
+
const port = Number(process.env.OPENXIANGDA_WEB_PORT || 5173);
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [react()],
|
|
10
|
+
base: './',
|
|
11
|
+
server: {
|
|
12
|
+
host: '127.0.0.1',
|
|
13
|
+
port,
|
|
14
|
+
strictPort: true,
|
|
15
|
+
proxy: {
|
|
16
|
+
'/service': { target: connectedProxy, changeOrigin: false },
|
|
17
|
+
'/api': { target: connectedProxy, changeOrigin: false },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
build: {
|
|
21
|
+
sourcemap: false,
|
|
22
|
+
rollupOptions: {
|
|
23
|
+
output: {
|
|
24
|
+
manualChunks: {
|
|
25
|
+
react: ['react', 'react-dom', 'react-router-dom'],
|
|
26
|
+
refine: ['@refinedev/core'],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineOpenXiangdaApp } from 'openxiangda/config';
|
|
2
|
+
|
|
3
|
+
/** Clean application scaffold: declare resources and fields, then generate CRUD. */
|
|
4
|
+
export default defineOpenXiangdaApp({
|
|
5
|
+
schemaVersion: 3,
|
|
6
|
+
app: { code: 'openxiangda-application', name: 'OpenXiangda 应用' },
|
|
7
|
+
frontend: { root: 'apps/web', routes: [] },
|
|
8
|
+
backend: {
|
|
9
|
+
root: 'apps/server',
|
|
10
|
+
runtime: 'node',
|
|
11
|
+
framework: 'nestjs',
|
|
12
|
+
enabled: false,
|
|
13
|
+
isolation: 'shared',
|
|
14
|
+
resourceProfile: 'light',
|
|
15
|
+
secrets: [],
|
|
16
|
+
operations: [],
|
|
17
|
+
},
|
|
18
|
+
platform: { root: 'platform' },
|
|
19
|
+
authz: {
|
|
20
|
+
capabilities: [],
|
|
21
|
+
roles: [],
|
|
22
|
+
scopeDimensions: [],
|
|
23
|
+
scopeSources: [],
|
|
24
|
+
dataPolicies: [],
|
|
25
|
+
authorizationTransitions: [],
|
|
26
|
+
},
|
|
27
|
+
data: { resources: [] },
|
|
28
|
+
events: { schemas: [], subscriptions: [], timers: [], dateTriggers: [] },
|
|
29
|
+
workflows: { definitions: [], bindings: [], activations: [], providers: [], editableParameters: [] },
|
|
30
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openxiangda-application",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"packageManager": "pnpm@10.15.1",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.12"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"openxiangda": "openxiangda",
|
|
11
|
+
"prepare": "pnpm workspace:build",
|
|
12
|
+
"workspace:build": "pnpm --filter @app/contracts build",
|
|
13
|
+
"predev": "pnpm workspace:build",
|
|
14
|
+
"dev": "pnpm --parallel --filter './apps/*' dev",
|
|
15
|
+
"dev:web": "pnpm --filter @app/web dev",
|
|
16
|
+
"dev:server": "pnpm --filter @app/server dev",
|
|
17
|
+
"precheck": "pnpm workspace:build",
|
|
18
|
+
"check": "pnpm --recursive check && node scripts/verify-template-budget.mjs",
|
|
19
|
+
"pretest": "pnpm workspace:build",
|
|
20
|
+
"test": "pnpm --recursive test",
|
|
21
|
+
"test:e2e": "pnpm --filter @app/web test:e2e",
|
|
22
|
+
"build": "pnpm --recursive build"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"openxiangda": "2.0.0-alpha.12",
|
|
26
|
+
"typescript": "5.9.3"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app/contracts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"openxiangda-source": "./src/index.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
17
|
+
"test": "node -e \"process.stdout.write('generated contracts checked by TypeScript\\n')\"",
|
|
18
|
+
"build": "tsc -p tsconfig.json"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"typescript": "5.9.3"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Generated by OpenXiangda 2.0 Native compiler. Do not edit by hand.
|
|
2
|
+
export const compilerContractVersion = "native-2" as const;
|
|
3
|
+
export const appCode = "openxiangda-application" as const;
|
|
4
|
+
export const appName = "OpenXiangda 应用" as const;
|
|
5
|
+
export const resourceCodes = [] as const;
|
|
6
|
+
export const resourceSurfaces = {} as const;
|
|
7
|
+
export const resourceDefinitions = {} as const;
|
|
8
|
+
export const capabilities = [
|
|
9
|
+
"app:openxiangda-application:directory:read",
|
|
10
|
+
"app:role:assign",
|
|
11
|
+
"app:role:define",
|
|
12
|
+
"app:role:grant-capability",
|
|
13
|
+
"app:role:read",
|
|
14
|
+
"app:scope-grant:manage",
|
|
15
|
+
"app:super-admin:manage"
|
|
16
|
+
] as const;
|
|
17
|
+
export const appOperations = {} as const;
|
|
18
|
+
export const appRoutes = {} as const;
|
|
19
|
+
export const eventTypes = [] as const;
|
|
20
|
+
export const eventSubscriptionCodes = [] as const;
|
|
21
|
+
export const eventHandlerManifest = {
|
|
22
|
+
"schemaVersion": "openxiangda.event-handler-manifest/v2",
|
|
23
|
+
"appCode": "openxiangda-application",
|
|
24
|
+
"handlers": []
|
|
25
|
+
} as const;
|
|
26
|
+
export const eventHandlers = {} as const;
|
|
27
|
+
export const eventSchemas = [] as const;
|
|
28
|
+
export const workflowCodes = [] as const;
|
|
29
|
+
|
|
30
|
+
export type ResourceCode = (typeof resourceCodes)[number];
|
|
31
|
+
export type Capability = (typeof capabilities)[number];
|
|
32
|
+
export type AppOperation = (typeof appOperations)[keyof typeof appOperations];
|
|
33
|
+
export type AppRoute = (typeof appRoutes)[keyof typeof appRoutes];
|
|
34
|
+
export type EventType = (typeof eventTypes)[number];
|
|
35
|
+
export type EventSubscriptionCode = (typeof eventSubscriptionCodes)[number];
|
|
36
|
+
export type EventTypesForSubscription<TCode extends EventSubscriptionCode> = never;
|
|
37
|
+
export type WorkflowCode = (typeof workflowCodes)[number];
|
|
38
|
+
|
|
39
|
+
export interface OpenXiangdaCloudEvent<TType extends EventType = EventType, TData extends Record<string, unknown> = Record<string, unknown>> {
|
|
40
|
+
specversion: "1.0";
|
|
41
|
+
id: string;
|
|
42
|
+
source: string;
|
|
43
|
+
type: TType;
|
|
44
|
+
subject?: string;
|
|
45
|
+
time: string;
|
|
46
|
+
datacontenttype: "application/json";
|
|
47
|
+
dataschema?: string;
|
|
48
|
+
data: TData;
|
|
49
|
+
tenantid: string;
|
|
50
|
+
appcode: typeof appCode;
|
|
51
|
+
environment: string;
|
|
52
|
+
traceid?: string;
|
|
53
|
+
schemaversion: string;
|
|
54
|
+
}
|
|
55
|
+
export type EventHandlerInput<TCode extends EventSubscriptionCode> = OpenXiangdaCloudEvent<EventTypesForSubscription<TCode>>;
|
|
56
|
+
|
|
57
|
+
export interface LabeledValue {
|
|
58
|
+
label: string;
|
|
59
|
+
value: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
color?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface ResourceReferenceValue extends LabeledValue {
|
|
64
|
+
resourceCode: string;
|
|
65
|
+
snapshot?: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
export interface DepartmentReferenceValue extends LabeledValue {
|
|
68
|
+
fullPath?: string;
|
|
69
|
+
path?: LabeledValue[];
|
|
70
|
+
parent?: LabeledValue;
|
|
71
|
+
}
|
|
72
|
+
export interface UserReferenceValue extends LabeledValue {
|
|
73
|
+
avatarUrl?: string;
|
|
74
|
+
employeeNo?: string;
|
|
75
|
+
title?: string;
|
|
76
|
+
mobile?: string;
|
|
77
|
+
email?: string;
|
|
78
|
+
departments?: DepartmentReferenceValue[];
|
|
79
|
+
}
|
|
80
|
+
export interface DateRangeValue { start: string; end: string; }
|
|
81
|
+
export type CascadePathValue = LabeledValue[];
|
|
82
|
+
export interface DataFileRef {
|
|
83
|
+
schemaVersion: 'openxiangda.data-file-ref/v2';
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
size: number;
|
|
87
|
+
contentType: string;
|
|
88
|
+
}
|
|
89
|
+
export interface DataImageRef extends DataFileRef {
|
|
90
|
+
width: number;
|
|
91
|
+
height: number;
|
|
92
|
+
thumbnailUrl: string;
|
|
93
|
+
previewUrl: string;
|
|
94
|
+
}
|
|
95
|
+
export interface StableSignatureValue {
|
|
96
|
+
file: Omit<DataFileRef, 'schemaVersion'>;
|
|
97
|
+
signer?: UserReferenceValue;
|
|
98
|
+
signedAt: string;
|
|
99
|
+
points?: Array<{ x: number; y: number; t: number }>;
|
|
100
|
+
hash: string;
|
|
101
|
+
}
|
|
102
|
+
export interface StableAddressValue {
|
|
103
|
+
country?: LabeledValue;
|
|
104
|
+
province?: LabeledValue;
|
|
105
|
+
city?: LabeledValue;
|
|
106
|
+
district?: LabeledValue;
|
|
107
|
+
street?: LabeledValue;
|
|
108
|
+
detail?: string;
|
|
109
|
+
fullAddress?: string;
|
|
110
|
+
}
|
|
111
|
+
export interface StableLocationSnapshot {
|
|
112
|
+
address?: string;
|
|
113
|
+
name?: string;
|
|
114
|
+
province?: string;
|
|
115
|
+
city?: string;
|
|
116
|
+
district?: string;
|
|
117
|
+
accuracy?: number;
|
|
118
|
+
capturedAt?: string;
|
|
119
|
+
}
|
|
120
|
+
export type StableLocationValue = StableLocationSnapshot & {
|
|
121
|
+
source: 'browser' | 'dingTalk';
|
|
122
|
+
longitude: number;
|
|
123
|
+
latitude: number;
|
|
124
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './generated.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const root = resolve(import.meta.dirname, '..');
|
|
5
|
+
const generated = /(?:^|\/)(?:node_modules|dist|coverage|playwright-report|test-results|\.openxiangda|\.turbo|\.turbopack|\.umi|\.umi-production)(?:\/|$)/;
|
|
6
|
+
const sourceExtensions = /\.(?:[cm]?[jt]sx?|css|html|json|md|ya?ml)$/;
|
|
7
|
+
const files = [];
|
|
8
|
+
function visit(directory) {
|
|
9
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
10
|
+
const path = join(directory, entry.name);
|
|
11
|
+
const name = relative(root, path).replaceAll('\\', '/');
|
|
12
|
+
if (
|
|
13
|
+
generated.test(name) ||
|
|
14
|
+
name === 'pnpm-lock.yaml' ||
|
|
15
|
+
name === 'packages/contracts/src/generated.ts'
|
|
16
|
+
) continue;
|
|
17
|
+
if (entry.isDirectory()) visit(path);
|
|
18
|
+
else if (entry.isFile() && sourceExtensions.test(name)) files.push(path);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
visit(root);
|
|
22
|
+
const bytes = files.reduce((total, file) => total + statSync(file).size, 0);
|
|
23
|
+
if (files.length > 50) throw new Error(`TEMPLATE_SOURCE_FILE_BUDGET_EXCEEDED:${files.length}>50`);
|
|
24
|
+
if (bytes > 500_000) throw new Error(`TEMPLATE_SOURCE_BYTE_BUDGET_EXCEEDED:${bytes}>500000`);
|
|
25
|
+
const productionFiles = files.filter(file => {
|
|
26
|
+
const name = relative(root, file).replaceAll('\\', '/');
|
|
27
|
+
return (
|
|
28
|
+
name === 'package.json' ||
|
|
29
|
+
name === 'openxiangda.config.ts' ||
|
|
30
|
+
name.startsWith('platform/') ||
|
|
31
|
+
(name.startsWith('apps/') &&
|
|
32
|
+
!name.includes('/test/') &&
|
|
33
|
+
!name.includes('/e2e/') &&
|
|
34
|
+
!name.includes('/scripts/'))
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
const source = productionFiles.map(file => readFileSync(file, 'utf8')).join('\n');
|
|
38
|
+
const agents = readFileSync(resolve(root, 'AGENTS.md'), 'utf8');
|
|
39
|
+
if (!agents.includes('pnpm openxiangda')) throw new Error('TEMPLATE_AGENT_PINNED_CLI_MISSING');
|
|
40
|
+
if (
|
|
41
|
+
!agents.includes("union of that user's application roles") ||
|
|
42
|
+
!agents.includes('does not transmit a RoleSession header')
|
|
43
|
+
) {
|
|
44
|
+
throw new Error('TEMPLATE_AGENT_ROLE_UNION_BOUNDARY_MISSING');
|
|
45
|
+
}
|
|
46
|
+
for (const marker of ['@umijs/', 'openxiangda-' + 'admin', 'openxiangda-' + 'user', 'instrument_query', 'instrument_save', 'function.invoke', 'departmentScopeKey', 'instrumentScopeKey', 'adminUserIds', 'college-' + 'am', 'user-' + 'wang', 'dept-' + 'am-test']) {
|
|
47
|
+
if (source.includes(marker)) throw new Error(`TEMPLATE_FORBIDDEN_MARKER:${marker}`);
|
|
48
|
+
}
|
|
49
|
+
process.stdout.write(`Verified compact template: ${files.length} source files, ${bytes} bytes.\n`);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"exactOptionalPropertyTypes": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"experimentalDecorators": true,
|
|
11
|
+
"emitDecoratorMetadata": true
|
|
12
|
+
}
|
|
13
|
+
}
|