@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.
- package/README.md +251 -0
- package/package.json +71 -0
- package/src/bin.js +15 -0
- package/src/cli/api-client.js +249 -0
- package/src/cli/browser-auth.js +212 -0
- package/src/cli/credential-store.js +101 -0
- package/src/cli/development-app.js +77 -0
- package/src/cli/development-logs.js +183 -0
- package/src/cli/development-watch.js +205 -0
- package/src/cli/index.js +256 -0
- package/src/cli/local-commands.js +80 -0
- package/src/cli/logs.js +56 -0
- package/src/cli/project-init.js +259 -0
- package/src/cli/upload-bundle.js +73 -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/external-url.js +9 -0
- package/src/client/index.d.ts +249 -0
- package/src/client/index.js +72 -0
- package/src/client/interactions.d.ts +23 -0
- package/src/client/interactions.js +62 -0
- package/src/client/points.generated.js +67 -0
- package/src/config.js +68 -0
- package/src/development-workspace.js +154 -0
- package/src/errors.js +20 -0
- package/src/forms/array-contract.d.ts +32 -0
- package/src/forms/array-contract.js +138 -0
- package/src/forms/components.generated.js +4 -0
- package/src/forms/factory.js +41 -0
- package/src/forms/field-array.d.ts +16 -0
- package/src/forms/field-array.js +52 -0
- package/src/forms/index.d.ts +118 -0
- package/src/forms/index.js +5 -0
- package/src/forms/normalize-options.js +19 -0
- package/src/forms/options.d.ts +30 -0
- package/src/forms/options.js +55 -0
- package/src/forms/record-options.d.ts +28 -0
- package/src/forms/record-options.js +92 -0
- package/src/forms/schema.d.ts +82 -0
- 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/index.d.ts +13 -0
- package/src/index.js +2 -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/project-binding.js +84 -0
- package/src/server/index.d.ts +318 -0
- package/src/server/index.js +242 -0
- package/src/server/steps/index.d.ts +71 -0
- package/src/server/steps/index.js +15 -0
- package/src/server/triggers/index.d.ts +68 -0
- package/src/server/triggers/index.js +27 -0
- package/src/server/workflow-blocks/index.d.ts +72 -0
- package/src/server/workflow-blocks/index.js +32 -0
- package/src/toolchain/allowed-packages.json +7 -0
- package/src/toolchain/canonical-json.js +17 -0
- package/src/toolchain/dependency-policy.js +181 -0
- package/src/toolchain/index.js +15 -0
- package/src/toolchain/real-build.js +459 -0
- package/src/toolchain/source-discovery.js +919 -0
- package/src/toolchain/transport-tar.js +168 -0
- package/src/toolchain/webhook-response.js +50 -0
- 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 +5 -0
- package/src/ui/events.d.ts +10 -0
- package/src/ui/factory.js +70 -0
- package/src/ui/icons.generated.js +2 -0
- package/src/ui/index.d.ts +343 -0
- package/src/ui/index.js +4 -0
- 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/style.d.ts +5 -0
- package/src/ui/style.generated.js +2 -0
- package/src/ui/style.js +15 -0
- package/src/ui/validation.js +86 -0
|
@@ -0,0 +1,919 @@
|
|
|
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
|
+
import { discoverWorkflowBlock } from "./workflow-blocks/discovery.js";
|
|
9
|
+
import { assertAllowedPackageImport } from "./dependency-policy.js";
|
|
10
|
+
|
|
11
|
+
const KEY_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
12
|
+
const RECORD_OBJECT_PATTERN = /^[a-z][a-z0-9_]{0,106}$/;
|
|
13
|
+
const RECORD_EXTENSION_OBJECT_PATTERN = /^(?:accounts|contacts|opportunities|custom_[a-z0-9_]{1,100})$/;
|
|
14
|
+
const RECORD_ACTION_LABEL_MAX_LENGTH = 80;
|
|
15
|
+
const RECORD_ACTION_OBJECTS_MAX_LENGTH = 32;
|
|
16
|
+
const RECORD_ACTION_TYPES = new Set(["record-action", "object-action", "bulk-record-action"]);
|
|
17
|
+
const RECORD_COMPONENT_TYPES = new Set(["record-widget", "record-section", "record-tab"]);
|
|
18
|
+
const PAGE_NAME_MAX_LENGTH = 80;
|
|
19
|
+
const PAGE_VARIANTS = new Set(["full", "centered"]);
|
|
20
|
+
const SDK_ICON_NAMES = new Set(sdkIconNames);
|
|
21
|
+
const SERVER_MODULE_PATTERN = /^server\/functions\/[a-z0-9][a-z0-9/-]*$/;
|
|
22
|
+
const LIFECYCLE_EVENTS = new Set([
|
|
23
|
+
"app-installed",
|
|
24
|
+
"app-uninstalled",
|
|
25
|
+
"connection-added",
|
|
26
|
+
"connection-removed",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export async function discoverAppSources(projectDirectory) {
|
|
30
|
+
const sourceRoot = resolve(projectDirectory, "src");
|
|
31
|
+
const files = await listFiles(sourceRoot);
|
|
32
|
+
const normalized = files.map((path) => ({
|
|
33
|
+
absolutePath: path,
|
|
34
|
+
relativePath: relative(sourceRoot, path).split(sep).join("/"),
|
|
35
|
+
}));
|
|
36
|
+
rejectUnsupportedPaths(normalized);
|
|
37
|
+
|
|
38
|
+
const parsedFiles = [];
|
|
39
|
+
for (const file of normalized) {
|
|
40
|
+
const sourceText = await readFile(file.absolutePath, "utf8");
|
|
41
|
+
const sourceFile = ts.createSourceFile(
|
|
42
|
+
file.relativePath,
|
|
43
|
+
sourceText,
|
|
44
|
+
ts.ScriptTarget.ES2022,
|
|
45
|
+
true,
|
|
46
|
+
file.relativePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
47
|
+
);
|
|
48
|
+
assertSyntacticallyValid(sourceFile);
|
|
49
|
+
parsedFiles.push({ ...file, sourceFile });
|
|
50
|
+
}
|
|
51
|
+
const sourceFilesByPath = new Map(parsedFiles.map(file => [file.relativePath, file.sourceFile]));
|
|
52
|
+
|
|
53
|
+
const frontendExtensions = [];
|
|
54
|
+
const lifecycleEvents = [];
|
|
55
|
+
const serverFunctions = [];
|
|
56
|
+
const webhookHandlers = [];
|
|
57
|
+
const workflowSteps = [];
|
|
58
|
+
const workflowTriggers = [];
|
|
59
|
+
const globalKeys = new Map();
|
|
60
|
+
for (const file of parsedFiles) {
|
|
61
|
+
const { sourceFile } = file;
|
|
62
|
+
validateImports(sourceFile, file.relativePath);
|
|
63
|
+
|
|
64
|
+
const extensionMatch = file.relativePath.match(
|
|
65
|
+
/^app\/extensions\/([a-z][a-z0-9-]{0,63})\/extension\.tsx$/,
|
|
66
|
+
);
|
|
67
|
+
const pageMatch = file.relativePath.match(
|
|
68
|
+
/^app\/pages\/([a-z][a-z0-9-]{0,63})\/page\.tsx$/,
|
|
69
|
+
);
|
|
70
|
+
const lifecycleMatch = file.relativePath.match(
|
|
71
|
+
/^app\/events\/(app-installed|app-uninstalled|connection-added|connection-removed)\.event\.ts$/,
|
|
72
|
+
);
|
|
73
|
+
const webhookMatch = file.relativePath.match(
|
|
74
|
+
/^app\/webhooks\/([a-z][a-z0-9-]{0,63})\.webhook\.ts$/,
|
|
75
|
+
);
|
|
76
|
+
const serverMatch = file.relativePath.match(
|
|
77
|
+
/^(server\/functions\/[a-z0-9][a-z0-9/-]*)\.server\.ts$/,
|
|
78
|
+
);
|
|
79
|
+
const workflowBlockMatch = file.relativePath.match(
|
|
80
|
+
/^app\/blocks\/([a-z][a-z0-9-]{0,63})\/block\.ts$/,
|
|
81
|
+
);
|
|
82
|
+
if (lifecycleMatch) {
|
|
83
|
+
const definition = parseLifecycleEvent(
|
|
84
|
+
sourceFile,
|
|
85
|
+
file.relativePath,
|
|
86
|
+
lifecycleMatch[1],
|
|
87
|
+
);
|
|
88
|
+
registerUniqueKey(globalKeys, definition.key, file.relativePath);
|
|
89
|
+
lifecycleEvents.push(definition);
|
|
90
|
+
} else if (pageMatch) {
|
|
91
|
+
const definition = parseWorkspacePage(
|
|
92
|
+
sourceFile,
|
|
93
|
+
file.relativePath,
|
|
94
|
+
pageMatch[1],
|
|
95
|
+
);
|
|
96
|
+
registerUniqueKey(globalKeys, definition.key, file.relativePath);
|
|
97
|
+
frontendExtensions.push(definition);
|
|
98
|
+
} else if (extensionMatch) {
|
|
99
|
+
const definition = parseFrontendExtension(
|
|
100
|
+
sourceFile,
|
|
101
|
+
file.relativePath,
|
|
102
|
+
extensionMatch[1],
|
|
103
|
+
);
|
|
104
|
+
registerUniqueKey(globalKeys, definition.key, file.relativePath);
|
|
105
|
+
frontendExtensions.push(definition);
|
|
106
|
+
} else if (webhookMatch) {
|
|
107
|
+
const definition = parseWebhookHandler(
|
|
108
|
+
sourceFile,
|
|
109
|
+
file.relativePath,
|
|
110
|
+
webhookMatch[1],
|
|
111
|
+
);
|
|
112
|
+
registerUniqueKey(globalKeys, definition.key, file.relativePath);
|
|
113
|
+
webhookHandlers.push(definition);
|
|
114
|
+
} else if (serverMatch) {
|
|
115
|
+
const definition = parseServerFunction(
|
|
116
|
+
sourceFile,
|
|
117
|
+
file.relativePath,
|
|
118
|
+
serverMatch[1],
|
|
119
|
+
);
|
|
120
|
+
registerUniqueKey(globalKeys, definition.key, file.relativePath);
|
|
121
|
+
serverFunctions.push(definition);
|
|
122
|
+
} else if (workflowBlockMatch) {
|
|
123
|
+
const definition = discoverWorkflowBlock(
|
|
124
|
+
sourceFile, file.relativePath, workflowBlockMatch[1], sourceFilesByPath,
|
|
125
|
+
);
|
|
126
|
+
if (definition.type === "step") {
|
|
127
|
+
registerUniqueKey(globalKeys, definition.entry, file.relativePath);
|
|
128
|
+
workflowSteps.push(definition);
|
|
129
|
+
} else {
|
|
130
|
+
for (const entry of Object.values(definition.entries)) registerUniqueKey(globalKeys, entry, file.relativePath);
|
|
131
|
+
workflowTriggers.push(definition);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (
|
|
136
|
+
frontendExtensions.length === 0 &&
|
|
137
|
+
lifecycleEvents.length === 0 &&
|
|
138
|
+
serverFunctions.length === 0 &&
|
|
139
|
+
webhookHandlers.length === 0 &&
|
|
140
|
+
workflowSteps.length === 0 &&
|
|
141
|
+
workflowTriggers.length === 0
|
|
142
|
+
) {
|
|
143
|
+
throw toolchainError(
|
|
144
|
+
"zhike_source_entry_missing",
|
|
145
|
+
"未发现 Lifecycle Event、Frontend Extension、Server Function、Webhook Handler 或 Workflow Block",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (frontendExtensions.filter(item => item.type === "settings").length > 1) {
|
|
149
|
+
throw toolchainError("zhike_settings_duplicate", "一个 App 至多声明一个 Settings");
|
|
150
|
+
}
|
|
151
|
+
return Object.freeze({
|
|
152
|
+
frontendExtensions: Object.freeze(sortByKey(frontendExtensions)),
|
|
153
|
+
lifecycleEvents: Object.freeze(sortByKey(lifecycleEvents)),
|
|
154
|
+
serverFunctions: Object.freeze(sortByKey(serverFunctions)),
|
|
155
|
+
sourceFiles: Object.freeze(parsedFiles.map((file) => file.relativePath).sort()),
|
|
156
|
+
webhookHandlers: Object.freeze(sortByKey(webhookHandlers)),
|
|
157
|
+
workflowSteps: Object.freeze(sortByKey(workflowSteps)),
|
|
158
|
+
workflowTriggers: Object.freeze(sortByKey(workflowTriggers)),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseLifecycleEvent(sourceFile, relativePath, fileEvent) {
|
|
163
|
+
if (hasSdkNamedImport(sourceFile, "@77-zhike/app-sdk/server", "defineLifecycleEvent")) {
|
|
164
|
+
throw toolchainError(
|
|
165
|
+
"zhike_lifecycle_event_wrapper_deprecated",
|
|
166
|
+
`${relativePath}: defineLifecycleEvent 已不再用于新 Build,请直接 default export async function`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const handler = requireDefaultAsyncFunction(sourceFile, relativePath);
|
|
170
|
+
if (!LIFECYCLE_EVENTS.has(fileEvent)) {
|
|
171
|
+
throw toolchainError(
|
|
172
|
+
"zhike_lifecycle_event_identity_invalid",
|
|
173
|
+
`${relativePath}: 文件名必须是受支持的生命周期事件`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return Object.freeze({ event: fileEvent, key: fileEvent, relativePath, handler });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function requireDefaultAsyncFunction(
|
|
180
|
+
sourceFile,
|
|
181
|
+
relativePath,
|
|
182
|
+
{
|
|
183
|
+
errorCode = "zhike_lifecycle_event_export_invalid",
|
|
184
|
+
entryName = "Lifecycle Event",
|
|
185
|
+
} = {},
|
|
186
|
+
) {
|
|
187
|
+
const functions = sourceFile.statements.filter(
|
|
188
|
+
(statement) =>
|
|
189
|
+
ts.isFunctionDeclaration(statement) &&
|
|
190
|
+
statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) &&
|
|
191
|
+
statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword),
|
|
192
|
+
);
|
|
193
|
+
const defaultAssignments = sourceFile.statements.filter(
|
|
194
|
+
(statement) => ts.isExportAssignment(statement) && !statement.isExportEquals,
|
|
195
|
+
);
|
|
196
|
+
if (functions.length !== 1 || defaultAssignments.length !== 0) {
|
|
197
|
+
throw toolchainError(
|
|
198
|
+
errorCode,
|
|
199
|
+
`${relativePath}: 必须有且只有一个 export default async function`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const handler = functions[0];
|
|
203
|
+
if (
|
|
204
|
+
!handler.body ||
|
|
205
|
+
!handler.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
|
|
206
|
+
) {
|
|
207
|
+
throw toolchainError(
|
|
208
|
+
errorCode,
|
|
209
|
+
`${relativePath}: ${entryName} default export 必须是 async function`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return handler.name?.text || null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseFrontendExtension(sourceFile, relativePath, directoryKey) {
|
|
216
|
+
requireSdkNamedImport(
|
|
217
|
+
sourceFile,
|
|
218
|
+
relativePath,
|
|
219
|
+
"@77-zhike/app-sdk/client",
|
|
220
|
+
"defineFrontendExtension",
|
|
221
|
+
);
|
|
222
|
+
const call = requireDefaultExportCall(
|
|
223
|
+
sourceFile,
|
|
224
|
+
relativePath,
|
|
225
|
+
"defineFrontendExtension",
|
|
226
|
+
);
|
|
227
|
+
const object = requireObjectLiteral(call.arguments[0], relativePath);
|
|
228
|
+
const key = requireStringProperty(object, "key", relativePath);
|
|
229
|
+
const type = requireStringProperty(object, "type", relativePath);
|
|
230
|
+
const point = frontendPoint(type);
|
|
231
|
+
if (!point || type === "page") throw toolchainError("zhike_frontend_extension_type_unsupported", `${relativePath}: 扩展类型尚未开放`);
|
|
232
|
+
const handlerName = point.kind === "view" ? (point.componentName || "component") : (point.callbackName || "handler");
|
|
233
|
+
const forbiddenNames = point.kind === "view"
|
|
234
|
+
? ["component", "Widget", "Section", "Tab", "Page", "handler", "onTrigger"].filter(name => name !== handlerName)
|
|
235
|
+
: ["component", "Widget", "Section", "Tab", "Page", ...(handlerName === "handler" ? ["onTrigger"] : ["handler"])];
|
|
236
|
+
if (forbiddenNames.some(name => findProperty(object, name))) {
|
|
237
|
+
throw toolchainError("zhike_frontend_definition_mixed", `${relativePath}: component 与 handler 不得混填`);
|
|
238
|
+
}
|
|
239
|
+
const property = findProperty(object, handlerName);
|
|
240
|
+
if (
|
|
241
|
+
point.kind === "view" &&
|
|
242
|
+
property &&
|
|
243
|
+
((ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer)) ||
|
|
244
|
+
ts.isShorthandPropertyAssignment(property))
|
|
245
|
+
) {
|
|
246
|
+
// 引用的组件由 TypeScript 检查可调用性;控制面不执行组件。
|
|
247
|
+
} else {
|
|
248
|
+
requireFunctionProperty(object, handlerName, relativePath);
|
|
249
|
+
}
|
|
250
|
+
if (key !== directoryKey || !KEY_PATTERN.test(key)) {
|
|
251
|
+
throw toolchainError(
|
|
252
|
+
"zhike_frontend_extension_key_invalid",
|
|
253
|
+
`${relativePath}: key 必须与扩展目录一致`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (RECORD_ACTION_TYPES.has(type)) {
|
|
257
|
+
return Object.freeze({
|
|
258
|
+
key,
|
|
259
|
+
type,
|
|
260
|
+
entry: key,
|
|
261
|
+
relativePath,
|
|
262
|
+
...parseRecordActionMetadata(object, relativePath),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (RECORD_COMPONENT_TYPES.has(type)) {
|
|
266
|
+
return Object.freeze({
|
|
267
|
+
key,
|
|
268
|
+
type,
|
|
269
|
+
entry: key,
|
|
270
|
+
relativePath,
|
|
271
|
+
...parseRecordComponentMetadata(object, relativePath, handlerName),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return Object.freeze({ key, type, entry: key, relativePath });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseWorkspacePage(sourceFile, relativePath, slug) {
|
|
278
|
+
requireSdkNamedImport(
|
|
279
|
+
sourceFile,
|
|
280
|
+
relativePath,
|
|
281
|
+
"@77-zhike/app-sdk/client",
|
|
282
|
+
"definePage",
|
|
283
|
+
);
|
|
284
|
+
const call = requireDefaultExportCall(sourceFile, relativePath, "definePage");
|
|
285
|
+
const object = requireObjectLiteral(call.arguments[0], relativePath);
|
|
286
|
+
assertOnlyProperties(
|
|
287
|
+
object,
|
|
288
|
+
["name", "order", "variant", "Page", "showInNavigation", "showHeader"],
|
|
289
|
+
relativePath,
|
|
290
|
+
"zhike_page_definition_invalid",
|
|
291
|
+
"Page definition 包含不支持的字段",
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const rawName = requireStringProperty(object, "name", relativePath);
|
|
295
|
+
const name = rawName.trim();
|
|
296
|
+
if (
|
|
297
|
+
[...name].length === 0 ||
|
|
298
|
+
[...name].length > PAGE_NAME_MAX_LENGTH ||
|
|
299
|
+
/\p{Cc}/u.test(name)
|
|
300
|
+
) {
|
|
301
|
+
throw toolchainError(
|
|
302
|
+
"zhike_page_name_invalid",
|
|
303
|
+
`${relativePath}: name trim 后必须是 1~${PAGE_NAME_MAX_LENGTH} 个非控制字符`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const variant = requireStringProperty(object, "variant", relativePath);
|
|
307
|
+
if (!PAGE_VARIANTS.has(variant)) {
|
|
308
|
+
throw toolchainError(
|
|
309
|
+
"zhike_page_variant_invalid",
|
|
310
|
+
`${relativePath}: variant 必须是 full 或 centered`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
const orderProperty = findProperty(object, "order");
|
|
314
|
+
const order = orderProperty === undefined
|
|
315
|
+
? undefined
|
|
316
|
+
: requireSafeIntegerProperty(orderProperty, relativePath);
|
|
317
|
+
const visibilityProperty = findProperty(object, "showInNavigation");
|
|
318
|
+
let showInNavigation;
|
|
319
|
+
if (visibilityProperty !== undefined) {
|
|
320
|
+
if (!ts.isPropertyAssignment(visibilityProperty)
|
|
321
|
+
|| ![ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword].includes(visibilityProperty.initializer.kind)) {
|
|
322
|
+
throw toolchainError("zhike_page_visibility_invalid", `${relativePath}: showInNavigation 必须是静态布尔值`);
|
|
323
|
+
}
|
|
324
|
+
showInNavigation = visibilityProperty.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
|
325
|
+
}
|
|
326
|
+
const headerProperty = findProperty(object, "showHeader");
|
|
327
|
+
let showHeader;
|
|
328
|
+
if (headerProperty !== undefined) {
|
|
329
|
+
if (!ts.isPropertyAssignment(headerProperty)
|
|
330
|
+
|| ![ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword].includes(headerProperty.initializer.kind)) {
|
|
331
|
+
throw toolchainError("zhike_page_header_invalid", `${relativePath}: showHeader 必须是静态布尔值`);
|
|
332
|
+
}
|
|
333
|
+
showHeader = headerProperty.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
|
334
|
+
}
|
|
335
|
+
const pageProperty = findProperty(object, "Page");
|
|
336
|
+
if (
|
|
337
|
+
!pageProperty ||
|
|
338
|
+
!ts.isPropertyAssignment(pageProperty) ||
|
|
339
|
+
!(ts.isIdentifier(pageProperty.initializer) || isFunctionLike(pageProperty.initializer))
|
|
340
|
+
) {
|
|
341
|
+
throw toolchainError(
|
|
342
|
+
"zhike_page_component_invalid",
|
|
343
|
+
`${relativePath}: Page 必须是 React component 标识符或函数`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return Object.freeze({
|
|
347
|
+
key: slug,
|
|
348
|
+
type: "page",
|
|
349
|
+
entry: slug,
|
|
350
|
+
relativePath,
|
|
351
|
+
name,
|
|
352
|
+
...(order === undefined ? {} : { order }),
|
|
353
|
+
...(showInNavigation === undefined ? {} : { showInNavigation }),
|
|
354
|
+
...(showHeader === undefined ? {} : { showHeader }),
|
|
355
|
+
variant,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function parseRecordActionMetadata(object, relativePath) {
|
|
360
|
+
assertOnlyProperties(object, ["key", "type", "label", "icon", "objects", "onTrigger"], relativePath);
|
|
361
|
+
const label = requireStringProperty(object, "label", relativePath).trim();
|
|
362
|
+
if (
|
|
363
|
+
[...label].length === 0 ||
|
|
364
|
+
[...label].length > RECORD_ACTION_LABEL_MAX_LENGTH ||
|
|
365
|
+
/\p{Cc}/u.test(label)
|
|
366
|
+
) {
|
|
367
|
+
throw toolchainError(
|
|
368
|
+
"zhike_record_action_label_invalid",
|
|
369
|
+
`${relativePath}: label 必须是 1~${RECORD_ACTION_LABEL_MAX_LENGTH} 个非控制字符`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const iconProperty = findProperty(object, "icon");
|
|
373
|
+
const icon = iconProperty ? requireStringProperty(object, "icon", relativePath) : null;
|
|
374
|
+
if (icon !== null && (!KEY_PATTERN.test(icon) || !SDK_ICON_NAMES.has(icon))) {
|
|
375
|
+
throw toolchainError(
|
|
376
|
+
"zhike_record_action_icon_invalid",
|
|
377
|
+
`${relativePath}: icon 必须来自 SDK 统一图标目录`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const objectsProperty = findProperty(object, "objects");
|
|
381
|
+
const objects = objectsProperty ? requireRecordObjects(objectsProperty, relativePath) : null;
|
|
382
|
+
return {
|
|
383
|
+
label,
|
|
384
|
+
...(icon === null ? {} : { icon }),
|
|
385
|
+
...(objects === null ? {} : { objects }),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function parseRecordComponentMetadata(object, relativePath, componentName) {
|
|
390
|
+
assertOnlyProperties(
|
|
391
|
+
object,
|
|
392
|
+
["key", "type", "label", "objects", componentName],
|
|
393
|
+
relativePath,
|
|
394
|
+
"zhike_record_component_definition_invalid",
|
|
395
|
+
"Record Page component definition 包含不支持的字段",
|
|
396
|
+
);
|
|
397
|
+
const label = requireStringProperty(object, "label", relativePath).trim();
|
|
398
|
+
if (
|
|
399
|
+
[...label].length === 0 ||
|
|
400
|
+
[...label].length > RECORD_ACTION_LABEL_MAX_LENGTH ||
|
|
401
|
+
/\p{Cc}/u.test(label)
|
|
402
|
+
) {
|
|
403
|
+
throw toolchainError(
|
|
404
|
+
"zhike_record_component_label_invalid",
|
|
405
|
+
`${relativePath}: label 必须是 1~${RECORD_ACTION_LABEL_MAX_LENGTH} 个非控制字符`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const objectsProperty = findProperty(object, "objects");
|
|
409
|
+
const objects = objectsProperty
|
|
410
|
+
? requireRecordObjects(
|
|
411
|
+
objectsProperty,
|
|
412
|
+
relativePath,
|
|
413
|
+
"zhike_record_component_objects_invalid",
|
|
414
|
+
true,
|
|
415
|
+
)
|
|
416
|
+
: null;
|
|
417
|
+
return {
|
|
418
|
+
label,
|
|
419
|
+
...(objects === null ? {} : { objects }),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function requireRecordObjects(
|
|
424
|
+
property,
|
|
425
|
+
relativePath,
|
|
426
|
+
errorCode = "zhike_record_action_objects_invalid",
|
|
427
|
+
rejectDuplicates = false,
|
|
428
|
+
) {
|
|
429
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
430
|
+
throw toolchainError(errorCode, `${relativePath}: objects 必须是字符串字面量或字符串字面量数组`);
|
|
431
|
+
}
|
|
432
|
+
const initializer = property.initializer;
|
|
433
|
+
const values = ts.isStringLiteral(initializer)
|
|
434
|
+
? [initializer.text]
|
|
435
|
+
: ts.isArrayLiteralExpression(initializer)
|
|
436
|
+
? initializer.elements.map(item => {
|
|
437
|
+
if (!ts.isStringLiteral(item)) {
|
|
438
|
+
throw toolchainError(errorCode, `${relativePath}: objects 必须是字符串字面量数组`);
|
|
439
|
+
}
|
|
440
|
+
return item.text;
|
|
441
|
+
})
|
|
442
|
+
: null;
|
|
443
|
+
if (
|
|
444
|
+
values === null ||
|
|
445
|
+
values.length === 0 ||
|
|
446
|
+
values.length > RECORD_ACTION_OBJECTS_MAX_LENGTH ||
|
|
447
|
+
(rejectDuplicates && new Set(values).size !== values.length) ||
|
|
448
|
+
values.some(value => !RECORD_OBJECT_PATTERN.test(value) || !RECORD_EXTENSION_OBJECT_PATTERN.test(value))
|
|
449
|
+
) {
|
|
450
|
+
throw toolchainError(
|
|
451
|
+
errorCode,
|
|
452
|
+
`${relativePath}: objects 只支持 accounts、contacts、opportunities 或 custom_*,且必须包含 1~${RECORD_ACTION_OBJECTS_MAX_LENGTH} 个合法公开对象标识`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
return Object.freeze(rejectDuplicates ? values : [...new Set(values)]);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function assertOnlyProperties(
|
|
459
|
+
object,
|
|
460
|
+
allowed,
|
|
461
|
+
relativePath,
|
|
462
|
+
code = "zhike_frontend_definition_unknown_property",
|
|
463
|
+
message = "Frontend Extension 包含不支持的字段",
|
|
464
|
+
) {
|
|
465
|
+
const allowedNames = new Set(allowed);
|
|
466
|
+
for (const property of object.properties) {
|
|
467
|
+
const name = property.name && (
|
|
468
|
+
ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)
|
|
469
|
+
) ? property.name.text : null;
|
|
470
|
+
if (!name || !allowedNames.has(name)) {
|
|
471
|
+
throw toolchainError(
|
|
472
|
+
code,
|
|
473
|
+
`${relativePath}: ${message}`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function parseServerFunction(sourceFile, relativePath, moduleId) {
|
|
480
|
+
if (hasSdkNamedImport(sourceFile, "@77-zhike/app-sdk/server", "defineServerFunction")) {
|
|
481
|
+
throw toolchainError(
|
|
482
|
+
"zhike_server_function_wrapper_deprecated",
|
|
483
|
+
`${relativePath}: defineServerFunction 已不再用于新 Build,请直接 export default async function`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
requireDefaultAsyncFunction(sourceFile, relativePath, {
|
|
487
|
+
errorCode: "zhike_server_function_export_invalid",
|
|
488
|
+
entryName: "Server Function",
|
|
489
|
+
});
|
|
490
|
+
if (!SERVER_MODULE_PATTERN.test(moduleId)) {
|
|
491
|
+
throw toolchainError(
|
|
492
|
+
"zhike_server_function_key_invalid",
|
|
493
|
+
`${relativePath}: Server Function module ID 不合法`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return Object.freeze({ key: moduleId, relativePath });
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function parseWebhookHandler(sourceFile, relativePath, fileKey) {
|
|
500
|
+
if (hasSdkNamedImport(sourceFile, "@77-zhike/app-sdk/server", "defineWebhookHandler")) {
|
|
501
|
+
throw toolchainError(
|
|
502
|
+
"zhike_webhook_handler_wrapper_deprecated",
|
|
503
|
+
`${relativePath}: defineWebhookHandler 已不再用于新 Build,请直接 default export async function`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
requireDefaultAsyncFunction(sourceFile, relativePath, {
|
|
507
|
+
errorCode: "zhike_webhook_handler_export_invalid",
|
|
508
|
+
entryName: "Webhook Handler",
|
|
509
|
+
});
|
|
510
|
+
return Object.freeze({
|
|
511
|
+
key: fileKey,
|
|
512
|
+
methods: Object.freeze(["POST"]),
|
|
513
|
+
relativePath,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function requireDefaultExportCall(sourceFile, relativePath, expectedCallee) {
|
|
518
|
+
const exports = sourceFile.statements.filter(
|
|
519
|
+
(statement) => ts.isExportAssignment(statement) && !statement.isExportEquals,
|
|
520
|
+
);
|
|
521
|
+
if (exports.length !== 1 || !ts.isCallExpression(exports[0].expression)) {
|
|
522
|
+
throw toolchainError(
|
|
523
|
+
"zhike_entry_default_export_invalid",
|
|
524
|
+
`${relativePath}: 必须有且只有一个直接 default export`,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
const call = exports[0].expression;
|
|
528
|
+
if (!ts.isIdentifier(call.expression) || call.expression.text !== expectedCallee) {
|
|
529
|
+
throw toolchainError(
|
|
530
|
+
"zhike_entry_definition_invalid",
|
|
531
|
+
`${relativePath}: default export 必须直接调用 ${expectedCallee}`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return call;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function requireSdkNamedImport(sourceFile, relativePath, specifier, importedName) {
|
|
538
|
+
const matches = sourceFile.statements.filter((statement) =>
|
|
539
|
+
hasNamedImport(statement, specifier, importedName),
|
|
540
|
+
);
|
|
541
|
+
if (matches.length !== 1) {
|
|
542
|
+
throw toolchainError(
|
|
543
|
+
"zhike_sdk_definition_import_invalid",
|
|
544
|
+
`${relativePath}: ${importedName} 必须从 ${specifier} 直接 named import且不得重命名`,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function hasSdkNamedImport(sourceFile, specifier, importedName) {
|
|
550
|
+
return sourceFile.statements.some((statement) =>
|
|
551
|
+
hasNamedImport(statement, specifier, importedName),
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function hasNamedImport(statement, specifier, importedName) {
|
|
556
|
+
if (
|
|
557
|
+
!ts.isImportDeclaration(statement) ||
|
|
558
|
+
statement.moduleSpecifier.text !== specifier ||
|
|
559
|
+
!statement.importClause?.namedBindings ||
|
|
560
|
+
!ts.isNamedImports(statement.importClause.namedBindings)
|
|
561
|
+
) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
return statement.importClause.namedBindings.elements.some(
|
|
565
|
+
(element) =>
|
|
566
|
+
(element.propertyName?.text || element.name.text) === importedName &&
|
|
567
|
+
element.name.text === importedName,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function validateImports(sourceFile, relativePath, options = {}) {
|
|
572
|
+
const allowsStandardFetch =
|
|
573
|
+
options.target === "server" || (!options.target && (
|
|
574
|
+
relativePath.startsWith("server/") ||
|
|
575
|
+
relativePath.startsWith("app/blocks/") ||
|
|
576
|
+
/^app\/events\/.+\.event\.ts$/.test(relativePath) ||
|
|
577
|
+
/^app\/webhooks\/.+\.webhook\.ts$/.test(relativePath)));
|
|
578
|
+
const target = allowsStandardFetch ? "server" : "client";
|
|
579
|
+
const visit = (node) => {
|
|
580
|
+
if (ts.isImportEqualsDeclaration(node) ||
|
|
581
|
+
(ts.isExportAssignment(node) && node.isExportEquals) ||
|
|
582
|
+
(ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require") ||
|
|
583
|
+
((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) &&
|
|
584
|
+
ts.isIdentifier(node.expression) && ["require", "module", "exports"].includes(node.expression.text))) {
|
|
585
|
+
throw toolchainError("zhike_commonjs_forbidden", relativePath + ": 只支持静态 ESM import/export,不支持 CommonJS");
|
|
586
|
+
}
|
|
587
|
+
if (
|
|
588
|
+
ts.isCallExpression(node) &&
|
|
589
|
+
node.expression.kind === ts.SyntaxKind.ImportKeyword
|
|
590
|
+
) {
|
|
591
|
+
throw toolchainError(
|
|
592
|
+
"zhike_dynamic_import_forbidden",
|
|
593
|
+
`${relativePath}: 一期禁止动态 import`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
if (
|
|
597
|
+
!allowsStandardFetch &&
|
|
598
|
+
ts.isCallExpression(node) &&
|
|
599
|
+
((ts.isIdentifier(node.expression) && node.expression.text === "fetch") ||
|
|
600
|
+
(ts.isPropertyAccessExpression(node.expression) &&
|
|
601
|
+
node.expression.name.text === "fetch" &&
|
|
602
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
603
|
+
["globalThis", "window", "self"].includes(
|
|
604
|
+
node.expression.expression.text,
|
|
605
|
+
)))
|
|
606
|
+
) {
|
|
607
|
+
throw toolchainError(
|
|
608
|
+
"zhike_network_egress_forbidden",
|
|
609
|
+
`${relativePath}: 前端网络能力必须由受控 Host capability 提供`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
if (
|
|
613
|
+
(ts.isNewExpression(node) &&
|
|
614
|
+
ts.isIdentifier(node.expression) &&
|
|
615
|
+
["EventSource", "WebSocket", "XMLHttpRequest"].includes(
|
|
616
|
+
node.expression.text,
|
|
617
|
+
)) ||
|
|
618
|
+
(ts.isCallExpression(node) &&
|
|
619
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
620
|
+
node.expression.name.text === "sendBeacon" &&
|
|
621
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
622
|
+
node.expression.expression.text === "navigator")
|
|
623
|
+
) {
|
|
624
|
+
throw toolchainError(
|
|
625
|
+
"zhike_network_egress_forbidden",
|
|
626
|
+
`${relativePath}: 一期禁止未托管的浏览器网络能力`,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
if (
|
|
630
|
+
(ts.isIdentifier(node) &&
|
|
631
|
+
["ZHIKE_CLIENT_SDK", "ZHIKE_SERVER_SDK"].includes(node.text)) ||
|
|
632
|
+
(ts.isPropertyAccessExpression(node) &&
|
|
633
|
+
["ZHIKE_CLIENT_SDK", "ZHIKE_SERVER_SDK"].includes(node.name.text))
|
|
634
|
+
) {
|
|
635
|
+
throw toolchainError(
|
|
636
|
+
"zhike_host_global_forbidden",
|
|
637
|
+
`${relativePath}: App 源码不得直接读取 Host SDK global`,
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
const moduleSpecifier = (ts.isImportDeclaration(node) || ts.isExportDeclaration(node))
|
|
641
|
+
? node.moduleSpecifier
|
|
642
|
+
: ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)
|
|
643
|
+
? node.argument.literal : null;
|
|
644
|
+
if (moduleSpecifier && ts.isStringLiteral(moduleSpecifier)) {
|
|
645
|
+
const specifier = moduleSpecifier.text;
|
|
646
|
+
if (specifier === "@77-zhike/app-sdk/server" && !allowsStandardFetch) {
|
|
647
|
+
throw toolchainError(
|
|
648
|
+
"zhike_server_sdk_client_forbidden",
|
|
649
|
+
`${relativePath}: 前端代码不得导入服务端 SDK`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
const clientTypeOnly = target === "server" && specifier === "@77-zhike/app-sdk/client"
|
|
653
|
+
&& (ts.isImportTypeNode(node) || node.isTypeOnly || (ts.isImportDeclaration(node) && node.importClause?.isTypeOnly));
|
|
654
|
+
if (!specifier.startsWith(".") && !clientTypeOnly) assertAllowedPackageImport(specifier, target, relativePath);
|
|
655
|
+
if (specifier.startsWith(".") && !options.dependency) {
|
|
656
|
+
const resolvedSourcePath = posix.normalize(
|
|
657
|
+
posix.join(posix.dirname(relativePath), specifier),
|
|
658
|
+
);
|
|
659
|
+
if (resolvedSourcePath === ".." || resolvedSourcePath.startsWith("../")) {
|
|
660
|
+
throw toolchainError(
|
|
661
|
+
"zhike_source_import_outside_root",
|
|
662
|
+
`${relativePath}: relative import 不得越过 src 根目录`,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (specifier.includes(".server.tsx") || specifier.endsWith(".server.tsx")) {
|
|
667
|
+
throw toolchainError(
|
|
668
|
+
"zhike_server_function_extension_invalid",
|
|
669
|
+
`${relativePath}: Server Function 不支持 .server.tsx`,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
ts.forEachChild(node, visit);
|
|
674
|
+
};
|
|
675
|
+
visit(sourceFile);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function rejectUnsupportedPaths(files) {
|
|
679
|
+
for (const file of files) {
|
|
680
|
+
const path = file.relativePath;
|
|
681
|
+
if (
|
|
682
|
+
path.startsWith("app/events/") &&
|
|
683
|
+
path.endsWith(".ts") &&
|
|
684
|
+
!/^app\/events\/(app-installed|app-uninstalled|connection-added|connection-removed)\.event\.ts$/.test(path)
|
|
685
|
+
) {
|
|
686
|
+
throw toolchainError(
|
|
687
|
+
"zhike_lifecycle_event_location_invalid",
|
|
688
|
+
`${path}: 生命周期事件文件名不受支持`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (path === "app.ts" || path === "app/index.ts") {
|
|
692
|
+
throw toolchainError(
|
|
693
|
+
"zhike_legacy_entry_forbidden",
|
|
694
|
+
`${path}: 一期不支持 Legacy app entry`,
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
if (/(^|\/)(workflows?|connections?)(\/|\.|$)/i.test(path)) {
|
|
698
|
+
throw toolchainError(
|
|
699
|
+
"zhike_capability_unsupported",
|
|
700
|
+
`${path}: 一期不支持 Workflow/Event/Connection/Settings`,
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
if (path.startsWith("server/steps/") || path.startsWith("server/triggers/")) {
|
|
704
|
+
throw toolchainError(
|
|
705
|
+
"zhike_workflow_block_location_invalid",
|
|
706
|
+
`${path}: Workflow Block 必须位于 src/app/blocks/<block-key>/`,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
if (path.startsWith("app/blocks/") && /\.(?:ts|tsx)$/.test(path)) {
|
|
710
|
+
const validBlockFile = /^app\/blocks\/[a-z][a-z0-9-]{0,63}\/(?:block|execute|activate|trigger|deactivate)\.ts$/.test(path);
|
|
711
|
+
const helperFile = /^app\/blocks\/[a-z][a-z0-9-]{0,63}\/[-a-zA-Z0-9._]+\.ts$/.test(path) &&
|
|
712
|
+
!/\/(?:block|execute|activate|trigger|deactivate)\./.test(path);
|
|
713
|
+
if (!validBlockFile && !helperFile) {
|
|
714
|
+
throw toolchainError(
|
|
715
|
+
"zhike_workflow_block_location_invalid",
|
|
716
|
+
`${path}: Block 必须位于 src/app/blocks/<block-key>/,固定入口只允许 .ts`,
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (path.endsWith(".server.tsx")) {
|
|
721
|
+
throw toolchainError(
|
|
722
|
+
"zhike_server_function_extension_invalid",
|
|
723
|
+
`${path}: Server Function 只允许 .server.ts`,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
if (
|
|
727
|
+
path.endsWith(".server.ts") &&
|
|
728
|
+
!/^server\/functions\/[a-z0-9][a-z0-9/-]*\.server\.ts$/.test(path)
|
|
729
|
+
) {
|
|
730
|
+
throw toolchainError(
|
|
731
|
+
"zhike_server_function_location_invalid",
|
|
732
|
+
`${path}: Server Function 必须位于 src/server/functions`,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if (
|
|
736
|
+
path.startsWith("app/extensions/") &&
|
|
737
|
+
path.endsWith(".tsx") &&
|
|
738
|
+
!/^app\/extensions\/[a-z][a-z0-9-]{0,63}\/extension\.tsx$/.test(path)
|
|
739
|
+
) {
|
|
740
|
+
throw toolchainError(
|
|
741
|
+
"zhike_frontend_extension_location_invalid",
|
|
742
|
+
`${path}: Frontend Extension 必须使用约定目录`,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
if (/^app\/pages\/[^/]+\/page\.tsx$/.test(path)) {
|
|
746
|
+
const slug = path.split("/")[2];
|
|
747
|
+
if (!KEY_PATTERN.test(slug)) {
|
|
748
|
+
throw toolchainError(
|
|
749
|
+
"zhike_page_slug_invalid",
|
|
750
|
+
`${path}: Page slug 必须是最多 64 字符的 lowercase kebab-case`,
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
} else if (path.startsWith("app/pages/") && /\/page\.(?:ts|tsx)$/.test(path)) {
|
|
754
|
+
throw toolchainError(
|
|
755
|
+
"zhike_page_location_invalid",
|
|
756
|
+
`${path}: Page 必须位于 src/app/pages/<slug>/page.tsx 一级目录`,
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
if (
|
|
760
|
+
path.startsWith("app/webhooks/") &&
|
|
761
|
+
path.endsWith(".ts") &&
|
|
762
|
+
!/^app\/webhooks\/[a-z][a-z0-9-]{0,63}\.webhook\.ts$/.test(path)
|
|
763
|
+
) {
|
|
764
|
+
throw toolchainError(
|
|
765
|
+
"zhike_webhook_location_invalid",
|
|
766
|
+
`${path}: Webhook Handler 必须使用 <key>.webhook.ts`,
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function requireObjectLiteral(node, relativePath) {
|
|
773
|
+
if (!node || !ts.isObjectLiteralExpression(node)) {
|
|
774
|
+
throw toolchainError(
|
|
775
|
+
"zhike_entry_definition_invalid",
|
|
776
|
+
`${relativePath}: definition 必须是可静态解析的对象字面量`,
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
return node;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function requireStringProperty(object, name, relativePath) {
|
|
783
|
+
const property = findProperty(object, name);
|
|
784
|
+
if (!property || !ts.isPropertyAssignment(property) || !ts.isStringLiteral(property.initializer)) {
|
|
785
|
+
throw toolchainError(
|
|
786
|
+
"zhike_entry_definition_invalid",
|
|
787
|
+
`${relativePath}: ${name} 必须是字符串字面量`,
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
return property.initializer.text;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function requireStringArrayProperty(object, name, relativePath) {
|
|
794
|
+
const property = findProperty(object, name);
|
|
795
|
+
if (!property || !ts.isPropertyAssignment(property) || !ts.isArrayLiteralExpression(property.initializer)) {
|
|
796
|
+
throw toolchainError(
|
|
797
|
+
"zhike_entry_definition_invalid",
|
|
798
|
+
`${relativePath}: ${name} 必须是字符串字面量数组`,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
return property.initializer.elements.map((item) => {
|
|
802
|
+
if (!ts.isStringLiteral(item)) {
|
|
803
|
+
throw toolchainError(
|
|
804
|
+
"zhike_entry_definition_invalid",
|
|
805
|
+
`${relativePath}: ${name} 必须是字符串字面量数组`,
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
return item.text.toUpperCase();
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function requireSafeIntegerProperty(property, relativePath) {
|
|
813
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
814
|
+
throw toolchainError(
|
|
815
|
+
"zhike_page_order_invalid",
|
|
816
|
+
`${relativePath}: order 必须是安全整数字面量`,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
const initializer = property.initializer;
|
|
820
|
+
const value = ts.isNumericLiteral(initializer)
|
|
821
|
+
? Number(initializer.text)
|
|
822
|
+
: ts.isPrefixUnaryExpression(initializer) &&
|
|
823
|
+
initializer.operator === ts.SyntaxKind.MinusToken &&
|
|
824
|
+
ts.isNumericLiteral(initializer.operand)
|
|
825
|
+
? -Number(initializer.operand.text)
|
|
826
|
+
: Number.NaN;
|
|
827
|
+
if (!Number.isSafeInteger(value)) {
|
|
828
|
+
throw toolchainError(
|
|
829
|
+
"zhike_page_order_invalid",
|
|
830
|
+
`${relativePath}: order 必须是安全整数字面量`,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
return value;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function requireFunctionProperty(object, name, relativePath, { requireAsync = false } = {}) {
|
|
837
|
+
const property = findProperty(object, name);
|
|
838
|
+
const functionNode =
|
|
839
|
+
property && ts.isMethodDeclaration(property)
|
|
840
|
+
? property
|
|
841
|
+
: property && ts.isPropertyAssignment(property) && isFunctionLike(property.initializer)
|
|
842
|
+
? property.initializer
|
|
843
|
+
: null;
|
|
844
|
+
if (
|
|
845
|
+
!functionNode ||
|
|
846
|
+
(requireAsync &&
|
|
847
|
+
!functionNode.modifiers?.some(
|
|
848
|
+
(modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword,
|
|
849
|
+
))
|
|
850
|
+
) {
|
|
851
|
+
throw toolchainError(
|
|
852
|
+
"zhike_entry_definition_invalid",
|
|
853
|
+
`${relativePath}: ${name} 必须是${requireAsync ? " async" : ""}函数`,
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
return functionNode;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function findProperty(object, name) {
|
|
860
|
+
return object.properties.find((property) => {
|
|
861
|
+
const propertyName = property.name;
|
|
862
|
+
return (
|
|
863
|
+
(ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName)) &&
|
|
864
|
+
propertyName.text === name
|
|
865
|
+
);
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function isFunctionLike(node) {
|
|
870
|
+
return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function registerUniqueKey(keys, key, relativePath) {
|
|
874
|
+
const existing = keys.get(key);
|
|
875
|
+
if (existing) {
|
|
876
|
+
throw toolchainError(
|
|
877
|
+
"zhike_definition_key_duplicate",
|
|
878
|
+
`definition key ${key} 同时出现在 ${existing} 和 ${relativePath}`,
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
keys.set(key, relativePath);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function assertSyntacticallyValid(sourceFile) {
|
|
885
|
+
if (sourceFile.parseDiagnostics.length === 0) return;
|
|
886
|
+
const diagnostic = sourceFile.parseDiagnostics[0];
|
|
887
|
+
throw toolchainError(
|
|
888
|
+
"zhike_source_syntax_invalid",
|
|
889
|
+
`${sourceFile.fileName}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function listFiles(root) {
|
|
894
|
+
let entries;
|
|
895
|
+
try {
|
|
896
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (error?.code === "ENOENT") return [];
|
|
899
|
+
throw error;
|
|
900
|
+
}
|
|
901
|
+
const result = [];
|
|
902
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
903
|
+
const path = resolve(root, entry.name);
|
|
904
|
+
if (entry.isDirectory()) {
|
|
905
|
+
result.push(...(await listFiles(path)));
|
|
906
|
+
} else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name)) {
|
|
907
|
+
result.push(path);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return result;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function sortByKey(items) {
|
|
914
|
+
return items.sort((left, right) => left.key.localeCompare(right.key));
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function toolchainError(code, message) {
|
|
918
|
+
return new ZhikeCliError(code, message);
|
|
919
|
+
}
|