콘텐츠로 이동
Oleaflyv0.4.7

Oleafly가 데스크톱 앱을 프런트엔드 워크스페이스, Rust 서비스, 고정된 사이드카로 나누는 방식을 설명합니다.

Oleafly의 프런트엔드는 pnpm 워크스페이스입니다. 앱 셸은 저장소 루트(src/)에 있고, 재사용 가능한 프런트엔드 코드는 packages/*에 열두 개의 역할이 뚜렷한 패키지로 들어 있습니다. Tauri 백엔드는 src-tauri/에 있고, 재사용 가능한 AI 제공자 및 에이전트 코드는 crates/oleafly-agent/에 있습니다. 프런트엔드/Rust/사이드카 구성은 README의 아키텍처 다이어그램 (새 탭에서 열림)을, 일상적인 명령은 개발을 참고하세요.

이 문서는 각 패키지가 무엇인지, 패키지를 순수하게 유지하는 의존성 역전 규칙, 기능을 셸에 연결하는 기여 레지스트리, 모듈 해석 방식, 패키지를 추가하거나 추출하는 방법을 다룹니다.

예전에는 모든 기능이 앱의 Zustand 스토어, Tauri IPC 클라이언트, shadcn UI 키트에 직접 손을 뻗었습니다. 그래서 기능을 셸에서 떼어 낼 수 없었고, 아무것도 따로 테스트하거나 재사용하거나 독립적으로 파악할 수 없었습니다. 워크스페이스는 이를 뒤집습니다. 패키지가 기능 로직을 갖고, 필요한 모든 것에 대해 포트라는 좁은 인터페이스를 선언합니다. 앱은 스토어와 Tauri 클라이언트 위에서 그 포트를 구현해 주입합니다. 앱이 패키지에 의존하고, 그 반대는 결코 없습니다.

패키지는 TypeScript 소스로 소비됩니다(main이 src/index.ts를 가리킵니다). 빌드 단계도, 배포도, 버전 불일치도 없습니다. Vite, Vitest, tsc가 모두 별칭을 통해 @oleafly/*를 패키지 소스로 곧바로 해석합니다(모듈 해석 참고).

패키지설명주입되는 포트주요 의존성
@oleafly/latex순수 LaTeX/그림 로직: 다이어그램 모델, TikZ 직렬화기(modelToTikz, 내장 모델 왕복 변환), 독립 문서 빌더(buildStandaloneDoc), 그림 이름/바이트 도우미없음(순수)없음
@oleafly/ai-core제공자 표시 메타데이터와 프런트엔드 모델 선택. Rust 카탈로그와 동일하게 유지됩니다없음(순수)없음
@oleafly/latex-intelligence편집기 인텔리전스가 쓰는 LaTeX 코퍼스의 검증된 타입과 데이터 접근없음(순수)없음
@oleafly/registry기여 레지스트리: 레일 탭, 팔레트/옴니바 명령, AI 툴셋(아래 자세히)없음(기여가 자체 동작을 가짐)react(타입)
@oleafly/preflight프리플라이트 엔진: 문서 유형 판별, 소스/PDF/참조 규칙, ATS 파싱, 접근성 내보내기 준비, 점수 계산. pdf.js가 node 테스트 그래프에 들어가지 않도록 pdf-extract는 @oleafly/preflight/pdf-extract 하위 경로로 제공됩니다없음(순수)pdfjs-dist(하위 경로만)
@oleafly/pdf-to-latex로컬 PDF-to-LaTeX 재구성 파이프라인: 줄 묶기, 제목과 단 감지, 스타일 추론, 기호 매핑, 그림 추출, 이스케이프, 문서 조립. 같은 node 테스트 그래프 사유로 pdf.js 어댑터는 @oleafly/pdf-to-latex/pdf-adapter 하위 경로로 제공됩니다없음(순수)pdfjs-dist(하위 경로만)
@oleafly/wysiwyg제한된 비주얼 편집기 코어: 공유 스키마, LaTeX와 Markdown 파싱/직렬화, 지원하지 않는 소스를 버리지 않고 보이게 유지하는 raw 인라인/블록 노드없음(순수)없음
@oleafly/editorLaTeX, Typst, Markdown용 CodeMirror 코어: CodeMirrorEditor, 편집기 뷰 컨트롤러(insertAtCursor, gotoLine, …), 언어 + 자동 완성, 테마, 접기, 고정 제목, 린터, latex-mask, 수식 미리보기(LaTeX는 KaTeX, Typst는 Typst), Typst 편집 보조와 LaTeX-to-Typst 수식 변환, LaTeX와 Typst의 제자리 비주얼 모드(@oleafly/editor/visual), 검색 패널, 맞춤법/문법 린터EditorHost(문서 모델 + 설정, 훅 형태), SpellHost(맞춤법 검사기/Harper/사전), setBibKeysProvider, extraExtensions/extraKeymap@codemirror/*, @replit/codemirror-vim, @replit/codemirror-emacs, katex
@oleafly/preview가상화된 pdf.js 뷰어(PdfViewer), SyncTeX 페이지 컨트롤러(gotoRect, pageClickToBp), pdf.js 워커, WebView 폴리필(@oleafly/preview/polyfills, main.tsx에서 가장 먼저 임포트)onOpenLink prop(시스템 브라우저 링크), setPdfLoggerpdfjs-dist
@oleafly/diagramTikZ, Typst(fletcher), Mermaid용 비주얼 다이어그램 작성기: React Flow 캔버스, 도형 인스펙터, 그리기/코드 탭, 컴파일-미리보기-삽입 흐름. src/languages/의 각 언어에는 작성기, 판독기, 삽입 및 파일 형식이 있으며, 캔버스와 코드가 양방향으로 맞춰집니다DiagramHost(LaTeX 컴파일, Typst와 Mermaid 렌더링, 파일 IO, 편집기 삽입, AI 수정), DiagramKit 컨텍스트(Button/Tooltip/Select/toast/theme)@xyflow/react, @codemirror/*, @oleafly/latex, @oleafly/editor
@oleafly/ai-tools프로젝트, 연구, 메모리, 그림 도구 정의. 변경 작업은 승인을 인식합니다AiToolsHost(파일, 컴파일, 심볼 인덱스, 그림 파이프라인, 편집기)@oleafly/latex
@oleafly/templates새 프로젝트 템플릿 갤러리(2단계 마법사, 카테고리, 미리보기, 진행률이 있는 일회성 에셋 다운로드)TemplatesHost(미리보기, 에셋 다운로드, 로깅), TemplatesKit(Button/Tooltip), 색 옵션은 props로UI 유틸리티 외에는 없음

엄격하게 강제되는 의존성 방향:

앱 셸과 패키지가 서로 의존하는 방식src/의 앱 셸에는 Zustand 스토어, UI 키트, Tauri 클라이언트, 포트 어댑터가 있습니다. 어댑터는 @oleafly 기능 패키지가 선언한 EditorHost 같은 포트를 구현하고, 앱이 이를 주입합니다. 기능 패키지는 latex, ai-core, registry 같은 순수한 리프 패키지에 의존할 수 있습니다. Tauri 클라이언트는 Tauri IPC로 Rust 백엔드와 통신합니다. 패키지는 앱, 앱의 스토어, Tauri API를 절대 import하지 않습니다.앱 셸, src/@oleafly/* 패키지스토어Zustand어댑터포트 구현UI 키트shadcnTauri 클라이언트src/lib/tauri.ts기능 패키지EditorHost 같은 포트 선언리프 패키지latex, ai-core, registryRust 백엔드src-tauri, crates/oleafly-agent주입Tauri IPC패키지는 앱, 앱의 스토어, Tauri API를 절대 import하지 않습니다.앱 셸과 패키지가 서로 의존하는 방식src/의 앱 셸에는 Zustand 스토어, UI 키트, Tauri 클라이언트, 포트 어댑터가 있습니다. 어댑터는 @oleafly 기능 패키지가 선언한 EditorHost 같은 포트를 구현하고, 앱이 이를 주입합니다. 기능 패키지는 latex, ai-core, registry 같은 순수한 리프 패키지에 의존할 수 있습니다. Tauri 클라이언트는 Tauri IPC로 Rust 백엔드와 통신합니다. 패키지는 앱, 앱의 스토어, Tauri API를 절대 import하지 않습니다.앱 셸, src/@oleafly/* 패키지스토어UI 키트Tauri 클라이언트어댑터Rust 백엔드src-tauri, crates기능 패키지포트를 선언리프 패키지latex, ai-core, registryTauri IPC주입패키지는 앱, 앱의 스토어,Tauri API를 절대 import하지 않습니다.
앱은 패키지에 의존하고, 패키지가 선언한 포트에 맞는 어댑터를 주입합니다. 패키지는 앱을 절대 import하지 않습니다.

패키지는 src/(@/…), Zustand 스토어, @tauri-apps/*, 앱의 UI 컴포넌트에서 절대 임포트하면 안 됩니다. 순수성 검사는 추출 위생의 일부입니다.

터미널 창
grep -rn 'from "@/\|@tauri\|zustand' packages/*/src # must return nothing

