@blocklet/pages-kit-block-studio 0.4.64 → 0.4.66

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.
@@ -0,0 +1,217 @@
1
+ import * as path from 'path';
2
+ import { Project, Node } from 'ts-morph';
3
+ /**
4
+ * 创建 ts-morph 项目
5
+ * @param tsConfigFilePath tsconfig.json 文件路径
6
+ */
7
+ export function createTsMorphProject(tsConfigFilePath = path.resolve(process.cwd(), 'tsconfig.json')) {
8
+ return new Project({
9
+ tsConfigFilePath,
10
+ });
11
+ }
12
+ /**
13
+ * 解析类型定义文件,提取指定类型及其依赖
14
+ * @param sourceFilePath 源文件路径
15
+ * @param typeName 要提取的类型名称
16
+ * @param tsConfigFilePath tsconfig.json 文件路径
17
+ */
18
+ export function extractTypeWithDependencies(sourceFilePath, typeName, tsConfigFilePath = path.resolve(process.cwd(), 'tsconfig.json')) {
19
+ // 创建项目
20
+ const project = createTsMorphProject(tsConfigFilePath);
21
+ // 添加源文件
22
+ const sourceFile = project.addSourceFileAtPath(sourceFilePath);
23
+ // 收集所有导入语句
24
+ const imports = sourceFile.getImportDeclarations().map((imp) => imp.getText());
25
+ // 查找指定接口或类型别名
26
+ const targetInterface = sourceFile.getInterface(typeName);
27
+ const targetTypeAlias = sourceFile.getTypeAlias(typeName);
28
+ // 存储所有收集到的类型定义
29
+ const dependencies = new Map();
30
+ if (targetInterface) {
31
+ // 如果是接口,获取其文本
32
+ const typeText = targetInterface.getText();
33
+ // 收集接口依赖
34
+ collectTypeDependencies(targetInterface, dependencies, project, imports);
35
+ return { typeText, dependencies, imports };
36
+ }
37
+ if (targetTypeAlias) {
38
+ // 如果是类型别名,获取其文本
39
+ const typeText = targetTypeAlias.getText();
40
+ // 收集类型别名依赖
41
+ collectTypeDependencies(targetTypeAlias, dependencies, project, imports);
42
+ return { typeText, dependencies, imports };
43
+ }
44
+ throw new Error(`Type '${typeName}' not found in ${sourceFilePath}`);
45
+ }
46
+ /**
47
+ * 收集类型依赖
48
+ * @param node 类型节点
49
+ * @param dependencies 依赖集合
50
+ * @param project ts-morph 项目
51
+ * @param imports 导入语句集合
52
+ */
53
+ function collectTypeDependencies(node, dependencies, project, imports) {
54
+ // 获取类型引用
55
+ const typeReferences = findTypeReferences(node);
56
+ // 处理每个类型引用
57
+ typeReferences.forEach((referenceName) => {
58
+ // 排除内置类型和已处理类型
59
+ if (isBuiltInType(referenceName) ||
60
+ dependencies.has(referenceName) ||
61
+ referenceName === node.getName() // 避免循环引用
62
+ ) {
63
+ return;
64
+ }
65
+ try {
66
+ // 在整个项目中查找类型定义
67
+ const { declaration, definitionText, sourceFileImports } = findTypeDefinitionInProject(referenceName, project);
68
+ if (declaration && definitionText) {
69
+ // 存储找到的类型定义
70
+ dependencies.set(referenceName, definitionText);
71
+ // 添加源文件的导入语句
72
+ if (sourceFileImports && sourceFileImports.length > 0) {
73
+ sourceFileImports.forEach((importText) => {
74
+ if (!imports.includes(importText)) {
75
+ imports.push(importText);
76
+ }
77
+ });
78
+ }
79
+ // 递归收集该类型的依赖
80
+ if (Node.isInterfaceDeclaration(declaration) || Node.isTypeAliasDeclaration(declaration)) {
81
+ collectTypeDependencies(declaration, dependencies, project, imports);
82
+ }
83
+ }
84
+ }
85
+ catch (error) {
86
+ console.warn(`Could not find definition for type: ${referenceName}`, error);
87
+ }
88
+ });
89
+ }
90
+ /**
91
+ * 从节点中查找所有类型引用
92
+ * @param node 类型节点
93
+ */
94
+ function findTypeReferences(node) {
95
+ const references = [];
96
+ // 递归查找所有类型引用
97
+ function findReferences(currentNode) {
98
+ if (Node.isTypeReference(currentNode)) {
99
+ const typeName = currentNode.getTypeName().getText();
100
+ if (!references.includes(typeName)) {
101
+ references.push(typeName);
102
+ }
103
+ }
104
+ currentNode.forEachChild(findReferences);
105
+ }
106
+ findReferences(node);
107
+ return references;
108
+ }
109
+ /**
110
+ * 判断是否为内置类型
111
+ * @param typeName 类型名称
112
+ */
113
+ function isBuiltInType(typeName) {
114
+ const builtInTypes = [
115
+ 'String',
116
+ 'Number',
117
+ 'Boolean',
118
+ 'Array',
119
+ 'Object',
120
+ 'Date',
121
+ 'RegExp',
122
+ 'Function',
123
+ 'Promise',
124
+ 'any',
125
+ 'unknown',
126
+ 'never',
127
+ 'void',
128
+ 'string',
129
+ 'number',
130
+ 'boolean',
131
+ 'symbol',
132
+ 'bigint',
133
+ 'undefined',
134
+ 'null',
135
+ 'Record',
136
+ 'Partial',
137
+ 'Required',
138
+ 'Readonly',
139
+ 'Pick',
140
+ 'Omit',
141
+ 'Exclude',
142
+ 'Extract',
143
+ 'NonNullable',
144
+ 'Parameters',
145
+ 'ConstructorParameters',
146
+ 'ReturnType',
147
+ 'InstanceType',
148
+ 'React',
149
+ 'ReactNode',
150
+ 'JSX',
151
+ ];
152
+ return builtInTypes.includes(typeName) || typeName.startsWith('React.');
153
+ }
154
+ /**
155
+ * 在项目中查找类型定义
156
+ * @param typeName 类型名称
157
+ * @param project ts-morph 项目
158
+ */
159
+ function findTypeDefinitionInProject(typeName, project) {
160
+ // 在所有源文件中查找
161
+ for (const sourceFile of project.getSourceFiles()) {
162
+ // 尝试查找接口定义
163
+ const interfaceDecl = sourceFile.getInterface(typeName);
164
+ if (interfaceDecl) {
165
+ const imports = sourceFile.getImportDeclarations().map((imp) => imp.getText());
166
+ return {
167
+ declaration: interfaceDecl,
168
+ definitionText: interfaceDecl.getText(),
169
+ sourceFileImports: imports,
170
+ };
171
+ }
172
+ // 尝试查找类型别名定义
173
+ const typeAliasDecl = sourceFile.getTypeAlias(typeName);
174
+ if (typeAliasDecl) {
175
+ const imports = sourceFile.getImportDeclarations().map((imp) => imp.getText());
176
+ return {
177
+ declaration: typeAliasDecl,
178
+ definitionText: typeAliasDecl.getText(),
179
+ sourceFileImports: imports,
180
+ };
181
+ }
182
+ // 尝试查找枚举定义
183
+ const enumDecl = sourceFile.getEnum(typeName);
184
+ if (enumDecl) {
185
+ const imports = sourceFile.getImportDeclarations().map((imp) => imp.getText());
186
+ return {
187
+ declaration: enumDecl,
188
+ definitionText: enumDecl.getText(),
189
+ sourceFileImports: imports,
190
+ };
191
+ }
192
+ }
193
+ return { declaration: undefined, definitionText: undefined };
194
+ }
195
+ /**
196
+ * 组合类型及其依赖为完整的类型定义文本
197
+ * @param typeText 主类型文本
198
+ * @param dependencies 依赖类型集合
199
+ * @param imports 导入语句集合
200
+ */
201
+ export function combineTypeDefinitions(typeText, dependencies, imports = []) {
202
+ let combinedText = '';
203
+ // 首先添加所有导入语句
204
+ if (imports.length > 0) {
205
+ imports.forEach((importText) => {
206
+ combinedText += `${importText}\n`;
207
+ });
208
+ combinedText += '\n';
209
+ }
210
+ // 添加所有依赖类型
211
+ dependencies.forEach((defText) => {
212
+ combinedText += `${defText}\n\n`;
213
+ });
214
+ // 添加主类型
215
+ combinedText += typeText;
216
+ return combinedText;
217
+ }
@@ -0,0 +1,269 @@
1
+ import config from '@blocklet/sdk/lib/config';
2
+ import { promises as fs } from 'fs';
3
+ import lowerFirst from 'lodash/lowerFirst';
4
+ import { nanoid } from 'nanoid';
5
+ import * as path from 'path';
6
+ import { generate } from 'ts-to-zod';
7
+ import { z } from 'zod';
8
+ import { zodToJsonSchema } from 'zod-to-json-schema';
9
+ import { zodToTs, printNode } from 'zod-to-ts';
10
+ import { NANOID_LENGTH } from './helper';
11
+ import { extractTypeWithDependencies, combineTypeDefinitions } from './ts-morph-utils';
12
+ const PROPERTY_PREFIX = '@description';
13
+ const JOIN_SYMBOL = ' | ';
14
+ // 转换为: type: string | visible: boolean
15
+ const stringifyZodDescribe = (properties, extra = {}) => {
16
+ return `${PROPERTY_PREFIX} ${Object.entries({
17
+ id: properties.id ?? '',
18
+ type: properties.type ?? 'string',
19
+ visible: properties.visible ?? true,
20
+ ...extra,
21
+ })
22
+ .map(([key, value]) => {
23
+ return `${key}: ${value}`;
24
+ })
25
+ .join(JOIN_SYMBOL)}`;
26
+ };
27
+ const parseZodDescribe = (describe) => {
28
+ const describeList = describe.split(JOIN_SYMBOL);
29
+ const result = {};
30
+ describeList.forEach((item) => {
31
+ const [key, value] = item.split(':');
32
+ const realKey = key.trim();
33
+ const realValue = value.trim();
34
+ if (['true', 'false'].includes(realValue)) {
35
+ result[realKey] = realValue === 'true';
36
+ }
37
+ else {
38
+ result[realKey] = realValue;
39
+ }
40
+ });
41
+ return result;
42
+ };
43
+ const getJSONSchemaDescribe = (jsonSchema) => {
44
+ let result = {};
45
+ if (jsonSchema.description) {
46
+ result = parseZodDescribe(jsonSchema.description);
47
+ }
48
+ return result;
49
+ };
50
+ /**
51
+ * Properties 类型与 Schema 类型的映射关系
52
+ * blocklets/pages-kit/src/pages/page-maker/custom-component/setting.tsx
53
+ */
54
+ export const PROPERTIES_TYPE_SCHEMA = {
55
+ // Properties 类型到 Zod schema 的映射
56
+ propertyToZod: {
57
+ string: () => z.string(),
58
+ multiline: () => z.string(),
59
+ number: () => z.number(),
60
+ decimal: () => z.number(),
61
+ boolean: () => z.boolean(),
62
+ color: () => z.string(),
63
+ url: () => z.object({
64
+ url: z.string(),
65
+ mediaKitUrl: z.string().optional(),
66
+ width: z.number().optional(),
67
+ height: z.number().optional(),
68
+ }),
69
+ json: (properties, { addZodDescribe, propLLMConfig }) => {
70
+ const { subProperties } = properties;
71
+ if (subProperties && Object.keys(subProperties)?.length > 0) {
72
+ // 递归处理子属性
73
+ return propertiesToZodSchema(subProperties, { addZodDescribe, llmConfig: propLLMConfig?.subProperties });
74
+ }
75
+ return z.object({});
76
+ },
77
+ yaml: (properties, { addZodDescribe, propLLMConfig }) => {
78
+ const { subProperties } = properties;
79
+ if (subProperties && Object.keys(subProperties)?.length > 0) {
80
+ // 使用联合类型:字符串或结构化对象
81
+ return z.union([
82
+ z.string(),
83
+ propertiesToZodSchema(subProperties, { addZodDescribe, llmConfig: propLLMConfig?.subProperties }),
84
+ ]);
85
+ }
86
+ return z.string();
87
+ },
88
+ array: (properties, { addZodDescribe, propLLMConfig }) => {
89
+ const { subProperties } = properties;
90
+ if (subProperties && Object.keys(subProperties)?.length > 0) {
91
+ // 递归处理子属性
92
+ return z.array(propertiesToZodSchema(subProperties, { addZodDescribe, llmConfig: propLLMConfig?.subProperties }));
93
+ }
94
+ return z.array(z.any());
95
+ },
96
+ component: () => z.any(),
97
+ custom: () => z.any(),
98
+ default: () => z.string(),
99
+ },
100
+ };
101
+ /**
102
+ * 从 Properties 对象创建 Zod schema
103
+ */
104
+ export function propertiesToZodSchema(properties, { addZodDescribe = false,
105
+ // 这个目前来说都是可选的,避免用户在使用的过程中疯狂报错,并且目前 setting 中没有必填标识符
106
+ isOptional = true, llmConfig, } = {}) {
107
+ const schemaObj = {};
108
+ Object.entries(properties || {}).forEach(([key, prop]) => {
109
+ if (!prop.data)
110
+ return;
111
+ // 如果key未定义,使用id
112
+ const propKey = prop.data.key || prop.data.id || key;
113
+ // prop llmConfig
114
+ const propLLMConfig = llmConfig?.[prop.data.id];
115
+ // 是否不需要 LLM 处理
116
+ if (propLLMConfig && !propLLMConfig.isNeedGenerate) {
117
+ return;
118
+ }
119
+ const propType = prop.data.type;
120
+ const getSchemaFn = (propType in PROPERTIES_TYPE_SCHEMA.propertyToZod
121
+ ? PROPERTIES_TYPE_SCHEMA.propertyToZod[propType]
122
+ : PROPERTIES_TYPE_SCHEMA.propertyToZod.default);
123
+ schemaObj[propKey] = getSchemaFn(prop.data, { addZodDescribe, propLLMConfig });
124
+ if (isOptional) {
125
+ schemaObj[propKey] = schemaObj[propKey].optional();
126
+ }
127
+ let propDescribe = '';
128
+ if (addZodDescribe) {
129
+ propDescribe = stringifyZodDescribe(prop.data, propLLMConfig?.describe ? { llmDescribe: propLLMConfig?.describe } : {});
130
+ }
131
+ else if (propLLMConfig?.describe) {
132
+ propDescribe = propLLMConfig?.describe;
133
+ }
134
+ if (propDescribe) {
135
+ schemaObj[propKey] = schemaObj[propKey].describe(propDescribe);
136
+ }
137
+ });
138
+ return z.object(schemaObj);
139
+ }
140
+ /**
141
+ * 从JSON Schema生成Properties对象
142
+ */
143
+ export function jsonSchemaToProperties(jsonSchema, { existingProperties, key, isRoot, index = 0, } = {}) {
144
+ let properties = {};
145
+ let loopProperties = {};
146
+ if (jsonSchema.type === 'array') {
147
+ loopProperties = jsonSchema.items?.properties;
148
+ }
149
+ else if (jsonSchema.type === 'object') {
150
+ loopProperties = jsonSchema.properties;
151
+ }
152
+ const schemaDescribe = getJSONSchemaDescribe(jsonSchema);
153
+ const { type, visible } = schemaDescribe;
154
+ const id = schemaDescribe.id || nanoid(NANOID_LENGTH);
155
+ properties[id] = {
156
+ index,
157
+ data: {
158
+ id,
159
+ key,
160
+ type,
161
+ visible,
162
+ locales: (id && existingProperties?.[id]?.data?.locales) || {
163
+ en: { name: key },
164
+ zh: { name: key },
165
+ },
166
+ },
167
+ };
168
+ if (loopProperties && Object.keys(loopProperties)?.length > 0) {
169
+ let subIndex = 0;
170
+ properties[id].data.subProperties = {};
171
+ // get sub existing properties
172
+ const subExistingProperties = existingProperties?.[id]?.data?.subProperties ?? existingProperties;
173
+ Object.entries(loopProperties).forEach(([key, item]) => {
174
+ properties[id].data.subProperties = {
175
+ ...properties[id].data.subProperties,
176
+ ...jsonSchemaToProperties(item, { existingProperties: subExistingProperties, key, index: subIndex++ }),
177
+ };
178
+ });
179
+ }
180
+ if (isRoot) {
181
+ properties = properties[id].data.subProperties;
182
+ }
183
+ return properties;
184
+ }
185
+ /**
186
+ * 从Zod schema生成TypeScript类型字符串
187
+ */
188
+ export function zodSchemaToTypeString(schema, typeName, removeUndefined = true) {
189
+ const { node } = zodToTs(schema, typeName);
190
+ let output = printNode(node);
191
+ if (removeUndefined) {
192
+ // 移除所有的 "| undefined"
193
+ output = output.replace(/\s+\|\s+undefined/g, '');
194
+ }
195
+ return output;
196
+ }
197
+ /**
198
+ * 从Zod schema生成JSON Schema
199
+ */
200
+ export function zodSchemaToJsonSchema(schema) {
201
+ return zodToJsonSchema(schema, { $refStrategy: 'none' });
202
+ }
203
+ /**
204
+ * 从TypeScript文件中的类型声明生成Zod schema
205
+ * 支持处理外部引用类型
206
+ * @param sourceFilePath 源文件路径
207
+ * @param typeName 类型名称
208
+ * @param tsConfigFilePath tsconfig.json 路径,默认为项目根目录下的tsconfig.json
209
+ */
210
+ export async function tsFileToZodSchema(sourceFilePath, typeName) {
211
+ try {
212
+ const { typeText, dependencies, imports } = extractTypeWithDependencies(sourceFilePath, typeName);
213
+ // set imports to empty array
214
+ const combinedTypeDefinition = combineTypeDefinitions(typeText, dependencies, []);
215
+ // 使用 ts-to-zod 生成代码,需要手动添加 imports 和 combinedTypeDefinition,因为 zod-to-ts 对 import 处理存在问题
216
+ const result = generate({
217
+ sourceText: combinedTypeDefinition,
218
+ keepComments: true,
219
+ });
220
+ // 提取生成的 Zod schema 代码
221
+ const zodSchemaText = result.getZodSchemasFile('zod');
222
+ // 创建临时目录
223
+ const tmpDir = path.resolve(config.env.dataDir, 'temp');
224
+ try {
225
+ await fs.stat(tmpDir);
226
+ }
227
+ catch (error) {
228
+ // 如果目录不存在,stat 会抛出错误
229
+ await fs.mkdir(tmpDir, { recursive: true });
230
+ }
231
+ const fileName = `zodSchema_${typeName}_${nanoid(NANOID_LENGTH)}.ts`;
232
+ const filePath = path.join(tmpDir, fileName);
233
+ // 添加 ts-to-zod 的 imports 和 zodSchemaText
234
+ const zodSchemaTextWithImports = `
235
+ ${imports.join('\n')}
236
+
237
+ ${zodSchemaText}
238
+ `;
239
+ // 写入临时文件
240
+ await fs.writeFile(filePath, zodSchemaTextWithImports);
241
+ // 动态导入生成的模块
242
+ const module = await import(/* @vite-ignore */ `file://${filePath}`);
243
+ // 首字母转换为小写
244
+ const schemaName = `${lowerFirst(typeName)}Schema`;
245
+ // 获取 schema
246
+ const schema = module[schemaName];
247
+ // 清理临时文件
248
+ await fs.unlink(filePath);
249
+ return schema;
250
+ }
251
+ catch (error) {
252
+ console.error('Failed to generate Zod schema from TypeScript file:', error);
253
+ console.error('Error details:', error);
254
+ return z.object({}).passthrough();
255
+ }
256
+ }
257
+ /**
258
+ * 从TypeScript文件中的类型生成Properties对象
259
+ * 支持处理外部引用类型
260
+ * @param sourceFilePath 源文件路径
261
+ * @param typeName 类型名称
262
+ * @param existingProperties 已存在的properties对象
263
+ */
264
+ export async function tsFileInterfaceToProperties(sourceFilePath, typeName, existingProperties = {}) {
265
+ const zodSchema = await tsFileToZodSchema(sourceFilePath, typeName);
266
+ const jsonSchema = zodSchemaToJsonSchema(zodSchema);
267
+ // get jsonSchemaToProperties and set isRoot to true
268
+ return jsonSchemaToProperties(jsonSchema, { existingProperties, key: '', isRoot: true });
269
+ }
@@ -1,13 +1,20 @@
1
1
  import React from 'react';
