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

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 (45) hide show
  1. package/package.json +57 -0
  2. package/src/bin.js +15 -0
  3. package/src/cli/api-client.js +245 -0
  4. package/src/cli/browser-auth.js +212 -0
  5. package/src/cli/credential-store.js +101 -0
  6. package/src/cli/development-app.js +77 -0
  7. package/src/cli/development-logs.js +180 -0
  8. package/src/cli/development-watch.js +205 -0
  9. package/src/cli/index.js +253 -0
  10. package/src/cli/local-commands.js +78 -0
  11. package/src/cli/project-init.js +371 -0
  12. package/src/cli/upload-bundle.js +73 -0
  13. package/src/cli/validate-bundle.js +36 -0
  14. package/src/client/index.d.ts +52 -0
  15. package/src/client/index.js +22 -0
  16. package/src/client/interactions.d.ts +8 -0
  17. package/src/client/interactions.js +23 -0
  18. package/src/client/points.generated.js +19 -0
  19. package/src/config.js +64 -0
  20. package/src/development-workspace.js +154 -0
  21. package/src/errors.js +20 -0
  22. package/src/forms/components.generated.js +4 -0
  23. package/src/forms/factory.js +27 -0
  24. package/src/forms/index.d.ts +54 -0
  25. package/src/forms/index.js +3 -0
  26. package/src/forms/normalize-options.js +19 -0
  27. package/src/forms/options.d.ts +25 -0
  28. package/src/forms/options.js +48 -0
  29. package/src/forms/schema.d.ts +18 -0
  30. package/src/index.d.ts +13 -0
  31. package/src/index.js +2 -0
  32. package/src/project-binding.js +84 -0
  33. package/src/server/index.d.ts +268 -0
  34. package/src/server/index.js +201 -0
  35. package/src/toolchain/canonical-json.js +17 -0
  36. package/src/toolchain/index.js +103 -0
  37. package/src/toolchain/real-build.js +371 -0
  38. package/src/toolchain/source-discovery.js +710 -0
  39. package/src/toolchain/transport-tar.js +168 -0
  40. package/src/toolchain/webhook-response.js +50 -0
  41. package/src/ui/components.generated.js +2 -0
  42. package/src/ui/factory.js +19 -0
  43. package/src/ui/icons.generated.js +2 -0
  44. package/src/ui/index.d.ts +111 -0
  45. package/src/ui/index.js +4 -0
