@unlockable/vite-plugin-unlock 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Olivier Belaud
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # @unlockable/vite-plugin-unlock
2
+
3
+ A Vite plugin that lets you override files from any npm package without forking it.
4
+
5
+ Drop a file with the same name as a module from the target package in your overrides directory. The plugin intercepts Vite's module resolution and serves your file instead of the original. At build time and dev time, with full HMR.
6
+
7
+ Beyond simple file replacement, the plugin supports a **patch system** that can transform target files using config files, enabling deeper customizations like rewriting the navigation menu of a dashboard without touching the original source.
8
+
9
+ ## Status
10
+
11
+ > **v0.1.0** - Early release. The plugin is functional and used in production, but the API may evolve.
12
+
13
+ This plugin was born from a concrete need: customizing the [Medusa](https://medusajs.com) admin dashboard for B2B clients without forking it.
14
+
15
+ Medusa's admin is a full React application: pages, components, hooks, layouts. When you need to rewrite an entire page, add columns to a table, change how sorting works, or customize the sidebar menu, the only option was to fork `@medusajs/dashboard` and maintain your own copy. That means losing the connection with upstream updates.
16
+
17
+ Instead of forking, this plugin intercepts Vite's module resolution to replace specific files at build time. The result: you keep the original package as a dependency, override only what you need, and stay on the upgrade path.
18
+
19
+ **What works today (Medusa):**
20
+ - Rewrite entire pages (orders list, product detail, any route)
21
+ - Rewrite individual files: components, hooks, or any `.ts`/`.tsx` file
22
+ - Rewrite the sidebar menu from a config file
23
+
24
+ **What's coming next:**
25
+
26
+ More override capabilities are landing in the next few days. Stay tuned.
27
+
28
+ The plugin is designed to work with any Vite-based project, but Medusa is the only ecosystem where it has been thoroughly tested so far. If you try it with another framework (Strapi, or even a utility library), feedback is very welcome.
29
+
30
+ ## Table of Contents
31
+
32
+ - [Using with Medusa](#using-with-medusa)
33
+ - [Generic Usage](#generic-usage)
34
+ - [How It Works](#how-it-works)
35
+ - [Options Reference](#options-reference)
36
+ - [License](#license)
37
+
38
+ ---
39
+
40
+ ## Using with Medusa
41
+
42
+ The Medusa preset provides zero-config setup for overriding `@medusajs/dashboard`. It handles entry point unbundling, CSS optimization, HMR boundaries, and menu patching out of the box.
43
+
44
+ ### Install
45
+
46
+ ```bash
47
+ npm install @unlockable/vite-plugin-unlock --save-dev
48
+ ```
49
+
50
+ ### Setup
51
+
52
+ Add the plugin to your `medusa-config.ts`:
53
+
54
+ ```ts
55
+ // medusa-config.ts
56
+ import { unlock } from "@unlockable/vite-plugin-unlock"
57
+ import { medusa } from "@unlockable/vite-plugin-unlock/medusa"
58
+
59
+ module.exports = defineConfig({
60
+ // ...
61
+ admin: {
62
+ vite: () => ({
63
+ plugins: [unlock(medusa())],
64
+ }),
65
+ },
66
+ })
67
+ ```
68
+
69
+ By default, the preset looks for override files in `./src/admin/overrides`. You can change this:
70
+
71
+ ```ts
72
+ unlock(medusa({
73
+ overrides: "./src/admin/my-custom-folder",
74
+ debug: true, // logs which files are being overridden
75
+ }))
76
+ ```
77
+
78
+ ### Override a Page
79
+
80
+ To rewrite an entire page, create a file in your overrides directory with the **same filename** as the original page file from `@medusajs/dashboard`.
81
+
82
+ For example, to replace the orders list page:
83
+
84
+ ```
85
+ src/admin/overrides/
86
+ order-list.tsx <- replaces the original order-list.tsx from @medusajs/dashboard
87
+ ```
88
+
89
+ Page overrides must export `{ Component }` for React Router's lazy loading:
90
+
91
+ ```tsx
92
+ // src/admin/overrides/order-list.tsx
93
+ import { Container, Heading } from "@medusajs/ui"
94
+
95
+ const OrderList = () => {
96
+ return (
97
+ <Container>
98
+ <Heading level="h1">Custom Orders Page</Heading>
99
+ {/* Your custom implementation */}
100
+ </Container>
101
+ )
102
+ }
103
+
104
+ export { OrderList as Component }
105
+ ```
106
+
107
+ ### Override a Component
108
+
109
+ Same principle, match the filename:
110
+
111
+ ```
112
+ src/admin/overrides/
113
+ order-customer-section.tsx <- replaces order-customer-section.tsx
114
+ avatar-box.tsx <- replaces avatar-box.tsx
115
+ ```
116
+
117
+ Component overrides are regular React components. No special export convention needed (unlike pages).
118
+
119
+ In your override files, you can import from the original dashboard source using the `~dashboard` alias:
120
+
121
+ ```tsx
122
+ // src/admin/overrides/order-customer-section.tsx
123
+ import { useOrder } from "~dashboard/hooks/api/orders"
124
+ import { Container, Heading, Text } from "@medusajs/ui"
125
+
126
+ export const OrderCustomerSection = ({ order }: { order: any }) => {
127
+ return (
128
+ <Container>
129
+ <Heading level="h2">Customer Info</Heading>
130
+ <Text>{order.customer?.email}</Text>
131
+ {/* Your custom layout */}
132
+ </Container>
133
+ )
134
+ }
135
+ ```
136
+
137
+ ### Override a Hook
138
+
139
+ Same approach. If the dashboard has `use-order-table-columns.tsx`, drop a file with the same name:
140
+
141
+ ```
142
+ src/admin/overrides/
143
+ use-order-table-columns.tsx <- replaces the original hook
144
+ ```
145
+
146
+ ### Organizing Overrides
147
+
148
+ Override files are scanned recursively. You can organize them in subdirectories to keep things tidy:
149
+
150
+ ```
151
+ src/admin/overrides/
152
+ pages/
153
+ order-list.tsx
154
+ product-detail.tsx
155
+ components/
156
+ order-customer-section.tsx
157
+ avatar-box.tsx
158
+ hooks/
159
+ use-order-table-columns.tsx
160
+ menu.config.ts
161
+ ```
162
+
163
+ The directory structure doesn't matter for matching. The plugin matches by **filename only**, regardless of how deep the file is nested.
164
+
165
+ ### Customize the Sidebar Menu
166
+
167
+ Create a `menu.config.ts` file in your overrides directory to fully rewrite the sidebar navigation:
168
+
169
+ ```ts
170
+ import type { MenuConfig } from "@unlockable/vite-plugin-unlock/medusa"
171
+ import { ShoppingCart, Buildings, Tag } from "@medusajs/icons"
172
+
173
+ const config: MenuConfig = {
174
+ items: [
175
+ { icon: ShoppingCart, label: "Orders", to: "/orders" },
176
+ { icon: Buildings, label: "Companies", to: "/companies" },
177
+ { icon: Tag, label: "Products", to: "/products" },
178
+ ],
179
+ }
180
+ export default config
181
+ ```
182
+
183
+ > Patch mode (add/remove individual items) and function mode (programmatic control) are being finalized and will be available in an upcoming release.
184
+
185
+ ### How Matching Works
186
+
187
+ The plugin scans all source files in `@medusajs/dashboard/src/` and builds a filename index. When you add a file to your overrides directory, it matches by **basename** (filename without path). If `order-list.tsx` exists anywhere in the dashboard source, your `order-list.tsx` override replaces it.
188
+
189
+ This means you don't need to replicate the directory structure of the original package. Just the filename.
190
+
191
+ ### What the Medusa Preset Does Under the Hood
192
+
193
+ - **Entry redirect**: Remaps `@medusajs/dashboard/dist/app.mjs` to `src/app.tsx` so Vite compiles from source instead of the bundled dist
194
+ - **CSS redirect**: Points CSS imports to the pre-built `dist/app.css` to avoid Tailwind reprocessing (~2-3s saved per HMR update)
195
+ - **HMR boundaries**: Injects `import.meta.hot.accept()` in files using `defineRouteConfig` or `defineWidgetConfig` to prevent full reloads
196
+ - **Menu patching**: Transforms `main-layout.tsx` at build time to inject your `menu.config.ts`
197
+
198
+ ---
199
+
200
+ ## Generic Usage
201
+
202
+ The plugin works with any npm package that ships source files (or has them accessible in `node_modules`).
203
+
204
+ ### Install
205
+
206
+ ```bash
207
+ npm install @unlockable/vite-plugin-unlock --save-dev
208
+ ```
209
+
210
+ ### Setup
211
+
212
+ ```ts
213
+ // vite.config.ts
214
+ import { unlock } from "@unlockable/vite-plugin-unlock"
215
+
216
+ export default defineConfig({
217
+ plugins: [
218
+ unlock({
219
+ targets: ["@acme/dashboard"],
220
+ overrides: "./src/overrides",
221
+ }),
222
+ ],
223
+ })
224
+ ```
225
+
226
+ ### Create Override Files
227
+
228
+ ```
229
+ src/overrides/
230
+ Button.tsx <- replaces Button.tsx from @acme/dashboard
231
+ useTheme.ts <- replaces useTheme.ts from @acme/dashboard
232
+ ```
233
+
234
+ ### Import Aliases
235
+
236
+ Each target gets an auto-generated alias so your override files can import from the original source:
237
+
238
+ | Package | Alias |
239
+ |---------|-------|
240
+ | `@acme/dashboard` | `~dashboard` |
241
+ | `@acme/ui` | `~ui` |
242
+ | `my-lib` | `~my-lib` |
243
+
244
+ ```tsx
245
+ // In an override file:
246
+ import { cn } from "~dashboard/lib/utils"
247
+ ```
248
+
249
+ ### Multi-Target
250
+
251
+ Override files from multiple packages:
252
+
253
+ ```ts
254
+ unlock({
255
+ targets: ["@acme/dashboard", "@acme/ui"],
256
+ overrides: "./src/overrides",
257
+ })
258
+ ```
259
+
260
+ Use **namespaced overrides** to avoid filename conflicts:
261
+
262
+ ```
263
+ src/overrides/
264
+ @acme/dashboard/Button.tsx <- only overrides @acme/dashboard
265
+ @acme/ui/Button.tsx <- only overrides @acme/ui
266
+ Header.tsx <- overrides in any target
267
+ ```
268
+
269
+ ### Skip Marker
270
+
271
+ Files and directories starting with `_` are ignored:
272
+
273
+ ```
274
+ src/overrides/
275
+ Button.tsx <- active override
276
+ _archive/Old.tsx <- ignored
277
+ ```
278
+
279
+ ---
280
+
281
+ ## How It Works
282
+
283
+ 1. **Scan**: On startup, scans the target package's `src/` directory and indexes all source files by basename.
284
+ 2. **Match**: Scans your overrides directory (recursively). Any file whose basename matches a target file becomes an active override.
285
+ 3. **Resolve**: During Vite's module resolution (`resolveId` and `load` hooks), imports pointing to overridden files are redirected to your override files.
286
+ 4. **HMR**: Content edits in override files trigger Vite's native HMR (React Fast Refresh). Adding or removing override files triggers a full reload.
287
+
288
+ ---
289
+
290
+ ## Options Reference
291
+
292
+ ```ts
293
+ unlock({
294
+ // Required: packages to unlock
295
+ targets: [
296
+ "@acme/dashboard",
297
+ // or with full config:
298
+ {
299
+ package: "@acme/dashboard",
300
+ alias: "~dashboard", // import alias (auto-generated if omitted)
301
+ srcDir: "src", // source subdirectory (default: "src")
302
+ entryRedirect: { // remap dist entry -> source entry
303
+ from: "dist/app.mjs",
304
+ to: "src/app.tsx",
305
+ },
306
+ hmr: {
307
+ cssRedirect: { // rewrite CSS import in entry
308
+ from: "./index.css",
309
+ to: "../dist/app.css",
310
+ },
311
+ entryBoundary: true, // inject HMR boundary at entry
312
+ },
313
+ },
314
+ ],
315
+
316
+ // Override directory (default: "./src/overrides")
317
+ overrides: "./src/overrides",
318
+
319
+ // Match strategy (default: "basename")
320
+ match: "basename", // or "path"
321
+
322
+ // Multi-target conflict handling (default: "error")
323
+ onConflict: "error", // "warn" or "first"
324
+
325
+ // Debug logging (default: false)
326
+ debug: false,
327
+
328
+ // Content patterns that trigger HMR boundary injection
329
+ hmrBoundaries: ["defineRouteConfig"],
330
+
331
+ // Patches: modify target files using config files
332
+ patches: [{
333
+ target: /layout\.tsx$/,
334
+ configFile: "layout.config",
335
+ apply(code, configPath) {
336
+ return `import config from "${configPath}";\n` + code
337
+ },
338
+ }],
339
+ })
340
+ ```
341
+
342
+ ---
343
+
344
+ ## License
345
+
346
+ MIT - [Olivier Belaud](https://olivierbelaud.dev)