Skip to content

Latest commit

 

History

History
70 lines (64 loc) · 10.3 KB

File metadata and controls

70 lines (64 loc) · 10.3 KB

Repository Guidelines

Project Structure & Module Organization

PicGo is an Electron + Vue 3 desktop client. Source lives in src/: src/main for main-process and IPC logic, src/renderer for Vue views, and src/universal for shared helpers (types/, events/constants.ts). background.ts wires Electron Builder. Static assets and locale YAML files stay in public/ (add languages under public/i18n/), while docs/ hosts user-facing guides. Automation scripts live in scripts/, and legacy tests sit under test/unit (Karma) and test/e2e (Spectron).

Build, Test, and Development Commands

  • pnpm install — install dependencies; npm install is unsupported. Only run this when the user explicitly asks/coordinates it.
  • Always add/remove dependencies with pnpm (never edit package.json versions by hand then install).
  • pnpm dev — electron-vite dev server for main/preload/renderer.
  • pnpm build — electron-vite build outputs to dist/main, dist/preload, dist/renderer; pnpm preview for preview mode.
  • Packaging config lives in electron-builder.yml (read by electron-builder via package.json build field/extraResources); set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ if downloads are slow.
  • pnpm lint / pnpm lint:fix — run or auto-fix ESLint (Standard, TypeScript, Vue rules).
  • pnpm lint:dpdm — fail fast on circular dependencies in src/.
  • pnpm check — run tsc + lint (run once before finishing a task).
  • Before completing a task, always run pnpm check and resolve any issues it reports.
  • i18n type files are auto-generated by the Vite i18nTypesPlugin when public/i18n/*.yml changes. Do not add or rely on a manual gen-i18n step.

Coding Style & Naming Conventions

Follow ESLint Standard defaults: two-space indentation, single quotes, trailing commas where allowed, and no stray semicolons. Author new modules in TypeScript. Keep renderer files browser-safe; route Node APIs through IPC helpers such as src/main/events/picgoCoreIPC.ts. Name Vue components in PascalCase (UploadPanel.vue) and use camelCase for utilities. Centralize IPC event names inside src/universal/events/constants.ts, and store enums/types under src/universal/types/ so they stay reusable. Static assets are served from public/ and resolved via getStaticPath/getStaticFileUrl (src/universal/utils/staticPath.ts); avoid using __static directly. Static assets are served from public/. In the main process use getStaticPath/getStaticFileUrl (src/universal/utils/staticPath.ts). In the renderer, place assets under public/ and resolve them via import.meta.env.BASE_URL + filename (helper: src/renderer/utils/static.ts); do not rely on __static in renderer code.

  • Do not use as any under any circumstances; keep typings explicit and safe.
  • Avoid as any in tests as well; build concrete typed stubs (e.g., IpcMainInvokeEvent) instead.
  • Do not prefix method calls with void (e.g. use store?.refreshPicBeds() rather than void store?.refreshPicBeds()).
  • Do not write void someMethod() or void object.method() anywhere in the codebase. If you need fire-and-forget behavior, use an async callback with await, or handle errors explicitly with try/catch and logging instead of swallowing them.
  • Because the renderer uses React Compiler, do not add useMemo or useCallback by default. Prefer plain values and inline functions unless a specific API requires stable identity or there is a proven performance issue.
  • For Zustand store state/actions, prefer reading them directly in the consuming component with useAppStore / useStore instead of passing them down through unnecessary prop layers. If a child can access the needed store value or action itself, do not thread it through parent props.
  • If a child component only needs a store action (for example providerStoreActions.toggleExpanded, settingsStoreActions.setSearchValue, or galleryStoreActions.setViewMode), do not pass that store action through props. Import and use the action directly inside the child component.
  • Renderer Zustand stores must follow the project store architecture:
    • src/renderer/store/app-store.ts is for true global renderer state only.
    • Feature/page-local UI state must live under src/renderer/store/<feature>/ (for example src/renderer/store/gallery/store.ts, src/renderer/store/gallery/actions.ts), not inside app-store.ts.
    • Do not add an extra nested store/ directory like src/renderer/store/gallery/store/store.ts.
  • Zustand state and actions must be separated:
    • Do not define state-mutating actions inside create().
    • Keep store files focused on state shape and initial state.
    • Put global actions in src/renderer/store/app-actions.ts.
    • Put feature actions in src/renderer/store/<feature>/actions.ts.
    • Actions must update state via useXxxStore.setState(...).
  • Follow strict IPC boundaries for Zustand actions:
    • Pure IPC/service calls without Zustand state changes should call the adapter/service directly from the component or helper, not through a Zustand action.
    • Flows that combine IPC/service work with Zustand state updates must live in actions files.
  • Keep server state and client state separated:
    • TanStack Query should own server state: remote API data, loading/error/stale status, refetching, cache, and request dedupe.
    • Zustand should own client/UI state: selected source, selected ids, filters, view mode, panel open state, and other local user intent.
    • Connect the two with ids, query params, and local UI state (for example albumSource, typeFilter, or searchValue), but do not mirror query response data back into Zustand.
    • If server state invalidates a local UI choice (for example a user becomes non-paid while albumSource is cloud), use a small effect/action to correct the local UI state instead of storing the whole server response in Zustand.
  • When updating nested Zustand state (especially config-like objects), use zustand/middleware/immer; do not reintroduce deep ...state spread chains for nested updates.
  • Components must consume Zustand state through auto-generated selectors (for example useAppStore.use.appConfig()), not by destructuring the whole store or writing ad-hoc hook selectors in components.
  • Renderer-side shared constants (for example responsive breakpoints, UI timing values, fixed dimensions, thresholds, and repeated literal values used across components) should be centralized in src/renderer/utils/consts.ts instead of being hardcoded inline in components. When a new renderer constant may be reused or affects shared behavior, add it there first.
  • Renderer-side date/time formatting should use dayjs and shared format constants from src/renderer/utils/consts.ts (for example DEFAULT_DATE_TIME_FORMAT) instead of Intl.DateTimeFormat or ad-hoc inline format strings.
  • Enum-like object constants declared with as const (for example status maps, option maps, and value registries) must use PascalCase names, not camelCase. Prefer names like PicGoCloudRequestStatusValues, SettingsAppearanceValues, or AppPlatformValues.
  • If a renderer → main request mutates persisted config/state without using saveConfig, call notifyAppConfigUpdated() in main to inform renderers.
  • Prefer enums over union types for discrete value sets (e.g., encryption methods). Avoid introducing new string literal union types.
  • Renderer page/component styles should prefer Tailwind utility classes; avoid adding new Vue <style> blocks unless there's no reasonable Tailwind equivalent.
  • New renderer ↔ main request/response APIs should be implemented via RPC routes (see src/main/events/rpc/routes/system.ts) with RPCRouter + IRPCActionType rather than adding ad-hoc IPC modules (e.g. picgoCloudIPC).
    • For request/response semantics in renderer, prefer invokeRPC (backed by ipcMain.handle(RPC_ACTIONS, ...) in src/main/events/rpc/index.ts).
  • AlertDialog async confirm buttons: Do not use AlertDialogAction for confirm buttons that perform async operations (API calls, etc.), because AlertDialogAction auto-closes the dialog on click regardless of event.preventDefault(). Use a plain <Button> instead, manage open state manually, and close the dialog only after the async operation completes (success or error). See src/renderer/components/main/gallery/gallery-delete-dialog.tsx for reference.
  • Nullish coalescing for optional checks: Prefer (value?.field ?? fallback) > 0 over verbose value !== null && value !== undefined && typeof value.field === 'number' && value.field > 0 chains. Use TypeScript's optional chaining (?.) and nullish coalescing (??) to keep boolean checks concise.

Testing Guidelines

Place renderer unit specs in test/unit/specs with the .spec.js suffix; Karma picks them up via require.context. Run them with npx karma start test/unit/karma.conf.js --single-run and ensure new renderer folders are covered. Spectron e2e cases live in test/e2e/specs; build first (pnpm build), then run npx mocha test/e2e/index.js so Spectron can launch dist/electron/main.js. Document any test data, IPC stubs, or fixtures you add to keep suites reproducible.

Commit & Pull Request Guidelines

Commits follow the PicGo conventional preset enforced by Husky (pnpm lint:dpdm + Commitlint). Stage your changes and run pnpm cz to craft messages that pass CI. Pull requests should explain the change, link related issues, and attach UI screenshots or recordings. Note how you validated the work (dev server, build, Karma, Spectron) and call out migration or configuration steps reviewers must perform.

Internationalization Tips

Add locales by creating public/i18n/<locale>.yml, exposing its LANG_DISPLAY_LABEL, and registering it in src/universal/i18n/index.ts. Typed i18n declarations are generated automatically from public/i18n/en.yml into src/universal/types/i18n.d.ts and src/renderer/i18n/i18next.d.ts.

  • Any user-facing copy (UI text, error messages, warnings, prompts, tips, notifications, etc.) MUST use i18n keys. Do not hardcode strings in code.
    • Renderer: use $T('KEY') from src/renderer/i18n/index.ts.
    • Main process: use T('KEY') from src/main/i18n/index.ts.
    • Add new keys to all locales under public/i18n/ (at least en.yml, zh-CN.yml, zh-TW.yml). The Vite i18n types plugin will regenerate the shared declarations automatically.