@blocklet/pages-kit-block-studio 0.5.40 → 0.5.42

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.
@@ -15,6 +15,52 @@ import { tsFileToZodSchema } from '../utils/ts-morph-utils';
15
15
  import { propertiesToZodSchema, zodSchemaToTypeString, zodSchemaToJsonSchema, jsonSchemaToProperties, } from '../utils/zod-utils';
16
16
  export const initBlockStudioRouter = Router();
17
17
  const BINARY_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg'];
18
+ // 递归删除目录
19
+ const removeDirectoryRecursive = (dirPath) => {
20
+ if (!fs.existsSync(dirPath)) {
21
+ return;
22
+ }
23
+ const stats = fs.statSync(dirPath);
24
+ if (stats.isDirectory()) {
25
+ const files = fs.readdirSync(dirPath);
26
+ for (const file of files) {
27
+ const filePath = path.join(dirPath, file);
28
+ const fileStats = fs.statSync(filePath);
29
+ if (fileStats.isDirectory()) {
30
+ removeDirectoryRecursive(filePath);
31
+ }
32
+ else {
33
+ fs.unlinkSync(filePath);
34
+ }
35
+ }
36
+ fs.rmdirSync(dirPath);
37
+ }
38
+ else {
39
+ fs.unlinkSync(dirPath);
40
+ }
41
+ };
42
+ // 根据路由查找对应的组件目录路径
43
+ const findComponentDirByRoute = async (route) => {
44
+ try {
45
+ // Remove leading slash from route to get component name
46
+ const componentName = route.replace(/^\//, '');
47
+ // Find all component files
48
+ const allComponents = await findComponentFiles({
49
+ strictExistMetadata: true,
50
+ });
51
+ // Find the component that matches the block name
52
+ const matchedComponent = allComponents.find((component) => component.blockName === componentName);
53
+ if (matchedComponent) {
54
+ // Return the directory path (parent of the index file)
55
+ return path.dirname(matchedComponent.fullPath);
56
+ }
57
+ return null;
58
+ }
59
+ catch (error) {
60
+ console.error('Error finding component directory:', error);
61
+ return null;
62
+ }
63
+ };
18
64
  // 共享的请求验证和文件路径处理
19
65
  const validateRequest = (req, res) => {
20
66
  if (!isDev) {
@@ -92,6 +138,10 @@ initBlockStudioRouter.post('/', async (req, res) => {
92
138
  if (!currentMetadata) {
93
139
  return res.status(404).json({ error: 'Metadata file not found' });
94
140
  }
141
+ // 核对 id 是否一致,不一致可能是手动修改过
142
+ if (currentMetadata.id !== content.id) {
143
+ return res.status(400).json({ error: 'ID is not consistent, please refresh the page' });
144
+ }
95
145
  const mergedContent = { ...currentMetadata, ...content };
96
146
  if (isEqual(currentMetadata, mergedContent)) {
97
147
  return res.json({ success: true, content: mergedContent, message: 'No changes' });
@@ -349,4 +399,74 @@ initBlockStudioRouter.post('/interface-to-properties', async (req, res) => {
349
399
  });
350
400
  }
351
401
  });
402
+ // 添加删除组件的路由
403
+ initBlockStudioRouter.delete('/delete', async (req, res) => {
404
+ if (!isDev) {
405
+ return res.status(403).json({ error: 'Only available in development mode' });
406
+ }
407
+ const { route } = req.body;
408
+ if (!route) {
409
+ return res.status(400).json({ error: 'Route is required' });
410
+ }
411
+ try {
412
+ // Find the component directory path
413
+ const componentDir = await findComponentDirByRoute(route);
414
+ if (!componentDir) {
415
+ return res.status(404).json({
416
+ success: false,
417
+ error: 'Component directory not found',
418
+ });
419
+ }
420
+ // Security check: ensure the path is safe and within expected bounds
421
+ if (!isPathSafe(componentDir)) {
422
+ return res.status(403).json({
423
+ success: false,
424
+ error: 'Invalid component directory path',
425
+ });
426
+ }
427
+ // Additional safety check: ensure it contains @metadata.json to confirm it's a component
428
+ const metadataPath = path.join(componentDir, METADATA_FILE_NAME);
429
+ if (!fs.existsSync(metadataPath)) {
430
+ return res.status(400).json({
431
+ success: false,
432
+ error: 'Invalid component directory: no metadata file found',
433
+ });
434
+ }
435
+ // Verify that this directory is indeed for component development
436
+ // Check if it's in the expected component structure
437
+ const pattern = getBlockEntryFilesPattern();
438
+ const baseDir = path.dirname(pattern.replace(/\*\*?/g, ''));
439
+ const relativePath = path.relative(baseDir, componentDir);
440
+ // Ensure the component is within the base directory and not trying to escape
441
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
442
+ return res.status(403).json({
443
+ success: false,
444
+ error: 'Component directory is outside allowed path',
445
+ });
446
+ }
447
+ // Perform the deletion
448
+ try {
449
+ removeDirectoryRecursive(componentDir);
450
+ return res.json({
451
+ success: true,
452
+ message: 'Component deleted successfully',
453
+ deletedPath: componentDir,
454
+ });
455
+ }
456
+ catch (deleteError) {
457
+ console.error('Failed to delete component directory:', deleteError);
458
+ return res.status(500).json({
459
+ success: false,
460
+ error: `Failed to delete component directory: ${deleteError.message}`,
461
+ });
462
+ }
463
+ }
464
+ catch (error) {
465
+ console.error('Failed to delete component:', error);
466
+ return res.status(500).json({
467
+ success: false,
468
+ error: `Failed to delete component: ${error.message}`,
469
+ });
470
+ }
471
+ });
352
472
  export default initBlockStudioRouter;
@@ -1,10 +1,11 @@
1
1
  import { getResourceExportDir } from '@blocklet/sdk/lib/component';
2
+ import { getMediaKitFileStream } from '@blocklet/uploader-server';
2
3
  import { spawn } from 'child_process';
3
4
  import { Router } from 'express';
4
5
  import fs from 'fs';
5
6
  import get from 'lodash/get';
6
7
  import set from 'lodash/set';
7
- import path, { join } from 'path';
8
+ import path, { join, basename } from 'path';
8
9
  import { findComponentFiles, libDir, getPreviewImageRelativePath, generateYaml, logger, getEditComponentBlockName, } from '../utils/helper';
9
10
  const DID = 'z2qa7BQdkEb3TwYyEYC1psK6uvmGnHSUHt5RM';
10
11
  const RESOURCE_TYPE = 'page';
@@ -139,7 +140,7 @@ initResourceRouter.post('/', async (req, res) => {
139
140
  // get @metadata.json by glob
140
141
  const canUseComponents = findComponentFiles({ cwd: rootDir, filter: componentIds, strictExistMetadata: true });
141
142
  // Filter and process metadata files
142
- const metadataList = canUseComponents.map(({ fullPath, blockName, metadata: _metadata }) => {
143
+ const metadataList = await Promise.all(canUseComponents.map(async ({ fullPath, blockName, metadata: _metadata }) => {
143
144
  // get metadata
144
145
  const metadata = _metadata;
145
146
  const jsName = `${blockName}.js`;
@@ -164,9 +165,6 @@ initResourceRouter.post('/', async (req, res) => {
164
165
  catch (error) {
165
166
  // ignore error
166
167
  }
167
- // write metadata to metadataPath
168
- const metadataYmlPath = path.join(componentsDir, `${metadata.name || 'unnamed'}.${metadata.id}.yml`);
169
- fs.writeFileSync(metadataYmlPath, generateYaml(metadata));
170
168
  // Handle preview image if exists
171
169
  if (metadata.previewImage) {
172
170
  const imagePath = path.join(path.dirname(fullPath), getPreviewImageRelativePath(metadata.previewImage));
@@ -175,8 +173,59 @@ initResourceRouter.post('/', async (req, res) => {
175
173
  fs.copyFileSync(imagePath, imageDestPath);
176
174
  }
177
175
  }
176
+ // Handle type:url properties url
177
+ if (metadata.properties) {
178
+ const downloadPromises = [];
179
+ Object.keys(metadata.properties).forEach((key) => {
180
+ const property = metadata.properties[key].data;
181
+ if (property.type === 'url') {
182
+ // 如果每个 locales 的默认值里面,存在 mediaKitUrl 的值,需要把图片下载放到 imagePath 里面
183
+ Object.keys(property.locales).forEach((locale) => {
184
+ if (property.locales[locale].defaultValue?.mediaKitUrl?.startsWith('mediakit://')) {
185
+ const downloadPromise = (async () => {
186
+ try {
187
+ const fileName = basename(property.locales[locale].defaultValue.mediaKitUrl);
188
+ const targetPath = path.join(componentsDir, fileName);
189
+ // Check if file already exists to avoid redundant downloads
190
+ if (fs.existsSync(targetPath)) {
191
+ return;
192
+ }
193
+ const resWithStream = await getMediaKitFileStream(property.locales[locale].defaultValue.mediaKitUrl);
194
+ if (!resWithStream.data) {
195
+ return;
196
+ }
197
+ // create write stream
198
+ const writeStream = fs.createWriteStream(targetPath);
199
+ resWithStream.data.pipe(writeStream);
200
+ // wait for write stream to finish
201
+ await new Promise((resolve, reject) => {
202
+ writeStream.on('finish', () => {
203
+ // 修改 property.locales[locale].defaultValue.url 变为 mediakit:// 开头
204
+ property.locales[locale].defaultValue.url = property.locales[locale].defaultValue.mediaKitUrl;
205
+ resolve();
206
+ });
207
+ writeStream.on('error', reject);
208
+ });
209
+ }
210
+ catch (error) {
211
+ console.error('Download media kit file failed:', error.message);
212
+ }
213
+ })();
214
+ downloadPromises.push(downloadPromise);
215
+ }
216
+ });
217
+ }
218
+ });
219
+ // Wait for all downloads to complete for this metadata
220
+ if (downloadPromises.length > 0) {
221
+ await Promise.allSettled(downloadPromises);
222
+ }
223
+ }
224
+ // write metadata to metadataPath
225
+ const metadataYmlPath = path.join(componentsDir, `${metadata.name || 'unnamed'}.${metadata.id}.yml`);
226
+ fs.writeFileSync(metadataYmlPath, generateYaml(metadata));
178
227
  return metadata;
179
- });
228
+ }));
180
229
  // cp chunks dir
181
230
  await copyRecursive(join(codeDir, 'chunks'), chunksDir);
182
231
  // write pages.config.yml