dual-edition-module-migration
Dual Edition Module Migration
When to apply
- A module must exist in both open-source and enterprise editions
- The user mentions
src/opensource,enterprise/, Vite overlay, or edition-specific behavior - A shared module needs to stay under
src/opensource/..., but enterprise behavior must override it - Old
src/variant/...orsrc/opensource/variant/...facades need to be removed - A migration must preserve the
src/opensourceimport boundary
Core invariants
1. src/opensource is the shared source of truth
- Code under
src/opensource/is the default implementation src/opensource/must not import commercial code- If open-source needs a dependency, migrate that dependency too or degrade the feature
- If a file under
src/opensource/orenterprise/src/opensource/imports a module outside theopensourcetree, move that dependency into the currentopensourceclosure instead of keeping a shim - Do not pull private infrastructure such as
keewoodClientorteamshareClientintosrc/opensource
2. enterprise/ is an overlay, not a second app
- Keep the same relative path under
enterprise/as the shared file undersrc/ - Runtime resolution priority is
enterprise/**first, thensrc/** - Enterprise files should be as thin as possible
- Prefer a re-export shim in
enterprise/...when it preserves behavior without duplication - Do not use re-export shims when the stable caller path is already
@/opensource/...and the shim would import from outside theopensourcetree - If a dependency chain is truly enterprise-only, keep the whole chain in
enterprise/... - Do not leave enterprise-only stores or services in
srcjust because enterprise files import them - Do not create an enterprise overlay file that re-imports the same logical
@/opensource/...path it overrides - If the stable caller path is
@/opensource/..., the enterprise implementation must live underenterprise/src/opensource/... - Do not move a dual-edition module into
enterprise/components/...or another non-mirrored path unless the user explicitly wants a commercial-only module
3. Open-source callers should import the real shared path
- Files under
src/opensource/should import@/opensource/...directly - Do not keep
@/variant/...in open-source code - If enterprise needs different behavior for that shared path, add an overlay file under
enterprise/src/opensource/...
4. Choose extension point by artifact type
Use this rule first:
| Artifact | Preferred mechanism |
|---|---|
| React view/component rendered through factory | ComponentFactory default in src/opensource plus enterprise or premium override where still needed |
| Hook / service / preload helper imported from open-source code | Direct @/opensource/... import plus enterprise/src/opensource/... overlay when behavior differs |
| Shared implementation used by both editions | Move to src/opensource/...; if callers already target @/opensource/..., migrate the real code and delete the old file instead of leaving a compatibility shim |
Legacy src/components/... module becoming dual-edition |
Migrate to src/opensource/... plus mirrored enterprise/src/opensource/...; then switch callers to @/opensource/... |
| Runtime-only indirection | src/runtime/<domain>/ only when direct imports and overlay cannot express the binding |
Architecture patterns
Pattern A: Shared baseline plus enterprise overlay
Use this for hooks, services, helpers, and other non-visual modules.
- Keep the default implementation in
src/opensource/... - Update callers to import that
@/opensource/...path directly - If enterprise behavior differs, add the same path under
enterprise/src/opensource/... - Let Vite resolve the enterprise file first at runtime
- Remove the old
variantfacade after callers are switched
Minimal example:
// src/opensource/components/business/RecordingSummary/hooks/useCancelRecord.tsx
export default function useCancelRecord() {
// shared implementation
}
// enterprise/src/opensource/components/business/RecordingSummary/hooks/useCancelRecord.tsx
export default function useCancelRecord() {
// enterprise implementation
}
import useCancelRecord from "@/opensource/components/business/RecordingSummary/hooks/useCancelRecord"
Pattern B: Component override
Use this when a UI component needs edition-specific rendering.
- Keep the default implementation in
src/opensource/... - Register it in
src/opensource/components/ComponentRender/config/defaultComponents.tsx - Keep commercial-only UI in
src/...only when it owns real delta - Register premium overrides only where the factory still needs explicit component replacement
- For helper imports such as preload code, prefer direct
@/opensource/...imports plus enterprise overlay
This keeps render-time switching and non-render imports aligned.
Pattern B.5: Shared facade plus enterprise child override
Use this when the only edition difference is a small injected block inside a much larger shared page or layout.
Rules:
- Extract the changing block into the smallest possible shared child component or hook under
src/opensource/... - Keep the shared baseline minimal:
null,false, no-op callback, or another tiny safe fallback - Mirror only that child component or hook under
enterprise/src/opensource/... - Update the large shared page or layout to import the stable
@/opensource/...facade instead of importing commercial modules directly - Do not override the whole page or layout when a banner, modal, slogan, model tag, or hook is the real variability point
Why:
- It minimizes duplicated page code
- It prevents accidental divergence between editions
- It makes review easier because the commercial delta is isolated to one extension point
Example use cases:
- Activity banner slot in
EmptyWorkspacePanel - Activity modal slot in
MainLayout - Login slogan slot in
SSOLayout - Enterprise-only hook such as
useFreePointsTrigger
Pattern C: Temporary enterprise shim over existing commercial file
Use this only as a short-lived transition when a large enterprise implementation already exists under src/... and the user has not required a hard opensource-only closure.
- Add
enterprise/src/opensource/<same-path>.ts(x) - Re-export from the existing commercial file under
src/... - Switch all shared callers to
@/opensource/... - Remove
variantfiles - Later, inline or fold the shim if the codebase is cleaned up further
Do not use this pattern when the user requires src/opensource/... and enterprise/src/opensource/... to avoid importing any dependency outside the opensource tree.
Pattern D: Enterprise-only dependency closure
Use this when a module and its dependencies only exist for enterprise or App-native behavior.
Rules:
- Move the whole closure into
enterprise/src/opensource/... - Do not add
src/opensourcestubs for enterprise-only stores or services unless the user explicitly wants a temporary bridge - Update enterprise callers to use the stable
@/opensource/...path, so overlay resolves the real file - Delete the old
src/...files once imports are switched
Example closure:
stores/recordingSummary/appNative.tsservices/recordSummaryAppNativeService/**services/AppAIRecordingService/**
Why:
- Leaving half the chain in
srcrecreates the old boundary violation - Enterprise-only dependencies should not look shared just because shared callers reference the logical path
Pattern E: Enterprise-only module with open-source stub
Use when a hook or component is moved entirely to enterprise/src/opensource/..., but src/opensource callers still import it (e.g. overlay disabled, or callers exist in both editions).
Rules:
- Keep a stub in
src/opensource/...at the same logical path - Stub returns no-op values: empty array for lists,
falsefor flags,() => {}for callbacks,nullfor ReactNode - Stub must match the enterprise interface exactly so all callers work without changes
- Enterprise overlay provides the full implementation when overlay is enabled
Stub minimization rules:
- Keep only the smallest surface that current open-source callers actually import
- Do not copy enterprise-only helper files, upload types, or private dependency adapters into
src/opensource/...if the stub does not use them - Prefer flattening a stub to one file when the open-source side only needs a no-op export plus one or two lightweight enums or helpers
- If a helper already exists globally, reuse it instead of creating a module-local duplicate in the stub
Example:
// src/opensource/.../useCollaboratorUpdatePanel.tsx (stub)
function useCollaboratorUpdatePanel({ selectedProject: _sp, onClose: _onClose }) {
return {
collaborators: [],
collaborationInfo: { is_collaboration_enabled: false, default_join_permission: "viewer" },
openManageModal: () => {},
CollaboratorUpdatePanel: null,
}
}
// enterprise/src/opensource/.../useCollaboratorUpdatePanel.tsx (full impl)
// Full implementation with CollaborationManageModal, useCollaborationManageData, etc.
Pattern F: Optional runtime facade
Use only when direct imports plus overlay are still not enough.
Rules:
- Create
src/runtime/<domain>/ - Export a stable runtime API from that layer
- Keep
src/opensource/off that runtime facade unless the user explicitly wants the extra indirection - Justify the runtime layer with a real binding problem, not habit
Enterprise overlay import resolution
When creating an enterprise overlay file (e.g. enterprise/src/opensource/.../ProjectCardContainer), relative imports like ../ProjectCard may fail TypeScript resolution if the target has no enterprise overlay (only partial overlay like ProjectCardShareSection).
Rule: use @/opensource/... absolute paths for modules that exist only in src or have no full enterprise overlay, so resolution falls through to src correctly.
Migration workflow
0. Find all importers before migration
Before moving or stubbing a module, run rg "ModuleName|useModuleName" (or equivalent) to find every caller.
Rule: each importer must either (a) get the stub when overlay is disabled, or (b) be updated to conditionally use the feature. Missing an importer causes build failures such as EISDIR or "module not found".
0.5. Choose the final logical path first
Before creating files, decide which import path should survive after migration.
Ask:
- Should callers end up on
@/opensource/...? - Is this actually a commercial-only module that should stay under
@/components/...? - Does the enterprise implementation need to mirror
src/opensource/...underenterprise/src/opensource/...?
Rule: do not start by copying code into enterprise/components/... when the long-term target is a dual-edition @/opensource/... path.
0.6. Choose the smallest override surface
Before creating any overlay file, ask:
- Is the real delta a whole page, or only one injected child block?
- Can the shared page keep working if that child becomes a
nullor no-op facade? - Would extracting a shared banner/modal/slogan/tag/hook avoid duplicating the page?
Rule: if the delta is only a small block inside a large shared file, first extract that block into a shared src/opensource/... facade and override only that facade in enterprise/src/opensource/....
1. Classify the target from first principles
Split the target into:
- Shared baseline
- Enterprise-only delta
- Open-source forbidden dependencies
- UI override points
- Helper or preload entrypoints
- Old
variantfacades that can be removed - Enterprise-only dependency closures such as native stores and native services
Ask:
- What must exist in both editions?
- What is truly enterprise-only?
- Which imports inside
src/opensourcestill point at indirection instead of the real shared file? - Can Vite overlay solve this directly with a mirrored enterprise path?
- Are there forbidden clients or App-only branches that must stay out of
src/opensource?
2. Keep the baseline in src/opensource
- Move or keep the shared implementation in
src/opensource/... - Rewrite internal imports to
@/opensource/... - If an internal import still points outside
opensource, migrate that dependency into the same closure or inline the needed code before proceeding - Remove App-only or enterprise-only branches from the shared copy
- Add safe stubs or degrade behavior instead of importing private clients
3. Preserve enterprise deltas with mirrored paths
- Add
enterprise/src/opensource/...only for real behavior differences - Prefer thin enterprise shims only when they do not violate the
opensourceboundary - If the enterprise implementation already lives in
src/...but callers must stay insideopensource, migrate the real implementation or the required subset into the mirroredenterprise/src/opensource/...path - If a dependency chain is enterprise-only, move the full chain instead of splitting it across
srcandenterprise
4. Remove variant
When old code uses @/variant/...:
- Replace each caller with the corresponding
@/opensource/...import - Create
enterprise/src/opensource/...overlays for any enterprise-only behavior - Delete
src/variant/... - Delete
src/opensource/variant/...
5. Verify boundaries
For the migrated domain, check:
src/opensource/...only imports shared-safe dependencies- No
src/opensourcefile imports@/variant/... - Enterprise overrides mirror the shared path under
enterprise/src/opensource/... src/opensourcestill avoids private clients and private infrastructure- Preload helpers import the same
@/opensource/...path that runtime code conceptually depends on - Enterprise-only stores and services do not remain in
src - If a shared initializer still needs enterprise-only restore logic, split the initializer: shared file keeps shared logic, enterprise overlay re-adds the enterprise branch
6. Verify compatibility
- Shared imports still work in open-source mode
- Enterprise runtime resolves overlay files as expected
- Existing explicit component overrides still work if they remain in use
- No leftover imports reference deleted
variantpaths
Boundary checklist
-
src/opensourcedoes not import commercial modules directly -
src/opensourcedoes not import@/variant/... - Shared dependencies needed by the migrated module are available under
src/opensource - Enterprise-only behavior is mirrored under
enterprise/src/opensource/... - Enterprise-only stores/services are not left behind in
src - Runtime-only facades are used only when direct imports plus overlay are insufficient
- Private clients such as
keewoodClientandteamshareClientare not introduced intosrc/opensource - Files under
src/opensourceandenterprise/src/opensourcedo not import modules outside theopensourcetree unless the user explicitly approves a temporary exception
Verification checklist
- Find all importers of the target module with
rgbefore migration - Run targeted lint checks for touched files
- Run focused tests for changed logic when available
- Run build verification and fix missing mirrored dependencies iteratively
- Search for leftover
@/variant/...imports - Confirm enterprise overlay paths match the shared path exactly
- Search for leftover imports to deleted enterprise-only source paths under
src - Check initializers and lazy imports for unresolved paths after moving enterprise-only modules
- If overlay is disabled: ensure open-source stubs exist and match the enterprise interface
- Search for leftover imports to the pre-migration commercial path such as
@/components/business/<module>
Common pitfalls
Pitfall 1: Keeping variant after the overlay exists
This preserves dead indirection and makes the import graph harder to reason about.
Rule: if direct @/opensource/... plus mirrored enterprise/... works, delete variant.
Pitfall 2: Letting src/opensource import commercial code
This breaks the hard boundary.
Rule: only enterprise/... may point back to commercial implementations as migration shims.
Pitfall 3: Forgetting helper imports
Render-time code may be correct while preload or utility code still points at old facades.
Rule: migrate preload helpers and utility imports together with the main implementation.
Pitfall 4: Copying enterprise code blindly
Large duplicate files increase drift immediately.
Rule: prefer a thin enterprise re-export shim first, then refactor if needed.
Pitfall 4.5: Leaving old commercial files behind after callers switch
This invites regressions because new code can accidentally import the pre-migration path again.
Rule: once callers are updated to @/opensource/..., delete the old src/components/... source files for that migrated closure.
Pitfall 5: Smuggling private clients into open-source
If a feature depends on private infrastructure, the shared layer should not import it.
Rule: degrade or stub the open-source path instead of copying the client.
Pitfall 6: Keeping enterprise-only stores or services in src
This makes the import graph look shared when the implementation is not actually shared.
Rule: if the module only exists for enterprise/App-native behavior, move the full chain into enterprise/src/opensource/....
Pitfall 7: Forgetting initializer split after moving enterprise-only services
A shared initializer may still dynamically import files that were intentionally removed from src.
Rule: keep the shared initializer web-safe, and add an enterprise overlay initializer when native restore logic must remain.
Pitfall 8: Making an overlay file import itself logically
If enterprise/src/opensource/foo.ts re-exports from @/opensource/foo, overlay resolution points back to the same enterprise file.
Rule: put the real enterprise implementation in the overlay file, or use a temporary non-overlay source path only during an explicit short-lived migration step.
Pitfall 9: Overlay disabled but modules moved to enterprise only
vitePluginEnterpriseOverlay may be commented out; the build then uses only src. Enterprise overlay files are never loaded.
Rule: when moving hooks/components to enterprise/src/opensource/... only, keep open-source stubs in src/opensource/... that satisfy all imports. Otherwise the build fails (e.g. EISDIR: illegal operation on a directory when importing a deleted module).
Pitfall 10: Stub interface mismatch
Open-source stubs must return the exact same interface as the enterprise implementation.
Rule: hooks must return the same shape (e.g. collaborators: [], collaborationInfo: { ... }, openManageModal: () => {}, CollaboratorUpdatePanel: null). Components must export the same default and accept the same props.
Pitfall 11: Putting the enterprise file under the wrong root
If callers are meant to import @/opensource/..., placing the real file under enterprise/components/... breaks the mirrored overlay model and leaves the import graph inconsistent.
Rule: dual-edition overlays for shared modules belong under enterprise/src/opensource/....
Pitfall 12: Overbuilding the open-source stub
Copying all enterprise files, types, and helpers into src/opensource/... increases drift and makes the stub look like a real shared implementation when it is not.
Rule: keep the stub minimal. Export only what open-source callers need right now.
Pitfall 13: Duplicating generic helpers inside the migrated module
A local helper such as utils/env.ts may duplicate an existing global utility and create extra cleanup work during migration.
Rule: prefer shared utilities like @/opensource/utils/env when they already express the same behavior.
Pitfall 14: Putting the real implementation into src/opensource by mistake
This usually happens when the user says "enterprise only" or "open-source should not include this", but the migration starts from caller paths instead of feature ownership.
Rule:
src/opensource/...may contain only the shared facade or no-op stub for that feature- The real enterprise implementation must live under the mirrored
enterprise/src/opensource/...path - If the user explicitly says the open-source edition must not include the feature, do not put visible runtime behavior into
src/opensource/...
Pitfall 15: Overriding a whole page for one small delta
Copying an entire page or layout just to swap one banner or modal creates unnecessary drift immediately.
Rule: extract the variable block into a dedicated shared child component or hook first, then override only that child in enterprise/src/opensource/....
RecordingSummary reference
Use RecordingSummary as the reference implementation:
src/opensource/...is the shared baseline- Open-source callers import
@/opensource/...directly - Enterprise-only
useCancelRecordanduseIsCurrentRecordingbehavior is exposed throughenterprise/src/opensource/... - App-native store and native recording services live only under
enterprise/src/opensource/... initRecordSummaryServiceis split so shared code keeps only web restore logic and enterprise overlay restores native logicpreloadRecordSummaryEditorPanelno longer needs avariantfacade- Old
src/variant/...andsrc/opensource/variant/...files are deleted after migration
Output expectations
When using this skill, produce:
- A short architecture decision: direct shared import vs enterprise overlay vs component override vs runtime facade
- A migration plan grouped by shared baseline, enterprise deltas, and forbidden dependencies
- The concrete file changes
- Verification status for lint, tests, and build
Keep it minimal
- Prefer the smallest boundary-preserving change
- Prefer enterprise shims over duplicated enterprise copies
- Prefer direct
@/opensource/...imports over extra facades - Do not migrate unrelated historical boundary violations unless the user expands scope
Additional resources
- For concrete decision examples, see examples.md
More from dtyq/magic
find-skill
Search and install skills from the platform skill library, skill market, or skillhub. Use when the agent needs to find or install a skill to expand its capabilities. Always search the platform first; fall back to skillhub only if nothing is found.
18ui-data-testid
Add stable `data-testid` attributes by default for new or refactored UI components. Use when implementing React/TSX views, shadcn/antd-style components, dropdown/menu configs, or interactive UI flows that need reliable selectors for unit/E2E tests.
17analyzing-data-html-report
Data analysis report development skill. Use when users need to develop data analysis reports, create analysis report projects, build static HTML analysis documents, or produce one-time analysis reports with visualization.
16skill-vetter
Security-first skill vetting protocol for AI agents. Use before installing any skill from the platform skill market, skillhub, GitHub, or other sources. Checks for red flags, permission scope, and suspicious patterns to determine whether a skill is safe to install.
16wiki-generator
通过分析代码结构与依赖,自动提取系统总体架构、核心特有功能模块的实现细节,并生成互相关联的多页面 Wiki 结构文档集。作为项目的架构知识库守护者,还负责在日常开发中解答疑问,并在架构变更时自动维护文档。
15env-manager
Manage persistent environment variables. Use when the user provides API keys or other configuration values that need to be saved and reused across sessions.
14