@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,710 @@
1
+ import { frontendPoint } from '../client/points.generated.js';
2
+ import { sdkIconNames } from '../ui/icons.generated.js';
3
+ import { readdir, readFile } from "node:fs/promises";
4
+ import { posix, relative, resolve, sep } from "node:path";
5
+ import ts from "typescript";
6
+
7
+ import { ZhikeCliError } from "../errors.js";
8
+
9
+ const KEY_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
10
+ const RECORD_OBJECT_PATTERN = /^[a-z][a-z0-9_]{0,106}$/;
11
+ const RECORD_ACTION_LABEL_MAX_LENGTH = 80;
12
+ const RECORD_ACTION_OBJECTS_MAX_LENGTH = 32;
13
+ const SDK_ICON_NAMES = new Set(sdkIconNames);
14
+ const SERVER_MODULE_PATTERN = /^server\/functions\/[a-z0-9][a-z0-9/-]*$/;
15
+ const LIFECYCLE_EVENTS = new Set([
16
+ "app-installed",
17
+ "app-uninstalled",
18
+ "connection-added",
19
+ "connection-removed",
20
+ ]);
21
+ const SDK_IMPORTS = new Set([
22
+ "@77-zhike/app-sdk",
23
+ "@77-zhike/app-sdk/client",
24
+ "@77-zhike/app-sdk/ui",
25
+ "@77-zhike/app-sdk/forms",
26
+ "react",
27
+ "react/jsx-runtime",
28
+ "react/jsx-dev-runtime",
29
+ "@77-zhike/app-sdk/server",
30
+ ]);
31
+ const NODE_BUILTINS = new Set([
32
+ "assert",
33
+ "buffer",
34
+ "child_process",
35
+ "cluster",
36
+ "crypto",
37
+ "dgram",
38
+ "dns",
39
+ "events",
40
+ "fs",
41
+ "http",
42
+ "https",
43
+ "module",
44
+ "net",
45
+ "os",
46
+ "path",
47
+ "perf_hooks",
48
+ "process",
49
+ "readline",
50
+ "stream",
51
+ "tls",
52
+ "url",
53
+ "util",
54
+ "v8",
55
+ "vm",
56
+ "worker_threads",
57
+ "zlib",
58
+ ]);
59
+
60
+ export async function discoverAppSources(projectDirectory) {
61
+ const sourceRoot = resolve(projectDirectory, "src");
62
+ const files = await listFiles(sourceRoot);
63
+ const normalized = files.map((path) => ({
64
+ absolutePath: path,
65
+ relativePath: relative(sourceRoot, path).split(sep).join("/"),
66
+ }));
67
+ rejectUnsupportedPaths(normalized);
68
+
69
+ const frontendExtensions = [];
70
+ const lifecycleEvents = [];
71
+ const serverFunctions = [];
72
+ const webhookHandlers = [];
73
+ const globalKeys = new Map();
74
+ for (const file of normalized) {
75
+ const sourceText = await readFile(file.absolutePath, "utf8");
76
+ const sourceFile = ts.createSourceFile(
77
+ file.relativePath,
78
+ sourceText,
79
+ ts.ScriptTarget.ES2022,
80
+ true,
81
+ file.relativePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
82
+ );
83
+ assertSyntacticallyValid(sourceFile);
84
+ validateImports(sourceFile, file.relativePath);
85
+
86
+ const extensionMatch = file.relativePath.match(
87
+ /^app\/extensions\/([a-z][a-z0-9-]{0,63})\/extension\.tsx$/,
88
+ );
89
+ const lifecycleMatch = file.relativePath.match(
90
+ /^app\/events\/(app-installed|app-uninstalled|connection-added|connection-removed)\.event\.ts$/,
91
+ );
92
+ const webhookMatch = file.relativePath.match(
93
+ /^app\/webhooks\/([a-z][a-z0-9-]{0,63})\.webhook\.ts$/,
94
+ );
95
+ const serverMatch = file.relativePath.match(
96
+ /^(server\/functions\/[a-z0-9][a-z0-9/-]*)\.server\.ts$/,
97
+ );
98
+ if (lifecycleMatch) {
99
+ const definition = parseLifecycleEvent(
100
+ sourceFile,
101
+ file.relativePath,
102
+ lifecycleMatch[1],
103
+ );
104
+ registerUniqueKey(globalKeys, definition.key, file.relativePath);
105
+ lifecycleEvents.push(definition);
106
+ } else if (extensionMatch) {
107
+ const definition = parseFrontendExtension(
108
+ sourceFile,
109
+ file.relativePath,
110
+ extensionMatch[1],
111
+ );
112
+ registerUniqueKey(globalKeys, definition.key, file.relativePath);
113
+ frontendExtensions.push(definition);
114
+ } else if (webhookMatch) {
115
+ const definition = parseWebhookHandler(
116
+ sourceFile,
117
+ file.relativePath,
118
+ webhookMatch[1],
119
+ );
120
+ registerUniqueKey(globalKeys, definition.key, file.relativePath);
121
+ webhookHandlers.push(definition);
122
+ } else if (serverMatch) {
123
+ const definition = parseServerFunction(
124
+ sourceFile,
125
+ file.relativePath,
126
+ serverMatch[1],
127
+ );
128
+ registerUniqueKey(globalKeys, definition.key, file.relativePath);
129
+ serverFunctions.push(definition);
130
+ }
131
+ }
132
+ if (
133
+ frontendExtensions.length === 0 &&
134
+ lifecycleEvents.length === 0 &&
135
+ serverFunctions.length === 0 &&
136
+ webhookHandlers.length === 0
137
+ ) {
138
+ throw toolchainError(
139
+ "zhike_source_entry_missing",
140
+ "未发现一期 Lifecycle Event、Frontend Extension、Server Function 或 Webhook Handler",
141
+ );
142
+ }
143
+ if (frontendExtensions.filter(item => item.type === "settings").length > 1) {
144
+ throw toolchainError("zhike_settings_duplicate", "一个 App 至多声明一个 Settings");
145
+ }
146
+ return Object.freeze({
147
+ frontendExtensions: Object.freeze(sortByKey(frontendExtensions)),
148
+ lifecycleEvents: Object.freeze(sortByKey(lifecycleEvents)),
149
+ serverFunctions: Object.freeze(sortByKey(serverFunctions)),
150
+ sourceFiles: Object.freeze(normalized.map((file) => file.relativePath).sort()),
151
+ webhookHandlers: Object.freeze(sortByKey(webhookHandlers)),
152
+ });
153
+ }
154
+
155
+ function parseLifecycleEvent(sourceFile, relativePath, fileEvent) {
156
+ if (hasSdkNamedImport(sourceFile, "@77-zhike/app-sdk/server", "defineLifecycleEvent")) {
157
+ throw toolchainError(
158
+ "zhike_lifecycle_event_wrapper_deprecated",
159
+ `${relativePath}: defineLifecycleEvent 已不再用于新 Build,请直接 default export async function`,
160
+ );
161
+ }
162
+ const handler = requireDefaultAsyncFunction(sourceFile, relativePath);
163
+ if (!LIFECYCLE_EVENTS.has(fileEvent)) {
164
+ throw toolchainError(
165
+ "zhike_lifecycle_event_identity_invalid",
166
+ `${relativePath}: 文件名必须是受支持的生命周期事件`,
167
+ );
168
+ }
169
+ return Object.freeze({ event: fileEvent, key: fileEvent, relativePath, handler });
170
+ }
171
+
172
+ function requireDefaultAsyncFunction(
173
+ sourceFile,
174
+ relativePath,
175
+ {
176
+ errorCode = "zhike_lifecycle_event_export_invalid",
177
+ entryName = "Lifecycle Event",
178
+ } = {},
179
+ ) {
180
+ const functions = sourceFile.statements.filter(
181
+ (statement) =>
182
+ ts.isFunctionDeclaration(statement) &&
183
+ statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) &&
184
+ statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword),
185
+ );
186
+ const defaultAssignments = sourceFile.statements.filter(
187
+ (statement) => ts.isExportAssignment(statement) && !statement.isExportEquals,
188
+ );
189
+ if (functions.length !== 1 || defaultAssignments.length !== 0) {
190
+ throw toolchainError(
191
+ errorCode,
192
+ `${relativePath}: 必须有且只有一个 export default async function`,
193
+ );
194
+ }
195
+ const handler = functions[0];
196
+ if (
197
+ !handler.body ||
198
+ !handler.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
199
+ ) {
200
+ throw toolchainError(
201
+ errorCode,
202
+ `${relativePath}: ${entryName} default export 必须是 async function`,
203
+ );
204
+ }
205
+ return handler.name?.text || null;
206
+ }
207
+
208
+ function parseFrontendExtension(sourceFile, relativePath, directoryKey) {
209
+ requireSdkNamedImport(
210
+ sourceFile,
211
+ relativePath,
212
+ "@77-zhike/app-sdk/client",
213
+ "defineFrontendExtension",
214
+ );
215
+ const call = requireDefaultExportCall(
216
+ sourceFile,
217
+ relativePath,
218
+ "defineFrontendExtension",
219
+ );
220
+ const object = requireObjectLiteral(call.arguments[0], relativePath);
221
+ const key = requireStringProperty(object, "key", relativePath);
222
+ const type = requireStringProperty(object, "type", relativePath);
223
+ const point = frontendPoint(type);
224
+ if (!point) throw toolchainError("zhike_frontend_extension_type_unsupported", `${relativePath}: 扩展类型尚未开放`);
225
+ const handlerName = point.kind === "view" ? "component" : (point.callbackName || "handler");
226
+ const forbiddenNames = point.kind === "view"
227
+ ? ["handler", "onTrigger"]
228
+ : ["component", ...(handlerName === "handler" ? ["onTrigger"] : ["handler"])];
229
+ if (forbiddenNames.some(name => findProperty(object, name))) {
230
+ throw toolchainError("zhike_frontend_definition_mixed", `${relativePath}: component 与 handler 不得混填`);
231
+ }
232
+ const property = findProperty(object, handlerName);
233
+ if (point.kind === "view" && property && ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer)) {
234
+ // 引用的组件由 TypeScript 检查可调用性;控制面不执行组件。
235
+ } else {
236
+ requireFunctionProperty(object, handlerName, relativePath);
237
+ }
238
+ if (key !== directoryKey || !KEY_PATTERN.test(key)) {
239
+ throw toolchainError(
240
+ "zhike_frontend_extension_key_invalid",
241
+ `${relativePath}: key 必须与扩展目录一致`,
242
+ );
243
+ }
244
+ if (type === "record-action") {
245
+ return Object.freeze({
246
+ key,
247
+ type,
248
+ entry: key,
249
+ relativePath,
250
+ ...parseRecordActionMetadata(object, relativePath),
251
+ });
252
+ }
253
+ return Object.freeze({ key, type, entry: key, relativePath });
254
+ }
255
+
256
+ function parseRecordActionMetadata(object, relativePath) {
257
+ assertOnlyProperties(object, ["key", "type", "label", "icon", "objects", "onTrigger"], relativePath);
258
+ const label = requireStringProperty(object, "label", relativePath).trim();
259
+ if (
260
+ [...label].length === 0 ||
261
+ [...label].length > RECORD_ACTION_LABEL_MAX_LENGTH ||
262
+ /\p{Cc}/u.test(label)
263
+ ) {
264
+ throw toolchainError(
265
+ "zhike_record_action_label_invalid",
266
+ `${relativePath}: label 必须是 1~${RECORD_ACTION_LABEL_MAX_LENGTH} 个非控制字符`,
267
+ );
268
+ }
269
+ const iconProperty = findProperty(object, "icon");
270
+ const icon = iconProperty ? requireStringProperty(object, "icon", relativePath) : null;
271
+ if (icon !== null && (!KEY_PATTERN.test(icon) || !SDK_ICON_NAMES.has(icon))) {
272
+ throw toolchainError(
273
+ "zhike_record_action_icon_invalid",
274
+ `${relativePath}: icon 必须来自 SDK 统一图标目录`,
275
+ );
276
+ }
277
+ const objectsProperty = findProperty(object, "objects");
278
+ const objects = objectsProperty ? requireRecordObjects(objectsProperty, relativePath) : null;
279
+ return {
280
+ label,
281
+ ...(icon === null ? {} : { icon }),
282
+ ...(objects === null ? {} : { objects }),
283
+ };
284
+ }
285
+
286
+ function requireRecordObjects(property, relativePath) {
287
+ if (!ts.isPropertyAssignment(property)) {
288
+ throw toolchainError("zhike_record_action_objects_invalid", `${relativePath}: objects 必须是字符串字面量或字符串字面量数组`);
289
+ }
290
+ const initializer = property.initializer;
291
+ const values = ts.isStringLiteral(initializer)
292
+ ? [initializer.text]
293
+ : ts.isArrayLiteralExpression(initializer)
294
+ ? initializer.elements.map(item => {
295
+ if (!ts.isStringLiteral(item)) {
296
+ throw toolchainError("zhike_record_action_objects_invalid", `${relativePath}: objects 必须是字符串字面量数组`);
297
+ }
298
+ return item.text;
299
+ })
300
+ : null;
301
+ if (
302
+ values === null ||
303
+ values.length === 0 ||
304
+ values.length > RECORD_ACTION_OBJECTS_MAX_LENGTH ||
305
+ values.some(value => !RECORD_OBJECT_PATTERN.test(value))
306
+ ) {
307
+ throw toolchainError(
308
+ "zhike_record_action_objects_invalid",
309
+ `${relativePath}: objects 必须包含 1~${RECORD_ACTION_OBJECTS_MAX_LENGTH} 个合法公开对象标识`,
310
+ );
311
+ }
312
+ return Object.freeze([...new Set(values)]);
313
+ }
314
+
315
+ function assertOnlyProperties(object, allowed, relativePath) {
316
+ const allowedNames = new Set(allowed);
317
+ for (const property of object.properties) {
318
+ const name = property.name && (
319
+ ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)
320
+ ) ? property.name.text : null;
321
+ if (!name || !allowedNames.has(name)) {
322
+ throw toolchainError(
323
+ "zhike_frontend_definition_unknown_property",
324
+ `${relativePath}: Frontend Extension 包含不支持的字段`,
325
+ );
326
+ }
327
+ }
328
+ }
329
+
330
+ function parseServerFunction(sourceFile, relativePath, moduleId) {
331
+ if (hasSdkNamedImport(sourceFile, "@77-zhike/app-sdk/server", "defineServerFunction")) {
332
+ throw toolchainError(
333
+ "zhike_server_function_wrapper_deprecated",
334
+ `${relativePath}: defineServerFunction 已不再用于新 Build,请直接 export default async function`,
335
+ );
336
+ }
337
+ requireDefaultAsyncFunction(sourceFile, relativePath, {
338
+ errorCode: "zhike_server_function_export_invalid",
339
+ entryName: "Server Function",
340
+ });
341
+ if (!SERVER_MODULE_PATTERN.test(moduleId)) {
342
+ throw toolchainError(
343
+ "zhike_server_function_key_invalid",
344
+ `${relativePath}: Server Function module ID 不合法`,
345
+ );
346
+ }
347
+ return Object.freeze({ key: moduleId, relativePath });
348
+ }
349
+
350
+ function parseWebhookHandler(sourceFile, relativePath, fileKey) {
351
+ if (hasSdkNamedImport(sourceFile, "@77-zhike/app-sdk/server", "defineWebhookHandler")) {
352
+ throw toolchainError(
353
+ "zhike_webhook_handler_wrapper_deprecated",
354
+ `${relativePath}: defineWebhookHandler 已不再用于新 Build,请直接 default export async function`,
355
+ );
356
+ }
357
+ requireDefaultAsyncFunction(sourceFile, relativePath, {
358
+ errorCode: "zhike_webhook_handler_export_invalid",
359
+ entryName: "Webhook Handler",
360
+ });
361
+ return Object.freeze({
362
+ key: fileKey,
363
+ methods: Object.freeze(["POST"]),
364
+ relativePath,
365
+ });
366
+ }
367
+
368
+ function requireDefaultExportCall(sourceFile, relativePath, expectedCallee) {
369
+ const exports = sourceFile.statements.filter(
370
+ (statement) => ts.isExportAssignment(statement) && !statement.isExportEquals,
371
+ );
372
+ if (exports.length !== 1 || !ts.isCallExpression(exports[0].expression)) {
373
+ throw toolchainError(
374
+ "zhike_entry_default_export_invalid",
375
+ `${relativePath}: 必须有且只有一个直接 default export`,
376
+ );
377
+ }
378
+ const call = exports[0].expression;
379
+ if (!ts.isIdentifier(call.expression) || call.expression.text !== expectedCallee) {
380
+ throw toolchainError(
381
+ "zhike_entry_definition_invalid",
382
+ `${relativePath}: default export 必须直接调用 ${expectedCallee}`,
383
+ );
384
+ }
385
+ return call;
386
+ }
387
+
388
+ function requireSdkNamedImport(sourceFile, relativePath, specifier, importedName) {
389
+ const matches = sourceFile.statements.filter((statement) =>
390
+ hasNamedImport(statement, specifier, importedName),
391
+ );
392
+ if (matches.length !== 1) {
393
+ throw toolchainError(
394
+ "zhike_sdk_definition_import_invalid",
395
+ `${relativePath}: ${importedName} 必须从 ${specifier} 直接 named import且不得重命名`,
396
+ );
397
+ }
398
+ }
399
+
400
+ function hasSdkNamedImport(sourceFile, specifier, importedName) {
401
+ return sourceFile.statements.some((statement) =>
402
+ hasNamedImport(statement, specifier, importedName),
403
+ );
404
+ }
405
+
406
+ function hasNamedImport(statement, specifier, importedName) {
407
+ if (
408
+ !ts.isImportDeclaration(statement) ||
409
+ statement.moduleSpecifier.text !== specifier ||
410
+ !statement.importClause?.namedBindings ||
411
+ !ts.isNamedImports(statement.importClause.namedBindings)
412
+ ) {
413
+ return false;
414
+ }
415
+ return statement.importClause.namedBindings.elements.some(
416
+ (element) =>
417
+ (element.propertyName?.text || element.name.text) === importedName &&
418
+ element.name.text === importedName,
419
+ );
420
+ }
421
+
422
+ function validateImports(sourceFile, relativePath) {
423
+ const allowsStandardFetch =
424
+ relativePath.startsWith("server/") ||
425
+ /^app\/events\/.+\.event\.ts$/.test(relativePath) ||
426
+ /^app\/webhooks\/.+\.webhook\.ts$/.test(relativePath);
427
+ const visit = (node) => {
428
+ if (
429
+ ts.isCallExpression(node) &&
430
+ node.expression.kind === ts.SyntaxKind.ImportKeyword
431
+ ) {
432
+ throw toolchainError(
433
+ "zhike_dynamic_import_forbidden",
434
+ `${relativePath}: 一期禁止动态 import`,
435
+ );
436
+ }
437
+ if (
438
+ !allowsStandardFetch &&
439
+ ts.isCallExpression(node) &&
440
+ ((ts.isIdentifier(node.expression) && node.expression.text === "fetch") ||
441
+ (ts.isPropertyAccessExpression(node.expression) &&
442
+ node.expression.name.text === "fetch" &&
443
+ ts.isIdentifier(node.expression.expression) &&
444
+ ["globalThis", "window", "self"].includes(
445
+ node.expression.expression.text,
446
+ )))
447
+ ) {
448
+ throw toolchainError(
449
+ "zhike_network_egress_forbidden",
450
+ `${relativePath}: 前端网络能力必须由受控 Host capability 提供`,
451
+ );
452
+ }
453
+ if (
454
+ (ts.isNewExpression(node) &&
455
+ ts.isIdentifier(node.expression) &&
456
+ ["EventSource", "WebSocket", "XMLHttpRequest"].includes(
457
+ node.expression.text,
458
+ )) ||
459
+ (ts.isCallExpression(node) &&
460
+ ts.isPropertyAccessExpression(node.expression) &&
461
+ node.expression.name.text === "sendBeacon" &&
462
+ ts.isIdentifier(node.expression.expression) &&
463
+ node.expression.expression.text === "navigator")
464
+ ) {
465
+ throw toolchainError(
466
+ "zhike_network_egress_forbidden",
467
+ `${relativePath}: 一期禁止未托管的浏览器网络能力`,
468
+ );
469
+ }
470
+ if (
471
+ (ts.isIdentifier(node) &&
472
+ ["ZHIKE_CLIENT_SDK", "ZHIKE_SERVER_SDK"].includes(node.text)) ||
473
+ (ts.isPropertyAccessExpression(node) &&
474
+ ["ZHIKE_CLIENT_SDK", "ZHIKE_SERVER_SDK"].includes(node.name.text))
475
+ ) {
476
+ throw toolchainError(
477
+ "zhike_host_global_forbidden",
478
+ `${relativePath}: App 源码不得直接读取 Host SDK global`,
479
+ );
480
+ }
481
+ if (ts.isImportDeclaration(node)) {
482
+ const specifier = node.moduleSpecifier.text;
483
+ if (specifier === "@77-zhike/app-sdk/server" && !allowsStandardFetch) {
484
+ throw toolchainError(
485
+ "zhike_server_sdk_client_forbidden",
486
+ `${relativePath}: 前端代码不得导入服务端 SDK`,
487
+ );
488
+ }
489
+ if (specifier.startsWith("node:") || NODE_BUILTINS.has(specifier)) {
490
+ throw toolchainError(
491
+ "zhike_node_builtin_forbidden",
492
+ `${relativePath}: 一期禁止 Node builtin ${specifier}`,
493
+ );
494
+ }
495
+ if (!specifier.startsWith(".") && !SDK_IMPORTS.has(specifier)) {
496
+ throw toolchainError(
497
+ "zhike_external_dependency_forbidden",
498
+ `${relativePath}: 一期禁止第三方或内部依赖 ${specifier}`,
499
+ );
500
+ }
501
+ if (specifier.startsWith(".")) {
502
+ const resolvedSourcePath = posix.normalize(
503
+ posix.join(posix.dirname(relativePath), specifier),
504
+ );
505
+ if (resolvedSourcePath === ".." || resolvedSourcePath.startsWith("../")) {
506
+ throw toolchainError(
507
+ "zhike_source_import_outside_root",
508
+ `${relativePath}: relative import 不得越过 src 根目录`,
509
+ );
510
+ }
511
+ }
512
+ if (specifier.includes(".server.tsx") || specifier.endsWith(".server.tsx")) {
513
+ throw toolchainError(
514
+ "zhike_server_function_extension_invalid",
515
+ `${relativePath}: Server Function 不支持 .server.tsx`,
516
+ );
517
+ }
518
+ }
519
+ ts.forEachChild(node, visit);
520
+ };
521
+ visit(sourceFile);
522
+ }
523
+
524
+ function rejectUnsupportedPaths(files) {
525
+ for (const file of files) {
526
+ const path = file.relativePath;
527
+ if (
528
+ path.startsWith("app/events/") &&
529
+ path.endsWith(".ts") &&
530
+ !/^app\/events\/(app-installed|app-uninstalled|connection-added|connection-removed)\.event\.ts$/.test(path)
531
+ ) {
532
+ throw toolchainError(
533
+ "zhike_lifecycle_event_location_invalid",
534
+ `${path}: 生命周期事件文件名不受支持`,
535
+ );
536
+ }
537
+ if (path === "app.ts" || path === "app/index.ts") {
538
+ throw toolchainError(
539
+ "zhike_legacy_entry_forbidden",
540
+ `${path}: 一期不支持 Legacy app entry`,
541
+ );
542
+ }
543
+ if (/(^|\/)(workflows?|connections?)(\/|\.|$)/i.test(path)) {
544
+ throw toolchainError(
545
+ "zhike_capability_unsupported",
546
+ `${path}: 一期不支持 Workflow/Event/Connection/Settings`,
547
+ );
548
+ }
549
+ if (path.endsWith(".server.tsx")) {
550
+ throw toolchainError(
551
+ "zhike_server_function_extension_invalid",
552
+ `${path}: Server Function 只允许 .server.ts`,
553
+ );
554
+ }
555
+ if (
556
+ path.endsWith(".server.ts") &&
557
+ !/^server\/functions\/[a-z0-9][a-z0-9/-]*\.server\.ts$/.test(path)
558
+ ) {
559
+ throw toolchainError(
560
+ "zhike_server_function_location_invalid",
561
+ `${path}: Server Function 必须位于 src/server/functions`,
562
+ );
563
+ }
564
+ if (
565
+ path.startsWith("app/extensions/") &&
566
+ path.endsWith(".tsx") &&
567
+ !/^app\/extensions\/[a-z][a-z0-9-]{0,63}\/extension\.tsx$/.test(path)
568
+ ) {
569
+ throw toolchainError(
570
+ "zhike_frontend_extension_location_invalid",
571
+ `${path}: Frontend Extension 必须使用约定目录`,
572
+ );
573
+ }
574
+ if (
575
+ path.startsWith("app/webhooks/") &&
576
+ path.endsWith(".ts") &&
577
+ !/^app\/webhooks\/[a-z][a-z0-9-]{0,63}\.webhook\.ts$/.test(path)
578
+ ) {
579
+ throw toolchainError(
580
+ "zhike_webhook_location_invalid",
581
+ `${path}: Webhook Handler 必须使用 <key>.webhook.ts`,
582
+ );
583
+ }
584
+ }
585
+ }
586
+
587
+ function requireObjectLiteral(node, relativePath) {
588
+ if (!node || !ts.isObjectLiteralExpression(node)) {
589
+ throw toolchainError(
590
+ "zhike_entry_definition_invalid",
591
+ `${relativePath}: definition 必须是可静态解析的对象字面量`,
592
+ );
593
+ }
594
+ return node;
595
+ }
596
+
597
+ function requireStringProperty(object, name, relativePath) {
598
+ const property = findProperty(object, name);
599
+ if (!property || !ts.isPropertyAssignment(property) || !ts.isStringLiteral(property.initializer)) {
600
+ throw toolchainError(
601
+ "zhike_entry_definition_invalid",
602
+ `${relativePath}: ${name} 必须是字符串字面量`,
603
+ );
604
+ }
605
+ return property.initializer.text;
606
+ }
607
+
608
+ function requireStringArrayProperty(object, name, relativePath) {
609
+ const property = findProperty(object, name);
610
+ if (!property || !ts.isPropertyAssignment(property) || !ts.isArrayLiteralExpression(property.initializer)) {
611
+ throw toolchainError(
612
+ "zhike_entry_definition_invalid",
613
+ `${relativePath}: ${name} 必须是字符串字面量数组`,
614
+ );
615
+ }
616
+ return property.initializer.elements.map((item) => {
617
+ if (!ts.isStringLiteral(item)) {
618
+ throw toolchainError(
619
+ "zhike_entry_definition_invalid",
620
+ `${relativePath}: ${name} 必须是字符串字面量数组`,
621
+ );
622
+ }
623
+ return item.text.toUpperCase();
624
+ });
625
+ }
626
+
627
+ function requireFunctionProperty(object, name, relativePath, { requireAsync = false } = {}) {
628
+ const property = findProperty(object, name);
629
+ const functionNode =
630
+ property && ts.isMethodDeclaration(property)
631
+ ? property
632
+ : property && ts.isPropertyAssignment(property) && isFunctionLike(property.initializer)
633
+ ? property.initializer
634
+ : null;
635
+ if (
636
+ !functionNode ||
637
+ (requireAsync &&
638
+ !functionNode.modifiers?.some(
639
+ (modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword,
640
+ ))
641
+ ) {
642
+ throw toolchainError(
643
+ "zhike_entry_definition_invalid",
644
+ `${relativePath}: ${name} 必须是${requireAsync ? " async" : ""}函数`,
645
+ );
646
+ }
647
+ return functionNode;
648
+ }
649
+
650
+ function findProperty(object, name) {
651
+ return object.properties.find((property) => {
652
+ const propertyName = property.name;
653
+ return (
654
+ (ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName)) &&
655
+ propertyName.text === name
656
+ );
657
+ });
658
+ }
659
+
660
+ function isFunctionLike(node) {
661
+ return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
662
+ }
663
+
664
+ function registerUniqueKey(keys, key, relativePath) {
665
+ const existing = keys.get(key);
666
+ if (existing) {
667
+ throw toolchainError(
668
+ "zhike_definition_key_duplicate",
669
+ `definition key ${key} 同时出现在 ${existing} 和 ${relativePath}`,
670
+ );
671
+ }
672
+ keys.set(key, relativePath);
673
+ }
674
+
675
+ function assertSyntacticallyValid(sourceFile) {
676
+ if (sourceFile.parseDiagnostics.length === 0) return;
677
+ const diagnostic = sourceFile.parseDiagnostics[0];
678
+ throw toolchainError(
679
+ "zhike_source_syntax_invalid",
680
+ `${sourceFile.fileName}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
681
+ );
682
+ }
683
+
684
+ async function listFiles(root) {
685
+ let entries;
686
+ try {
687
+ entries = await readdir(root, { withFileTypes: true });
688
+ } catch (error) {
689
+ if (error?.code === "ENOENT") return [];
690
+ throw error;
691
+ }
692
+ const result = [];
693
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
694
+ const path = resolve(root, entry.name);
695
+ if (entry.isDirectory()) {
696
+ result.push(...(await listFiles(path)));
697
+ } else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name)) {
698
+ result.push(path);
699
+ }
700
+ }
701
+ return result;
702
+ }
703
+
704
+ function sortByKey(items) {
705
+ return items.sort((left, right) => left.key.localeCompare(right.key));
706
+ }
707
+
708
+ function toolchainError(code, message) {
709
+ return new ZhikeCliError(code, message);
710
+ }