mirror of
https://github.com/raven-pensieve/obsidian-ace-code-editor.git
synced 2026-07-22 05:48:56 +00:00
chore: 重构脚本工具链,使用pnpm (#101)
移除 copy-to-vault.mjs 脚本,简化项目构建流程。 更新 version-bump.mjs 使用 node: 前缀导入,并 修复 Beta 版本发布时的 versions.json 更新逻辑。 规范化 sync-i18n.mjs 代码风格,统一使用单引号 和标准缩进格式,提升代码可维护性。
This commit is contained in:
parent
5e790eae6e
commit
ffd29e7ee2
10 changed files with 5972 additions and 8479 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -29,4 +29,5 @@ Thumbs.db
|
|||
*.bat
|
||||
*.zip
|
||||
.env
|
||||
dist
|
||||
dist
|
||||
.backup/
|
||||
8040
package-lock.json
generated
8040
package-lock.json
generated
File diff suppressed because it is too large
Load diff
51
package.json
51
package.json
|
|
@ -4,14 +4,13 @@
|
|||
"description": "An enhanced code editor using Ace editor, providing syntax highlighting, code folding, and other advanced editing features.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node scripts/esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node scripts/esbuild.config.mjs production",
|
||||
"build:local": "tsc -noEmit -skipLibCheck && node scripts/esbuild.config.mjs production && node scripts/copy-to-vault.mjs",
|
||||
"version": "npm run build && node scripts/version-bump.mjs && npm run changelog:u && npm i",
|
||||
"dev": "node scripts/deploy.mjs link && node scripts/esbuild.config.mjs dev",
|
||||
"build": "tsc -noEmit -skipLibCheck && node scripts/esbuild.config.mjs prod && node scripts/deploy.mjs copy",
|
||||
"version": "pnpm run build && node scripts/version-bump.mjs && pnpm run changelog:u && pnpm i",
|
||||
"changelog:u": "conventional-changelog -p angular -i CHANGELOG.md -s -u -n ./scripts/changelog-option.js",
|
||||
"changelog:all": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 -n ./scripts/changelog-option.js",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"i18n:typesafe": "typesafe-i18n",
|
||||
"i18n:sync": "node scripts/sync-i18n.mjs"
|
||||
},
|
||||
|
|
@ -25,37 +24,39 @@
|
|||
"engines": {
|
||||
"node": ">=18.x"
|
||||
},
|
||||
"overrides": {
|
||||
"@conventional-changelog/git-client": "^2.0.0"
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@conventional-changelog/git-client": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.0.2",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/node": "^25.3.3",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "^8.49.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"builtin-modules": "5.0.0",
|
||||
"conventional-changelog-cli": "^5.0.0",
|
||||
"esbuild": "0.27.1",
|
||||
"eslint": "^9.39.2",
|
||||
"esbuild": "0.27.3",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-plugin-obsidianmd": "^0.1.9",
|
||||
"fs-extra": "^11.3.2",
|
||||
"obsidian": "^1.11.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-nesting": "^13.0.2",
|
||||
"fs-extra": "^11.3.4",
|
||||
"obsidian": "^1.12.3",
|
||||
"postcss": "^8.5.8",
|
||||
"postcss-nesting": "^14.0.0",
|
||||
"tslib": "2.8.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/ace": "^0.0.52",
|
||||
"ace-builds": "^1.43.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"html-react-parser": "^5.2.10",
|
||||
"lucide-react": "^0.561.0",
|
||||
"ace-builds": "^1.43.6",
|
||||
"dotenv": "^17.3.1",
|
||||
"html-react-parser": "^5.2.17",
|
||||
"lucide-react": "^0.577.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.3",
|
||||
"react": "^19.2.4",
|
||||
"react-ace": "^14.0.1",
|
||||
"react-dom": "^19.2.3",
|
||||
"typesafe-i18n": "^5.26.2"
|
||||
"react-dom": "^19.2.4",
|
||||
"typesafe-i18n": "^5.27.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5432
pnpm-lock.yaml
Normal file
5432
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,88 +0,0 @@
|
|||
import dotenv from "dotenv";
|
||||
import { existsSync } from "fs";
|
||||
import { copyFile, mkdir, readFile } from "fs/promises";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(__dirname, "..");
|
||||
|
||||
dotenv.config({ quiet: true });
|
||||
const VAULT_PATH = process.env.VAULT_PATH;
|
||||
if (!VAULT_PATH) {
|
||||
throw new Error(
|
||||
"VAULT_PATH is not defined. Please create a .env file in the project root and add the line: VAULT_PATH=/path/to/your/vault"
|
||||
);
|
||||
}
|
||||
|
||||
const fileConfig = [
|
||||
{
|
||||
name: "manifest.json",
|
||||
sourcePath: projectRoot,
|
||||
},
|
||||
{
|
||||
name: "main.js",
|
||||
sourcePath: projectRoot,
|
||||
},
|
||||
{
|
||||
name: "styles.css",
|
||||
sourcePath: projectRoot,
|
||||
},
|
||||
];
|
||||
|
||||
async function copyToVault() {
|
||||
try {
|
||||
// 获取 manifest.json 配置,从 fileConfig 中找到对应文件
|
||||
const manifestConfig = fileConfig.find(
|
||||
(file) => file.name === "manifest.json"
|
||||
);
|
||||
if (!manifestConfig) {
|
||||
throw new Error("manifest.json not found in fileConfig");
|
||||
}
|
||||
|
||||
const manifestPath = join(
|
||||
manifestConfig.sourcePath,
|
||||
manifestConfig.name
|
||||
);
|
||||
const manifestContent = await readFile(manifestPath, "utf8");
|
||||
const manifest = JSON.parse(manifestContent);
|
||||
const pluginId = manifest.id;
|
||||
|
||||
if (!pluginId) {
|
||||
throw new Error("Plugin ID not found in manifest.json");
|
||||
}
|
||||
|
||||
const targetPluginDir = join(
|
||||
VAULT_PATH,
|
||||
".obsidian",
|
||||
"plugins",
|
||||
pluginId
|
||||
);
|
||||
|
||||
if (!existsSync(targetPluginDir)) {
|
||||
await mkdir(targetPluginDir, { recursive: true });
|
||||
console.log(`创建目录: ${targetPluginDir}`);
|
||||
}
|
||||
|
||||
// 复制文件
|
||||
for (const fileInfo of fileConfig) {
|
||||
const sourcePath = join(fileInfo.sourcePath, fileInfo.name);
|
||||
const destPath = join(targetPluginDir, fileInfo.name);
|
||||
|
||||
if (!existsSync(sourcePath)) {
|
||||
console.warn(`警告: 源文件不存在 ${sourcePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await copyFile(sourcePath, destPath);
|
||||
// console.log(`复制文件: ${fileInfo.name} -> ${destPath}`);
|
||||
}
|
||||
|
||||
console.log(`插件 ${pluginId} 已成功复制到 vault`);
|
||||
} catch (error) {
|
||||
console.error("复制文件时出错:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
copyToVault();
|
||||
304
scripts/deploy.mjs
Normal file
304
scripts/deploy.mjs
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
// scripts/deploy.mjs
|
||||
// 统一的部署脚本:支持多种部署模式
|
||||
// 用法: node scripts/deploy.mjs [link|copy] [--vault-path=/path/to/vault]
|
||||
|
||||
import dotenv from "dotenv";
|
||||
import fs from "fs-extra";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.resolve(__dirname, "..");
|
||||
const distDir = path.join(projectRoot, "dist");
|
||||
const manifestPath = path.join(projectRoot, "manifest.json");
|
||||
const envPath = path.join(projectRoot, ".env");
|
||||
const backupDir = path.join(projectRoot, ".backup");
|
||||
|
||||
// ==================== 参数解析 ====================
|
||||
const args = process.argv.slice(2);
|
||||
const deployMode = args[0] || "link"; // link | copy
|
||||
const customVaultPath = args
|
||||
.find((arg) => arg.startsWith("--vault-path="))
|
||||
?.split("=")[1];
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
const log = {
|
||||
success: (msg) => console.log(`✅ ${msg}`),
|
||||
error: (msg) => console.error(`❌ ${msg}`),
|
||||
info: (msg) => console.log(`ℹ️ ${msg}`),
|
||||
warn: (msg) => console.warn(`⚠️ ${msg}`),
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查并加载环境配置
|
||||
*/
|
||||
function loadVaultPath() {
|
||||
if (customVaultPath) {
|
||||
return path.resolve(customVaultPath);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(envPath)) {
|
||||
log.warn(".env 文件不存在,跳过部署");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
dotenv.config({ path: envPath });
|
||||
const vaultPath = process.env.VAULT_PATH;
|
||||
|
||||
if (!vaultPath) {
|
||||
log.error(
|
||||
"未设置 VAULT_PATH,请在 .env 文件中设置或使用 --vault-path 参数",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return path.resolve(vaultPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取插件ID
|
||||
*/
|
||||
function getPluginId() {
|
||||
// 优先从 dist/manifest.json 读取(构建后)
|
||||
const distManifestPath = path.join(distDir, "manifest.json");
|
||||
const sourceManifestPath = manifestPath;
|
||||
|
||||
const manifestFile = fs.existsSync(distManifestPath)
|
||||
? distManifestPath
|
||||
: sourceManifestPath;
|
||||
|
||||
if (!fs.existsSync(manifestFile)) {
|
||||
log.error("找不到 manifest.json 文件");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const manifest = fs.readJsonSync(manifestFile);
|
||||
if (!manifest.id) {
|
||||
throw new Error("manifest.json 中没有 id 字段");
|
||||
}
|
||||
return manifest.id;
|
||||
} catch (error) {
|
||||
log.error(`读取 manifest.json 失败: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保 dist 目录和必要文件存在
|
||||
*/
|
||||
function ensureDistReady() {
|
||||
if (!fs.existsSync(distDir)) {
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 确保 .hotreload 文件存在(用于 Obsidian 热重载)
|
||||
const hotreloadPath = path.join(distDir, ".hotreload");
|
||||
if (!fs.existsSync(hotreloadPath)) {
|
||||
fs.writeFileSync(hotreloadPath, "");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 备份 data.json 到独立备份目录
|
||||
*/
|
||||
function backupDataJson(targetPluginDir) {
|
||||
const dataJsonPath = path.join(targetPluginDir, "data.json");
|
||||
if (fs.existsSync(dataJsonPath)) {
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
const backupPath = path.join(backupDir, "data.json");
|
||||
fs.copyFileSync(dataJsonPath, backupPath);
|
||||
log.info("已备份 data.json 到 .backup/");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归复制目录
|
||||
*/
|
||||
function copyDirRecursive(src, dest) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
|
||||
const entries = fs.readdirSync(src, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
copyDirRecursive(srcPath, destPath);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 部署模式 ====================
|
||||
|
||||
/**
|
||||
* 模式1: link - 创建软链接(开发模式)
|
||||
* Windows 使用 junction,Linux/Mac 使用 symlink
|
||||
*/
|
||||
async function deployLink(vaultPath, pluginId) {
|
||||
const targetPluginDir = path.join(
|
||||
vaultPath,
|
||||
".obsidian",
|
||||
"plugins",
|
||||
pluginId,
|
||||
);
|
||||
|
||||
// 检查是否指向同一位置(避免循环链接)
|
||||
if (path.resolve(targetPluginDir) === path.resolve(distDir)) {
|
||||
log.info("目标路径与 dist 目录相同,跳过链接");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 处理已存在的目标路径
|
||||
if (fs.existsSync(targetPluginDir)) {
|
||||
const stats = fs.lstatSync(targetPluginDir);
|
||||
|
||||
if (stats.isSymbolicLink()) {
|
||||
const linkTarget = fs.readlinkSync(targetPluginDir);
|
||||
const resolvedLink = path.resolve(
|
||||
path.dirname(targetPluginDir),
|
||||
linkTarget,
|
||||
);
|
||||
|
||||
if (resolvedLink === path.resolve(distDir)) {
|
||||
log.success(`软链接已存在: dist → ${pluginId}`);
|
||||
return;
|
||||
}
|
||||
} else if (stats.isDirectory()) {
|
||||
// 备份 data.json 到独立备份目录
|
||||
backupDataJson(targetPluginDir);
|
||||
}
|
||||
|
||||
// 删除旧的路径
|
||||
fs.rmSync(targetPluginDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// 确保父目录存在
|
||||
fs.mkdirSync(path.dirname(targetPluginDir), { recursive: true });
|
||||
|
||||
// 创建软链接
|
||||
try {
|
||||
const linkType = process.platform === "win32" ? "junction" : "dir";
|
||||
fs.symlinkSync(distDir, targetPluginDir, linkType);
|
||||
log.success(`软链接已创建: dist → ${pluginId}`);
|
||||
|
||||
// 自动从备份恢复 data.json 到 dist/
|
||||
const backupPath = path.join(backupDir, "data.json");
|
||||
if (fs.existsSync(backupPath)) {
|
||||
const distDataJsonPath = path.join(distDir, "data.json");
|
||||
fs.copyFileSync(backupPath, distDataJsonPath);
|
||||
log.success("已从 .backup/ 恢复 data.json 到 dist/");
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`创建软链接失败: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模式2: copy - 完整复制(生产构建)
|
||||
* 复制整个 dist 目录,保护用户的 data.json
|
||||
*/
|
||||
async function deployCopy(vaultPath, pluginId) {
|
||||
const targetPluginDir = path.join(
|
||||
vaultPath,
|
||||
".obsidian",
|
||||
"plugins",
|
||||
pluginId,
|
||||
);
|
||||
|
||||
// 检查是否指向同一位置
|
||||
if (path.resolve(targetPluginDir) === path.resolve(distDir)) {
|
||||
log.info("目标路径与 dist 目录相同,跳过复制");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 确保 dist 目录存在
|
||||
if (!fs.existsSync(distDir)) {
|
||||
log.error("dist 目录不存在,请先运行构建命令");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 保护 data.json:双重保护机制
|
||||
// 1. 内存临时保存(用于立即恢复)
|
||||
// 2. 备份到 .backup/ (用于意外情况恢复)
|
||||
let dataJsonContent = null;
|
||||
const dataJsonPath = path.join(targetPluginDir, "data.json");
|
||||
if (fs.existsSync(dataJsonPath)) {
|
||||
try {
|
||||
dataJsonContent = fs.readFileSync(dataJsonPath, "utf8");
|
||||
log.info("已保存现有的 data.json");
|
||||
|
||||
// 同时备份到 .backup/ 作为安全保障
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(backupDir, "data.json"), dataJsonContent);
|
||||
log.info("已备份到 .backup/data.json");
|
||||
} catch (error) {
|
||||
log.warn(`读取 data.json 失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除旧的目标目录
|
||||
if (fs.existsSync(targetPluginDir)) {
|
||||
fs.rmSync(targetPluginDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// 复制整个 dist 目录
|
||||
try {
|
||||
copyDirRecursive(distDir, targetPluginDir);
|
||||
const fileCount = fs.readdirSync(distDir).length;
|
||||
log.success(`已复制 ${fileCount} 个文件到 ${pluginId}`);
|
||||
} catch (error) {
|
||||
log.error(`复制失败: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 恢复 data.json
|
||||
if (dataJsonContent) {
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(targetPluginDir, "data.json"),
|
||||
dataJsonContent,
|
||||
);
|
||||
log.success("已恢复 data.json");
|
||||
} catch (error) {
|
||||
log.error(`恢复 data.json 失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 主函数 ====================
|
||||
async function main() {
|
||||
try {
|
||||
log.info(`部署模式: ${deployMode}`);
|
||||
|
||||
ensureDistReady();
|
||||
const vaultPath = loadVaultPath();
|
||||
const pluginId = getPluginId();
|
||||
|
||||
log.info(`目标 Vault: ${vaultPath}`);
|
||||
log.info(`插件 ID: ${pluginId}`);
|
||||
|
||||
switch (deployMode) {
|
||||
case "link":
|
||||
await deployLink(vaultPath, pluginId);
|
||||
break;
|
||||
case "copy":
|
||||
await deployCopy(vaultPath, pluginId);
|
||||
break;
|
||||
default:
|
||||
log.error(`未知的部署模式: ${deployMode}`);
|
||||
log.info("支持的模式: link | copy");
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`部署失败: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import builtins from "builtin-modules";
|
||||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import fs from "fs-extra";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import postcss from "postcss";
|
||||
import postcssNesting from "postcss-nesting";
|
||||
import builtins from "builtin-modules";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
|
|
@ -12,31 +12,42 @@ if you want to view the source, please visit the github repository of this plugi
|
|||
*/
|
||||
`;
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
// 参数解析:支持 dev 和 prod 两种模式
|
||||
// 用法: node esbuild.config.mjs [dev|prod|production]
|
||||
const args = process.argv.slice(2);
|
||||
const mode = args[0] || "dev";
|
||||
const prod = mode === "production" || mode === "prod";
|
||||
const outDir = "dist";
|
||||
|
||||
/**
|
||||
* CSS 文件重命名插件
|
||||
* 在构建完成后,将 esbuild 生成的 main.css 重命名为 styles.css
|
||||
* @returns {Object} esbuild 插件对象
|
||||
*/
|
||||
const renamePlugin = () => ({
|
||||
name: "rename-plugin",
|
||||
setup(build) {
|
||||
build.onEnd(async (result) => {
|
||||
const file = build.initialOptions.outfile;
|
||||
const parent = path.dirname(file);
|
||||
const cssFileName = path.join(parent, "main.css");
|
||||
const newCssFileName = path.join(parent, "styles.css");
|
||||
build.onEnd(async () => {
|
||||
const src = path.join(outDir, "main.css");
|
||||
const dest = path.join(outDir, "styles.css");
|
||||
|
||||
if (fs.existsSync(cssFileName)) {
|
||||
if (fs.existsSync(src)) {
|
||||
try {
|
||||
if (fs.existsSync(newCssFileName)) {
|
||||
fs.unlinkSync(newCssFileName);
|
||||
}
|
||||
fs.renameSync(cssFileName, newCssFileName);
|
||||
} catch (e) {
|
||||
console.error("Failed to rename CSS file:", e);
|
||||
fs.moveSync(src, dest, { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error("Failed to rename CSS file:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* CSS 处理插件
|
||||
* 使用 PostCSS 和 postcss-nesting 插件处理 CSS 文件,支持 CSS 嵌套语法
|
||||
* 在构建过程中自动处理所有 .css 文件
|
||||
* @returns {Object} esbuild 插件对象
|
||||
*/
|
||||
const cssReBuild = () => ({
|
||||
name: "css-rebuild",
|
||||
|
||||
|
|
@ -55,14 +66,8 @@ const cssReBuild = () => ({
|
|||
from: args.path, // 指定源文件路径,用于生成正确的源映射
|
||||
});
|
||||
|
||||
// 获取输出目录路径
|
||||
// build.initialOptions.outfile 是最终 JS 输出文件的路径
|
||||
const outDir = path.dirname(build.initialOptions.outfile);
|
||||
|
||||
// 确保输出目录存在,如果不存在则创建
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
fs.ensureDirSync(outDir);
|
||||
|
||||
// 返回处理后的 CSS 内容
|
||||
// contents: 处理后的 CSS 代码
|
||||
|
|
@ -86,6 +91,56 @@ const cssReBuild = () => ({
|
|||
},
|
||||
});
|
||||
|
||||
// 复制 manifest.json 到输出目录
|
||||
function copyManifest() {
|
||||
// 确保输出目录存在,如果不存在则创建
|
||||
fs.ensureDirSync(outDir);
|
||||
|
||||
const src = path.join(process.cwd(), "manifest.json");
|
||||
const dest = path.join(process.cwd(), outDir, "manifest.json");
|
||||
try {
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log("✓ 已复制 manifest.json 到 dist 目录");
|
||||
} catch (e) {
|
||||
console.error("⚠ 复制 manifest.json 失败:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 manifest.json 变化(带防抖和内容比较,避免重复触发)
|
||||
function watchManifest() {
|
||||
const manifestPath = path.join(process.cwd(), "manifest.json");
|
||||
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let copyTimer = null;
|
||||
let lastContent = fs.readFileSync(manifestPath, "utf8");
|
||||
|
||||
fs.watch(manifestPath, (eventType) => {
|
||||
if (eventType === "change") {
|
||||
// 防抖:延迟 100ms 执行,避免短时间内多次触发
|
||||
if (copyTimer) {
|
||||
clearTimeout(copyTimer);
|
||||
}
|
||||
copyTimer = setTimeout(() => {
|
||||
try {
|
||||
const newContent = fs.readFileSync(manifestPath, "utf8");
|
||||
// 只有内容真正改变时才复制
|
||||
if (newContent !== lastContent) {
|
||||
lastContent = newContent;
|
||||
copyManifest();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("⚠ 读取 manifest.json 失败:", e.message);
|
||||
}
|
||||
copyTimer = null;
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 创建 esbuild context
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
|
|
@ -114,16 +169,25 @@ const context = await esbuild.context({
|
|||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
outdir: outDir,
|
||||
minify: prod,
|
||||
drop: prod ? ["console"] : [],
|
||||
loader: {
|
||||
".ttf": "base64",
|
||||
},
|
||||
});
|
||||
|
||||
// 初始构建时复制 manifest.json
|
||||
copyManifest();
|
||||
|
||||
if (prod) {
|
||||
// 生产模式:构建一次后退出
|
||||
await context.rebuild();
|
||||
console.log("✅ 生产构建完成");
|
||||
process.exit(0);
|
||||
} else {
|
||||
// 开发模式:监听文件变化
|
||||
console.log("👀 开发模式启动,监听文件变化...");
|
||||
watchManifest();
|
||||
await context.watch();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,143 +0,0 @@
|
|||
import { spawn } from "child_process";
|
||||
import dotenv from "dotenv";
|
||||
import { existsSync } from "fs";
|
||||
import { lstat, readFile, symlink, unlink } from "fs/promises";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(__dirname, "..");
|
||||
|
||||
dotenv.config({ quiet: true });
|
||||
const VAULT_PATH = process.env.VAULT_PATH;
|
||||
if (!VAULT_PATH) {
|
||||
throw new Error(
|
||||
"VAULT_PATH is not defined. Please create a .env file in the project root and add the line: VAULT_PATH=/path/to/your/vault"
|
||||
);
|
||||
}
|
||||
|
||||
const manifestPath = join(projectRoot, "manifest.json");
|
||||
|
||||
// 检查是否以管理员身份运行
|
||||
function isAdmin() {
|
||||
if (process.platform !== "win32") return true;
|
||||
|
||||
try {
|
||||
const { execSync } = require("child_process");
|
||||
execSync("net session >nul 2>&1", { stdio: "ignore" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试以管理员身份重新运行
|
||||
async function runAsAdmin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const scriptPath = process.argv[1];
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// 使用 PowerShell 以管理员身份运行
|
||||
const psCommand = `Start-Process -FilePath "node" -ArgumentList "${scriptPath}",${args
|
||||
.map((arg) => `"${arg}"`)
|
||||
.join(",")} -Verb RunAs -Wait`;
|
||||
|
||||
const ps = spawn("powershell.exe", ["-Command", psCommand], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
ps.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`PowerShell 退出码: ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
ps.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function linkDataJson() {
|
||||
try {
|
||||
// 从 manifest.json 读取插件 ID
|
||||
const manifestContent = await readFile(manifestPath, "utf8");
|
||||
const manifest = JSON.parse(manifestContent);
|
||||
const pluginId = manifest.id;
|
||||
|
||||
if (!pluginId) {
|
||||
throw new Error("无法从 manifest.json 中获取插件 ID");
|
||||
}
|
||||
|
||||
// 构建路径
|
||||
const vaultPluginDir = join(
|
||||
VAULT_PATH,
|
||||
".obsidian",
|
||||
"plugins",
|
||||
pluginId
|
||||
);
|
||||
const sourceDataPath = join(vaultPluginDir, "data.json");
|
||||
const targetDataPath = join(rootDir, "data.json");
|
||||
|
||||
// 检查源文件是否存在
|
||||
if (!existsSync(sourceDataPath)) {
|
||||
console.log(`警告: 源文件不存在: ${sourceDataPath}`);
|
||||
console.log("请确保插件已在测试库中运行过并生成了 data.json 文件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查目标位置是否已有文件或链接
|
||||
if (existsSync(targetDataPath)) {
|
||||
try {
|
||||
const stats = await lstat(targetDataPath);
|
||||
if (stats.isSymbolicLink()) {
|
||||
console.log("删除现有的软链接...");
|
||||
await unlink(targetDataPath);
|
||||
} else {
|
||||
console.log(
|
||||
"警告: 目标位置已存在普通文件,请手动处理后重试"
|
||||
);
|
||||
console.log(`文件路径: ${targetDataPath}`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("检查目标文件时出错:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建软链接
|
||||
try {
|
||||
await symlink(sourceDataPath, targetDataPath, "file");
|
||||
console.log(`成功创建软链接:`);
|
||||
console.log(` 源文件: ${sourceDataPath}`);
|
||||
console.log(` 目标位置: ${targetDataPath}`);
|
||||
console.log("");
|
||||
console.log(
|
||||
"现在项目根目录的 data.json 将实时反映测试库中的插件配置!"
|
||||
);
|
||||
} catch (symlinkError) {
|
||||
if (symlinkError.code === "EPERM") {
|
||||
console.log("软链接创建失败,权限不足。");
|
||||
|
||||
// 检查是否以管理员身份运行
|
||||
if (!isAdmin()) {
|
||||
console.log("尝试以管理员身份重新运行...");
|
||||
try {
|
||||
await runAsAdmin();
|
||||
return;
|
||||
} catch (adminError) {
|
||||
console.log("自动提权失败:", adminError.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw symlinkError;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("执行过程中出错:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
linkDataJson();
|
||||
|
|
@ -3,136 +3,113 @@
|
|||
* 深度合并:多删少补,保证与基准文件始终一致
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT_DIR = path.resolve(__dirname, "..");
|
||||
const I18N_DIR = path.join(ROOT_DIR, "src", "i18n");
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
const I18N_DIR = path.join(ROOT_DIR, 'src', 'i18n');
|
||||
|
||||
// 日志
|
||||
const log = {
|
||||
success: (msg) => console.log(`\x1b[32m✔\x1b[0m ${msg}`),
|
||||
error: (msg) => console.log(`\x1b[31m✖\x1b[0m ${msg}`),
|
||||
success: (msg) => console.log(`\x1b[32m✔\x1b[0m ${msg}`),
|
||||
error: (msg) => console.log(`\x1b[31m✖\x1b[0m ${msg}`),
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取 typesafe-i18n 配置
|
||||
*/
|
||||
function loadConfig() {
|
||||
const configPath = path.join(ROOT_DIR, ".typesafe-i18n.json");
|
||||
if (!fs.existsSync(configPath)) {
|
||||
throw new Error(".typesafe-i18n.json 配置文件不存在");
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
const configPath = path.join(ROOT_DIR, '.typesafe-i18n.json');
|
||||
if (!fs.existsSync(configPath)) {
|
||||
throw new Error('.typesafe-i18n.json 配置文件不存在');
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 TypeScript 文件中提取翻译对象
|
||||
*/
|
||||
function extractTranslationObject(filePath) {
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const match = content.match(
|
||||
/const\s+\w+\s*=\s*(\{[\s\S]*?\})\s*satisfies\s+BaseTranslation/
|
||||
);
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const match = content.match(/const\s+\w+\s*=\s*(\{[\s\S]*?\})\s*satisfies\s+BaseTranslation/);
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
try {
|
||||
return new Function(`return ${match[1]}`)();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new Function(`return ${match[1]}`)();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的所有扁平化键路径
|
||||
*/
|
||||
function getKeys(obj, prefix = "") {
|
||||
const keys = new Set();
|
||||
for (const key of Object.keys(obj)) {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
const value = obj[key];
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value)
|
||||
) {
|
||||
getKeys(value, fullKey).forEach((k) => keys.add(k));
|
||||
} else {
|
||||
keys.add(fullKey);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
function getKeys(obj, prefix = '') {
|
||||
const keys = new Set();
|
||||
for (const key of Object.keys(obj)) {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
const value = obj[key];
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
getKeys(value, fullKey).forEach(k => keys.add(k));
|
||||
} else {
|
||||
keys.add(fullKey);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度合并:按照基准结构重建对象
|
||||
*/
|
||||
function deepMerge(baseObj, targetObj) {
|
||||
const result = {};
|
||||
|
||||
for (const key of Object.keys(baseObj)) {
|
||||
const baseValue = baseObj[key];
|
||||
const targetValue = targetObj?.[key];
|
||||
|
||||
if (
|
||||
typeof baseValue === "object" &&
|
||||
baseValue !== null &&
|
||||
!Array.isArray(baseValue)
|
||||
) {
|
||||
result[key] = deepMerge(
|
||||
baseValue,
|
||||
typeof targetValue === "object" ? targetValue : {}
|
||||
);
|
||||
} else if (
|
||||
targetValue !== undefined &&
|
||||
typeof targetValue !== "object"
|
||||
) {
|
||||
result[key] = targetValue;
|
||||
} else {
|
||||
result[key] = `TODO: ${baseValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
const result = {};
|
||||
|
||||
for (const key of Object.keys(baseObj)) {
|
||||
const baseValue = baseObj[key];
|
||||
const targetValue = targetObj?.[key];
|
||||
|
||||
if (typeof baseValue === 'object' && baseValue !== null && !Array.isArray(baseValue)) {
|
||||
result[key] = deepMerge(baseValue, typeof targetValue === 'object' ? targetValue : {});
|
||||
} else if (targetValue !== undefined && typeof targetValue !== 'object') {
|
||||
result[key] = targetValue;
|
||||
} else {
|
||||
result[key] = `TODO: ${baseValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象序列化为 TypeScript 格式
|
||||
*/
|
||||
function toTypeScript(obj, indent = 1) {
|
||||
const pad = "\t".repeat(indent);
|
||||
const lines = Object.keys(obj).map((key) => {
|
||||
const value = obj[key];
|
||||
const formattedKey = /[^a-zA-Z0-9_]/.test(key) ? `"${key}"` : key;
|
||||
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value)
|
||||
) {
|
||||
return `${pad}${formattedKey}: ${toTypeScript(value, indent + 1)},`;
|
||||
}
|
||||
const escaped = String(value)
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, "\\n");
|
||||
return `${pad}${formattedKey}: "${escaped}",`;
|
||||
});
|
||||
|
||||
return `{\n${lines.join("\n")}\n${"\t".repeat(indent - 1)}}`;
|
||||
const pad = '\t'.repeat(indent);
|
||||
const lines = Object.keys(obj).map(key => {
|
||||
const value = obj[key];
|
||||
const formattedKey = /[^a-zA-Z0-9_]/.test(key) ? `"${key}"` : key;
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
return `${pad}${formattedKey}: ${toTypeScript(value, indent + 1)},`;
|
||||
}
|
||||
const escaped = String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
||||
return `${pad}${formattedKey}: "${escaped}",`;
|
||||
});
|
||||
|
||||
return `{\n${lines.join('\n')}\n${'\t'.repeat(indent - 1)}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成翻译文件内容
|
||||
*/
|
||||
function generateContent(locale, obj) {
|
||||
const varName = locale.replace(/-/g, "_");
|
||||
return `import type { BaseTranslation } from "../i18n-types";
|
||||
const varName = locale.replace(/-/g, '_');
|
||||
return `import type { BaseTranslation } from '../i18n-types'
|
||||
|
||||
const ${varName} = ${toTypeScript(obj)} satisfies BaseTranslation;
|
||||
|
||||
|
|
@ -144,77 +121,63 @@ export default ${varName};
|
|||
* 主函数
|
||||
*/
|
||||
function main() {
|
||||
const { baseLocale } = loadConfig();
|
||||
const { baseLocale } = loadConfig();
|
||||
|
||||
// 获取所有 locale 目录
|
||||
const locales = fs.readdirSync(I18N_DIR, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory())
|
||||
.map(e => e.name);
|
||||
|
||||
// 获取所有 locale 目录
|
||||
const locales = fs
|
||||
.readdirSync(I18N_DIR, { withFileTypes: true })
|
||||
.filter((e) => e.isDirectory())
|
||||
.map((e) => e.name);
|
||||
if (!locales.includes(baseLocale)) {
|
||||
throw new Error(`基准语言 "${baseLocale}" 目录不存在`);
|
||||
}
|
||||
|
||||
if (!locales.includes(baseLocale)) {
|
||||
throw new Error(`基准语言 "${baseLocale}" 目录不存在`);
|
||||
}
|
||||
// 解析基准语言
|
||||
const baseObj = extractTranslationObject(path.join(I18N_DIR, baseLocale, 'index.ts'));
|
||||
if (!baseObj) {
|
||||
throw new Error('无法解析基准语言文件');
|
||||
}
|
||||
|
||||
// 解析基准语言
|
||||
const baseObj = extractTranslationObject(
|
||||
path.join(I18N_DIR, baseLocale, "index.ts")
|
||||
);
|
||||
if (!baseObj) {
|
||||
throw new Error("无法解析基准语言文件");
|
||||
}
|
||||
const baseKeys = getKeys(baseObj);
|
||||
let syncedCount = 0;
|
||||
|
||||
const baseKeys = getKeys(baseObj);
|
||||
let syncedCount = 0;
|
||||
// 同步其他语言
|
||||
for (const locale of locales.filter(l => l !== baseLocale)) {
|
||||
const filePath = path.join(I18N_DIR, locale, 'index.ts');
|
||||
|
||||
// 检查文件是否存在,不存在则生成初始文件
|
||||
if (!fs.existsSync(filePath)) {
|
||||
const initialContent = generateContent(locale, deepMerge(baseObj, {}));
|
||||
fs.writeFileSync(filePath, initialContent, 'utf-8');
|
||||
log.success(`${locale} (新建,+${baseKeys.size} 个键)`);
|
||||
syncedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 同步其他语言
|
||||
for (const locale of locales.filter((l) => l !== baseLocale)) {
|
||||
const filePath = path.join(I18N_DIR, locale, "index.ts");
|
||||
const targetObj = extractTranslationObject(filePath);
|
||||
|
||||
// 检查文件是否存在,不存在则生成初始文件
|
||||
if (!fs.existsSync(filePath)) {
|
||||
const initialContent = generateContent(
|
||||
locale,
|
||||
deepMerge(baseObj, {})
|
||||
);
|
||||
fs.writeFileSync(filePath, initialContent, "utf-8");
|
||||
log.success(`${locale} (新建,+${baseKeys.size} 个键)`);
|
||||
syncedCount++;
|
||||
continue;
|
||||
}
|
||||
if (!targetObj) {
|
||||
log.error(`无法解析: ${locale}/index.ts`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetObj = extractTranslationObject(filePath);
|
||||
const targetKeys = getKeys(targetObj);
|
||||
const missing = [...baseKeys].filter(k => !targetKeys.has(k));
|
||||
const extra = [...targetKeys].filter(k => !baseKeys.has(k));
|
||||
|
||||
if (!targetObj) {
|
||||
log.error(`无法解析: ${locale}/index.ts`);
|
||||
continue;
|
||||
}
|
||||
if (missing.length === 0 && extra.length === 0) continue;
|
||||
|
||||
const targetKeys = getKeys(targetObj);
|
||||
const missing = [...baseKeys].filter((k) => !targetKeys.has(k));
|
||||
const extra = [...targetKeys].filter((k) => !baseKeys.has(k));
|
||||
// 执行同步
|
||||
fs.writeFileSync(filePath, generateContent(locale, deepMerge(baseObj, targetObj)), 'utf-8');
|
||||
|
||||
const changes = [];
|
||||
if (missing.length > 0) changes.push(`+${missing.length}`);
|
||||
if (extra.length > 0) changes.push(`-${extra.length}`);
|
||||
log.success(`${locale} (${changes.join(', ')})`);
|
||||
syncedCount++;
|
||||
}
|
||||
|
||||
if (missing.length === 0 && extra.length === 0) continue;
|
||||
|
||||
// 执行同步
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
generateContent(locale, deepMerge(baseObj, targetObj)),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
const changes = [];
|
||||
if (missing.length > 0) changes.push(`+${missing.length}`);
|
||||
if (extra.length > 0) changes.push(`-${extra.length}`);
|
||||
log.success(`${locale} (${changes.join(", ")})`);
|
||||
syncedCount++;
|
||||
}
|
||||
|
||||
log.success(
|
||||
syncedCount > 0
|
||||
? `同步完成,已更新 ${syncedCount} 个语言文件`
|
||||
: "所有语言已同步"
|
||||
);
|
||||
log.success(syncedCount > 0 ? `同步完成,已更新 ${syncedCount} 个语言文件` : '所有语言已同步');
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env node
|
||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import readline from "readline";
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import readline from "node:readline";
|
||||
|
||||
// 检查是否为直接调用模式
|
||||
const directVersion = process.argv[2];
|
||||
|
|
@ -194,8 +193,10 @@ function updateAllFiles(version, isBeta = false) {
|
|||
// 2. 更新 manifest.json 或 manifest-beta.json
|
||||
const minAppVersion = updateManifestJson(version, isBeta);
|
||||
|
||||
// 3. 更新 versions.json
|
||||
updateVersionsJson(version, minAppVersion);
|
||||
// 3. 更新 versions.json (仅非Beta版本)
|
||||
if (!isBeta) {
|
||||
updateVersionsJson(version, minAppVersion);
|
||||
}
|
||||
|
||||
// 提示提交更改
|
||||
if (isFirstRelease) {
|
||||
|
|
@ -205,9 +206,7 @@ function updateAllFiles(version, isBeta = false) {
|
|||
}
|
||||
|
||||
if (isBeta) {
|
||||
console.log(
|
||||
`git add package.json manifest-beta.json versions.json`
|
||||
);
|
||||
console.log(`git add package.json manifest-beta.json`);
|
||||
} else {
|
||||
console.log(`git add package.json manifest.json versions.json`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue