@77-zhike/app-sdk 0.2.0-d7b1.10

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.
Files changed (87) hide show
  1. package/README.md +251 -0
  2. package/package.json +71 -0
  3. package/src/bin.js +15 -0
  4. package/src/cli/api-client.js +249 -0
  5. package/src/cli/browser-auth.js +212 -0
  6. package/src/cli/credential-store.js +101 -0
  7. package/src/cli/development-app.js +77 -0
  8. package/src/cli/development-logs.js +183 -0
  9. package/src/cli/development-watch.js +205 -0
  10. package/src/cli/index.js +256 -0
  11. package/src/cli/local-commands.js +80 -0
  12. package/src/cli/logs.js +56 -0
  13. package/src/cli/project-init.js +259 -0
  14. package/src/cli/upload-bundle.js +73 -0
  15. package/src/client/analysis.d.ts +4 -0
  16. package/src/client/analysis.js +12 -0
  17. package/src/client/destinations.d.ts +25 -0
  18. package/src/client/destinations.js +70 -0
  19. package/src/client/external-url.js +9 -0
  20. package/src/client/index.d.ts +249 -0
  21. package/src/client/index.js +72 -0
  22. package/src/client/interactions.d.ts +23 -0
  23. package/src/client/interactions.js +62 -0
  24. package/src/client/points.generated.js +67 -0
  25. package/src/config.js +68 -0
  26. package/src/development-workspace.js +154 -0
  27. package/src/errors.js +20 -0
  28. package/src/forms/array-contract.d.ts +32 -0
  29. package/src/forms/array-contract.js +138 -0
  30. package/src/forms/components.generated.js +4 -0
  31. package/src/forms/factory.js +41 -0
  32. package/src/forms/field-array.d.ts +16 -0
  33. package/src/forms/field-array.js +52 -0
  34. package/src/forms/index.d.ts +118 -0
  35. package/src/forms/index.js +5 -0
  36. package/src/forms/normalize-options.js +19 -0
  37. package/src/forms/options.d.ts +30 -0
  38. package/src/forms/options.js +55 -0
  39. package/src/forms/record-options.d.ts +28 -0
  40. package/src/forms/record-options.js +92 -0
  41. package/src/forms/schema.d.ts +82 -0
  42. package/src/forms/schema.js +128 -0
  43. package/src/forms/state.d.ts +30 -0
  44. package/src/forms/state.js +82 -0
  45. package/src/forms/validation.d.ts +9 -0
  46. package/src/forms/validation.js +81 -0
  47. package/src/forms/values.d.ts +5 -0
  48. package/src/forms/values.js +38 -0
  49. package/src/index.d.ts +13 -0
  50. package/src/index.js +2 -0
  51. package/src/jsx-dev-runtime.d.ts +2 -0
  52. package/src/jsx-dev-runtime.js +1 -0
  53. package/src/jsx-runtime.d.ts +2 -0
  54. package/src/jsx-runtime.js +1 -0
  55. package/src/project-binding.js +84 -0
  56. package/src/server/index.d.ts +318 -0
  57. package/src/server/index.js +242 -0
  58. package/src/server/steps/index.d.ts +71 -0
  59. package/src/server/steps/index.js +15 -0
  60. package/src/server/triggers/index.d.ts +68 -0
  61. package/src/server/triggers/index.js +27 -0
  62. package/src/server/workflow-blocks/index.d.ts +72 -0
  63. package/src/server/workflow-blocks/index.js +32 -0
  64. package/src/toolchain/allowed-packages.json +7 -0
  65. package/src/toolchain/canonical-json.js +17 -0
  66. package/src/toolchain/dependency-policy.js +181 -0
  67. package/src/toolchain/index.js +15 -0
  68. package/src/toolchain/real-build.js +459 -0
  69. package/src/toolchain/source-discovery.js +919 -0
  70. package/src/toolchain/transport-tar.js +168 -0
  71. package/src/toolchain/webhook-response.js +50 -0
  72. package/src/toolchain/workflow-blocks/discovery.js +133 -0
  73. package/src/toolchain/workflow-steps/manifest.js +99 -0
  74. package/src/toolchain/workflow-triggers/manifest.js +47 -0
  75. package/src/ui/components.generated.js +5 -0
  76. package/src/ui/events.d.ts +10 -0
  77. package/src/ui/factory.js +70 -0
  78. package/src/ui/icons.generated.js +2 -0
  79. package/src/ui/index.d.ts +343 -0
  80. package/src/ui/index.js +4 -0
  81. package/src/ui/jsx.generated.d.ts +359 -0
  82. package/src/ui/native-dom.d.ts +49 -0
  83. package/src/ui/native-dom.js +44 -0
  84. package/src/ui/style.d.ts +5 -0
  85. package/src/ui/style.generated.js +2 -0
  86. package/src/ui/style.js +15 -0
  87. package/src/ui/validation.js +86 -0
