5.3 KiB
Monorepo Refactoring Plan: Decoupling Designer, Engines, and Materials
This document outlines the step-by-step plan to refactor the current form-designer monolithic structure into a clean, decoupled Monorepo architecture.
Core Philosophy: Separation of Concerns. The Designer acts as a shell, Engines enforce logic, and Materials provide UI implementation.
🏗 Architecture Overview
| Layer | Package | Responsibility |
|---|---|---|
| Foundation | @lingshu/types |
Shared interfaces, DSL definitions, and constants. |
@lingshu/core-utils |
Pure JS utilities (framework agnostic). | |
| Logic Engines | @lingshu/user-script |
Runtime for standard User Scripts (Hooks, Actions) and Rule Engine (Low-code logical linkage). |
| Materials | @lingshu/widget-pc |
PC Vue components, widget configurations, and property panels. |
@lingshu/widget-mobile |
Mobile Vue components (Future). | |
| Host/Editor | @lingshu/form-designer |
The visual editor shell. Implements adapters for engines and exposes extension points for materials. |
| Application | apps/lcdp |
The assembler entry point that wires everything together. |
📅 Phased Execution Plan
The refactoring is divided into 3 autonomous phases. At the end of each phase, the project MUST be buildable and runnable.
Phase 1: Foundation Construction (Infrastructure)
Goal: Establish a shared language (types) to break circular dependencies and prepare for code migration.
- Boundary: No functional logic changes. Only moving definitions and constants.
- Deliverable: Codebase uses
@lingshu/typesfor shared entities instead of relative imports. - Verification:
pnpm buildpasses for all packages.npm run devinform-designerworks exactly as before.
Tasks:
- Define Shared Types: Extract
WidgetSchema,UserScriptContext,RuntimeEventApiBOto@lingshu/types. - Define Injection Keys: Extract
USER_SCRIPT_EVENT_BUS_KEYto@lingshu/types. - Refactor Imports: Update
form-designerandwidget-pcto import from@lingshu/types.
Phase 2: Logic Engine Extraction (The Brain)
Goal: Isolate the "User Script" and "Rule Engine" logic into a standalone package that doesn't depend on UI stores directly.
- Boundary:
packages/user-scriptcontains pure logic. Access to Store/API is done via Dependency Injection. - Deliverable: A new
@lingshu/user-scriptpackage.form-designerinitializes this engine by injecting its internal state adapters. - Verification:
- User Scripts (e.g.,
onClicklogs) work in the Designer preview. - Rule Linkages (e.g., input A changes -> input B hides) work in the Designer preview.
- User Scripts (e.g.,
Tasks:
- Create Package: Set up
packages/user-scriptworkspace. - Migrate Logic: Move
src/config/user-script(Definitions) andsrc/utils/user-script(Runtime) to the new package. - Refactor for DI: Transform functions that directly import
PageStoreto acceptcontextoradapterarguments. - Wire Up: In
form-designer/main.ts(or boot sequence), callinitScriptEngine(adapters)from the new package.
Phase 3: Material Decoupling (The Body)
Goal: Move all PC-specific widget configurations and implementations out of the Designer.
- Boundary:
form-designerbecomes unaware of specific widgets. It loads whatever is passed to its.use()method. - Deliverable:
packages/widget-pccontains all material definitions (config/*) and components components. - Verification:
- Designer starts empty initially (theoretically).
- After injecting
WidgetPC, the left palette shows PC components. - Dragging components to canvas works correctly.
Tasks:
- Move Configs: Relocate
form-designer/src/config(Materials, Settings) topackages/widget-pc/src/config. - Refactor Hooks: Move specific widget hooks (like
components/anchor/useCompEvent) towidget-pc. - Plugin Architecture: create a
registerWidgetsexport inwidget-pc. - Injection: In the App Entry/Main, import
registerWidgetsand pass it to the Designer instance.
🔄 Dependency Injection Strategy (Crucial)
To achieve decoupling, we use standard Dependency Injection.
1. Service Injection (Logic Layer)
Instead of importing axios or pinia in user-script:
// packages/user-script/src/index.ts
export function initEngine(context: IScriptContext) {
// context.network.request(...)
// context.store.getFieldValue(...)
}
2. Event Injection (Material Layer)
Widgets use inject to communicate with the engine, without importing it.
// packages/widget-pc/src/hooks/useCompEvent.ts
const bus = inject(USER_SCRIPT_EVENT_BUS_KEY);
bus.emit("CLIENT_EVENT", payload);