2
- export interface HelloWorldProps {
2
+ export interface BlockProps {
3
+ /** @description id: gs1rn5jmxfvpxptx | type: string | visible: true */
3
4
  title?: string;
4
- logo?: string | {
5
+ /** @description id: 9ajrz12ik7esfk1z | type: string | visible: true */
6
+ description?: string;
7
+ /** @description id: 3ckcfvf6b7zyskk8 | type: url | visible: true */
8
+ logo?: {
5
9
  url: string;
10
+ mediaKitUrl?: string;
11
+ width?: number;
12
+ height?: number;
6
13
  };
7
- description?: string;
14
+ /** @description id: x3lqht8ikble1itx | type: string | visible: false */
8
15
  copyright?: string;
9
16
  }
10
- export default function HelloWorld({ title, logo, description, copyright }: HelloWorldProps): import("react/jsx-runtime").JSX.Element;
11
- export declare const EditComponent: React.FC<HelloWorldProps & {
12
- onChange?: (value: HelloWorldProps) => void;
17
+ export default function HelloWorld({ title, logo, description, copyright }: BlockProps): import("react/jsx-runtime").JSX.Element;
18
+ export declare const EditComponent: React.FC<BlockProps & {
19
+ onChange?: (value: BlockProps) => void;
13
20
  }>;
@@ -1 +1,5 @@
1
- export default function Theme(): import("react/jsx-runtime").JSX.Element;
1
+ declare function LayoutWrapper({ loadState, loadedData, ...rest }: {
2
+ loadState: any;
3
+ loadedData: any;
4
+ }): import("react/jsx-runtime").JSX.Element;
5
+ export default LayoutWrapper;