패키지는 인터페이스를 선언합니다. 앱은 그것을 한 번 구현하며, 보통 스토어를 감싸는 모듈 수준 어댑터 객체로 구현합니다. 반복해서 나타나는 형태는 네 가지입니다.

호스트 포트: 필요한 서비스를 담는 패키지당 interface XHost 하나입니다. 앱은 명령형 스토어 읽기로 어댑터를 만듭니다.

// src/lib/ai-tools.ts (app adapter, abridged)
const HOST: AiToolsHost = {
getProjectId: () => useFilesStore.getState().projectId,
readFileContent, // Tauri client passthrough
recompile: () => useCompileStore.getState().recompile(),
insertAtCursor, // editor controller
// ...
};
export const createOleaflyTools = (opts?) => createOleaflyToolsCore(HOST, opts);

앱에만 해당하는 관심사를 어댑터 안에 두면 의존성도 패키지 밖에 머뭅니다. 제공자 자격 증명과 요청은 Rust에 남으므로, 프런트엔드 패키지는 인터페이스가 요구하는 상태와 결과만 받습니다.

UI 키트: 구조적으로 타입이 지정된 컴포넌트를 담은 React 컨텍스트(또는 prop)입니다. Button: ComponentType<{ variant?: …; onClick?: () => void }> 같은 형태입니다. 더 작은 구조적 타입이 호환되므로 앱은 shadcn 컴포넌트를 그대로 넘깁니다. 키트에는 toast와 useThemeMode() 훅도 들어 있습니다.

