@77-zhike/app-sdk 0.2.0-d7b1.7 → 0.2.0-d7b1.9
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 -7
- package/package.json +9 -3
- package/src/cli/development-logs.js +5 -2
- package/src/cli/index.js +11 -23
- package/src/cli/local-commands.js +1 -0
- package/src/cli/project-init.js +2 -1
- package/src/forms/array-contract.d.ts +32 -0
- package/src/forms/array-contract.js +138 -0
- package/src/forms/factory.js +9 -4
- package/src/forms/field-array.d.ts +16 -0
- package/src/forms/field-array.js +52 -0
- package/src/forms/index.d.ts +5 -1
- package/src/forms/schema.d.ts +30 -11
- package/src/forms/schema.js +29 -6
- package/src/forms/state.d.ts +6 -4
- package/src/forms/state.js +14 -24
- package/src/forms/validation.d.ts +3 -3
- package/src/forms/validation.js +24 -26
- package/src/server/index.d.ts +40 -5
- package/src/server/index.js +21 -0
- package/src/server/steps/index.d.ts +71 -0
- package/src/server/steps/index.js +15 -0
- package/src/server/triggers/index.d.ts +68 -0
- package/src/server/triggers/index.js +27 -0
- package/src/server/workflow-blocks/index.d.ts +72 -0
- package/src/server/workflow-blocks/index.js +32 -0
- package/src/toolchain/allowed-packages.json +7 -0
- package/src/toolchain/dependency-policy.js +175 -0
- package/src/toolchain/index.js +0 -88
- package/src/toolchain/real-build.js +59 -17
- package/src/toolchain/source-discovery.js +76 -65
- package/src/toolchain/workflow-blocks/discovery.js +133 -0
- package/src/toolchain/workflow-steps/manifest.js +99 -0
- package/src/toolchain/workflow-triggers/manifest.js +47 -0
- package/src/ui/components.generated.js +3 -2
- package/src/ui/factory.js +1 -1
- package/src/ui/index.d.ts +5 -5
- package/src/ui/index.js +1 -1
- package/src/ui/validation.js +3 -3
- package/src/cli/validate-bundle.js +0 -36
|
@@ -5,6 +5,8 @@ import { posix, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import ts from "typescript";
|
|
6
6
|
|
|
7
7
|
import { ZhikeCliError } from "../errors.js";
|
|
8
|
+
import { discoverWorkflowBlock } from "./workflow-blocks/discovery.js";
|
|
9
|
+
import { assertAllowedPackageImport } from "./dependency-policy.js";
|
|
8
10
|
|
|
9
11
|
const KEY_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
10
12
|
const RECORD_OBJECT_PATTERN = /^[a-z][a-z0-9_]{0,106}$/;
|
|
@@ -23,44 +25,6 @@ const LIFECYCLE_EVENTS = new Set([
|
|
|
23
25
|
"connection-added",
|
|
24
26
|
"connection-removed",
|
|
25
27
|
]);
|
|
26
|
-
const SDK_IMPORTS = new Set([
|
|
27
|
-
"@77-zhike/app-sdk",
|
|
28
|
-
"@77-zhike/app-sdk/client",
|
|
29
|
-
"@77-zhike/app-sdk/ui",
|
|
30
|
-
"@77-zhike/app-sdk/forms",
|
|
31
|
-
"react",
|
|
32
|
-
"react/jsx-runtime",
|
|
33
|
-
"react/jsx-dev-runtime",
|
|
34
|
-
"@77-zhike/app-sdk/server",
|
|
35
|
-
]);
|
|
36
|
-
const NODE_BUILTINS = new Set([
|
|
37
|
-
"assert",
|
|
38
|
-
"buffer",
|
|
39
|
-
"child_process",
|
|
40
|
-
"cluster",
|
|
41
|
-
"crypto",
|
|
42
|
-
"dgram",
|
|
43
|
-
"dns",
|
|
44
|
-
"events",
|
|
45
|
-
"fs",
|
|
46
|
-
"http",
|
|
47
|
-
"https",
|
|
48
|
-
"module",
|
|
49
|
-
"net",
|
|
50
|
-
"os",
|
|
51
|
-
"path",
|
|
52
|
-
"perf_hooks",
|
|
53
|
-
"process",
|
|
54
|
-
"readline",
|
|
55
|
-
"stream",
|
|
56
|
-
"tls",
|
|
57
|
-
"url",
|
|
58
|
-
"util",
|
|
59
|
-
"v8",
|
|
60
|
-
"vm",
|
|
61
|
-
"worker_threads",
|
|
62
|
-
"zlib",
|
|
63
|
-
]);
|
|
64
28
|
|
|
65
29
|
export async function discoverAppSources(projectDirectory) {
|
|
66
30
|
const sourceRoot = resolve(projectDirectory, "src");
|
|
@@ -71,11 +35,7 @@ export async function discoverAppSources(projectDirectory) {
|
|
|
71
35
|
}));
|
|
72
36
|
rejectUnsupportedPaths(normalized);
|
|
73
37
|
|
|
74
|
-
const
|
|
75
|
-
const lifecycleEvents = [];
|
|
76
|
-
const serverFunctions = [];
|
|
77
|
-
const webhookHandlers = [];
|
|
78
|
-
const globalKeys = new Map();
|
|
38
|
+
const parsedFiles = [];
|
|
79
39
|
for (const file of normalized) {
|
|
80
40
|
const sourceText = await readFile(file.absolutePath, "utf8");
|
|
81
41
|
const sourceFile = ts.createSourceFile(
|
|
@@ -86,6 +46,19 @@ export async function discoverAppSources(projectDirectory) {
|
|
|
86
46
|
file.relativePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
87
47
|
);
|
|
88
48
|
assertSyntacticallyValid(sourceFile);
|
|
49
|
+
parsedFiles.push({ ...file, sourceFile });
|
|
50
|
+
}
|
|
51
|
+
const sourceFilesByPath = new Map(parsedFiles.map(file => [file.relativePath, file.sourceFile]));
|
|
52
|
+
|
|
53
|
+
const frontendExtensions = [];
|
|
54
|
+
const lifecycleEvents = [];
|
|
55
|
+
const serverFunctions = [];
|
|
56
|
+
const webhookHandlers = [];
|
|
57
|
+
const workflowSteps = [];
|
|
58
|
+
const workflowTriggers = [];
|
|
59
|
+
const globalKeys = new Map();
|
|
60
|
+
for (const file of parsedFiles) {
|
|
61
|
+
const { sourceFile } = file;
|
|
89
62
|
validateImports(sourceFile, file.relativePath);
|
|
90
63
|
|
|
91
64
|
const extensionMatch = file.relativePath.match(
|
|
@@ -103,6 +76,9 @@ export async function discoverAppSources(projectDirectory) {
|
|
|
103
76
|
const serverMatch = file.relativePath.match(
|
|
104
77
|
/^(server\/functions\/[a-z0-9][a-z0-9/-]*)\.server\.ts$/,
|
|
105
78
|
);
|
|
79
|
+
const workflowBlockMatch = file.relativePath.match(
|
|
80
|
+
/^app\/blocks\/([a-z][a-z0-9-]{0,63})\/block\.ts$/,
|
|
81
|
+
);
|
|
106
82
|
if (lifecycleMatch) {
|
|
107
83
|
const definition = parseLifecycleEvent(
|
|
108
84
|
sourceFile,
|
|
@@ -143,17 +119,30 @@ export async function discoverAppSources(projectDirectory) {
|
|
|
143
119
|
);
|
|
144
120
|
registerUniqueKey(globalKeys, definition.key, file.relativePath);
|
|
145
121
|
serverFunctions.push(definition);
|
|
122
|
+
} else if (workflowBlockMatch) {
|
|
123
|
+
const definition = discoverWorkflowBlock(
|
|
124
|
+
sourceFile, file.relativePath, workflowBlockMatch[1], sourceFilesByPath,
|
|
125
|
+
);
|
|
126
|
+
if (definition.type === "step") {
|
|
127
|
+
registerUniqueKey(globalKeys, definition.entry, file.relativePath);
|
|
128
|
+
workflowSteps.push(definition);
|
|
129
|
+
} else {
|
|
130
|
+
for (const entry of Object.values(definition.entries)) registerUniqueKey(globalKeys, entry, file.relativePath);
|
|
131
|
+
workflowTriggers.push(definition);
|
|
132
|
+
}
|
|
146
133
|
}
|
|
147
134
|
}
|
|
148
135
|
if (
|
|
149
136
|
frontendExtensions.length === 0 &&
|
|
150
137
|
lifecycleEvents.length === 0 &&
|
|
151
138
|
serverFunctions.length === 0 &&
|
|
152
|
-
webhookHandlers.length === 0
|
|
139
|
+
webhookHandlers.length === 0 &&
|
|
140
|
+
workflowSteps.length === 0 &&
|
|
141
|
+
workflowTriggers.length === 0
|
|
153
142
|
) {
|
|
154
143
|
throw toolchainError(
|
|
155
144
|
"zhike_source_entry_missing",
|
|
156
|
-
"
|
|
145
|
+
"未发现 Lifecycle Event、Frontend Extension、Server Function、Webhook Handler 或 Workflow Block",
|
|
157
146
|
);
|
|
158
147
|
}
|
|
159
148
|
if (frontendExtensions.filter(item => item.type === "settings").length > 1) {
|
|
@@ -163,8 +152,10 @@ export async function discoverAppSources(projectDirectory) {
|
|
|
163
152
|
frontendExtensions: Object.freeze(sortByKey(frontendExtensions)),
|
|
164
153
|
lifecycleEvents: Object.freeze(sortByKey(lifecycleEvents)),
|
|
165
154
|
serverFunctions: Object.freeze(sortByKey(serverFunctions)),
|
|
166
|
-
sourceFiles: Object.freeze(
|
|
155
|
+
sourceFiles: Object.freeze(parsedFiles.map((file) => file.relativePath).sort()),
|
|
167
156
|
webhookHandlers: Object.freeze(sortByKey(webhookHandlers)),
|
|
157
|
+
workflowSteps: Object.freeze(sortByKey(workflowSteps)),
|
|
158
|
+
workflowTriggers: Object.freeze(sortByKey(workflowTriggers)),
|
|
168
159
|
});
|
|
169
160
|
}
|
|
170
161
|
|
|
@@ -567,12 +558,22 @@ function hasNamedImport(statement, specifier, importedName) {
|
|
|
567
558
|
);
|
|
568
559
|
}
|
|
569
560
|
|
|
570
|
-
function validateImports(sourceFile, relativePath) {
|
|
561
|
+
export function validateImports(sourceFile, relativePath, options = {}) {
|
|
571
562
|
const allowsStandardFetch =
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
563
|
+
options.target === "server" || (!options.target && (
|
|
564
|
+
relativePath.startsWith("server/") ||
|
|
565
|
+
relativePath.startsWith("app/blocks/") ||
|
|
566
|
+
/^app\/events\/.+\.event\.ts$/.test(relativePath) ||
|
|
567
|
+
/^app\/webhooks\/.+\.webhook\.ts$/.test(relativePath)));
|
|
568
|
+
const target = allowsStandardFetch ? "server" : "client";
|
|
575
569
|
const visit = (node) => {
|
|
570
|
+
if (ts.isImportEqualsDeclaration(node) ||
|
|
571
|
+
(ts.isExportAssignment(node) && node.isExportEquals) ||
|
|
572
|
+
(ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require") ||
|
|
573
|
+
((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) &&
|
|
574
|
+
ts.isIdentifier(node.expression) && ["require", "module", "exports"].includes(node.expression.text))) {
|
|
575
|
+
throw toolchainError("zhike_commonjs_forbidden", relativePath + ": 只支持静态 ESM import/export,不支持 CommonJS");
|
|
576
|
+
}
|
|
576
577
|
if (
|
|
577
578
|
ts.isCallExpression(node) &&
|
|
578
579
|
node.expression.kind === ts.SyntaxKind.ImportKeyword
|
|
@@ -626,27 +627,20 @@ function validateImports(sourceFile, relativePath) {
|
|
|
626
627
|
`${relativePath}: App 源码不得直接读取 Host SDK global`,
|
|
627
628
|
);
|
|
628
629
|
}
|
|
629
|
-
|
|
630
|
-
|
|
630
|
+
const moduleSpecifier = (ts.isImportDeclaration(node) || ts.isExportDeclaration(node))
|
|
631
|
+
? node.moduleSpecifier
|
|
632
|
+
: ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)
|
|
633
|
+
? node.argument.literal : null;
|
|
634
|
+
if (moduleSpecifier && ts.isStringLiteral(moduleSpecifier)) {
|
|
635
|
+
const specifier = moduleSpecifier.text;
|
|
631
636
|
if (specifier === "@77-zhike/app-sdk/server" && !allowsStandardFetch) {
|
|
632
637
|
throw toolchainError(
|
|
633
638
|
"zhike_server_sdk_client_forbidden",
|
|
634
639
|
`${relativePath}: 前端代码不得导入服务端 SDK`,
|
|
635
640
|
);
|
|
636
641
|
}
|
|
637
|
-
if (specifier.startsWith("
|
|
638
|
-
|
|
639
|
-
"zhike_node_builtin_forbidden",
|
|
640
|
-
`${relativePath}: 一期禁止 Node builtin ${specifier}`,
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
|
-
if (!specifier.startsWith(".") && !SDK_IMPORTS.has(specifier)) {
|
|
644
|
-
throw toolchainError(
|
|
645
|
-
"zhike_external_dependency_forbidden",
|
|
646
|
-
`${relativePath}: 一期禁止第三方或内部依赖 ${specifier}`,
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
if (specifier.startsWith(".")) {
|
|
642
|
+
if (!specifier.startsWith(".")) assertAllowedPackageImport(specifier, target, relativePath);
|
|
643
|
+
if (specifier.startsWith(".") && !options.dependency) {
|
|
650
644
|
const resolvedSourcePath = posix.normalize(
|
|
651
645
|
posix.join(posix.dirname(relativePath), specifier),
|
|
652
646
|
);
|
|
@@ -694,6 +688,23 @@ function rejectUnsupportedPaths(files) {
|
|
|
694
688
|
`${path}: 一期不支持 Workflow/Event/Connection/Settings`,
|
|
695
689
|
);
|
|
696
690
|
}
|
|
691
|
+
if (path.startsWith("server/steps/") || path.startsWith("server/triggers/")) {
|
|
692
|
+
throw toolchainError(
|
|
693
|
+
"zhike_workflow_block_location_invalid",
|
|
694
|
+
`${path}: Workflow Block 必须位于 src/app/blocks/<block-key>/`,
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
if (path.startsWith("app/blocks/") && /\.(?:ts|tsx)$/.test(path)) {
|
|
698
|
+
const validBlockFile = /^app\/blocks\/[a-z][a-z0-9-]{0,63}\/(?:block|execute|activate|trigger|deactivate)\.ts$/.test(path);
|
|
699
|
+
const helperFile = /^app\/blocks\/[a-z][a-z0-9-]{0,63}\/[-a-zA-Z0-9._]+\.ts$/.test(path) &&
|
|
700
|
+
!/\/(?:block|execute|activate|trigger|deactivate)\./.test(path);
|
|
701
|
+
if (!validBlockFile && !helperFile) {
|
|
702
|
+
throw toolchainError(
|
|
703
|
+
"zhike_workflow_block_location_invalid",
|
|
704
|
+
`${path}: Block 必须位于 src/app/blocks/<block-key>/,固定入口只允许 .ts`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
697
708
|
if (path.endsWith(".server.tsx")) {
|
|
698
709
|
throw toolchainError(
|
|
699
710
|
"zhike_server_function_extension_invalid",
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
import { ZhikeCliError } from "../../errors.js";
|
|
4
|
+
import { validateWorkflowStepDefinition } from "../workflow-steps/manifest.js";
|
|
5
|
+
import { validateWorkflowTriggerDefinition } from "../workflow-triggers/manifest.js";
|
|
6
|
+
|
|
7
|
+
const HANDLERS = Object.freeze({
|
|
8
|
+
step: Object.freeze({ execute: "defineWorkflowBlockExecute" }),
|
|
9
|
+
trigger: Object.freeze({
|
|
10
|
+
activate: "defineWorkflowBlockActivate",
|
|
11
|
+
trigger: "defineWorkflowBlockTrigger",
|
|
12
|
+
deactivate: "defineWorkflowBlockDeactivate",
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function invalid(path, detail) {
|
|
17
|
+
throw new ZhikeCliError("zhike_workflow_block_invalid", `${path}: ${detail}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function literal(node, path) {
|
|
21
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
22
|
+
if (ts.isNumericLiteral(node)) return Number(node.text);
|
|
23
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
24
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
25
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
26
|
+
if (ts.isArrayLiteralExpression(node)) return node.elements.map(item => literal(item, path));
|
|
27
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
28
|
+
const result = {};
|
|
29
|
+
for (const property of node.properties) {
|
|
30
|
+
if (!ts.isPropertyAssignment(property) ||
|
|
31
|
+
!(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
|
|
32
|
+
invalid(path, "声明只能包含静态 JSON 属性");
|
|
33
|
+
}
|
|
34
|
+
const key = property.name.text;
|
|
35
|
+
if (Object.hasOwn(result, key)) invalid(path, `重复字段 ${key}`);
|
|
36
|
+
result[key] = literal(property.initializer, `${path}.${key}`);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
invalid(path, "声明必须是 JSON 字面量,不能读取变量或运行时代码");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function namedImports(sourceFile, moduleName = "@77-zhike/app-sdk/server") {
|
|
44
|
+
return new Set(sourceFile.statements.flatMap(statement =>
|
|
45
|
+
ts.isImportDeclaration(statement) && statement.moduleSpecifier.text === moduleName &&
|
|
46
|
+
statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings)
|
|
47
|
+
? statement.importClause.namedBindings.elements
|
|
48
|
+
.filter(item => !item.propertyName).map(item => item.name.text)
|
|
49
|
+
: []));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseDefinition(sourceFile, relativePath) {
|
|
53
|
+
if (!namedImports(sourceFile).has("defineWorkflowBlock")) {
|
|
54
|
+
invalid(relativePath, "block.ts 必须直接导入 defineWorkflowBlock");
|
|
55
|
+
}
|
|
56
|
+
const exports = sourceFile.statements.filter(statement =>
|
|
57
|
+
ts.isExportAssignment(statement) && !statement.isExportEquals);
|
|
58
|
+
if (exports.length !== 1 || !ts.isCallExpression(exports[0].expression)) {
|
|
59
|
+
invalid(relativePath, "block.ts 必须直接默认导出唯一的 defineWorkflowBlock({...})");
|
|
60
|
+
}
|
|
61
|
+
const call = exports[0].expression;
|
|
62
|
+
if (!ts.isIdentifier(call.expression) || call.expression.text !== "defineWorkflowBlock" ||
|
|
63
|
+
call.arguments.length !== 1) {
|
|
64
|
+
invalid(relativePath, "block.ts 必须直接默认导出 defineWorkflowBlock({...})");
|
|
65
|
+
}
|
|
66
|
+
return literal(call.arguments[0], relativePath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function defaultBlockImport(sourceFile, relativePath) {
|
|
70
|
+
const imports = sourceFile.statements.filter(statement =>
|
|
71
|
+
ts.isImportDeclaration(statement) && ["./block", "./block.js"].includes(statement.moduleSpecifier.text) &&
|
|
72
|
+
statement.importClause?.name);
|
|
73
|
+
if (imports.length !== 1) invalid(relativePath, "处理器必须从 ./block.js 默认导入当前 Block");
|
|
74
|
+
return imports[0].importClause.name.text;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateHandler(sourceFile, relativePath, api) {
|
|
78
|
+
if (!namedImports(sourceFile).has(api)) {
|
|
79
|
+
invalid(relativePath, `必须直接导入 ${api}`);
|
|
80
|
+
}
|
|
81
|
+
const blockName = defaultBlockImport(sourceFile, relativePath);
|
|
82
|
+
const exports = sourceFile.statements.filter(statement =>
|
|
83
|
+
ts.isExportAssignment(statement) && !statement.isExportEquals);
|
|
84
|
+
if (exports.length !== 1 || !ts.isCallExpression(exports[0].expression)) {
|
|
85
|
+
invalid(relativePath, `必须直接默认导出 ${api}(block, async (...) => ...)`);
|
|
86
|
+
}
|
|
87
|
+
const call = exports[0].expression;
|
|
88
|
+
if (!ts.isIdentifier(call.expression) || call.expression.text !== api || call.arguments.length !== 2 ||
|
|
89
|
+
!ts.isIdentifier(call.arguments[0]) || call.arguments[0].text !== blockName ||
|
|
90
|
+
!(ts.isArrowFunction(call.arguments[1]) || ts.isFunctionExpression(call.arguments[1])) ||
|
|
91
|
+
!call.arguments[1].modifiers?.some(item => item.kind === ts.SyntaxKind.AsyncKeyword)) {
|
|
92
|
+
invalid(relativePath, `必须直接默认导出 ${api}(block, async (...) => ...)`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function discoverWorkflowBlock(sourceFile, relativePath, directoryKey, sourceFilesByPath) {
|
|
97
|
+
const value = parseDefinition(sourceFile, relativePath);
|
|
98
|
+
if (!["step", "trigger"].includes(value.type)) {
|
|
99
|
+
invalid(relativePath, 'type 必须是 "step" 或 "trigger"');
|
|
100
|
+
}
|
|
101
|
+
const handlers = HANDLERS[value.type];
|
|
102
|
+
const handlerPaths = {};
|
|
103
|
+
for (const [name, api] of Object.entries(handlers)) {
|
|
104
|
+
const handlerPath = `app/blocks/${directoryKey}/${name}.ts`;
|
|
105
|
+
const handlerSource = sourceFilesByPath.get(handlerPath);
|
|
106
|
+
if (!handlerSource) invalid(relativePath, `${value.type} Block 缺少 ${name}.ts`);
|
|
107
|
+
validateHandler(handlerSource, handlerPath, api);
|
|
108
|
+
handlerPaths[name] = handlerPath;
|
|
109
|
+
}
|
|
110
|
+
const unexpected = value.type === "step"
|
|
111
|
+
? ["activate", "trigger", "deactivate"].filter(name => sourceFilesByPath.has(`app/blocks/${directoryKey}/${name}.ts`))
|
|
112
|
+
: ["execute"].filter(name => sourceFilesByPath.has(`app/blocks/${directoryKey}/${name}.ts`));
|
|
113
|
+
if (unexpected.length > 0) invalid(relativePath, `${value.type} Block 不应包含 ${unexpected.join(", ")}.ts`);
|
|
114
|
+
|
|
115
|
+
const { type, ...definition } = value;
|
|
116
|
+
try {
|
|
117
|
+
if (type === "step") {
|
|
118
|
+
const step = validateWorkflowStepDefinition(definition, relativePath, directoryKey);
|
|
119
|
+
return Object.freeze({ ...step, type, handlerPaths: Object.freeze(handlerPaths) });
|
|
120
|
+
}
|
|
121
|
+
const trigger = validateWorkflowTriggerDefinition(definition, relativePath, directoryKey);
|
|
122
|
+
return Object.freeze({ ...trigger, type, handlerPaths: Object.freeze({
|
|
123
|
+
activate: handlerPaths.activate,
|
|
124
|
+
onEvent: handlerPaths.trigger,
|
|
125
|
+
deactivate: handlerPaths.deactivate,
|
|
126
|
+
}) });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error?.code === "zhike_workflow_step_invalid" || error?.code === "zhike_workflow_trigger_invalid") {
|
|
129
|
+
invalid(relativePath, error.message);
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ZhikeCliError } from "../../errors.js";
|
|
2
|
+
|
|
3
|
+
const ID = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
4
|
+
const FIELD = /^[a-z][a-zA-Z0-9_]{0,63}$/;
|
|
5
|
+
const DISPLAY = new Set(["title", "description", "examples"]);
|
|
6
|
+
|
|
7
|
+
function invalid(path, detail) {
|
|
8
|
+
throw new ZhikeCliError("zhike_workflow_step_invalid", `${path}: ${detail}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function object(value, path) {
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalid(path, "必须是对象");
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function exactKeys(value, allowed, path) {
|
|
17
|
+
for (const key of Object.keys(value)) {
|
|
18
|
+
if (!allowed.has(key)) invalid(path, `不支持字段 ${key}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function display(value, path) {
|
|
23
|
+
for (const key of ["title", "description"]) {
|
|
24
|
+
if (value[key] !== undefined && (typeof value[key] !== "string" || value[key].length > 300)) {
|
|
25
|
+
invalid(path, `${key} 无效`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (value.examples !== undefined && (
|
|
29
|
+
!Array.isArray(value.examples) || value.examples.length > 3 ||
|
|
30
|
+
value.examples.some(item => !["string", "number", "boolean"].includes(typeof item))
|
|
31
|
+
)) invalid(path, "examples 无效");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function schema(value, path) {
|
|
35
|
+
object(value, path);
|
|
36
|
+
exactKeys(value, new Set(["type", "properties", "required", "additionalProperties", ...DISPLAY]), path);
|
|
37
|
+
if (value.type !== "object" || value.additionalProperties !== false) invalid(path, "只支持封闭 object schema");
|
|
38
|
+
object(value.properties, `${path}.properties`);
|
|
39
|
+
const fields = Object.entries(value.properties);
|
|
40
|
+
if (fields.length > 32) invalid(path, "字段超过 32 个");
|
|
41
|
+
const required = value.required ?? [];
|
|
42
|
+
if (!Array.isArray(required) || new Set(required).size !== required.length ||
|
|
43
|
+
required.some(key => typeof key !== "string" || !Object.hasOwn(value.properties, key))) {
|
|
44
|
+
invalid(path, "required 必须引用唯一的已声明字段");
|
|
45
|
+
}
|
|
46
|
+
for (const [key, field] of fields) {
|
|
47
|
+
if (!FIELD.test(key)) invalid(path, `字段名 ${key} 无效`);
|
|
48
|
+
object(field, `${path}.${key}`);
|
|
49
|
+
const fieldPath = `${path}.properties.${key}`;
|
|
50
|
+
const allowed = field.type === "string"
|
|
51
|
+
? new Set(["type", "minLength", "enum", ...DISPLAY])
|
|
52
|
+
: new Set(["type", ...DISPLAY]);
|
|
53
|
+
exactKeys(field, allowed, fieldPath);
|
|
54
|
+
if (!["string", "number", "boolean"].includes(field.type)) invalid(fieldPath, "只支持 string/number/boolean");
|
|
55
|
+
if (field.minLength !== undefined && (!Number.isInteger(field.minLength) || field.minLength < 0 || field.minLength > 1024)) {
|
|
56
|
+
invalid(fieldPath, "minLength 无效");
|
|
57
|
+
}
|
|
58
|
+
if (field.enum !== undefined && (
|
|
59
|
+
!Array.isArray(field.enum) || field.enum.length < 1 || field.enum.length > 32 ||
|
|
60
|
+
field.enum.some(item => typeof item !== "string" || item.length > 300) ||
|
|
61
|
+
new Set(field.enum).size !== field.enum.length
|
|
62
|
+
)) invalid(fieldPath, "enum 无效");
|
|
63
|
+
display(field, fieldPath);
|
|
64
|
+
}
|
|
65
|
+
display(value, path);
|
|
66
|
+
if (Buffer.byteLength(JSON.stringify(value), "utf8") > 16 * 1024) invalid(path, "schema 超过 16 KiB");
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function validateWorkflowStepDefinition(value, relativePath, fileKey) {
|
|
71
|
+
object(value, relativePath);
|
|
72
|
+
exactKeys(value, new Set(["key", "title", "description", "connection", "configSchema", "outcomes"]), relativePath);
|
|
73
|
+
if (!ID.test(value.key) || value.key !== fileKey) invalid(relativePath, "Step key 必须与文件名一致");
|
|
74
|
+
if (typeof value.title !== "string" || value.title.length < 1 || value.title.length > 80) invalid(relativePath, "title 无效");
|
|
75
|
+
if (value.description !== undefined && (typeof value.description !== "string" || value.description.length > 300)) invalid(relativePath, "description 无效");
|
|
76
|
+
if (!["workspace", "none"].includes(value.connection)) invalid(relativePath, "connection 无效");
|
|
77
|
+
schema(value.configSchema, `${relativePath}.configSchema`);
|
|
78
|
+
if (!Array.isArray(value.outcomes) || value.outcomes.length < 1 || value.outcomes.length > 8) {
|
|
79
|
+
invalid(relativePath, "outcomes 必须包含 1–8 个静态出口");
|
|
80
|
+
}
|
|
81
|
+
const ids = new Set();
|
|
82
|
+
for (const [index, outcome] of value.outcomes.entries()) {
|
|
83
|
+
const path = `${relativePath}.outcomes[${index}]`;
|
|
84
|
+
object(outcome, path);
|
|
85
|
+
exactKeys(outcome, new Set(["id", "label", "dataSchema"]), path);
|
|
86
|
+
if (!ID.test(outcome.id) || ["in", "error"].includes(outcome.id) || ids.has(outcome.id)) {
|
|
87
|
+
invalid(path, "出口 ID 重复、保留或不合法");
|
|
88
|
+
}
|
|
89
|
+
ids.add(outcome.id);
|
|
90
|
+
if (typeof outcome.label !== "string" || outcome.label.length < 1 || outcome.label.length > 80) invalid(path, "label 无效");
|
|
91
|
+
if (outcome.dataSchema !== null) schema(outcome.dataSchema, `${path}.dataSchema`);
|
|
92
|
+
}
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
...value,
|
|
95
|
+
artifact: "server",
|
|
96
|
+
entry: `steps/${value.key}`,
|
|
97
|
+
relativePath,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ZhikeCliError } from "../../errors.js";
|
|
2
|
+
import { validateWorkflowStepDefinition } from "../workflow-steps/manifest.js";
|
|
3
|
+
|
|
4
|
+
const HEADER = /^[a-z][a-z0-9-]{0,63}$/;
|
|
5
|
+
const BLOCKED_HEADERS = new Set(["authorization", "cookie", "set-cookie", "host", "content-length"]);
|
|
6
|
+
|
|
7
|
+
function invalid(path, detail) {
|
|
8
|
+
throw new ZhikeCliError("zhike_workflow_trigger_invalid", `${path}: ${detail}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function validateWorkflowTriggerDefinition(value, relativePath, fileKey) {
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value) ||
|
|
13
|
+
Object.keys(value).some(key => !["key", "title", "description", "connection", "requestHeaders", "configSchema", "outcomes"].includes(key))) {
|
|
14
|
+
invalid(relativePath, "Trigger 声明字段无效");
|
|
15
|
+
}
|
|
16
|
+
let common;
|
|
17
|
+
try {
|
|
18
|
+
const { requestHeaders, ...stepShape } = value;
|
|
19
|
+
common = validateWorkflowStepDefinition(stepShape, relativePath, fileKey);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
invalid(relativePath, error.message || "schema 无效");
|
|
22
|
+
}
|
|
23
|
+
if (value.outcomes.some(outcome => outcome.dataSchema === null)) {
|
|
24
|
+
invalid(relativePath, "Trigger 的每个出口必须声明数据 schema");
|
|
25
|
+
}
|
|
26
|
+
const requestHeaders = value.requestHeaders ?? [];
|
|
27
|
+
if (!Array.isArray(requestHeaders) || requestHeaders.length > 16 ||
|
|
28
|
+
requestHeaders.some(name => typeof name !== "string" || !HEADER.test(name) ||
|
|
29
|
+
BLOCKED_HEADERS.has(name) || name.startsWith("x-forwarded-") || name.startsWith("proxy-")) ||
|
|
30
|
+
new Set(requestHeaders).size !== requestHeaders.length) {
|
|
31
|
+
invalid(relativePath, "requestHeaders 必须是至多 16 个唯一的安全小写名称");
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
key: common.key, title: common.title,
|
|
35
|
+
...(common.description === undefined ? {} : { description: common.description }),
|
|
36
|
+
connection: common.connection,
|
|
37
|
+
configSchema: common.configSchema,
|
|
38
|
+
outcomes: common.outcomes,
|
|
39
|
+
requestHeaders: Object.freeze([...requestHeaders]),
|
|
40
|
+
entries: Object.freeze({
|
|
41
|
+
activate: `triggers/${common.key}/activate`,
|
|
42
|
+
onEvent: `triggers/${common.key}/onEvent`,
|
|
43
|
+
deactivate: `triggers/${common.key}/deactivate`,
|
|
44
|
+
}),
|
|
45
|
+
relativePath,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// 此文件由 scripts/generate-components.mjs 生成,请修改产品能力声明。
|
|
2
|
-
export const componentTypes = Object.freeze(["Icon","Stack","SplitLayout","SplitLayoutMain","SplitLayoutAside","SplitLayoutActions","Section","Banner","Badge","LoadingState","Card","Grid","EmptyState","
|
|
3
|
-
export const
|
|
2
|
+
export const componentTypes = Object.freeze(["Icon","Stack","SplitLayout","SplitLayoutMain","SplitLayoutAside","SplitLayoutActions","Section","Banner","Badge","LoadingState","Card","Grid","EmptyState","ZhikeLink","Button","Switch","LookupCell","Tabs","TabsList","TabsTrigger","TabsContent","Table","TableHeader","TableBody","TableRow","TableCell","Avatar","DescriptionList","DescriptionListItem","DescriptionListMenu","DescriptionListMenuItem","Divider","Json","Link","StatusBadge","TextBlock","TypographyTitle","TypographyBody","TypographyCaption","TableHeaderCell","WidgetTextWidget","WidgetTitle","WidgetTextPrimary","WidgetTextSecondary","WidgetLoading","WidgetDecoration"]);
|
|
3
|
+
export const publicComponentNames = Object.freeze(["Icon","Stack","SplitLayout","SplitLayoutMain","SplitLayoutAside","SplitLayoutActions","Section","Banner","Badge","LoadingState","Card","Grid","EmptyState","ZhikeLink","Button","Switch","LookupCell","Tabs","TabsList","TabsTrigger","TabsContent","Table","TableHeader","TableBody","TableRow","TableCell","Avatar","DescriptionList","DescriptionListItem","DescriptionListMenu","DescriptionListMenuItem","Divider","Json","Link","StatusBadge","TextBlock","TypographyTitle","TypographyBody","TypographyCaption","TableHeaderCell","WidgetTextWidget","WidgetTitle","WidgetTextPrimary","WidgetTextSecondary","WidgetLoading","WidgetDecoration"]);
|
|
4
|
+
export const legacyComponentTypes = Object.freeze(["Text","Icon","Stack","SplitLayout","SplitLayoutMain","SplitLayoutAside","SplitLayoutActions","Section","Banner","Badge","LoadingState","Card","Grid","EmptyState","ZhikeLink","Button","Switch","LookupCell","Tabs","TabsList","TabsTrigger","TabsContent","Table","TableHeader","TableBody","TableRow","TableCell","Avatar","DescriptionList","DescriptionListItem","DescriptionListMenu","DescriptionListMenuItem","Divider","Json","Link","StatusBadge","TextBlock","TypographyTitle","TypographyBody","TypographyCaption","TableHeaderCell","WidgetTextWidget","WidgetTitle","WidgetTextPrimary","WidgetTextSecondary","WidgetLoading","WidgetDecoration"]);
|
|
4
5
|
export const badgeColors = Object.freeze(["amber","blue","cyan","green","grey","lavender","lime","orange","pink","purple","red","yellow"]);
|
package/src/ui/factory.js
CHANGED
|
@@ -30,7 +30,7 @@ export function createUiComponents(React, { legacy = false, useOwner = () => nul
|
|
|
30
30
|
return handlers[index]();
|
|
31
31
|
} });
|
|
32
32
|
}
|
|
33
|
-
if (type === '
|
|
33
|
+
if (type === 'Link') {
|
|
34
34
|
const owner = useOwner();
|
|
35
35
|
const { href: input, onOpen, ...rest } = props;
|
|
36
36
|
if (onOpen !== undefined) throw new Error('runtime_ui_contract_invalid');
|