feat: 完成 Web2App 核心功能实现
- Electron 主进程:窗口管理、IPC、项目读写、打包引擎 - Vue 3 前端:4 个视图页面、3 个组件、Pinia 状态管理 - 生成应用模板:Express 后端 + Electron 窗口 - 支持资源导入、应用配置、跨平台打包
This commit is contained in:
231
electron/builder.js
Normal file
231
electron/builder.js
Normal file
@@ -0,0 +1,231 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync, spawn } = require('child_process')
|
||||
const { getConfigPath, getSrcDir } = require('./project')
|
||||
|
||||
let isCancelled = false
|
||||
|
||||
function cancelPack() {
|
||||
isCancelled = true
|
||||
}
|
||||
|
||||
async function startPack(projectPath, outputPath, onProgress) {
|
||||
isCancelled = false
|
||||
const config = JSON.parse(fs.readFileSync(getConfigPath(projectPath), 'utf-8'))
|
||||
const srcDir = getSrcDir(projectPath)
|
||||
const tempDir = path.join(projectPath, '.web2app', 'temp-build')
|
||||
|
||||
onProgress({ stage: 'prepare', message: '准备打包环境...', percent: 5 })
|
||||
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
if (isCancelled) return { success: false, cancelled: true }
|
||||
onProgress({ stage: 'prepare', message: '复制模板文件...', percent: 10 })
|
||||
|
||||
copyTemplateSync(tempDir)
|
||||
|
||||
if (isCancelled) return { success: false, cancelled: true }
|
||||
onProgress({ stage: 'copy-files', message: '复制前端资源...', percent: 20 })
|
||||
|
||||
const frontendDest = path.join(tempDir, 'frontend')
|
||||
fs.mkdirSync(frontendDest, { recursive: true })
|
||||
copyDirContentsSync(srcDir, frontendDest)
|
||||
|
||||
if (isCancelled) return { success: false, cancelled: true }
|
||||
onProgress({ stage: 'configure', message: '配置应用信息...', percent: 35 })
|
||||
|
||||
const templatePackagePath = path.join(tempDir, 'package.json')
|
||||
const templatePackage = JSON.parse(fs.readFileSync(templatePackagePath, 'utf-8'))
|
||||
templatePackage.name = config.name.toLowerCase().replace(/\s+/g, '-')
|
||||
templatePackage.productName = config.name
|
||||
templatePackage.version = config.version
|
||||
templatePackage.description = config.description || `Generated by Web2App - ${config.name}`
|
||||
templatePackage.author = config.author || ''
|
||||
templatePackage.build = {
|
||||
appId: `com.web2app.${templatePackage.name}`,
|
||||
productName: config.name,
|
||||
artifactName: `${config.name}-Setup-\${version}.\${ext}`,
|
||||
directories: {
|
||||
output: 'dist'
|
||||
},
|
||||
files: [
|
||||
'main.js',
|
||||
'preload.js',
|
||||
'frontend/**/*',
|
||||
'backend/**/*',
|
||||
'node_modules/**/*'
|
||||
],
|
||||
extraResources: config.icon ? [
|
||||
{
|
||||
from: path.join(frontendDest, config.icon),
|
||||
to: 'icon.png'
|
||||
}
|
||||
] : [],
|
||||
win: {
|
||||
target: [
|
||||
{
|
||||
target: 'nsis',
|
||||
arch: ['x64']
|
||||
}
|
||||
],
|
||||
icon: config.icon ? path.join(frontendDest, config.icon) : undefined
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
perMachine: false,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
createDesktopShortcut: true,
|
||||
shortcutName: config.name
|
||||
},
|
||||
mac: {
|
||||
target: ['dmg'],
|
||||
icon: config.icon ? path.join(frontendDest, config.icon) : undefined
|
||||
},
|
||||
linux: {
|
||||
target: ['AppImage'],
|
||||
icon: config.icon ? path.join(frontendDest, config.icon) : undefined
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(templatePackagePath, JSON.stringify(templatePackage, null, 2))
|
||||
|
||||
if (isCancelled) return { success: false, cancelled: true }
|
||||
onProgress({ stage: 'install', message: '安装依赖 (首次较慢)...', percent: 50 })
|
||||
|
||||
try {
|
||||
execSync('npm.cmd install --production', {
|
||||
cwd: tempDir,
|
||||
stdio: 'pipe',
|
||||
timeout: 300000
|
||||
})
|
||||
} catch (err) {
|
||||
onProgress({ stage: 'install', message: `依赖安装失败: ${err.message}`, percent: 50 })
|
||||
return { success: false, error: err.message }
|
||||
}
|
||||
|
||||
if (isCancelled) return { success: false, cancelled: true }
|
||||
onProgress({ stage: 'build', message: '正在打包生成安装包...', percent: 65 })
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const builderPath = path.join(tempDir, 'node_modules', '.bin', 'electron-builder')
|
||||
const builderCmd = process.platform === 'win32' ? `${builderPath}.cmd` : builderPath
|
||||
|
||||
const builderProcess = spawn(builderCmd, ['--config', templatePackagePath], {
|
||||
cwd: tempDir,
|
||||
shell: true,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
})
|
||||
|
||||
let buildOutput = ''
|
||||
|
||||
builderProcess.stdout.on('data', (data) => {
|
||||
buildOutput += data.toString()
|
||||
const msg = data.toString()
|
||||
|
||||
if (msg.includes('packaging')) {
|
||||
onProgress({ stage: 'build', message: '正在打包...', percent: 75 })
|
||||
} else if (msg.includes('signing')) {
|
||||
onProgress({ stage: 'sign', message: '正在签名...', percent: 85 })
|
||||
} else {
|
||||
onProgress({ stage: 'build', message: msg.trim(), percent: 75 })
|
||||
}
|
||||
})
|
||||
|
||||
builderProcess.stderr.on('data', (data) => {
|
||||
buildOutput += data.toString()
|
||||
})
|
||||
|
||||
builderProcess.on('close', (code) => {
|
||||
if (isCancelled) {
|
||||
resolve({ success: false, cancelled: true })
|
||||
return
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
const errMsg = buildOutput.split('\n').filter(l => l.includes('error')).join('\n') || '打包失败'
|
||||
onProgress({ stage: 'error', message: errMsg, percent: 0 })
|
||||
resolve({ success: false, error: errMsg, output: buildOutput })
|
||||
return
|
||||
}
|
||||
|
||||
onProgress({ stage: 'complete', message: '打包完成!', percent: 100 })
|
||||
|
||||
const distDir = path.join(tempDir, 'dist')
|
||||
let installerPath = null
|
||||
if (fs.existsSync(distDir)) {
|
||||
const files = fs.readdirSync(distDir)
|
||||
.filter(f => f.endsWith('.exe') || f.endsWith('.dmg') || f.endsWith('.AppImage'))
|
||||
.sort((a, b) => fs.statSync(path.join(distDir, b)).mtimeMs - fs.statSync(path.join(distDir, a)).mtimeMs)
|
||||
if (files.length > 0) {
|
||||
installerPath = path.join(distDir, files[0])
|
||||
if (outputPath) {
|
||||
try {
|
||||
fs.copyFileSync(installerPath, outputPath)
|
||||
installerPath = outputPath
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve({ success: true, outputPath: installerPath })
|
||||
})
|
||||
|
||||
builderProcess.on('error', (err) => {
|
||||
resolve({ success: false, error: err.message })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function copyTemplateSync(destDir) {
|
||||
const templateDir = path.join(__dirname, '..', 'template')
|
||||
const entries = fs.readdirSync(templateDir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith('.')) continue
|
||||
const srcPath = path.join(templateDir, entry.name)
|
||||
const destPath = path.join(destDir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
copyDirSync(srcPath, destPath)
|
||||
} else {
|
||||
fs.mkdirSync(path.dirname(destPath), { recursive: true })
|
||||
fs.copyFileSync(srcPath, destPath)
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.join(destDir, 'frontend'), { recursive: true })
|
||||
fs.mkdirSync(path.join(destDir, 'backend'), { recursive: true })
|
||||
}
|
||||
|
||||
function copyDirSync(src, dest) {
|
||||
fs.mkdirSync(dest, { recursive: true })
|
||||
const entries = fs.readdirSync(src, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith('.')) continue
|
||||
const srcPath = path.join(src, entry.name)
|
||||
const destPath = path.join(dest, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
copyDirSync(srcPath, destPath)
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyDirContentsSync(src, dest) {
|
||||
if (!fs.existsSync(src)) return
|
||||
fs.mkdirSync(dest, { recursive: true })
|
||||
const entries = fs.readdirSync(src, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith('.')) continue
|
||||
const srcPath = path.join(src, entry.name)
|
||||
const destPath = path.join(dest, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
copyDirSync(srcPath, destPath)
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { startPack, cancelPack }
|
||||
163
electron/main.js
Normal file
163
electron/main.js
Normal file
@@ -0,0 +1,163 @@
|
||||
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { createProject, loadProject, saveProject, importFiles, removeFile, setProjectIcon } = require('./project')
|
||||
const { startPack, cancelPack } = require('./builder')
|
||||
|
||||
let mainWindow
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
minWidth: 900,
|
||||
minHeight: 600,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false
|
||||
},
|
||||
title: 'Web2App',
|
||||
show: false
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV === 'development' || process.argv.includes('--dev')) {
|
||||
mainWindow.loadURL('http://localhost:5173')
|
||||
mainWindow.webContents.openDevTools()
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html'))
|
||||
}
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
}
|
||||
|
||||
ipcMain.handle('dialog:selectDirectory', async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openDirectory']
|
||||
})
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
return result.filePaths[0]
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
ipcMain.handle('dialog:selectFile', async (_event, options) => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: options?.filters || [
|
||||
{ name: '所有文件', extensions: ['*'] }
|
||||
]
|
||||
})
|
||||
if (!result.canceled) {
|
||||
return result.filePaths
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
ipcMain.handle('dialog:selectIcon', async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: '图标文件', extensions: ['ico', 'png', 'icns'] }
|
||||
]
|
||||
})
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
return result.filePaths[0]
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
ipcMain.handle('dialog:saveFile', async (_event, options) => {
|
||||
const result = await dialog.showSaveDialog(mainWindow, {
|
||||
defaultPath: options?.defaultPath,
|
||||
filters: options?.filters || [{ name: '安装包', extensions: ['exe', 'dmg', 'AppImage'] }]
|
||||
})
|
||||
if (!result.canceled && result.filePath) {
|
||||
return result.filePath
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
ipcMain.handle('dir:readTree', async (_event, dirPath) => {
|
||||
return readDirectoryTree(dirPath)
|
||||
})
|
||||
|
||||
ipcMain.handle('project:create', async (_event, name, dir) => {
|
||||
return createProject(name, dir)
|
||||
})
|
||||
|
||||
ipcMain.handle('project:load', async (_event, projectPath) => {
|
||||
return loadProject(projectPath)
|
||||
})
|
||||
|
||||
ipcMain.handle('project:save', async (_event, projectPath, data) => {
|
||||
return saveProject(projectPath, data)
|
||||
})
|
||||
|
||||
ipcMain.handle('project:importFiles', async (_event, projectPath, files) => {
|
||||
return importFiles(projectPath, files)
|
||||
})
|
||||
|
||||
ipcMain.handle('project:removeFile', async (_event, projectPath, relativePath) => {
|
||||
return removeFile(projectPath, relativePath)
|
||||
})
|
||||
|
||||
ipcMain.handle('project:setIcon', async (_event, projectPath, iconPath) => {
|
||||
return setProjectIcon(projectPath, iconPath)
|
||||
})
|
||||
|
||||
ipcMain.handle('pack:start', async (_event, projectPath, outputPath) => {
|
||||
const result = await startPack(projectPath, outputPath, (progress) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('pack:progress', progress)
|
||||
}
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
ipcMain.handle('pack:cancel', () => {
|
||||
cancelPack()
|
||||
return true
|
||||
})
|
||||
|
||||
function readDirectoryTree(dirPath) {
|
||||
const items = fs.readdirSync(dirPath, { withFileTypes: true })
|
||||
const result = []
|
||||
for (const item of items) {
|
||||
if (item.name.startsWith('.')) continue
|
||||
const fullPath = path.join(dirPath, item.name)
|
||||
if (item.isDirectory()) {
|
||||
result.push({
|
||||
name: item.name,
|
||||
path: fullPath,
|
||||
type: 'directory',
|
||||
children: readDirectoryTree(fullPath)
|
||||
})
|
||||
} else {
|
||||
result.push({
|
||||
name: item.name,
|
||||
path: fullPath,
|
||||
type: 'file',
|
||||
size: fs.statSync(fullPath).size,
|
||||
ext: path.extname(item.name).toLowerCase()
|
||||
})
|
||||
}
|
||||
}
|
||||
return result.sort((a, b) => {
|
||||
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
30
electron/preload.js
Normal file
30
electron/preload.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
|
||||
contextBridge.exposeInMainWorld('api', {
|
||||
dialog: {
|
||||
selectDirectory: () => ipcRenderer.invoke('dialog:selectDirectory'),
|
||||
selectFile: (options) => ipcRenderer.invoke('dialog:selectFile', options),
|
||||
selectIcon: () => ipcRenderer.invoke('dialog:selectIcon'),
|
||||
saveFile: (options) => ipcRenderer.invoke('dialog:saveFile', options)
|
||||
},
|
||||
dir: {
|
||||
readTree: (dirPath) => ipcRenderer.invoke('dir:readTree', dirPath)
|
||||
},
|
||||
project: {
|
||||
create: (name, dir) => ipcRenderer.invoke('project:create', name, dir),
|
||||
load: (projectPath) => ipcRenderer.invoke('project:load', projectPath),
|
||||
save: (projectPath, data) => ipcRenderer.invoke('project:save', projectPath, data),
|
||||
importFiles: (projectPath, files) => ipcRenderer.invoke('project:importFiles', projectPath, files),
|
||||
removeFile: (projectPath, relativePath) => ipcRenderer.invoke('project:removeFile', projectPath, relativePath),
|
||||
setIcon: (projectPath, iconPath) => ipcRenderer.invoke('project:setIcon', projectPath, iconPath)
|
||||
},
|
||||
pack: {
|
||||
start: (projectPath, outputPath) => ipcRenderer.invoke('pack:start', projectPath, outputPath),
|
||||
cancel: () => ipcRenderer.invoke('pack:cancel'),
|
||||
onProgress: (callback) => {
|
||||
const handler = (_event, progress) => callback(progress)
|
||||
ipcRenderer.on('pack:progress', handler)
|
||||
return () => ipcRenderer.removeListener('pack:progress', handler)
|
||||
}
|
||||
}
|
||||
})
|
||||
186
electron/project.js
Normal file
186
electron/project.js
Normal file
@@ -0,0 +1,186 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
function getProjectDir(projectPath) {
|
||||
return projectPath
|
||||
}
|
||||
|
||||
function getSrcDir(projectPath) {
|
||||
const dir = path.join(projectPath, '.web2app', 'src')
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
return dir
|
||||
}
|
||||
|
||||
function getConfigPath(projectPath) {
|
||||
return path.join(projectPath, '.web2app', 'project.json')
|
||||
}
|
||||
|
||||
function getDistDir(projectPath) {
|
||||
const dir = path.join(projectPath, '.web2app', 'dist')
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
return dir
|
||||
}
|
||||
|
||||
function createProject(name, dir) {
|
||||
const projectPath = path.join(dir, name)
|
||||
if (fs.existsSync(projectPath)) {
|
||||
throw new Error(`目录 ${projectPath} 已存在`)
|
||||
}
|
||||
fs.mkdirSync(path.join(projectPath, '.web2app', 'src'), { recursive: true })
|
||||
fs.mkdirSync(path.join(projectPath, '.web2app', 'dist'), { recursive: true })
|
||||
|
||||
const config = {
|
||||
name,
|
||||
version: '1.0.0',
|
||||
description: '',
|
||||
author: '',
|
||||
entry: 'index.html',
|
||||
backendEntry: '',
|
||||
windowWidth: 1200,
|
||||
windowHeight: 800,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
resizable: true,
|
||||
icon: '',
|
||||
copyright: '',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
fs.writeFileSync(getConfigPath(projectPath), JSON.stringify(config, null, 2))
|
||||
return { projectPath, config }
|
||||
}
|
||||
|
||||
function loadProject(projectPath) {
|
||||
const configPath = getConfigPath(projectPath)
|
||||
if (!fs.existsSync(configPath)) {
|
||||
throw new Error('不是有效的 Web2App 项目')
|
||||
}
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
||||
|
||||
const srcDir = getSrcDir(projectPath)
|
||||
const files = readFileTree(srcDir)
|
||||
|
||||
return { projectPath, config, files }
|
||||
}
|
||||
|
||||
function saveProject(projectPath, data) {
|
||||
const configPath = getConfigPath(projectPath)
|
||||
const existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
||||
const updated = { ...existing, ...data, updatedAt: new Date().toISOString() }
|
||||
fs.writeFileSync(configPath, JSON.stringify(updated, null, 2))
|
||||
return updated
|
||||
}
|
||||
|
||||
function importFiles(projectPath, files) {
|
||||
const srcDir = getSrcDir(projectPath)
|
||||
const imported = []
|
||||
|
||||
for (const filePath of files) {
|
||||
const stat = fs.statSync(filePath)
|
||||
if (stat.isDirectory()) {
|
||||
const destDir = path.join(srcDir, path.basename(filePath))
|
||||
copyDirSync(filePath, destDir)
|
||||
imported.push({ source: filePath, dest: destDir })
|
||||
} else {
|
||||
const destPath = path.join(srcDir, path.basename(filePath))
|
||||
fs.copyFileSync(filePath, destPath)
|
||||
imported.push({ source: filePath, dest: destPath })
|
||||
}
|
||||
}
|
||||
|
||||
return readFileTree(srcDir)
|
||||
}
|
||||
|
||||
function removeFile(projectPath, relativePath) {
|
||||
const srcDir = getSrcDir(projectPath)
|
||||
const fullPath = path.join(srcDir, relativePath)
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
throw new Error(`文件不存在: ${relativePath}`)
|
||||
}
|
||||
|
||||
const stat = fs.statSync(fullPath)
|
||||
if (stat.isDirectory()) {
|
||||
fs.rmSync(fullPath, { recursive: true, force: true })
|
||||
} else {
|
||||
fs.unlinkSync(fullPath)
|
||||
}
|
||||
|
||||
return readFileTree(srcDir)
|
||||
}
|
||||
|
||||
function setProjectIcon(projectPath, iconPath) {
|
||||
const srcDir = getSrcDir(projectPath)
|
||||
const ext = path.extname(iconPath)
|
||||
const destName = `app-icon${ext}`
|
||||
const destPath = path.join(srcDir, destName)
|
||||
fs.copyFileSync(iconPath, destPath)
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(getConfigPath(projectPath), 'utf-8'))
|
||||
config.icon = destName
|
||||
fs.writeFileSync(getConfigPath(projectPath), JSON.stringify(config, null, 2))
|
||||
return config
|
||||
}
|
||||
|
||||
function readFileTree(dirPath, basePath = '') {
|
||||
const items = []
|
||||
if (!fs.existsSync(dirPath)) return items
|
||||
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith('.')) continue
|
||||
const relativePath = basePath ? path.join(basePath, entry.name) : entry.name
|
||||
const fullPath = path.join(dirPath, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
items.push({
|
||||
name: entry.name,
|
||||
path: relativePath,
|
||||
type: 'directory',
|
||||
children: readFileTree(fullPath, relativePath)
|
||||
})
|
||||
} else {
|
||||
items.push({
|
||||
name: entry.name,
|
||||
path: relativePath,
|
||||
type: 'file',
|
||||
size: fs.statSync(fullPath).size,
|
||||
ext: path.extname(entry.name).toLowerCase()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
items.sort((a, b) => {
|
||||
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
function copyDirSync(src, dest) {
|
||||
fs.mkdirSync(dest, { recursive: true })
|
||||
const entries = fs.readdirSync(src, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith('.')) continue
|
||||
const srcPath = path.join(src, entry.name)
|
||||
const destPath = path.join(dest, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
copyDirSync(srcPath, destPath)
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createProject,
|
||||
loadProject,
|
||||
saveProject,
|
||||
importFiles,
|
||||
removeFile,
|
||||
setProjectIcon,
|
||||
getSrcDir,
|
||||
getDistDir,
|
||||
getConfigPath,
|
||||
getProjectDir
|
||||
}
|
||||
Reference in New Issue
Block a user