@@ -0,0 +1,371 @@
1
+ import { createRequire } from "node:module";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdir, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, relative, resolve, sep } from "node:path";
5
+ import { build } from "esbuild";
6
+ import ts from "typescript";
7
+
8
+ import { serializeWebhookResponse } from "./webhook-response.js";
9
+
10
+ import { TOOLING_VERSION } from "../config.js";
11
+ import { ZhikeCliError } from "../errors.js";
12
+ import { readProjectBinding } from "../project-binding.js";
13
+ import { canonicalJsonBytes } from "./canonical-json.js";
14
+ import { discoverAppSources } from "./source-discovery.js";
15
+ import { createTransportTar } from "./transport-tar.js";
16
+
17
+ export const APP_BUNDLE_PACKAGE_FORMAT = "zhike.app-bundle.v1";
18
+ export const APP_BUNDLE_CONTENT_TYPE =
19
+ "application/vnd.qiqizhike.app-bundle+tar";
20
+ export const APP_MANIFEST_MEDIA_TYPE =
21
+ "application/vnd.qiqizhike.app-manifest+json";
22
+ export const JAVASCRIPT_MEDIA_TYPE = "application/javascript";
23
+
24
+ const SDK_SOURCE_ROOT = resolve(import.meta.dirname, "..");
25
+
26
+ export async function buildAppBundle({
27
+ projectDirectory = process.cwd(),
28
+ expectedEnvironment = null,
29
+ writeToDist = false,
30
+ tarOptions = {},
31
+ development = false,
32
+ } = {}) {
33
+ await readProjectBinding(projectDirectory, expectedEnvironment);
34
+ const sources = await discoverAppSources(projectDirectory);
35
+ await assertTypeScriptProgram(projectDirectory, sources);
36
+
37
+ const clientBytes =
38
+ sources.frontendExtensions.length > 0
39
+ ? await buildRemoteReactClient(projectDirectory, sources, development)
40
+ : null;
41
+ const serverBytes =
42
+ sources.lifecycleEvents.length > 0 ||
43
+ sources.serverFunctions.length > 0 ||
44
+ sources.webhookHandlers.length > 0
45
+ ? await buildServerArtifact(projectDirectory, sources)
46
+ : null;
47
+ const manifest = createManifest({
48
+ clientBytes,
49
+ serverBytes,
50
+ sources,
51
+ });
52
+ const manifestBytes = canonicalJsonBytes(manifest);
53
+ const entries = [
54
+ { name: "manifest.json", bytes: manifestBytes },
55
+ ...(clientBytes ? [{ name: "client.js", bytes: clientBytes }] : []),
56
+ ...(serverBytes ? [{ name: "server.js", bytes: serverBytes }] : []),
57
+ ];
58
+ const tarBytes = createTransportTar(entries, tarOptions);
59
+ const artifacts = Object.freeze([
60
+ createArtifact("manifest", "manifest.json", APP_MANIFEST_MEDIA_TYPE, manifestBytes),
61
+ ...(clientBytes
62
+ ? [createArtifact("client", "client.js", JAVASCRIPT_MEDIA_TYPE, clientBytes)]
63
+ : []),
64
+ ...(serverBytes
65
+ ? [createArtifact("server", "server.js", JAVASCRIPT_MEDIA_TYPE, serverBytes)]
66
+ : []),
67
+ ]);
68
+ if (writeToDist) {
69
+ const distDirectory = resolve(projectDirectory, "dist");
70
+ await rm(distDirectory, { recursive: true, force: true });
71
+ await mkdir(distDirectory, { recursive: true });
72
+ for (const artifact of artifacts) {
73
+ await writeFile(resolve(distDirectory, artifact.path), artifact.bytes, {
74
+ flag: "wx",
75
+ });
76
+ }
77
+ await writeFile(resolve(distDirectory, "app-bundle.tar"), tarBytes, {
78
+ flag: "wx",
79
+ });
80
+ }
81
+ return Object.freeze({
82
+ artifacts,
83
+ bytes: tarBytes,
84
+ manifest: Object.freeze(manifest),
85
+ packageFormat: APP_BUNDLE_PACKAGE_FORMAT,
86
+ contentType: APP_BUNDLE_CONTENT_TYPE,
87
+ sha256: sha256(tarBytes),
88
+ size: tarBytes.byteLength,
89
+ toolingVersion: TOOLING_VERSION,
90
+ });
91
+ }
92
+
93
+ async function assertTypeScriptProgram(projectDirectory, { sourceFiles, serverFunctions }) {
94
+ const paths = {
95
+ "@77-zhike/app-sdk": [resolve(SDK_SOURCE_ROOT, "index.d.ts")],
96
+ "@77-zhike/app-sdk/client": [resolve(SDK_SOURCE_ROOT, "client", "index.d.ts")],
97
+ "@77-zhike/app-sdk/server": [resolve(SDK_SOURCE_ROOT, "server", "index.d.ts")],
98
+ "@77-zhike/app-sdk/ui": [resolve(SDK_SOURCE_ROOT, "ui", "index.d.ts")],
99
+ "@77-zhike/app-sdk/forms": [resolve(SDK_SOURCE_ROOT, "forms", "index.d.ts")],
100
+ "react": [resolve(dirname(createRequire(import.meta.url).resolve("@types/react/package.json")), "index.d.ts")],
101
+ "react/jsx-runtime": [resolve(dirname(createRequire(import.meta.url).resolve("@types/react/package.json")), "jsx-runtime.d.ts")],
102
+ "react/jsx-dev-runtime": [resolve(dirname(createRequire(import.meta.url).resolve("@types/react/package.json")), "jsx-dev-runtime.d.ts")],
103
+ };
104
+ const options = {
105
+ baseUrl: projectDirectory,
106
+ jsx: ts.JsxEmit.ReactJSX,
107
+ lib: ["lib.es2022.d.ts", "lib.dom.d.ts"],
108
+ module: ts.ModuleKind.ESNext,
109
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
110
+ noEmit: true,
111
+ noUncheckedIndexedAccess: true,
112
+ paths,
113
+ skipLibCheck: false,
114
+ strict: true,
115
+ target: ts.ScriptTarget.ES2022,
116
+ };
117
+ const rootNames = sourceFiles.map((path) => resolve(projectDirectory, "src", path));
118
+ // 仅供 TypeScript 检查,不落盘、不进 Artifact;复用原包装的参数/结果合同。
119
+ const contractPath = resolve(projectDirectory, ".zhike/server-function-contracts.ts");
120
+ const contractSource = [
121
+ 'import type { ServerFunction } from "@77-zhike/app-sdk/server";',
122
+ ...serverFunctions.flatMap(({ relativePath }, index) => [
123
+ `import handler${index} from ${JSON.stringify(`../src/${relativePath.slice(0, -3)}`)};`,
124
+ `type Contract${index} = ServerFunction<Parameters<typeof handler${index}>, Awaited<ReturnType<typeof handler${index}>>>;`,
125
+ ]),
126
+ ].join("\n");
127
+ const host = ts.createCompilerHost(options);
128
+ const getSourceFile = host.getSourceFile.bind(host);
129
+ host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) =>
130
+ resolve(fileName) === contractPath
131
+ ? ts.createSourceFile(fileName, contractSource, languageVersion, true)
132
+ : getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
133
+ if (serverFunctions.length) rootNames.push(contractPath);
134
+ const program = ts.createProgram({ rootNames, options, host });
135
+ const diagnostics = ts.getPreEmitDiagnostics(program);
136
+ if (diagnostics.length === 0) return;
137
+ const diagnostic = diagnostics[0];
138
+ const contractLine = diagnostic.file?.fileName === contractPath
139
+ ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start || 0).line : -1;
140
+ const contractOwner = contractLine > 0 ? serverFunctions[Math.floor((contractLine - 1) / 2)] : null;
141
+ const location = contractOwner ? `src/${contractOwner.relativePath}(参数/返回类型)` : diagnostic.file
142
+ ? `${relative(projectDirectory, diagnostic.file.fileName).split(sep).join("/")}:${
143
+ diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start || 0).line + 1
144
+ }`
145
+ : "TypeScript";
146
+ throw new ZhikeCliError(
147
+ "zhike_typescript_diagnostic",
148
+ `${location}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
149
+ );
150
+ }
151
+
152
+ async function buildRemoteReactClient(projectDirectory, sources, development) {
153
+ const imports = sources.frontendExtensions.map((item, i) =>
154
+ `import extension${i} from "./src/${item.relativePath}";`).join("\n");
155
+ const entries = sources.frontendExtensions.map((item, i) =>
156
+ `${JSON.stringify(item.key)}: extension${i}`).join(",");
157
+ const result = await build({
158
+ absWorkingDir: projectDirectory, bundle: true, charset: "utf8", format: "cjs",
159
+ legalComments: "none", logLevel: "silent", minify: !development, platform: "browser",
160
+ 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"],
161
+ plugins: [createSdkBuildPlugin(projectDirectory, sources, "remote-client")],
162
+ sourcemap: development ? "inline" : false,
163
+ 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;' },
164
+ footer: { js: 'return module.exports;\n}' },
165
+ jsx: "automatic", jsxDev: development,
166
+ stdin: { contents: `${imports}\nexport const extensions = Object.freeze({${entries}});`, loader: "tsx", resolveDir: projectDirectory, sourcefile: "zhike-client-entry.tsx" },
167
+ // 原生 await 不经过 Promise.then;降级后的 helper 必须使用工厂内受管 Promise。
168
+ target: "es2022", supported: { "async-await": false, "async-generator": false }, treeShaking: true, write: false,
169
+ });
170
+ const bytes = normalizeJavascriptOutput(result.outputFiles[0].contents);
171
+ if (bytes.byteLength > 2 * 1024 * 1024) {
172
+ throw new ZhikeCliError("zhike_client_too_large", "client Artifact(包含源码映射)超过 2 MiB");
173
+ }
174
+ return bytes;
175
+ }
176
+
177
+ async function buildServerArtifact(projectDirectory, sources) {
178
+ const hasConnectionLifecycle = sources.lifecycleEvents.some(
179
+ ({ event }) => event === "connection-added" || event === "connection-removed",
180
+ );
181
+ const imports = [
182
+ ...(hasConnectionLifecycle
183
+ ? ['import { getWorkspaceConnection as getLifecycleConnection } from "@77-zhike/app-sdk/server";']
184
+ : []),
185
+ ...sources.lifecycleEvents.map(
186
+ (definition, index) =>
187
+ `import lifecycleEvent${index} from "./src/${definition.relativePath}";`,
188
+ ),
189
+ ...sources.serverFunctions.map(
190
+ (definition, index) =>
191
+ `import serverFunction${index} from "./src/${definition.relativePath}";`,
192
+ ),
193
+ ...sources.webhookHandlers.map(
194
+ (definition, index) =>
195
+ `import webhookHandler${index} from "./src/${definition.relativePath}";`,
196
+ ),
197
+ ].join("\n");
198
+ const lifecycleEntries = sources.lifecycleEvents
199
+ .map(
200
+ (definition, index) =>
201
+ `[${JSON.stringify(definition.key)}, { event: ${JSON.stringify(definition.event)}, handler: lifecycleEvent${index} }]`,
202
+ )
203
+ .join(",\n");
204
+ const serverEntries = sources.serverFunctions
205
+ .map(
206
+ (definition, index) =>
207
+ `[${JSON.stringify(definition.key)}, serverFunction${index}]`,
208
+ )
209
+ .join(",\n");
210
+ const webhookEntries = sources.webhookHandlers
211
+ .map(
212
+ (definition, index) =>
213
+ `[${JSON.stringify(definition.key)}, webhookHandler${index}]`,
214
+ )
215
+ .join(",\n");
216
+ const stdin = `${imports}\n${serializeWebhookResponse.toString()}\nconst lifecycleEvents = new Map([${lifecycleEntries}]);\nconst serverFunctions = new Map([${serverEntries}]);\nconst webhookHandlers = new Map([${webhookEntries}]);\nexport 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 === "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`;
217
+ const result = await build({
218
+ absWorkingDir: projectDirectory,
219
+ bundle: true,
220
+ charset: "utf8",
221
+ format: "esm",
222
+ legalComments: "none",
223
+ logLevel: "silent",
224
+ minify: true,
225
+ platform: "browser",
226
+ plugins: [createSdkBuildPlugin(projectDirectory, sources, "server")],
227
+ sourcemap: false,
228
+ stdin: { contents: stdin, loader: "ts", resolveDir: projectDirectory, sourcefile: "zhike-server-entry.ts" },
229
+ target: "es2022",
230
+ treeShaking: true,
231
+ write: false,
232
+ });
233
+ const bytes = normalizeJavascriptOutput(result.outputFiles[0].contents);
234
+ const definitionsDigest = createServerDefinitionsDigest(sources);
235
+ return Buffer.concat([
236
+ bytes,
237
+ Buffer.from(`//# zhike-server-definitions-sha256=${definitionsDigest}\n`, "utf8"),
238
+ ]);
239
+ }
240
+
241
+ function createSdkBuildPlugin(projectDirectory, sources, target) {
242
+ const serverKeys = new Set(sources.serverFunctions.map((item) => item.key));
243
+ return {
244
+ name: "zhike-app-sdk-runtime",
245
+ setup(buildApi) {
246
+ if (target !== "remote-client") buildApi.onResolve({ filter: /^@77-zhike\/app-sdk\/(client|server)$/ }, (args) => ({
247
+ external: target === "server" && args.path.endsWith("/server"),
248
+ path: args.path,
249
+ ...(target === "server" && args.path.endsWith("/server")
250
+ ? {}
251
+ : { namespace: "zhike-sdk-runtime" }),
252
+ }));
253
+ buildApi.onLoad({ filter: /.*/, namespace: "zhike-sdk-runtime" }, (args) => {
254
+ if (args.path.endsWith("/client")) {
255
+ return { contents: "export const defineFrontendExtension = (definition) => definition;", loader: "js" };
256
+ }
257
+ 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" };
258
+ });
259
+ if (target === "client" || target === "remote-client") {
260
+ buildApi.onResolve({ filter: /\.server(?:\.ts)?$/ }, (args) => {
261
+ let absolute = resolve(args.resolveDir, args.path);
262
+ if (!absolute.endsWith(".ts")) absolute = `${absolute}.ts`;
263
+ const normalizedAbsolute = absolute.split(sep).join("/");
264
+ const sourceMarker = "/src/";
265
+ const sourceIndex = normalizedAbsolute.lastIndexOf(sourceMarker);
266
+ const moduleId =
267
+ sourceIndex === -1
268
+ ? ""
269
+ : normalizedAbsolute
270
+ .slice(sourceIndex + sourceMarker.length)
271
+ .replace(/\.server\.ts$/, "");
272
+ if (!serverKeys.has(moduleId)) {
273
+ return { errors: [{ text: `未声明的 Server Function import:${moduleId}` }] };
274
+ }
275
+ return { path: moduleId, namespace: "zhike-server-rpc" };
276
+ });
277
+ buildApi.onLoad({ filter: /.*/, namespace: "zhike-server-rpc" }, (args) => ({
278
+ contents: target === "remote-client"
279
+ ? `const runtime = require("@77-zhike/app-sdk/runtime"); export default (...args) => runtime.invokeServerFunction(${JSON.stringify(args.path)}, args);`
280
+ : `export default (...args) => globalThis.ZHIKE_CLIENT_SDK.invokeServerFunction(${JSON.stringify(args.path)}, args);`,
281
+ loader: "js",
282
+ }));
283
+ }
284
+ },
285
+ };
286
+ }
287
+
288
+ function createManifest({ clientBytes, serverBytes, sources }) {
289
+ return {
290
+ schemaVersion: "zhike.app-manifest.v1",
291
+ sdkAbiVersion: "2",
292
+ toolchainVersion: TOOLING_VERSION,
293
+ ...(sources.frontendExtensions.length > 0
294
+ ? { clientRuntime: { runtimeVersion: "remote-react.v2" } } : {}),
295
+ artifacts: {
296
+ ...(clientBytes
297
+ ? {
298
+ client: {
299
+ mediaType: JAVASCRIPT_MEDIA_TYPE,
300
+ path: "client.js",
301
+ sha256: sha256(clientBytes),
302
+ size: clientBytes.byteLength,
303
+ },
304
+ }
305
+ : {}),
306
+ ...(serverBytes
307
+ ? {
308
+ server: {
309
+ mediaType: JAVASCRIPT_MEDIA_TYPE,
310
+ path: "server.js",
311
+ sha256: sha256(serverBytes),
312
+ size: serverBytes.byteLength,
313
+ },
314
+ }
315
+ : {}),
316
+ },
317
+ frontendExtensions: sources.frontendExtensions.map(({ key, type, entry, label, icon, objects }) => ({
318
+ artifact: "client",
319
+ entry,
320
+ key,
321
+ type,
322
+ ...(label === undefined ? {} : { label }),
323
+ ...(icon === undefined ? {} : { icon }),
324
+ ...(objects === undefined ? {} : { objects }),
325
+ })),
326
+ lifecycleEvents: sources.lifecycleEvents.map(({ event, key }) => ({
327
+ artifact: "server",
328
+ event,
329
+ key,
330
+ })),
331
+ serverFunctions: sources.serverFunctions.map(({ key }) => ({
332
+ artifact: "server",
333
+ key,
334
+ })),
335
+ webhookHandlers: sources.webhookHandlers.map(({ key, methods }) => ({
336
+ artifact: "server",
337
+ key,
338
+ methods,
339
+ })),
340
+ };
341
+ }
342
+
343
+ function createServerDefinitionsDigest(sources) {
344
+ return sha256(
345
+ canonicalJsonBytes({
346
+ lifecycleEvents: sources.lifecycleEvents.map(({ event, key }) => ({ event, key })),
347
+ serverFunctions: sources.serverFunctions.map(({ key }) => ({ key })),
348
+ webhookHandlers: sources.webhookHandlers.map(({ key, methods }) => ({ key, methods })),
349
+ }),
350
+ );
351
+ }
352
+
353
+ function createArtifact(kind, path, mediaType, bytes) {
354
+ return Object.freeze({
355
+ bytes: Buffer.from(bytes),
356
+ kind,
357
+ mediaType,
358
+ path,
359
+ sha256: sha256(bytes),
360
+ size: bytes.byteLength,
361
+ });
362
+ }
363
+
364
+ function normalizeJavascriptOutput(bytes) {
365
+ const text = Buffer.from(bytes).toString("utf8").replace(/\r\n/g, "\n");
366
+ return Buffer.from(text.endsWith("\n") ? text : `${text}\n`, "utf8");
367
+ }
368
+
369
+ function sha256(bytes) {
370
+ return createHash("sha256").update(bytes).digest("hex");
371
+ }