@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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkflowObjectSchema,
|
|
3
|
+
WorkflowSchemaValue,
|
|
4
|
+
WorkflowStepDefinition,
|
|
5
|
+
WorkflowStepOutcome,
|
|
6
|
+
WorkflowStepResult,
|
|
7
|
+
} from "../steps/index.js";
|
|
8
|
+
import type {
|
|
9
|
+
WorkflowTriggerDefinition,
|
|
10
|
+
WorkflowTriggerEventResult,
|
|
11
|
+
WorkflowTriggerOutcome,
|
|
12
|
+
} from "../triggers/index.js";
|
|
13
|
+
|
|
14
|
+
export type WorkflowStepBlockDefinition<
|
|
15
|
+
Config extends WorkflowObjectSchema = WorkflowObjectSchema,
|
|
16
|
+
Outcomes extends readonly WorkflowStepOutcome[] = readonly WorkflowStepOutcome[],
|
|
17
|
+
> = WorkflowStepDefinition<Config, Outcomes> & { readonly type: "step" };
|
|
18
|
+
|
|
19
|
+
export type WorkflowTriggerBlockDefinition<
|
|
20
|
+
Config extends WorkflowObjectSchema = WorkflowObjectSchema,
|
|
21
|
+
Outcomes extends readonly WorkflowTriggerOutcome[] = readonly WorkflowTriggerOutcome[],
|
|
22
|
+
> = WorkflowTriggerDefinition<Config, Outcomes> & { readonly type: "trigger" };
|
|
23
|
+
|
|
24
|
+
export type WorkflowBlockDefinition = WorkflowStepBlockDefinition | WorkflowTriggerBlockDefinition;
|
|
25
|
+
|
|
26
|
+
export function defineWorkflowBlock<
|
|
27
|
+
const Config extends WorkflowObjectSchema,
|
|
28
|
+
const Outcomes extends readonly [WorkflowStepOutcome, ...WorkflowStepOutcome[]],
|
|
29
|
+
>(definition: WorkflowStepBlockDefinition<Config, Outcomes>): WorkflowStepBlockDefinition<Config, Outcomes>;
|
|
30
|
+
|
|
31
|
+
export function defineWorkflowBlock<
|
|
32
|
+
const Config extends WorkflowObjectSchema,
|
|
33
|
+
const Outcomes extends readonly [WorkflowTriggerOutcome, ...WorkflowTriggerOutcome[]],
|
|
34
|
+
>(definition: WorkflowTriggerBlockDefinition<Config, Outcomes>): WorkflowTriggerBlockDefinition<Config, Outcomes>;
|
|
35
|
+
|
|
36
|
+
export function defineWorkflowBlockExecute<
|
|
37
|
+
Config extends WorkflowObjectSchema,
|
|
38
|
+
Outcomes extends readonly WorkflowStepOutcome[],
|
|
39
|
+
>(
|
|
40
|
+
block: WorkflowStepBlockDefinition<Config, Outcomes>,
|
|
41
|
+
execute: (params: Readonly<{ config: WorkflowSchemaValue<Config> }>) =>
|
|
42
|
+
WorkflowStepResult<Outcomes> | Promise<WorkflowStepResult<Outcomes>>,
|
|
43
|
+
): typeof execute;
|
|
44
|
+
|
|
45
|
+
export function defineWorkflowBlockActivate<Config extends WorkflowObjectSchema>(
|
|
46
|
+
block: WorkflowTriggerBlockDefinition<Config>,
|
|
47
|
+
handler: (params: Readonly<{
|
|
48
|
+
config: WorkflowSchemaValue<Config>;
|
|
49
|
+
activationId: string;
|
|
50
|
+
triggerCallbackUrl: string;
|
|
51
|
+
}>) => Promise<Readonly<{ subscriptionRef: string | null }>>,
|
|
52
|
+
): typeof handler;
|
|
53
|
+
|
|
54
|
+
export function defineWorkflowBlockTrigger<
|
|
55
|
+
Config extends WorkflowObjectSchema,
|
|
56
|
+
Outcomes extends readonly WorkflowTriggerOutcome[],
|
|
57
|
+
>(
|
|
58
|
+
block: WorkflowTriggerBlockDefinition<Config, Outcomes>,
|
|
59
|
+
handler: (params: Readonly<{
|
|
60
|
+
config: WorkflowSchemaValue<Config>;
|
|
61
|
+
body: string;
|
|
62
|
+
headers: Readonly<Record<string, string>>;
|
|
63
|
+
}>) => Promise<WorkflowTriggerEventResult<Outcomes>>,
|
|
64
|
+
): typeof handler;
|
|
65
|
+
|
|
66
|
+
export function defineWorkflowBlockDeactivate(
|
|
67
|
+
block: WorkflowTriggerBlockDefinition,
|
|
68
|
+
handler: (params: Readonly<{
|
|
69
|
+
activationId: string;
|
|
70
|
+
subscriptionRef: string | null;
|
|
71
|
+
}>) => Promise<void>,
|
|
72
|
+
): typeof handler;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const WORKFLOW_BLOCK_MARKER = Symbol.for("zhike.workflow-block.v1");
|
|
2
|
+
|
|
3
|
+
export function defineWorkflowBlock(definition) {
|
|
4
|
+
if (!definition || typeof definition !== "object" || Array.isArray(definition) ||
|
|
5
|
+
!["step", "trigger"].includes(definition.type)) {
|
|
6
|
+
throw new TypeError("workflow_block_definition_invalid");
|
|
7
|
+
}
|
|
8
|
+
return Object.freeze({ ...definition, [WORKFLOW_BLOCK_MARKER]: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function bindWorkflowBlockHandler(block, handler, type, kind) {
|
|
12
|
+
if (!block?.[WORKFLOW_BLOCK_MARKER] || block.type !== type || typeof handler !== "function") {
|
|
13
|
+
throw new TypeError(`workflow_block_${kind}_invalid`);
|
|
14
|
+
}
|
|
15
|
+
return handler;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function defineWorkflowBlockExecute(block, handler) {
|
|
19
|
+
return bindWorkflowBlockHandler(block, handler, "step", "execute");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function defineWorkflowBlockActivate(block, handler) {
|
|
23
|
+
return bindWorkflowBlockHandler(block, handler, "trigger", "activate");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function defineWorkflowBlockTrigger(block, handler) {
|
|
27
|
+
return bindWorkflowBlockHandler(block, handler, "trigger", "trigger");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function defineWorkflowBlockDeactivate(block, handler) {
|
|
31
|
+
return bindWorkflowBlockHandler(block, handler, "trigger", "deactivate");
|
|
32
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { builtinModules } from "node:module";
|
|
4
|
+
import { dirname, extname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
import { ZhikeCliError } from "../errors.js";
|
|
7
|
+
|
|
8
|
+
const packages = JSON.parse(readFileSync(new URL("./allowed-packages.json", import.meta.url), "utf8"));
|
|
9
|
+
export const SDK_IMPORTS = new Set([
|
|
10
|
+
"@77-zhike/app-sdk", "@77-zhike/app-sdk/client", "@77-zhike/app-sdk/ui",
|
|
11
|
+
"@77-zhike/app-sdk/forms", "@77-zhike/app-sdk/server",
|
|
12
|
+
"react", "react/jsx-runtime", "react/jsx-dev-runtime",
|
|
13
|
+
]);
|
|
14
|
+
const builtins = new Set(builtinModules.map(name => name.replace(/^node:/, "")));
|
|
15
|
+
|
|
16
|
+
function fail(code, source, message) {
|
|
17
|
+
throw new ZhikeCliError(code, source + ": " + message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function splitPackage(specifier) {
|
|
21
|
+
const parts = specifier.split("/");
|
|
22
|
+
const length = specifier.startsWith("@") ? 2 : 1;
|
|
23
|
+
return { name: parts.slice(0, length).join("/"),
|
|
24
|
+
subpath: parts.length === length ? "." : "./" + parts.slice(length).join("/") };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function assertAllowedPackageImport(specifier, target, source) {
|
|
28
|
+
if (specifier.startsWith("node:") || builtins.has(specifier)) {
|
|
29
|
+
fail("zhike_node_builtin_forbidden", source, "禁止 Node builtin " + specifier);
|
|
30
|
+
}
|
|
31
|
+
if (SDK_IMPORTS.has(specifier)) {
|
|
32
|
+
if (target === "client" && specifier === "@77-zhike/app-sdk/server") {
|
|
33
|
+
fail("zhike_server_sdk_client_forbidden", source, "前端代码不得导入服务端 SDK");
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const { name, subpath } = splitPackage(specifier);
|
|
38
|
+
const policy = Object.hasOwn(packages, name) ? packages[name] : null;
|
|
39
|
+
if (!policy) fail("zhike_external_dependency_forbidden", source, "未获准依赖 " + specifier);
|
|
40
|
+
if (!policy.targets.includes(target)) {
|
|
41
|
+
fail("zhike_dependency_target_forbidden", source, "该运行端不允许导入 " + name);
|
|
42
|
+
}
|
|
43
|
+
if (!policy.subpaths.includes(subpath)) {
|
|
44
|
+
fail("zhike_dependency_subpath_forbidden", source, "未获准包入口 " + specifier);
|
|
45
|
+
}
|
|
46
|
+
return { name, policy };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function within(root, path) {
|
|
50
|
+
const suffix = relative(root, path);
|
|
51
|
+
return suffix === "" || (!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(".." + sep));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 构建准入规则,不执行依赖代码;最终安全与授权仍由平台沙箱保证。
|
|
55
|
+
export async function createDependencyPolicyPlugin(projectDirectory, target, validateSource) {
|
|
56
|
+
const projectRoot = await realpath(projectDirectory);
|
|
57
|
+
const sourceRoot = resolve(projectRoot, "src");
|
|
58
|
+
const roots = new Map();
|
|
59
|
+
const packageInfos = new Map();
|
|
60
|
+
const label = path => relative(projectRoot, path).split(sep).join("/");
|
|
61
|
+
|
|
62
|
+
async function packageInfo(path) {
|
|
63
|
+
let directory = dirname(path);
|
|
64
|
+
while (within(projectRoot, directory)) {
|
|
65
|
+
if (packageInfos.has(directory)) return packageInfos.get(directory);
|
|
66
|
+
let bytes;
|
|
67
|
+
try { bytes = await readFile(resolve(directory, "package.json"), "utf8"); }
|
|
68
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
69
|
+
if (bytes !== undefined) {
|
|
70
|
+
let metadata;
|
|
71
|
+
try { metadata = JSON.parse(bytes); }
|
|
72
|
+
catch { fail("zhike_dependency_metadata_invalid", label(path), "包元数据不是合法 JSON"); }
|
|
73
|
+
const info = { root: directory, name: metadata.name, version: metadata.version };
|
|
74
|
+
packageInfos.set(directory, info);
|
|
75
|
+
return info;
|
|
76
|
+
}
|
|
77
|
+
const parent = dirname(directory);
|
|
78
|
+
if (parent === directory) break;
|
|
79
|
+
directory = parent;
|
|
80
|
+
}
|
|
81
|
+
fail("zhike_dependency_path_forbidden", label(path), "依赖必须位于项目内的独立 npm 包");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function owner(path) {
|
|
85
|
+
return [...roots.values()].filter(info => within(info.root, path))
|
|
86
|
+
.sort((a, b) => b.root.length - a.root.length)[0] ?? null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function approve(path, expectedName) {
|
|
90
|
+
if (!within(projectRoot, path)) {
|
|
91
|
+
fail("zhike_dependency_path_forbidden", "dependency", "依赖真实路径不得越过项目目录");
|
|
92
|
+
}
|
|
93
|
+
const info = await packageInfo(path);
|
|
94
|
+
if (expectedName && info.name !== expectedName) {
|
|
95
|
+
fail("zhike_dependency_path_forbidden", label(path), "实际包身份与导入包名不一致");
|
|
96
|
+
}
|
|
97
|
+
const approved = assertAllowedPackageImport(info.name ?? "", target, label(path));
|
|
98
|
+
if (!approved) fail("zhike_dependency_path_forbidden", label(path), "不得经相对路径导入平台模块");
|
|
99
|
+
if (!approved.policy.versions.includes(info.version)) {
|
|
100
|
+
fail("zhike_dependency_version_forbidden", label(path), "未获准包版本 " + info.name + "@" + info.version);
|
|
101
|
+
}
|
|
102
|
+
roots.set(info.root, info);
|
|
103
|
+
return info;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
name: "zhike-package-policy",
|
|
108
|
+
setup(buildApi) {
|
|
109
|
+
async function resolveChecked(args, expectedName) {
|
|
110
|
+
const resolved = await buildApi.resolve(args.path, { resolveDir: args.resolveDir,
|
|
111
|
+
importer: args.importer, kind: args.kind,
|
|
112
|
+
pluginData: { zhikePolicyResolution: true } });
|
|
113
|
+
if (resolved.errors.length) {
|
|
114
|
+
fail("zhike_dependency_resolution_failed", args.path, "依赖无法解析,请安装项目声明的依赖");
|
|
115
|
+
}
|
|
116
|
+
if (resolved.external || resolved.namespace !== "file") {
|
|
117
|
+
fail("zhike_dependency_path_forbidden", args.path, "白名单包必须静态合并到 Artifact");
|
|
118
|
+
}
|
|
119
|
+
const path = await realpath(resolved.path);
|
|
120
|
+
let parent = null;
|
|
121
|
+
if (args.importer) {
|
|
122
|
+
try { parent = owner(await realpath(args.importer)); }
|
|
123
|
+
catch (error) { if (error.code !== "ENOENT") throw error; } // esbuild 的虚拟入口不在磁盘上。
|
|
124
|
+
}
|
|
125
|
+
if (!expectedName && parent && !within(parent.root, path)) {
|
|
126
|
+
fail("zhike_dependency_path_forbidden", label(path), "相对依赖不得越过所属包");
|
|
127
|
+
}
|
|
128
|
+
if (expectedName || parent) await approve(path, expectedName);
|
|
129
|
+
else if (!within(sourceRoot, path)) {
|
|
130
|
+
fail("zhike_source_import_outside_root", label(path), "App relative import 不得越过 src 根目录");
|
|
131
|
+
}
|
|
132
|
+
return { path };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
buildApi.onResolve({ filter: /.*/ }, async args => {
|
|
136
|
+
if (args.pluginData?.zhikePolicyResolution || args.namespace !== "file") return;
|
|
137
|
+
if (args.path.startsWith(".") || isAbsolute(args.path)) return resolveChecked(args);
|
|
138
|
+
const approved = assertAllowedPackageImport(args.path, target, args.path);
|
|
139
|
+
if (approved) return resolveChecked(args, approved.name);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
buildApi.onLoad({ filter: /.*/, namespace: "file" }, async args => {
|
|
143
|
+
const path = await realpath(args.path);
|
|
144
|
+
const info = owner(path);
|
|
145
|
+
if (!info) return;
|
|
146
|
+
await approve(path, info.name);
|
|
147
|
+
const extension = extname(path);
|
|
148
|
+
if (extension === ".json") return;
|
|
149
|
+
const loaders = { ".ts": "ts", ".tsx": "tsx", ".js": "js", ".mjs": "js", ".jsx": "jsx" };
|
|
150
|
+
if (!Object.hasOwn(loaders, extension)) {
|
|
151
|
+
fail("zhike_dependency_file_unsupported", label(path), "依赖只支持静态 ESM JS/TS 和 JSON");
|
|
152
|
+
}
|
|
153
|
+
const contents = await readFile(path, "utf8");
|
|
154
|
+
const sourceFile = ts.createSourceFile(label(path), contents, ts.ScriptTarget.ES2022, true,
|
|
155
|
+
extension === ".tsx" || extension === ".jsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
156
|
+
validateSource(sourceFile, label(path), { target, dependency: true });
|
|
157
|
+
// 在解析优化之前逐条解析,未使用的 import 也不能隐藏未批准依赖或路径逃逸。
|
|
158
|
+
for (const statement of sourceFile.statements) {
|
|
159
|
+
const specifier = (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement))
|
|
160
|
+
&& statement.moduleSpecifier;
|
|
161
|
+
if (specifier && ts.isStringLiteral(specifier)) {
|
|
162
|
+
if (specifier.text.startsWith(".")) await resolveChecked({ path: specifier.text,
|
|
163
|
+
resolveDir: dirname(path), importer: path, kind: "import-statement" });
|
|
164
|
+
else {
|
|
165
|
+
const approved = assertAllowedPackageImport(specifier.text, target, label(path));
|
|
166
|
+
if (approved) await resolveChecked({ path: specifier.text, resolveDir: dirname(path),
|
|
167
|
+
importer: path, kind: "import-statement" }, approved.name);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { contents, loader: loaders[extension], resolveDir: dirname(path) };
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
package/src/toolchain/index.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { TOOLING_VERSION } from "../config.js";
|
|
6
|
-
import { ZhikeCliError } from "../errors.js";
|
|
7
|
-
import { readProjectBinding } from "../project-binding.js";
|
|
8
|
-
|
|
9
1
|
export { discoverAppSources } from "./source-discovery.js";
|
|
10
2
|
export {
|
|
11
3
|
APP_BUNDLE_CONTENT_TYPE,
|
|
@@ -21,83 +13,3 @@ export {
|
|
|
21
13
|
createTransportTar,
|
|
22
14
|
parseTransportTar,
|
|
23
15
|
} from "./transport-tar.js";
|
|
24
|
-
|
|
25
|
-
export const MOCK_PACKAGE_FORMAT = "zhike.mock-build.v1";
|
|
26
|
-
export const MOCK_ARTIFACT_KIND = "mock";
|
|
27
|
-
export const MOCK_ARTIFACT_MAX_BYTES = 1024 * 1024;
|
|
28
|
-
|
|
29
|
-
export async function buildMockArtifact({
|
|
30
|
-
projectDirectory = process.cwd(),
|
|
31
|
-
expectedEnvironment = null,
|
|
32
|
-
writeToDist = false,
|
|
33
|
-
} = {}) {
|
|
34
|
-
await readProjectBinding(projectDirectory, expectedEnvironment);
|
|
35
|
-
const payload = {
|
|
36
|
-
format: MOCK_PACKAGE_FORMAT,
|
|
37
|
-
kind: MOCK_ARTIFACT_KIND,
|
|
38
|
-
payload: {
|
|
39
|
-
message: "qiqizhike developer app mock build",
|
|
40
|
-
},
|
|
41
|
-
toolingVersion: TOOLING_VERSION,
|
|
42
|
-
};
|
|
43
|
-
const bytes = Buffer.from(`${JSON.stringify(payload)}\n`, "utf8");
|
|
44
|
-
if (bytes.byteLength > MOCK_ARTIFACT_MAX_BYTES) {
|
|
45
|
-
throw new ZhikeCliError(
|
|
46
|
-
"zhike_mock_artifact_too_large",
|
|
47
|
-
"Mock Artifact 超过 1 MiB 上限",
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
const result = Object.freeze({
|
|
51
|
-
bytes,
|
|
52
|
-
size: bytes.byteLength,
|
|
53
|
-
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
54
|
-
packageFormat: MOCK_PACKAGE_FORMAT,
|
|
55
|
-
kind: MOCK_ARTIFACT_KIND,
|
|
56
|
-
toolingVersion: TOOLING_VERSION,
|
|
57
|
-
});
|
|
58
|
-
if (writeToDist) {
|
|
59
|
-
const distDirectory = resolve(projectDirectory, "dist");
|
|
60
|
-
await mkdir(distDirectory, { recursive: true });
|
|
61
|
-
await writeFile(resolve(distDirectory, "mock-build.json"), bytes, {
|
|
62
|
-
flag: "w",
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
return result;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function validateMockArtifact(bytes) {
|
|
69
|
-
if (!Buffer.isBuffer(bytes) || bytes.byteLength > MOCK_ARTIFACT_MAX_BYTES) {
|
|
70
|
-
throw new ZhikeCliError(
|
|
71
|
-
"zhike_mock_artifact_invalid",
|
|
72
|
-
"Mock Artifact 无效或超过 1 MiB 上限",
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
let value;
|
|
76
|
-
try {
|
|
77
|
-
value = JSON.parse(bytes.toString("utf8"));
|
|
78
|
-
} catch (error) {
|
|
79
|
-
throw new ZhikeCliError(
|
|
80
|
-
"zhike_mock_artifact_invalid",
|
|
81
|
-
"Mock Artifact 不是有效 JSON",
|
|
82
|
-
{ cause: error },
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
const keys = Object.keys(value).sort();
|
|
86
|
-
if (
|
|
87
|
-
keys.join(",") !== "format,kind,payload,toolingVersion" ||
|
|
88
|
-
value.format !== MOCK_PACKAGE_FORMAT ||
|
|
89
|
-
value.kind !== MOCK_ARTIFACT_KIND ||
|
|
90
|
-
value.toolingVersion !== TOOLING_VERSION ||
|
|
91
|
-
!value.payload ||
|
|
92
|
-
typeof value.payload !== "object" ||
|
|
93
|
-
Array.isArray(value.payload) ||
|
|
94
|
-
Object.keys(value.payload).join(",") !== "message" ||
|
|
95
|
-
value.payload.message !== "qiqizhike developer app mock build"
|
|
96
|
-
) {
|
|
97
|
-
throw new ZhikeCliError(
|
|
98
|
-
"zhike_mock_artifact_invalid",
|
|
99
|
-
"Mock Artifact 不符合 zhike.mock-build.v1 严格合同",
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
return value;
|
|
103
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { publicComponentNames } from '../ui/components.generated.js';
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
@@ -12,7 +12,8 @@ import { TOOLING_VERSION } from "../config.js";
|
|
|
12
12
|
import { ZhikeCliError } from "../errors.js";
|
|
13
13
|
import { readProjectBinding } from "../project-binding.js";
|
|
14
14
|
import { canonicalJsonBytes } from "./canonical-json.js";
|
|
15
|
-
import { discoverAppSources } from "./source-discovery.js";
|
|
15
|
+
import { discoverAppSources, validateImports } from "./source-discovery.js";
|
|
16
|
+
import { createDependencyPolicyPlugin } from "./dependency-policy.js";
|
|
16
17
|
import { createTransportTar } from "./transport-tar.js";
|
|
17
18
|
|
|
18
19
|
export const APP_BUNDLE_PACKAGE_FORMAT = "zhike.app-bundle.v1";
|
|
@@ -33,18 +34,21 @@ export async function buildAppBundle({
|
|
|
33
34
|
} = {}) {
|
|
34
35
|
await readProjectBinding(projectDirectory, expectedEnvironment);
|
|
35
36
|
const sources = await discoverAppSources(projectDirectory);
|
|
37
|
+
let clientBytes, serverBytes;
|
|
38
|
+
try {
|
|
39
|
+
clientBytes = sources.frontendExtensions.length > 0
|
|
40
|
+
? await buildRemoteReactClient(projectDirectory, sources, development) : null;
|
|
41
|
+
serverBytes = sources.lifecycleEvents.length > 0 ||
|
|
42
|
+
sources.serverFunctions.length > 0 ||
|
|
43
|
+
sources.webhookHandlers.length > 0 ||
|
|
44
|
+
sources.workflowSteps.length > 0 ||
|
|
45
|
+
sources.workflowTriggers.length > 0
|
|
46
|
+
? await buildServerArtifact(projectDirectory, sources) : null;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const policyError = error.errors?.find(item => item.detail instanceof ZhikeCliError)?.detail;
|
|
49
|
+
throw policyError ?? error;
|
|
50
|
+
}
|
|
36
51
|
await assertTypeScriptProgram(projectDirectory, sources);
|
|
37
|
-
|
|
38
|
-
const clientBytes =
|
|
39
|
-
sources.frontendExtensions.length > 0
|
|
40
|
-
? await buildRemoteReactClient(projectDirectory, sources, development)
|
|
41
|
-
: null;
|
|
42
|
-
const serverBytes =
|
|
43
|
-
sources.lifecycleEvents.length > 0 ||
|
|
44
|
-
sources.serverFunctions.length > 0 ||
|
|
45
|
-
sources.webhookHandlers.length > 0
|
|
46
|
-
? await buildServerArtifact(projectDirectory, sources)
|
|
47
|
-
: null;
|
|
48
52
|
const manifest = createManifest({
|
|
49
53
|
clientBytes,
|
|
50
54
|
serverBytes,
|
|
@@ -159,7 +163,8 @@ async function buildRemoteReactClient(projectDirectory, sources, development) {
|
|
|
159
163
|
absWorkingDir: projectDirectory, bundle: true, charset: "utf8", format: "cjs",
|
|
160
164
|
legalComments: "none", logLevel: "silent", minify: true, platform: "browser",
|
|
161
165
|
external: ["@77-zhike/app-sdk/runtime", "react", "react/jsx-runtime", "react/jsx-dev-runtime", "@77-zhike/app-sdk/client", "@77-zhike/app-sdk/ui", "@77-zhike/app-sdk/forms"],
|
|
162
|
-
plugins: [createSdkBuildPlugin(projectDirectory, sources, "remote-client")
|
|
166
|
+
plugins: [createSdkBuildPlugin(projectDirectory, sources, "remote-client"),
|
|
167
|
+
await createDependencyPolicyPlugin(projectDirectory, "client", validateImports)],
|
|
163
168
|
sourcemap: development ? "inline" : false,
|
|
164
169
|
banner: { js: 'export function createClientApp(platformModules) {\nconst module = { exports: {} };\nconst exports = module.exports;\nconst require = name => { if (!Object.hasOwn(platformModules, name)) throw new Error("runtime_module_unavailable"); return platformModules[name]; };\nconst Promise = require("@77-zhike/app-sdk/runtime").Promise;' },
|
|
165
170
|
footer: { js: 'return module.exports;\n}' },
|
|
@@ -197,6 +202,14 @@ async function buildServerArtifact(projectDirectory, sources) {
|
|
|
197
202
|
(definition, index) =>
|
|
198
203
|
`import webhookHandler${index} from "./src/${definition.relativePath}";`,
|
|
199
204
|
),
|
|
205
|
+
...sources.workflowSteps.map(
|
|
206
|
+
(definition, index) =>
|
|
207
|
+
`import workflowStep${index} from "./src/${definition.handlerPaths.execute}";`,
|
|
208
|
+
),
|
|
209
|
+
...sources.workflowTriggers.map(
|
|
210
|
+
(definition, index) => Object.entries(definition.handlerPaths).map(([name, path]) =>
|
|
211
|
+
`import workflowTrigger${index}_${name} from "./src/${path}";`).join("\n"),
|
|
212
|
+
),
|
|
200
213
|
].join("\n");
|
|
201
214
|
const lifecycleEntries = sources.lifecycleEvents
|
|
202
215
|
.map(
|
|
@@ -216,7 +229,19 @@ async function buildServerArtifact(projectDirectory, sources) {
|
|
|
216
229
|
`[${JSON.stringify(definition.key)}, webhookHandler${index}]`,
|
|
217
230
|
)
|
|
218
231
|
.join(",\n");
|
|
219
|
-
const
|
|
232
|
+
const workflowEntries = sources.workflowSteps
|
|
233
|
+
.map((definition, index) => `[${JSON.stringify(definition.entry)}, workflowStep${index}]`)
|
|
234
|
+
.join(",\n");
|
|
235
|
+
const triggerEntries = sources.workflowTriggers.flatMap((definition, index) =>
|
|
236
|
+
Object.entries(definition.entries).map(([name, entry]) =>
|
|
237
|
+
`[${JSON.stringify(entry)}, workflowTrigger${index}_${name}]`)).join(",\n");
|
|
238
|
+
const triggerDeclarations = sources.workflowTriggers.length > 0
|
|
239
|
+
? `const workflowTriggers = new Map([${triggerEntries}]);\n`
|
|
240
|
+
: "";
|
|
241
|
+
const triggerDispatch = sources.workflowTriggers.length > 0
|
|
242
|
+
? ` if (request.kind === "workflow_trigger") {\n const handler = workflowTriggers.get(request.entryKey);\n if (!handler) throw new Error("runtime_workflow_trigger_not_found");\n const result = await handler(Object.freeze({ ...request.params }));\n return request.entryKey.endsWith("/deactivate") ? null : result;\n }\n`
|
|
243
|
+
: "";
|
|
244
|
+
const stdin = `${imports}\n${serializeWebhookResponse.toString()}\nconst lifecycleEvents = new Map([${lifecycleEntries}]);\nconst serverFunctions = new Map([${serverEntries}]);\nconst webhookHandlers = new Map([${webhookEntries}]);\nconst workflowSteps = new Map([${workflowEntries}]);\n${triggerDeclarations}export async function dispatchServerInvocation(request) {\n if (!request || typeof request !== "object") throw new Error("runtime_invocation_invalid");\n if (request.kind === "lifecycle_event") {\n const definition = lifecycleEvents.get(request.entryKey);\n if (!definition) throw new Error("runtime_lifecycle_event_not_found");\n const params = definition.event === "app-installed" || definition.event === "app-uninstalled" ? request.context : Object.freeze({ connection: getLifecycleConnection() });\n await definition.handler(params);\n return null;\n }\n if (request.kind === "server_function") {\n const handler = serverFunctions.get(request.entryKey);\n if (!handler) throw new Error("runtime_server_function_not_found");\n return handler(...request.args);\n }\n if (request.kind === "workflow_step") {\n const handler = workflowSteps.get(request.entryKey);\n if (!handler) throw new Error("runtime_workflow_step_not_found");\n return handler(Object.freeze({ config: request.config }));\n }\n${triggerDispatch} if (request.kind === "webhook_handler") {\n const handler = webhookHandlers.get(request.entryKey);\n if (!handler) throw new Error("runtime_webhook_handler_not_found");\n const input = request.webRequest;\n const webRequest = new Request(input.url, { method: input.method, headers: input.headers, body: ["GET", "HEAD"].includes(input.method) ? undefined : input.body });\n const response = await handler(webRequest, Object.freeze({ ...request.context }));\n if (!(response instanceof Response)) throw new Error("runtime_webhook_response_invalid");\n return serializeWebhookResponse(response);\n }\n throw new Error("runtime_invocation_kind_invalid");\n}\n`;
|
|
220
245
|
const result = await build({
|
|
221
246
|
absWorkingDir: projectDirectory,
|
|
222
247
|
bundle: true,
|
|
@@ -226,7 +251,8 @@ async function buildServerArtifact(projectDirectory, sources) {
|
|
|
226
251
|
logLevel: "silent",
|
|
227
252
|
minify: true,
|
|
228
253
|
platform: "browser",
|
|
229
|
-
plugins: [createSdkBuildPlugin(projectDirectory, sources, "server")
|
|
254
|
+
plugins: [createSdkBuildPlugin(projectDirectory, sources, "server"),
|
|
255
|
+
await createDependencyPolicyPlugin(projectDirectory, "server", validateImports)],
|
|
230
256
|
sourcemap: false,
|
|
231
257
|
stdin: { contents: stdin, loader: "ts", resolveDir: projectDirectory, sourcefile: "zhike-server-entry.ts" },
|
|
232
258
|
target: "es2022",
|
|
@@ -261,7 +287,7 @@ function createSdkBuildPlugin(projectDirectory, sources, target) {
|
|
|
261
287
|
};
|
|
262
288
|
export const TableCell = Object.defineProperties(props => createElement(ui.TableCell, props), { Avatar: { get: () => ui.Avatar }, Badge: { value: Badge } });
|
|
263
289
|
export const Table = Object.defineProperties(props => createElement(ui.Table, props), { Header: { get: () => ui.Table.Header }, HeaderCell: { get: () => ui.Table.HeaderCell }, Body: { get: () => ui.Table.Body }, Row: { get: () => ui.Table.Row }, Cell: { value: TableCell } });
|
|
264
|
-
export const { ${[...
|
|
290
|
+
export const { ${[...publicComponentNames.filter(name => !["TextBlock", "Badge", "Table", "TableCell"].includes(name)), "Typography", "Widget"].join(", ")} } = ui;
|
|
265
291
|
` }));
|
|
266
292
|
}
|
|
267
293
|
if (target !== "remote-client") buildApi.onResolve({ filter: /^@77-zhike\/app-sdk\/(client|server)$/ }, (args) => ({
|
|
@@ -365,6 +391,20 @@ function createManifest({ clientBytes, serverBytes, sources }) {
|
|
|
365
391
|
key,
|
|
366
392
|
methods,
|
|
367
393
|
})),
|
|
394
|
+
...(sources.workflowSteps.length > 0 ? { workflowSteps: sources.workflowSteps.map(
|
|
395
|
+
({ key, title, description, connection, configSchema, outcomes, entry }) => ({
|
|
396
|
+
artifact: "server", key, entry, title,
|
|
397
|
+
...(description === undefined ? {} : { description }),
|
|
398
|
+
connection, configSchema, outcomes,
|
|
399
|
+
}),
|
|
400
|
+
) } : {}),
|
|
401
|
+
...(sources.workflowTriggers.length > 0 ? { workflowTriggers: sources.workflowTriggers.map(
|
|
402
|
+
({ key, title, description, connection, requestHeaders, configSchema, outcomes, entries }) => ({
|
|
403
|
+
artifact: "server", key, entries, title,
|
|
404
|
+
...(description === undefined ? {} : { description }),
|
|
405
|
+
connection, requestHeaders, configSchema, outcomes,
|
|
406
|
+
}),
|
|
407
|
+
) } : {}),
|
|
368
408
|
};
|
|
369
409
|
}
|
|
370
410
|
|
|
@@ -374,6 +414,8 @@ function createServerDefinitionsDigest(sources) {
|
|
|
374
414
|
lifecycleEvents: sources.lifecycleEvents.map(({ event, key }) => ({ event, key })),
|
|
375
415
|
serverFunctions: sources.serverFunctions.map(({ key }) => ({ key })),
|
|
376
416
|
webhookHandlers: sources.webhookHandlers.map(({ key, methods }) => ({ key, methods })),
|
|
417
|
+
...(sources.workflowSteps.length > 0 ? { workflowSteps: sources.workflowSteps.map(({ key, entry }) => ({ key, entry })) } : {}),
|
|
418
|
+
...(sources.workflowTriggers.length > 0 ? { workflowTriggers: sources.workflowTriggers.map(({ key, entries }) => ({ key, entries })) } : {}),
|
|
377
419
|
}),
|
|
378
420
|
);
|
|
379
421
|
}
|