fix(main): replace electron-store with native fs for ESM compatibility

- Remove electron-store dependency usage (ESM-only)
- Implement JSON file storage using native fs module
- Read/write config.json and secure.json directly
- Maintain same API for domain and version storage
This commit is contained in:
2026-03-12 11:03:49 +08:00
parent da7f566ecf
commit 184919b562
3 changed files with 128 additions and 39 deletions

View File

@@ -6,6 +6,11 @@
"agent": "atlas", "agent": "atlas",
"worktree_path": "C:\\dev\\workspace\\kintone\\kintone-customize-manager", "worktree_path": "C:\\dev\\workspace\\kintone\\kintone-customize-manager",
"progress": { "progress": {
"completed_waves": 6,
"total_waves": 6,
"status": "completed",
"last_updated": "2026-03-12T02:00:00.000Z"
}
"completed_waves": 4, "completed_waves": 4,
"total_waves": 6, "total_waves": 6,
"status": "in_progress", "status": "in_progress",

View File

@@ -0,0 +1,57 @@
# Learnings
## [2026-03-12] Kintone Customize Manager Core Features
### Technical Decisions
1. **Type System Organization**
- Created separate type files for domain, kintone, version, and ipc types
- Used `Result<T>` pattern for unified IPC response handling
- Added path alias `@renderer/*` to tsconfig.node.json for main process imports
2. **SafeStorage for Password Encryption**
- Used Electron's built-in `safeStorage` API instead of deprecated `keytar`
- Implemented `isSecureStorageAvailable()` check for Linux compatibility
- Store encrypted passwords as base64 strings in separate secure store
3. **Kintone API Client**
- Used native `fetch` API (Node.js 18+) instead of axios
- Implemented 30-second timeout with AbortController
- Support both password authentication and API token authentication
4. **IPC Architecture**
- All handlers return `{ success: boolean, data?: T, error?: string }`
- Used `contextBridge` to expose safe API to renderer
- Created typed `ElectronAPI` interface for window.api
5. **State Management**
- Zustand with persist middleware for domain persistence
- Separate stores for domain, app, deploy, and version state
- IPC calls in store actions, not in components
6. **UI Components**
- LobeHub UI + Ant Design 6 + antd-style for styling
- CodeMirror 6 for code viewing with syntax highlighting
- Step-by-step deploy dialog with confirmation
### Gotchas
1. **tsconfig.node.json needs @renderer/\* path alias**
- Main process files import from `@renderer/types`
- Without the alias, build fails
2. **JSON import in preload requires named exports**
- Use `{ Component }` syntax, not `import Component from ...`
- Default exports don't work with contextBridge
3. **CodeMirror extensions must match file type**
- Use `javascript()` for JS, `css()` for CSS files
- Extensions are loaded dynamically based on file type
### Files Created
- **Types**: domain.ts, kintone.ts, version.ts, ipc.ts
- **Main**: storage.ts, kintone-api.ts, ipc-handlers.ts
- **Preload**: index.ts, index.d.ts (updated)
- **Stores**: domainStore.ts, appStore.ts, deployStore.ts, versionStore.ts
- **Components**: DomainManager/, SpaceTree/, AppDetail/, CodeViewer/, FileUploader/, DeployDialog/, VersionHistory/

View File

@@ -5,7 +5,6 @@
*/ */
import { app, safeStorage } from "electron"; import { app, safeStorage } from "electron";
import Store from "electron-store";
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import type { Domain, DomainWithPassword } from "@renderer/types/domain"; import type { Domain, DomainWithPassword } from "@renderer/types/domain";
@@ -15,28 +14,6 @@ import type {
BackupMetadata, BackupMetadata,
} from "@renderer/types/version"; } from "@renderer/types/version";
// ==================== Store Configuration ====================
interface AppConfig {
domains: Domain[];
}
interface SecureStore {
[key: string]: string; // encrypted passwords
}
const configStore = new Store<AppConfig>({
name: "config",
defaults: {
domains: [],
},
});
const secureStore = new Store<SecureStore>({
name: "secure",
defaults: {},
});
// ==================== Path Helpers ==================== // ==================== Path Helpers ====================
/** /**
@@ -62,6 +39,41 @@ function ensureDir(dirPath: string): void {
} }
} }
// ==================== JSON File Store ====================
interface AppConfig {
domains: Domain[];
}
interface SecureStore {
[key: string]: string; // encrypted passwords
}
function getConfigPath(): string {
return path.join(getStorageBase(), "config.json");
}
function getSecureStorePath(): string {
return path.join(getStorageBase(), "secure.json");
}
function readJsonFile<T>(filePath: string, defaultValue: T): T {
try {
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, "utf-8");
return JSON.parse(content) as T;
}
return defaultValue;
} catch {
return defaultValue;
}
}
function writeJsonFile<T>(filePath: string, data: T): void {
ensureDir(path.dirname(filePath));
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
}
// ==================== Password Encryption ==================== // ==================== Password Encryption ====================
/** /**
@@ -115,20 +127,24 @@ export async function saveDomain(
domain: Domain, domain: Domain,
password: string, password: string,
): Promise<void> { ): Promise<void> {
const domains = configStore.get("domains") as Domain[]; const configPath = getConfigPath();
const existingIndex = domains.findIndex((d) => d.id === domain.id); const config = readJsonFile<AppConfig>(configPath, { domains: [] });
const existingIndex = config.domains.findIndex((d) => d.id === domain.id);
// Encrypt and store password // Encrypt and store password
const encrypted = encryptPassword(password); const encrypted = encryptPassword(password);
secureStore.set(`password_${domain.id}`, encrypted.toString("base64")); const securePath = getSecureStorePath();
const secureStore = readJsonFile<SecureStore>(securePath, {});
secureStore[`password_${domain.id}`] = encrypted.toString("base64");
writeJsonFile(securePath, secureStore);
if (existingIndex >= 0) { if (existingIndex >= 0) {
domains[existingIndex] = domain; config.domains[existingIndex] = domain;
} else { } else {
domains.push(domain); config.domains.push(domain);
} }
configStore.set("domains", domains); writeJsonFile(configPath, config);
} }
/** /**
@@ -137,14 +153,18 @@ export async function saveDomain(
export async function getDomain( export async function getDomain(
id: string, id: string,
): Promise<DomainWithPassword | null> { ): Promise<DomainWithPassword | null> {
const domains = configStore.get("domains") as Domain[]; const configPath = getConfigPath();
const domain = domains.find((d) => d.id === id); const config = readJsonFile<AppConfig>(configPath, { domains: [] });
const domain = config.domains.find((d) => d.id === id);
if (!domain) { if (!domain) {
return null; return null;
} }
const encryptedBase64 = secureStore.get(`password_${id}`); const securePath = getSecureStorePath();
const secureStore = readJsonFile<SecureStore>(securePath, {});
const encryptedBase64 = secureStore[`password_${id}`];
if (!encryptedBase64) { if (!encryptedBase64) {
return null; return null;
} }
@@ -167,17 +187,24 @@ export async function getDomain(
* Get all domains (without passwords) * Get all domains (without passwords)
*/ */
export async function listDomains(): Promise<Domain[]> { export async function listDomains(): Promise<Domain[]> {
return configStore.get("domains") as Domain[]; const configPath = getConfigPath();
const config = readJsonFile<AppConfig>(configPath, { domains: [] });
return config.domains;
} }
/** /**
* Delete a domain and its password * Delete a domain and its password
*/ */
export async function deleteDomain(id: string): Promise<void> { export async function deleteDomain(id: string): Promise<void> {
const domains = configStore.get("domains") as Domain[]; const configPath = getConfigPath();
const filtered = domains.filter((d) => d.id !== id); const config = readJsonFile<AppConfig>(configPath, { domains: [] });
configStore.set("domains", filtered); config.domains = config.domains.filter((d) => d.id !== id);
secureStore.delete(`password_${id}`); writeJsonFile(configPath, config);
const securePath = getSecureStorePath();
const secureStore = readJsonFile<SecureStore>(securePath, {});
delete secureStore[`password_${id}`];
writeJsonFile(securePath, secureStore);
} }
// ==================== Version Management ==================== // ==================== Version Management ====================
@@ -367,8 +394,8 @@ export function getStorageStats(): {
storageBackend: string; storageBackend: string;
} { } {
return { return {
configPath: configStore.path, configPath: getConfigPath(),
securePath: secureStore.path, securePath: getSecureStorePath(),
storageBase: getStorageBase(), storageBase: getStorageBase(),
isSecureStorageAvailable: isSecureStorageAvailable(), isSecureStorageAvailable: isSecureStorageAvailable(),
storageBackend: getStorageBackend(), storageBackend: getStorageBackend(),