훅 형태의 포트: 앱 상태를 구독해야 하는 패키지용입니다. 편집기의 EditorHost에는 훅 멤버(useActivePath(), useSettings())와 이벤트 리스너를 위한 명령형 짝(getActivePath())이 있습니다. 규칙은 이렇습니다. 호스트 객체는 모듈 수준이어야 하고(안정적인 정체성), use* 멤버는 훅을 무조건 호출해야 합니다.

모듈 싱글턴: 횡단 관심사를 잇는 접합부로, setPdfLogger(fn), setSpellHost(host), setBibKeysProvider(fn)에는 아무 일도 하지 않는 기본값이 있고 앱 심이 임포트 부수 효과로 설치합니다.

모든 추출은 예전 @/… 경로에 패키지를 다시 내보내는(또는 감싸는) 얇은 파일을 남겼으므로, 소비하는 쪽과 기존 vi.mock(...) 경로가 흔들린 적이 없습니다. 예를 들면 src/components/editor/cm/controller.ts(다시 내보내기), src/components/pdf/PdfViewer.tsx(셸 플러그인 링크 열기 기능을 주입하는 래퍼), src/lib/ai-tools.ts(어댑터 + 원래 팩토리 시그니처)입니다. 새 코드는 @oleafly/*를 직접 임포트해도 됩니다. 심은 호환성을 위해 있으며 권장 경로가 아닙니다.

@oleafly/registry는 기능이 셸이 알지 못하는 채로 앱 셸에 끼워지는 방식입니다. 기여 종류는 세 가지입니다.

  • 레일 탭(registerRailTab): 아이콘, 섹션(explore / review / assist, 비어 있지 않은 섹션 사이에 구분선이 그려집니다), 선택적인 when(ctx) 표시 조건, 선택적인 useBadge() 훅(예: Git 변경 개수), 사이드바 panel 컴포넌트.
  • 명령(registerCommand): surfaces에 따라 명령 팔레트(Cmd+K, group별로 묶임) 그리고/또는 옴니바(Cmd+P)에 제공됩니다. 레이블과 아이콘은 컨텍스트의 함수일 수 있습니다(예: “Switch to light theme”).
  • AI 툴셋(registerAiToolset): 채팅 모드("chat", "figure")를 툴셋 팩토리에 매핑합니다. 채팅 패널은 하나를 고정해 두지 않고 활성 모드의 툴셋을 찾아봅니다.

AppContext({ projectId, projectKind, theme })는 각 렌더링 화면이 자기 구독으로 만들므로 when/label 조건이 반응성을 유지합니다.

등록은 정적입니다. src/contributions/가 모든 내장 항목(탭, 명령, 툴셋)을 등록하고, main.tsx가 셸이 마운트되기 전에 registerContributions()를 한 번 호출합니다. Rail, Sidebar, CommandPalette, SearchOmnibar, ChatPanel은 등록된 것만 렌더링합니다.

기능의 탭이나 명령을 추가하려면 등록하면 됩니다. 셸을 편집할 필요가 없습니다.

// src/contributions/my-feature.tsx (called from src/contributions/index.ts)
registerRailTab({
id: "review", label: "Review", icon: MessageSquare,
section: "review", order: 55,
when: (ctx) => ctx.projectKind !== "image",
panel: ReviewPanel,
});
registerCommand({
id: "omnibar.review", surfaces: ["omnibar"], order: 45,
label: "Open review", keywords: "review comments",
run: () => useSettingsStore.getState().setRailTab("review"),
});

이것은 플러그인 SDK가 아니라 내부 API입니다. 형태는 자유롭게 바뀔 수 있고, 동적으로 로드되는 것은 없습니다. 실제 소비자가 생길 때까지 미뤄 둔 것은 내보내기 메뉴 형식, 설정 스키마, 프로젝트 종류별 동작을 기여로 만드는 일입니다.

패키지를 소스로 소비하므로 같은 별칭이 세 곳에 선언되어 있으며, 패키지를 추가할 때 세 곳 모두를 일치시켜야 합니다.

  1. tsconfig.json → compilerOptions.paths. @oleafly/preflight/pdf-extract 같은 하위 경로에는 명시적인 항목이 따로 필요합니다.
  2. vite.config.ts → resolve.alias. 문자열 별칭은 접두사로 일치하므로 하위 경로는 항목을 더 두지 않아도 해석됩니다.
  3. vitest.config.ts → 같은 resolve.alias, 그리고 test.include가 packages/**/*.test.ts를 포함해야 합니다. 그렇지 않으면 옮긴 테스트가 조용히 실행에서 빠집니다.