@@ -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
+ }
@@ -0,0 +1,15 @@
1
+ export { discoverAppSources } from "./source-discovery.js";
2
+ export {
3
+ APP_BUNDLE_CONTENT_TYPE,
4
+ APP_BUNDLE_PACKAGE_FORMAT,
5
+ APP_MANIFEST_MEDIA_TYPE,
6
+ JAVASCRIPT_MEDIA_TYPE,
7
+ buildAppBundle,
8
+ } from "./real-build.js";
9
+ export {
10
+ APP_BUNDLE_ALLOWED_MEMBERS,
11
+ APP_BUNDLE_MEMBER_MAX_BYTES,
12
+ APP_BUNDLE_TAR_MAX_BYTES,
13
+ createTransportTar,
14
+ parseTransportTar,
15
+ } from "./transport-tar.js";
@@ -0,0 +1,459 @@
1
+ import { publicComponentNames } from '../ui/components.generated.js';
2
+ import { createRequire } from "node:module";
3
+ import { createHash } from "node:crypto";
4
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
+ import { dirname, relative, resolve, sep } from "node:path";
6
+ import { build } from "esbuild";
7
+ import ts from "typescript";
8
+
9
+ import { serializeWebhookResponse } from "./webhook-response.js";
10
+
11
+ import { TOOLING_VERSION } from "../config.js";
12
+ import { ZhikeCliError } from "../errors.js";
13
+ import { readProjectBinding } from "../project-binding.js";
14
+ import { canonicalJsonBytes } from "./canonical-json.js";
15
+ import { discoverAppSources, validateImports } from "./source-discovery.js";
16
+ import { createDependencyPolicyPlugin } from "./dependency-policy.js";
17
+ import { createTransportTar } from "./transport-tar.js";
18
+
19
+ export const APP_BUNDLE_PACKAGE_FORMAT = "zhike.app-bundle.v1";
20
+ export const APP_BUNDLE_CONTENT_TYPE =
21
+ "application/vnd.qiqizhike.app-bundle+tar";
22
+ export const APP_MANIFEST_MEDIA_TYPE =
23
+ "application/vnd.qiqizhike.app-manifest+json";
24
+ export const JAVASCRIPT_MEDIA_TYPE = "application/javascript";
25
+
26
+ const SDK_SOURCE_ROOT = resolve(import.meta.dirname, "..");
27
+
28
+ export async function buildAppBundle({
29
+ projectDirectory = process.cwd(),
30
+ expectedEnvironment = null,
31
+ writeToDist = false,
32
+ tarOptions = {},
33
+ development = false,
34
+ } = {}) {
35
+ await readProjectBinding(projectDirectory, expectedEnvironment);
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
+ }
51
+ await assertTypeScriptProgram(projectDirectory, sources);
52
+ const manifest = createManifest({
53
+ clientBytes,
54
+ serverBytes,
55
+ sources,
56
+ });
57
+ const manifestBytes = canonicalJsonBytes(manifest);
58
+ const entries = [
59
+ { name: "manifest.json", bytes: manifestBytes },
60
+ ...(clientBytes ? [{ name: "client.js", bytes: clientBytes }] : []),
61
+ ...(serverBytes ? [{ name: "server.js", bytes: serverBytes }] : []),
62
+ ];
63
+ const tarBytes = createTransportTar(entries, tarOptions);
64
+ const artifacts = Object.freeze([
65
+ createArtifact("manifest", "manifest.json", APP_MANIFEST_MEDIA_TYPE, manifestBytes),
66
+ ...(clientBytes
67
+ ? [createArtifact("client", "client.js", JAVASCRIPT_MEDIA_TYPE, clientBytes)]
68
+ : []),
69
+ ...(serverBytes
70
+ ? [createArtifact("server", "server.js", JAVASCRIPT_MEDIA_TYPE, serverBytes)]
71
+ : []),
72
+ ]);
73
+ if (writeToDist) {
74
+ const distDirectory = resolve(projectDirectory, "dist");
75
+ await rm(distDirectory, { recursive: true, force: true });
76
+ await mkdir(distDirectory, { recursive: true });
77
+ for (const artifact of artifacts) {
78
+ await writeFile(resolve(distDirectory, artifact.path), artifact.bytes, {
79
+ flag: "wx",
80
+ });
81
+ }
82
+ await writeFile(resolve(distDirectory, "app-bundle.tar"), tarBytes, {
83
+ flag: "wx",
84
+ });
85
+ }
86
+ return Object.freeze({
87
+ artifacts,
88
+ bytes: tarBytes,
89
+ manifest: Object.freeze(manifest),
90
+ packageFormat: APP_BUNDLE_PACKAGE_FORMAT,
91
+ contentType: APP_BUNDLE_CONTENT_TYPE,
92
+ sha256: sha256(tarBytes),
93
+ size: tarBytes.byteLength,
94
+ toolingVersion: TOOLING_VERSION,
95
+ });
96
+ }
97
+
98
+ async function assertTypeScriptProgram(projectDirectory, { sourceFiles, serverFunctions }) {
99
+ const paths = {
100
+ "@77-zhike/app-sdk": [resolve(SDK_SOURCE_ROOT, "index.d.ts")],
101
+ "@77-zhike/app-sdk/client": [resolve(SDK_SOURCE_ROOT, "client", "index.d.ts")],
102
+ "@77-zhike/app-sdk/server": [resolve(SDK_SOURCE_ROOT, "server", "index.d.ts")],
103
+ "@77-zhike/app-sdk/ui": [resolve(SDK_SOURCE_ROOT, "ui", "index.d.ts")],
104
+ "@77-zhike/app-sdk/forms": [resolve(SDK_SOURCE_ROOT, "forms", "index.d.ts")],
105
+ "@77-zhike/app-sdk/jsx-runtime": [resolve(SDK_SOURCE_ROOT, "jsx-runtime.d.ts")],
106
+ "@77-zhike/app-sdk/jsx-dev-runtime": [resolve(SDK_SOURCE_ROOT, "jsx-dev-runtime.d.ts")],
107
+ "react": [resolve(dirname(createRequire(import.meta.url).resolve("@types/react/package.json")), "index.d.ts")],
108
+ "react/jsx-runtime": [resolve(dirname(createRequire(import.meta.url).resolve("@types/react/package.json")), "jsx-runtime.d.ts")],
109
+ "react/jsx-dev-runtime": [resolve(dirname(createRequire(import.meta.url).resolve("@types/react/package.json")), "jsx-dev-runtime.d.ts")],
110
+ };
111
+ const options = {
112
+ baseUrl: projectDirectory,
113
+ jsx: ts.JsxEmit.ReactJSX,
114
+ jsxImportSource: "@77-zhike/app-sdk",
115
+ lib: ["lib.es2022.d.ts", "lib.dom.d.ts"],
116
+ module: ts.ModuleKind.ESNext,
117
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
118
+ noEmit: true,
119
+ noUncheckedIndexedAccess: true,
120
+ paths,
121
+ skipLibCheck: false,
122
+ strict: true,
123
+ target: ts.ScriptTarget.ES2022,
124
+ };
125
+ const rootNames = sourceFiles.map((path) => resolve(projectDirectory, "src", path));
126
+ // 仅供 TypeScript 检查,不落盘、不进 Artifact;复用原包装的参数/结果合同。
127
+ const contractPath = resolve(projectDirectory, ".zhike/server-function-contracts.ts");
128
+ const contractSource = [
129
+ 'import type { ServerFunction } from "@77-zhike/app-sdk/server";',
130
+ ...serverFunctions.flatMap(({ relativePath }, index) => [
131
+ `import handler${index} from ${JSON.stringify(`../src/${relativePath.slice(0, -3)}`)};`,
132
+ `type Contract${index} = ServerFunction<Parameters<typeof handler${index}>, Awaited<ReturnType<typeof handler${index}>>>;`,
133
+ ]),
134
+ ].join("\n");
135
+ const host = ts.createCompilerHost(options);
136
+ const getSourceFile = host.getSourceFile.bind(host);
137
+ host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) =>
138
+ resolve(fileName) === contractPath
139
+ ? ts.createSourceFile(fileName, contractSource, languageVersion, true)
140
+ : getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
141
+ if (serverFunctions.length) rootNames.push(contractPath);
142
+ const program = ts.createProgram({ rootNames, options, host });
143
+ const diagnostics = ts.getPreEmitDiagnostics(program);
144
+ if (diagnostics.length === 0) return;
145
+ const diagnostic = diagnostics[0];
146
+ const contractLine = diagnostic.file?.fileName === contractPath
147
+ ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start || 0).line : -1;
148
+ const contractOwner = contractLine > 0 ? serverFunctions[Math.floor((contractLine - 1) / 2)] : null;
149
+ const location = contractOwner ? `src/${contractOwner.relativePath}(参数/返回类型)` : diagnostic.file
150
+ ? `${relative(projectDirectory, diagnostic.file.fileName).split(sep).join("/")}:${
151
+ diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start || 0).line + 1
152
+ }`
153
+ : "TypeScript";
154
+ throw new ZhikeCliError(
155
+ "zhike_typescript_diagnostic",
156
+ `${location}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
157
+ );
158
+ }
159
+
160
+ async function buildRemoteReactClient(projectDirectory, sources, development) {
161
+ const imports = sources.frontendExtensions.map((item, i) =>
162
+ `import extension${i} from "./src/${item.relativePath}";`).join("\n");
163
+ const entries = sources.frontendExtensions.map((item, i) =>
164
+ `${JSON.stringify(item.key)}: extension${i}`).join(",");
165
+ const result = await build({
166
+ absWorkingDir: projectDirectory, bundle: true, charset: "utf8", format: "cjs",
167
+ legalComments: "none", logLevel: "silent", minify: true, platform: "browser",
168
+ 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"],
169
+ plugins: [createSdkBuildPlugin(projectDirectory, sources, "remote-client"),
170
+ await createDependencyPolicyPlugin(projectDirectory, "client", validateImports)],
171
+ sourcemap: development ? "inline" : false,
172
+ 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;' },
173
+ footer: { js: 'return module.exports;\n}' },
174
+ // dev 只增加源码映射;编译语义与正式发布一致,不能依赖宿主的 React 开发构建。
175
+ jsx: "automatic", jsxDev: false, jsxImportSource: "@77-zhike/app-sdk",
176
+ define: { "process.env.NODE_ENV": '"production"' },
177
+ stdin: { contents: `${imports}\nexport const extensions = Object.freeze({${entries}});`, loader: "tsx", resolveDir: projectDirectory, sourcefile: "zhike-client-entry.tsx" },
178
+ // 原生 await 不经过 Promise.then;降级后的 helper 必须使用工厂内受管 Promise。
179
+ target: "es2022", supported: { "async-await": false, "async-generator": false }, treeShaking: true, write: false,
180
+ });
181
+ const bytes = normalizeJavascriptOutput(result.outputFiles[0].contents);
182
+ if (bytes.byteLength > 2 * 1024 * 1024) {
183
+ throw new ZhikeCliError("zhike_client_too_large", "client Artifact(包含源码映射)超过 2 MiB");
184
+ }
185
+ return bytes;
186
+ }
187
+
188
+ async function buildServerArtifact(projectDirectory, sources) {
189
+ const hasConnectionLifecycle = sources.lifecycleEvents.some(
190
+ ({ event }) => event === "connection-added" || event === "connection-removed",
191
+ );
192
+ const imports = [
193
+ ...(hasConnectionLifecycle
194
+ ? ['import { getWorkspaceConnection as getLifecycleConnection } from "@77-zhike/app-sdk/server";']
195
+ : []),
196
+ ...sources.lifecycleEvents.map(
197
+ (definition, index) =>
198
+ `import lifecycleEvent${index} from "./src/${definition.relativePath}";`,
199
+ ),
200
+ ...sources.serverFunctions.map(
201
+ (definition, index) =>
202
+ `import serverFunction${index} from "./src/${definition.relativePath}";`,
203
+ ),
204
+ ...sources.webhookHandlers.map(
205
+ (definition, index) =>
206
+ `import webhookHandler${index} from "./src/${definition.relativePath}";`,
207
+ ),
208
+ ...sources.workflowSteps.map(
209
+ (definition, index) =>
210
+ `import workflowStep${index} from "./src/${definition.handlerPaths.execute}";`,
211
+ ),
212
+ ...sources.workflowTriggers.map(
213
+ (definition, index) => Object.entries(definition.handlerPaths).map(([name, path]) =>
214
+ `import workflowTrigger${index}_${name} from "./src/${path}";`).join("\n"),
215
+ ),
216
+ ].join("\n");
217
+ const lifecycleEntries = sources.lifecycleEvents
218
+ .map(
219
+ (definition, index) =>
220
+ `[${JSON.stringify(definition.key)}, { event: ${JSON.stringify(definition.event)}, handler: lifecycleEvent${index} }]`,
221
+ )
222
+ .join(",\n");
223
+ const serverEntries = sources.serverFunctions
224
+ .map(
225
+ (definition, index) =>
226
+ `[${JSON.stringify(definition.key)}, serverFunction${index}]`,
227
+ )
228
+ .join(",\n");
229
+ const webhookEntries = sources.webhookHandlers
230
+ .map(
231
+ (definition, index) =>
232
+ `[${JSON.stringify(definition.key)}, webhookHandler${index}]`,
233
+ )
234
+ .join(",\n");
235
+ const workflowEntries = sources.workflowSteps
236
+ .map((definition, index) => `[${JSON.stringify(definition.entry)}, workflowStep${index}]`)
237
+ .join(",\n");
238
+ const triggerEntries = sources.workflowTriggers.flatMap((definition, index) =>
239
+ Object.entries(definition.entries).map(([name, entry]) =>
240
+ `[${JSON.stringify(entry)}, workflowTrigger${index}_${name}]`)).join(",\n");
241
+ const triggerDeclarations = sources.workflowTriggers.length > 0
242
+ ? `const workflowTriggers = new Map([${triggerEntries}]);\n`
243
+ : "";
244
+ const triggerDispatch = sources.workflowTriggers.length > 0
245
+ ? ` 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`
246
+ : "";
247
+ 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`;
248
+ const result = await build({
249
+ absWorkingDir: projectDirectory,
250
+ bundle: true,
251
+ charset: "utf8",
252
+ format: "esm",
253
+ legalComments: "none",
254
+ logLevel: "silent",
255
+ minify: true,
256
+ platform: "browser",
257
+ plugins: [createSdkBuildPlugin(projectDirectory, sources, "server"),
258
+ await createDependencyPolicyPlugin(projectDirectory, "server", validateImports)],
259
+ sourcemap: false,
260
+ stdin: { contents: stdin, loader: "ts", resolveDir: projectDirectory, sourcefile: "zhike-server-entry.ts" },
261
+ target: "es2022",
262
+ treeShaking: true,
263
+ write: false,
264
+ });
265
+ const bytes = normalizeJavascriptOutput(result.outputFiles[0].contents);
266
+ const definitionsDigest = createServerDefinitionsDigest(sources);
267
+ return Buffer.concat([
268
+ bytes,
269
+ Buffer.from(`//# zhike-server-definitions-sha256=${definitionsDigest}\n`, "utf8"),
270
+ ]);
271
+ }
272
+
273
+ function createSdkBuildPlugin(projectDirectory, sources, target) {
274
+ const serverKeys = new Set(sources.serverFunctions.map((item) => item.key));
275
+ return {
276
+ name: "zhike-app-sdk-runtime",
277
+ setup(buildApi) {
278
+ buildApi.onResolve({ filter: /^@77-zhike\/app-sdk\/jsx(?:-dev)?-runtime$/ }, args => ({ path: args.path, namespace: "zhike-jsx" }));
279
+ buildApi.onLoad({ filter: /.*/, namespace: "zhike-jsx" }, args => ({
280
+ contents: args.path.endsWith("jsx-dev-runtime")
281
+ ? 'export { jsxDEV, Fragment } from "react/jsx-dev-runtime";'
282
+ : 'export { jsx, jsxs, Fragment } from "react/jsx-runtime";', loader: "js",
283
+ }));
284
+ if (target === 'remote-client') {
285
+ buildApi.onResolve({ filter: /\.css\?inline$/ }, args => args.path.startsWith('.')
286
+ ? { path: resolve(args.resolveDir, args.path.slice(0, -7)), namespace: 'zhike-native-css' }
287
+ : { errors: [{ text: 'NativeDOM 样式必须使用本地相对路径' }] });
288
+ buildApi.onLoad({ filter: /.*/, namespace: 'zhike-native-css' }, async args => ({
289
+ contents: await readFile(args.path, 'utf8'), loader: 'text', watchFiles: [args.path],
290
+ }));
291
+ }
292
+ if (target === "remote-client") {
293
+ // 新构建固化 authoring 默认值;Guest 仍为不可变旧 Bundle 提供兼容模块。
294
+ buildApi.onResolve({ filter: /^@77-zhike\/app-sdk\/ui$/ }, args => args.namespace === "zhike-ui-authoring"
295
+ ? { path: args.path, external: true }
296
+ : { path: args.path, namespace: "zhike-ui-authoring" });
297
+ buildApi.onLoad({ filter: /.*/, namespace: "zhike-ui-authoring" }, () => ({ loader: "js", contents: `
298
+ import * as ui from "@77-zhike/app-sdk/ui";
299
+ import { createElement } from "react";
300
+ export const TextBlock = props => createElement(ui.TextBlock, { ...props, align: props.align ?? "left" });
301
+ export const Badge = props => {
302
+ if (props.tone !== undefined) throw new Error("Badge.tone 已移除,请使用 StatusBadge");
303
+ return createElement(ui.Badge, props);
304
+ };
305
+ export const TableCell = Object.defineProperties(props => createElement(ui.TableCell, props), { Avatar: { get: () => ui.Avatar }, Badge: { value: Badge } });
306
+ 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 } });
307
+ export const { ${[...publicComponentNames.filter(name => !["TextBlock", "Badge", "Table", "TableCell"].includes(name)), "Typography", "Widget"].join(", ")} } = ui;
308
+ ` }));
309
+ }
310
+ if (target !== "remote-client") buildApi.onResolve({ filter: /^@77-zhike\/app-sdk\/(client|server)$/ }, (args) => ({
311
+ external: target === "server" && args.path.endsWith("/server"),
312
+ path: args.path,
313
+ ...(target === "server" && args.path.endsWith("/server")
314
+ ? {}
315
+ : { namespace: "zhike-sdk-runtime" }),
316
+ }));
317
+ buildApi.onLoad({ filter: /.*/, namespace: "zhike-sdk-runtime" }, (args) => {
318
+ if (args.path.endsWith("/client")) {
319
+ return {
320
+ contents: "export const defineFrontendExtension = (definition) => definition; export const definePage = (definition) => ({ ...definition, type: 'page' });",
321
+ loader: "js",
322
+ };
323
+ }
324
+ return { contents: "export const defineLifecycleEvent = (definition) => definition; export const defineServerFunction = (handler) => handler; export const defineWebhookHandler = (definition) => definition; export const createWebhookHandler = async () => { throw new Error('webhook_handler_runtime_unavailable'); }; export const listWebhookHandlers = async () => { throw new Error('webhook_handler_runtime_unavailable'); }; export const deleteWebhookHandler = async () => { throw new Error('webhook_handler_runtime_unavailable'); };", loader: "js" };
325
+ });
326
+ if (target === "client" || target === "remote-client") {
327
+ buildApi.onResolve({ filter: /\.server(?:\.ts)?$/ }, (args) => {
328
+ let absolute = resolve(args.resolveDir, args.path);
329
+ if (!absolute.endsWith(".ts")) absolute = `${absolute}.ts`;
330
+ const normalizedAbsolute = absolute.split(sep).join("/");
331
+ const sourceMarker = "/src/";
332
+ const sourceIndex = normalizedAbsolute.lastIndexOf(sourceMarker);
333
+ const moduleId =
334
+ sourceIndex === -1
335
+ ? ""
336
+ : normalizedAbsolute
337
+ .slice(sourceIndex + sourceMarker.length)
338
+ .replace(/\.server\.ts$/, "");
339
+ if (!serverKeys.has(moduleId)) {
340
+ return { errors: [{ text: `未声明的 Server Function import:${moduleId}` }] };
341
+ }
342
+ return { path: moduleId, namespace: "zhike-server-rpc" };
343
+ });
344
+ buildApi.onLoad({ filter: /.*/, namespace: "zhike-server-rpc" }, (args) => ({
345
+ contents: target === "remote-client"
346
+ ? `const runtime = require("@77-zhike/app-sdk/runtime"); export default (...args) => runtime.invokeServerFunction(${JSON.stringify(args.path)}, args);`
347
+ : `export default (...args) => globalThis.ZHIKE_CLIENT_SDK.invokeServerFunction(${JSON.stringify(args.path)}, args);`,
348
+ loader: "js",
349
+ }));
350
+ }
351
+ },
352
+ };
353
+ }
354
+
355
+ function createManifest({ clientBytes, serverBytes, sources }) {
356
+ return {
357
+ schemaVersion: "zhike.app-manifest.v1",
358
+ sdkAbiVersion: "2",
359
+ toolchainVersion: TOOLING_VERSION,
360
+ ...(sources.frontendExtensions.length > 0
361
+ ? { clientRuntime: { runtimeVersion: "remote-react.v3" } } : {}),
362
+ artifacts: {
363
+ ...(clientBytes
364
+ ? {
365
+ client: {
366
+ mediaType: JAVASCRIPT_MEDIA_TYPE,
367
+ path: "client.js",
368
+ sha256: sha256(clientBytes),
369
+ size: clientBytes.byteLength,
370
+ },
371
+ }
372
+ : {}),
373
+ ...(serverBytes
374
+ ? {
375
+ server: {
376
+ mediaType: JAVASCRIPT_MEDIA_TYPE,
377
+ path: "server.js",
378
+ sha256: sha256(serverBytes),
379
+ size: serverBytes.byteLength,
380
+ },
381
+ }
382
+ : {}),
383
+ },
384
+ frontendExtensions: sources.frontendExtensions.map(({ key, type, entry, label, icon, objects, name, order, variant, showInNavigation, showHeader }) => ({
385
+ artifact: "client",
386
+ entry,
387
+ key,
388
+ type,
389
+ ...(label === undefined ? {} : { label }),
390
+ ...(icon === undefined ? {} : { icon }),
391
+ ...(objects === undefined ? {} : { objects }),
392
+ ...(name === undefined ? {} : { name }),
393
+ ...(order === undefined ? {} : { order }),
394
+ ...(variant === undefined ? {} : { variant }),
395
+ ...(showInNavigation === undefined ? {} : { showInNavigation }),
396
+ ...(showHeader === undefined ? {} : { showHeader }),
397
+ })),
398
+ lifecycleEvents: sources.lifecycleEvents.map(({ event, key }) => ({
399
+ artifact: "server",
400
+ event,
401
+ key,
402
+ })),
403
+ serverFunctions: sources.serverFunctions.map(({ key }) => ({
404
+ artifact: "server",
405
+ key,
406
+ })),
407
+ webhookHandlers: sources.webhookHandlers.map(({ key, methods }) => ({
408
+ artifact: "server",
409
+ key,
410
+ methods,
411
+ })),
412
+ ...(sources.workflowSteps.length > 0 ? { workflowSteps: sources.workflowSteps.map(
413
+ ({ key, title, description, connection, configSchema, outcomes, entry }) => ({
414
+ artifact: "server", key, entry, title,
415
+ ...(description === undefined ? {} : { description }),
416
+ connection, configSchema, outcomes,
417
+ }),
418
+ ) } : {}),
419
+ ...(sources.workflowTriggers.length > 0 ? { workflowTriggers: sources.workflowTriggers.map(
420
+ ({ key, title, description, connection, requestHeaders, configSchema, outcomes, entries }) => ({
421
+ artifact: "server", key, entries, title,
422
+ ...(description === undefined ? {} : { description }),
423
+ connection, requestHeaders, configSchema, outcomes,
424
+ }),
425
+ ) } : {}),
426
+ };
427
+ }
428
+
429
+ function createServerDefinitionsDigest(sources) {
430
+ return sha256(
431
+ canonicalJsonBytes({
432
+ lifecycleEvents: sources.lifecycleEvents.map(({ event, key }) => ({ event, key })),
433
+ serverFunctions: sources.serverFunctions.map(({ key }) => ({ key })),
434
+ webhookHandlers: sources.webhookHandlers.map(({ key, methods }) => ({ key, methods })),
435
+ ...(sources.workflowSteps.length > 0 ? { workflowSteps: sources.workflowSteps.map(({ key, entry }) => ({ key, entry })) } : {}),
436
+ ...(sources.workflowTriggers.length > 0 ? { workflowTriggers: sources.workflowTriggers.map(({ key, entries }) => ({ key, entries })) } : {}),
437
+ }),
438
+ );
439
+ }
440
+
441
+ function createArtifact(kind, path, mediaType, bytes) {
442
+ return Object.freeze({
443
+ bytes: Buffer.from(bytes),
444
+ kind,
445
+ mediaType,
446
+ path,
447
+ sha256: sha256(bytes),
448
+ size: bytes.byteLength,
449
+ });
450
+ }
451
+
452
+ function normalizeJavascriptOutput(bytes) {
453
+ const text = Buffer.from(bytes).toString("utf8").replace(/\r\n/g, "\n");
454
+ return Buffer.from(text.endsWith("\n") ? text : `${text}\n`, "utf8");
455
+ }
456
+
457
+ function sha256(bytes) {
458
+ return createHash("sha256").update(bytes).digest("hex");
459
+ }