@77-zhike/app-sdk 0.2.0-d7b1.2 → 0.2.0-d7b1.20
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 +281 -0
- package/package.json +17 -3
- package/src/cli/api-client.js +4 -0
- package/src/cli/browser-auth.js +64 -18
- package/src/cli/credential-store.js +21 -0
- package/src/cli/development-logs.js +5 -2
- package/src/cli/index.js +59 -42
- package/src/cli/local-commands.js +6 -3
- package/src/cli/logs.js +56 -0
- package/src/cli/project-init.js +111 -179
- package/src/cli/windows-credential-bridge.ps1 +131 -0
- package/src/cli/windows-credential-store.js +112 -0
- package/src/client/analysis.d.ts +4 -0
- package/src/client/analysis.js +12 -0
- package/src/client/destinations.d.ts +25 -0
- package/src/client/destinations.js +70 -0
- package/src/client/documents.d.ts +21 -0
- package/src/client/documents.js +39 -0
- package/src/client/external-url.js +9 -0
- package/src/client/index.d.ts +225 -3
- package/src/client/index.js +75 -3
- package/src/client/interactions.d.ts +23 -3
- package/src/client/interactions.js +53 -3
- package/src/client/points.generated.js +56 -0
- package/src/config.js +11 -7
- package/src/forms/array-contract.d.ts +32 -0
- package/src/forms/array-contract.js +138 -0
- package/src/forms/components.generated.js +2 -2
- package/src/forms/factory.js +21 -7
- package/src/forms/field-array.d.ts +16 -0
- package/src/forms/field-array.js +52 -0
- package/src/forms/index.d.ts +74 -3
- package/src/forms/index.js +2 -0
- package/src/forms/options.d.ts +7 -2
- package/src/forms/options.js +8 -1
- package/src/forms/record-options.d.ts +28 -0
- package/src/forms/record-options.js +92 -0
- package/src/forms/schema.d.ts +73 -9
- package/src/forms/schema.js +128 -0
- package/src/forms/state.d.ts +30 -0
- package/src/forms/state.js +82 -0
- package/src/forms/validation.d.ts +9 -0
- package/src/forms/validation.js +81 -0
- package/src/forms/values.d.ts +5 -0
- package/src/forms/values.js +38 -0
- package/src/jsx-dev-runtime.d.ts +2 -0
- package/src/jsx-dev-runtime.js +1 -0
- package/src/jsx-runtime.d.ts +2 -0
- package/src/jsx-runtime.js +1 -0
- package/src/server/agents/index.d.ts +62 -0
- package/src/server/agents/index.js +40 -0
- package/src/server/index.d.ts +60 -5
- package/src/server/index.js +43 -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/agents/discovery.js +126 -0
- package/src/toolchain/agents/exposure.js +31 -0
- package/src/toolchain/agents/manifest.js +84 -0
- package/src/toolchain/ai-capabilities.js +69 -0
- package/src/toolchain/allowed-packages.json +7 -0
- package/src/toolchain/dependency-policy.js +181 -0
- package/src/toolchain/index.js +0 -88
- package/src/toolchain/real-build.js +135 -27
- package/src/toolchain/source-discovery.js +330 -81
- 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 +4 -1
- package/src/ui/conversation.d.ts +81 -0
- package/src/ui/conversation.js +82 -0
- package/src/ui/data-table-model.d.ts +14 -0
- package/src/ui/data-table-model.js +98 -0
- package/src/ui/data-table.d.ts +70 -0
- package/src/ui/data-table.js +74 -0
- package/src/ui/events.d.ts +10 -0
- package/src/ui/factory.js +87 -3
- package/src/ui/index.d.ts +351 -8
- package/src/ui/index.js +1 -1
- package/src/ui/jsx.generated.d.ts +359 -0
- package/src/ui/native-dom.d.ts +49 -0
- package/src/ui/native-dom.js +44 -0
- package/src/ui/scroll-viewport.d.ts +22 -0
- package/src/ui/scroll-viewport.js +23 -0
- package/src/ui/slider.d.ts +27 -0
- package/src/ui/slider.js +26 -0
- package/src/ui/style.d.ts +5 -0
- package/src/ui/style.generated.js +2 -0
- package/src/ui/style.js +15 -0
- package/src/ui/text-selection.d.ts +22 -0
- package/src/ui/text-selection.js +23 -0
- package/src/ui/validation.js +86 -0
- package/src/cli/validate-bundle.js +0 -36
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { assertToolManifest, assertAgentSchema } from './agents/manifest.js';
|
|
2
|
+
import { assertAiExposure } from './agents/exposure.js';
|
|
3
|
+
|
|
4
|
+
export function assertAiCapabilities(manifest) {
|
|
5
|
+
const invalid = () => { throw new TypeError('app_manifest_ai_capabilities_invalid'); };
|
|
6
|
+
const actions = manifest.actions === undefined ? [] : manifest.actions;
|
|
7
|
+
const data = manifest.dataCapabilities === undefined ? [] : manifest.dataCapabilities;
|
|
8
|
+
const agents = manifest.agents ?? [];
|
|
9
|
+
const hasAiFields = manifest.actions !== undefined || manifest.dataCapabilities !== undefined ||
|
|
10
|
+
agents.some(agent => agent.aiExposure !== undefined || agent.actionRefs !== undefined ||
|
|
11
|
+
agent.tools.some(tool => tool.aiExposure !== undefined));
|
|
12
|
+
if (manifest.aiCapabilitiesVersion !== undefined && manifest.aiCapabilitiesVersion !== '1') invalid();
|
|
13
|
+
if (hasAiFields && manifest.aiCapabilitiesVersion !== '1') invalid();
|
|
14
|
+
if (manifest.aiCapabilitiesVersion !== undefined && manifest.sdkAbiVersion !== '2') invalid();
|
|
15
|
+
if (!Array.isArray(actions) || actions.length > 64 || !Array.isArray(data) || data.length > 64) invalid();
|
|
16
|
+
if (manifest.frontendExtensions !== undefined) {
|
|
17
|
+
const renderers = new Set(manifest.frontendExtensions.filter(item => item.type === 'tool-renderer').map(item => item.key));
|
|
18
|
+
for (const tool of [...actions, ...agents.flatMap(agent => agent.tools)]) {
|
|
19
|
+
if (tool.toolRenderer && !renderers.has(tool.toolRenderer.key)) invalid();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const byKey = new Map();
|
|
23
|
+
for (const action of actions) {
|
|
24
|
+
assertToolManifest(action, `action/${action.key}`);
|
|
25
|
+
if (byKey.has(action.key) || action.clientResult !== 'none') invalid();
|
|
26
|
+
byKey.set(action.key, action);
|
|
27
|
+
}
|
|
28
|
+
const dataKeys = new Set();
|
|
29
|
+
for (const item of data) {
|
|
30
|
+
assertDataCapability(item);
|
|
31
|
+
if (dataKeys.has(item.key)) invalid();
|
|
32
|
+
dataKeys.add(item.key);
|
|
33
|
+
if (item.kind === 'live') {
|
|
34
|
+
const action = byKey.get(item.actionRef);
|
|
35
|
+
if (!action?.aiExposure || item.sourceBinding !== undefined ||
|
|
36
|
+
item.subjectTypes.some(type => !action.aiExposure.subjectTypes.includes(type))) invalid();
|
|
37
|
+
} else if (item.kind === 'published') {
|
|
38
|
+
if (typeof item.sourceBinding !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(item.sourceBinding) || item.actionRef !== undefined) invalid();
|
|
39
|
+
} else invalid();
|
|
40
|
+
}
|
|
41
|
+
for (const agent of agents) {
|
|
42
|
+
const refs = agent.actionRefs === undefined ? [] : agent.actionRefs;
|
|
43
|
+
if (!Array.isArray(refs) || refs.length > 32 || new Set(refs).size !== refs.length || refs.some(ref => !byKey.has(ref))) invalid();
|
|
44
|
+
if (agent.aiExposure !== undefined) {
|
|
45
|
+
assertAiExposure(agent.aiExposure, null, true);
|
|
46
|
+
assertAgentSchema(agent.aiExposure.outputSchema);
|
|
47
|
+
if (refs.some(ref => !byKey.get(ref).aiExposure) ||
|
|
48
|
+
(agent.aiExposure.toolKeys ?? []).some(key => !agent.tools.find(tool => tool.key === key)?.aiExposure)) invalid();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return manifest;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function assertDataCapability(item) {
|
|
55
|
+
const invalid = () => { throw new TypeError('app_manifest_ai_capabilities_invalid'); };
|
|
56
|
+
if (!item || typeof item !== 'object' || Array.isArray(item) ||
|
|
57
|
+
Object.keys(item).some(key => !['key', 'name', 'description', 'subjectTypes', 'kind', 'sourceBinding', 'actionRef', 'usage'].includes(key)) ||
|
|
58
|
+
typeof item.key !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(item.key)) invalid();
|
|
59
|
+
for (const [key, max] of [['name', 80], ['description', 1200], ['usage', 1200]]) {
|
|
60
|
+
if (typeof item[key] !== 'string' || !item[key].trim() || item[key].length > max) invalid();
|
|
61
|
+
}
|
|
62
|
+
assertAiExposure({subjectTypes:item.subjectTypes, usage:item.usage, limitations:item.description, effect:'query'});
|
|
63
|
+
if (item.kind === 'published') {
|
|
64
|
+
if (typeof item.sourceBinding !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(item.sourceBinding) || item.actionRef !== undefined) invalid();
|
|
65
|
+
} else if (item.kind === 'live') {
|
|
66
|
+
if (typeof item.actionRef !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(item.actionRef) || item.sourceBinding !== undefined) invalid();
|
|
67
|
+
} else invalid();
|
|
68
|
+
return item;
|
|
69
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
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/jsx-runtime", "@77-zhike/app-sdk/jsx-dev-runtime",
|
|
12
|
+
"@77-zhike/app-sdk/forms", "@77-zhike/app-sdk/server",
|
|
13
|
+
"react", "react/jsx-runtime", "react/jsx-dev-runtime",
|
|
14
|
+
]);
|
|
15
|
+
const builtins = new Set(builtinModules.map(name => name.replace(/^node:/, "")));
|
|
16
|
+
|
|
17
|
+
function fail(code, source, message) {
|
|
18
|
+
throw new ZhikeCliError(code, source + ": " + message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function splitPackage(specifier) {
|
|
22
|
+
const parts = specifier.split("/");
|
|
23
|
+
const length = specifier.startsWith("@") ? 2 : 1;
|
|
24
|
+
return { name: parts.slice(0, length).join("/"),
|
|
25
|
+
subpath: parts.length === length ? "." : "./" + parts.slice(length).join("/") };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function assertAllowedPackageImport(specifier, target, source) {
|
|
29
|
+
if (specifier.startsWith("node:") || builtins.has(specifier)) {
|
|
30
|
+
fail("zhike_node_builtin_forbidden", source, "禁止 Node builtin " + specifier);
|
|
31
|
+
}
|
|
32
|
+
if (SDK_IMPORTS.has(specifier)) {
|
|
33
|
+
if (target === "client" && specifier === "@77-zhike/app-sdk/server") {
|
|
34
|
+
fail("zhike_server_sdk_client_forbidden", source, "前端代码不得导入服务端 SDK");
|
|
35
|
+
}
|
|
36
|
+
if (target === "server" && specifier === "@77-zhike/app-sdk/client") {
|
|
37
|
+
fail("zhike_client_sdk_server_forbidden", source, "服务端代码不得导入客户端 SDK 运行时");
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const { name, subpath } = splitPackage(specifier);
|
|
42
|
+
const policy = Object.hasOwn(packages, name) ? packages[name] : null;
|
|
43
|
+
if (!policy) fail("zhike_external_dependency_forbidden", source, "未获准依赖 " + specifier);
|
|
44
|
+
if (!policy.targets.includes(target)) {
|
|
45
|
+
fail("zhike_dependency_target_forbidden", source, "该运行端不允许导入 " + name);
|
|
46
|
+
}
|
|
47
|
+
if (!policy.subpaths.includes(subpath)) {
|
|
48
|
+
fail("zhike_dependency_subpath_forbidden", source, "未获准包入口 " + specifier);
|
|
49
|
+
}
|
|
50
|
+
return { name, policy };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function within(root, path) {
|
|
54
|
+
const suffix = relative(root, path);
|
|
55
|
+
return suffix === "" || (!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(".." + sep));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 构建准入规则,不执行依赖代码;最终安全与授权仍由平台沙箱保证。
|
|
59
|
+
export async function createDependencyPolicyPlugin(projectDirectory, target, validateSource) {
|
|
60
|
+
const projectRoot = await realpath(projectDirectory);
|
|
61
|
+
const sourceRoot = resolve(projectRoot, "src");
|
|
62
|
+
const roots = new Map();
|
|
63
|
+
const packageInfos = new Map();
|
|
64
|
+
const label = path => relative(projectRoot, path).split(sep).join("/");
|
|
65
|
+
|
|
66
|
+
async function packageInfo(path) {
|
|
67
|
+
let directory = dirname(path);
|
|
68
|
+
while (within(projectRoot, directory)) {
|
|
69
|
+
if (packageInfos.has(directory)) return packageInfos.get(directory);
|
|
70
|
+
let bytes;
|
|
71
|
+
try { bytes = await readFile(resolve(directory, "package.json"), "utf8"); }
|
|
72
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
73
|
+
if (bytes !== undefined) {
|
|
74
|
+
let metadata;
|
|
75
|
+
try { metadata = JSON.parse(bytes); }
|
|
76
|
+
catch { fail("zhike_dependency_metadata_invalid", label(path), "包元数据不是合法 JSON"); }
|
|
77
|
+
const info = { root: directory, name: metadata.name, version: metadata.version };
|
|
78
|
+
packageInfos.set(directory, info);
|
|
79
|
+
return info;
|
|
80
|
+
}
|
|
81
|
+
const parent = dirname(directory);
|
|
82
|
+
if (parent === directory) break;
|
|
83
|
+
directory = parent;
|
|
84
|
+
}
|
|
85
|
+
fail("zhike_dependency_path_forbidden", label(path), "依赖必须位于项目内的独立 npm 包");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function owner(path) {
|
|
89
|
+
return [...roots.values()].filter(info => within(info.root, path))
|
|
90
|
+
.sort((a, b) => b.root.length - a.root.length)[0] ?? null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function approve(path, expectedName) {
|
|
94
|
+
if (!within(projectRoot, path)) {
|
|
95
|
+
fail("zhike_dependency_path_forbidden", "dependency", "依赖真实路径不得越过项目目录");
|
|
96
|
+
}
|
|
97
|
+
const info = await packageInfo(path);
|
|
98
|
+
if (expectedName && info.name !== expectedName) {
|
|
99
|
+
fail("zhike_dependency_path_forbidden", label(path), "实际包身份与导入包名不一致");
|
|
100
|
+
}
|
|
101
|
+
const approved = assertAllowedPackageImport(info.name ?? "", target, label(path));
|
|
102
|
+
if (!approved) fail("zhike_dependency_path_forbidden", label(path), "不得经相对路径导入平台模块");
|
|
103
|
+
if (!approved.policy.versions.includes(info.version)) {
|
|
104
|
+
fail("zhike_dependency_version_forbidden", label(path), "未获准包版本 " + info.name + "@" + info.version);
|
|
105
|
+
}
|
|
106
|
+
roots.set(info.root, info);
|
|
107
|
+
return info;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
name: "zhike-package-policy",
|
|
112
|
+
setup(buildApi) {
|
|
113
|
+
async function resolveChecked(args, expectedName) {
|
|
114
|
+
const resolved = await buildApi.resolve(args.path, { resolveDir: args.resolveDir,
|
|
115
|
+
importer: args.importer, kind: args.kind,
|
|
116
|
+
pluginData: { zhikePolicyResolution: true } });
|
|
117
|
+
if (resolved.errors.length) {
|
|
118
|
+
fail("zhike_dependency_resolution_failed", args.path, "依赖无法解析,请安装项目声明的依赖");
|
|
119
|
+
}
|
|
120
|
+
if (resolved.external || resolved.namespace !== "file") {
|
|
121
|
+
fail("zhike_dependency_path_forbidden", args.path, "白名单包必须静态合并到 Artifact");
|
|
122
|
+
}
|
|
123
|
+
const path = await realpath(resolved.path);
|
|
124
|
+
let parent = null;
|
|
125
|
+
if (args.importer) {
|
|
126
|
+
try { parent = owner(await realpath(args.importer)); }
|
|
127
|
+
catch (error) { if (error.code !== "ENOENT") throw error; } // esbuild 的虚拟入口不在磁盘上。
|
|
128
|
+
}
|
|
129
|
+
if (!expectedName && parent && !within(parent.root, path)) {
|
|
130
|
+
fail("zhike_dependency_path_forbidden", label(path), "相对依赖不得越过所属包");
|
|
131
|
+
}
|
|
132
|
+
if (expectedName || parent) await approve(path, expectedName);
|
|
133
|
+
else if (!within(sourceRoot, path)) {
|
|
134
|
+
fail("zhike_source_import_outside_root", label(path), "App relative import 不得越过 src 根目录");
|
|
135
|
+
}
|
|
136
|
+
return { path };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
buildApi.onResolve({ filter: /.*/ }, async args => {
|
|
140
|
+
if (args.pluginData?.zhikePolicyResolution || args.namespace !== "file") return;
|
|
141
|
+
if (args.path.startsWith(".") || isAbsolute(args.path)) return resolveChecked(args);
|
|
142
|
+
const approved = assertAllowedPackageImport(args.path, target, args.path);
|
|
143
|
+
if (approved) return resolveChecked(args, approved.name);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
buildApi.onLoad({ filter: /.*/, namespace: "file" }, async args => {
|
|
147
|
+
const path = await realpath(args.path);
|
|
148
|
+
const info = owner(path);
|
|
149
|
+
if (!info) return;
|
|
150
|
+
await approve(path, info.name);
|
|
151
|
+
const extension = extname(path);
|
|
152
|
+
if (extension === ".json") return;
|
|
153
|
+
const loaders = { ".ts": "ts", ".tsx": "tsx", ".js": "js", ".mjs": "js", ".jsx": "jsx" };
|
|
154
|
+
if (!Object.hasOwn(loaders, extension)) {
|
|
155
|
+
fail("zhike_dependency_file_unsupported", label(path), "依赖只支持静态 ESM JS/TS 和 JSON");
|
|
156
|
+
}
|
|
157
|
+
const contents = await readFile(path, "utf8");
|
|
158
|
+
const sourceFile = ts.createSourceFile(label(path), contents, ts.ScriptTarget.ES2022, true,
|
|
159
|
+
extension === ".tsx" || extension === ".jsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
160
|
+
validateSource(sourceFile, label(path), { target, dependency: true });
|
|
161
|
+
// 在解析优化之前逐条解析,未使用的 import 也不能隐藏未批准依赖或路径逃逸。
|
|
162
|
+
for (const statement of sourceFile.statements) {
|
|
163
|
+
const specifier = (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement))
|
|
164
|
+
&& statement.moduleSpecifier;
|
|
165
|
+
if (specifier && ts.isStringLiteral(specifier)) {
|
|
166
|
+
if (target === "server" && specifier.text === "@77-zhike/app-sdk/client"
|
|
167
|
+
&& (statement.isTypeOnly || (ts.isImportDeclaration(statement) && statement.importClause?.isTypeOnly))) continue;
|
|
168
|
+
if (specifier.text.startsWith(".")) await resolveChecked({ path: specifier.text,
|
|
169
|
+
resolveDir: dirname(path), importer: path, kind: "import-statement" });
|
|
170
|
+
else {
|
|
171
|
+
const approved = assertAllowedPackageImport(specifier.text, target, label(path));
|
|
172
|
+
if (approved) await resolveChecked({ path: specifier.text, resolveDir: dirname(path),
|
|
173
|
+
importer: path, kind: "import-statement" }, approved.name);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { contents, loader: loaders[extension], resolveDir: dirname(path) };
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
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
|
-
}
|