또한 루트 package.json은 각 패키지에 "workspace:*"로 의존하고, src/styles/globals.css에는 Tailwind v4가 클래스 이름을 찾도록 패키지 소스를 스캔하는 @source "../../packages"가 있습니다.

소스를 가리키는 별칭 덕분에 Vite 전용 임포트(?worker&url 워커, CSS 임포트)는 패키지 소스에서도 그대로 동작합니다. node 환경 테스트가 임포트하는 배럴에는 pdf.js를 끌고 오는 것이 들어가지 않게 하세요. pdf-extract와 preview 패키지의 뷰어가 DOM이 있는 곳에서만 접근 가능한 이유입니다.

  • 테스트는 소스 옆(packages/*/src/*.test.ts)에 있고 앱 테스트와 같은 pnpm test로 실행됩니다. 파일을 옮길 때는 테스트 개수를 지켜보세요.
  • 의존성은 느슨하게("react": "*") 선언합니다. pnpm이 루트와 중복을 제거하며, 실제 버전 하한은 루트에 있습니다. 패키지가 임포트하지 않는 의존성은 선언하지 마세요.
  • 패키지를 바꿀 때의 관문: pnpm exec tsc --noEmit, pnpm build, pnpm test(개수 변화 없음), 위의 순수성 grep.
  • 새로 추출하기: git mv로 파일을 옮기고(이력이 따라옵니다), 포트를 정의하고, 예전 경로에 심을 남기고, 세 별칭을 연결한 다음 관문을 실행하세요. 한창 다듬는 중인 기능은 추출하지 마세요.
  • 라이선스: 모든 패키지는 앱과 같은 AGPL-3.0-or-later입니다.

src/에는 진짜 프런트엔드 앱인 것이 모두 남습니다. Zustand 스토어, Tauri IPC 클라이언트(src/lib/tauri.ts), shadcn UI 키트, 레이아웃/셸 컴포넌트, 포트 어댑터와 심, src/contributions/, 그리고 본질적으로 스토어에 묶여 있는 기능 접합부(프리플라이트 패널 UI, 인라인 AI 편집 플러그인, extraExtensions로 편집기에 주입되는 코드 및 호버 인텔리전스)입니다. Tauri 명령과 운영 체제 통합은 src-tauri/에 남습니다. 제공자 클라이언트, 스트리밍, 에이전트 오케스트레이션은 crates/oleafly-agent/에 속하므로, 시크릿과 제공자 요청은 웹뷰 밖에 있습니다.

이미지 미리보기

AI 답변을 불러오지 못했습니다

다시 시도하거나 문서를 검색하세요.