@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 +21 -0
- package/README.md +346 -0
- package/dist/index.cjs +809 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +772 -0
- package/dist/index.js.map +1 -0
- package/dist/medusa.cjs +136 -0
- package/dist/medusa.cjs.map +1 -0
- package/dist/medusa.d.cts +75 -0
- package/dist/medusa.d.ts +75 -0
- package/dist/medusa.js +109 -0
- package/dist/medusa.js.map +1 -0
- package/dist/types-C3wWFKIK.d.cts +105 -0
- package/dist/types-C3wWFKIK.d.ts +105 -0
- package/package.json +77 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import path5 from "path";
|
|
3
|
+
import fs4 from "fs";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
import path from "path";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
|
|
10
|
+
// src/constants.ts
|
|
11
|
+
var DEFAULT_EXTENSIONS = [
|
|
12
|
+
".tsx",
|
|
13
|
+
".ts",
|
|
14
|
+
".jsx",
|
|
15
|
+
".js",
|
|
16
|
+
".vue",
|
|
17
|
+
".svelte",
|
|
18
|
+
".mts",
|
|
19
|
+
".mjs"
|
|
20
|
+
];
|
|
21
|
+
var DEFAULT_OVERRIDE_DIR = "./src/overrides";
|
|
22
|
+
var DEFAULT_SRC_DIR = "src";
|
|
23
|
+
var MAX_SCAN_DEPTH = 20;
|
|
24
|
+
var PLUGIN_NAME = "vite-plugin-unlock";
|
|
25
|
+
|
|
26
|
+
// src/utils.ts
|
|
27
|
+
var PREFIX = `[${PLUGIN_NAME}]`;
|
|
28
|
+
function createLogger(debug) {
|
|
29
|
+
return {
|
|
30
|
+
info(msg) {
|
|
31
|
+
if (debug) console.log(`${PREFIX} ${msg}`);
|
|
32
|
+
},
|
|
33
|
+
warn(msg) {
|
|
34
|
+
console.warn(`${PREFIX} ${msg}`);
|
|
35
|
+
},
|
|
36
|
+
error(msg) {
|
|
37
|
+
console.error(`${PREFIX} ${msg}`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function normalizePath(p) {
|
|
42
|
+
return p.replace(/\\/g, "/");
|
|
43
|
+
}
|
|
44
|
+
function stripExtension(filename) {
|
|
45
|
+
return filename.replace(/\.(tsx?|jsx?|mts|mjs|vue|svelte)$/, "");
|
|
46
|
+
}
|
|
47
|
+
function generateAlias(packageName) {
|
|
48
|
+
const parts = packageName.split("/");
|
|
49
|
+
const lastPart = parts[parts.length - 1];
|
|
50
|
+
return `~${lastPart}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/config.ts
|
|
54
|
+
function findPackageSrcPath(packageName, srcDir) {
|
|
55
|
+
const cwd = process.cwd();
|
|
56
|
+
try {
|
|
57
|
+
const req = createRequire(path.join(cwd, "package.json"));
|
|
58
|
+
const pkgJsonPath = req.resolve(`${packageName}/package.json`);
|
|
59
|
+
const pkgRoot = path.dirname(pkgJsonPath);
|
|
60
|
+
const srcPath = path.join(pkgRoot, srcDir);
|
|
61
|
+
if (fs.existsSync(srcPath)) return srcPath;
|
|
62
|
+
return pkgRoot;
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
const parts = packageName.startsWith("@") ? packageName.split("/") : [packageName];
|
|
66
|
+
const candidates = [
|
|
67
|
+
path.join(cwd, "node_modules", ...parts, srcDir),
|
|
68
|
+
path.join(cwd, ".yalc", ...parts, srcDir)
|
|
69
|
+
];
|
|
70
|
+
for (const dir of candidates) {
|
|
71
|
+
if (fs.existsSync(dir)) return dir;
|
|
72
|
+
}
|
|
73
|
+
const rootCandidates = [
|
|
74
|
+
path.join(cwd, "node_modules", ...parts),
|
|
75
|
+
path.join(cwd, ".yalc", ...parts)
|
|
76
|
+
];
|
|
77
|
+
for (const dir of rootCandidates) {
|
|
78
|
+
if (fs.existsSync(dir)) return dir;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
function resolveTarget(input) {
|
|
83
|
+
const target = typeof input === "string" ? {
|
|
84
|
+
package: input,
|
|
85
|
+
alias: generateAlias(input),
|
|
86
|
+
srcDir: DEFAULT_SRC_DIR,
|
|
87
|
+
srcPath: ""
|
|
88
|
+
} : {
|
|
89
|
+
package: input.package,
|
|
90
|
+
alias: input.alias ?? generateAlias(input.package),
|
|
91
|
+
srcDir: input.srcDir ?? DEFAULT_SRC_DIR,
|
|
92
|
+
srcPath: "",
|
|
93
|
+
entryRedirect: input.entryRedirect,
|
|
94
|
+
hmr: input.hmr
|
|
95
|
+
};
|
|
96
|
+
const srcPath = findPackageSrcPath(target.package, target.srcDir);
|
|
97
|
+
if (!srcPath) return null;
|
|
98
|
+
target.srcPath = srcPath;
|
|
99
|
+
if (target.entryRedirect && target.hmr) {
|
|
100
|
+
const pkgDir = path.dirname(srcPath);
|
|
101
|
+
const entryFile = path.resolve(pkgDir, target.entryRedirect.to);
|
|
102
|
+
if (fs.existsSync(entryFile)) {
|
|
103
|
+
target.entryFilePath = entryFile;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return target;
|
|
107
|
+
}
|
|
108
|
+
var CONFIG_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
109
|
+
function findConfigFile(basename, overrideDirs) {
|
|
110
|
+
for (const dir of overrideDirs) {
|
|
111
|
+
for (const ext of CONFIG_EXTENSIONS) {
|
|
112
|
+
const p = path.resolve(dir, `${basename}${ext}`);
|
|
113
|
+
if (fs.existsSync(p)) return p;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function resolveOptions(options) {
|
|
119
|
+
const extensions = options.extensions ?? DEFAULT_EXTENSIONS;
|
|
120
|
+
const overrideInput = options.overrides ?? DEFAULT_OVERRIDE_DIR;
|
|
121
|
+
const overrideDirs = (Array.isArray(overrideInput) ? overrideInput : [overrideInput]).map((dir) => path.resolve(process.cwd(), dir));
|
|
122
|
+
const targets = [];
|
|
123
|
+
for (const input of options.targets) {
|
|
124
|
+
const resolved = resolveTarget(input);
|
|
125
|
+
if (resolved) {
|
|
126
|
+
targets.push(resolved);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const patches = (options.patches ?? []).map((p) => ({
|
|
130
|
+
target: p.target,
|
|
131
|
+
configFile: p.configFile,
|
|
132
|
+
apply: p.apply,
|
|
133
|
+
configPath: findConfigFile(p.configFile, overrideDirs)
|
|
134
|
+
}));
|
|
135
|
+
return {
|
|
136
|
+
targets,
|
|
137
|
+
overrideDirs,
|
|
138
|
+
match: options.match ?? "basename",
|
|
139
|
+
onConflict: options.onConflict ?? "error",
|
|
140
|
+
debug: options.debug ?? false,
|
|
141
|
+
extensions,
|
|
142
|
+
extensionSet: new Set(extensions),
|
|
143
|
+
patches,
|
|
144
|
+
hmrBoundaries: options.hmrBoundaries ?? []
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/scanner.ts
|
|
149
|
+
import path2 from "path";
|
|
150
|
+
import fs2 from "fs";
|
|
151
|
+
function collectFiles(dir, extensionSet, depth = 0) {
|
|
152
|
+
if (depth > MAX_SCAN_DEPTH) return [];
|
|
153
|
+
const results = [];
|
|
154
|
+
let entries;
|
|
155
|
+
try {
|
|
156
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
157
|
+
} catch {
|
|
158
|
+
return results;
|
|
159
|
+
}
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
162
|
+
const fullPath = path2.resolve(dir, entry.name);
|
|
163
|
+
if (entry.isDirectory()) {
|
|
164
|
+
results.push(...collectFiles(fullPath, extensionSet, depth + 1));
|
|
165
|
+
} else if (entry.isFile()) {
|
|
166
|
+
const ext = path2.extname(entry.name);
|
|
167
|
+
if (extensionSet.has(ext)) {
|
|
168
|
+
results.push(fullPath);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return results;
|
|
173
|
+
}
|
|
174
|
+
function getOverrideKey(filePath, baseDir, match) {
|
|
175
|
+
const normalized = normalizePath(filePath);
|
|
176
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
177
|
+
if (parts.length === 0) return null;
|
|
178
|
+
const fileName = parts[parts.length - 1];
|
|
179
|
+
const baseName = stripExtension(fileName);
|
|
180
|
+
if (!baseName) return null;
|
|
181
|
+
if (match === "path") {
|
|
182
|
+
const normalizedBase = normalizePath(baseDir);
|
|
183
|
+
const relative = normalizePath(
|
|
184
|
+
path2.relative(normalizedBase, normalized)
|
|
185
|
+
);
|
|
186
|
+
return stripExtension(relative);
|
|
187
|
+
}
|
|
188
|
+
if (baseName === "index") {
|
|
189
|
+
return parts.length >= 2 ? parts[parts.length - 2] || null : null;
|
|
190
|
+
}
|
|
191
|
+
return baseName;
|
|
192
|
+
}
|
|
193
|
+
function scanTarget(target, opts) {
|
|
194
|
+
const map = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const f of collectFiles(target.srcPath, opts.extensionSet)) {
|
|
196
|
+
const key = getOverrideKey(f, target.srcPath, opts.match);
|
|
197
|
+
if (key) map.set(key, f);
|
|
198
|
+
}
|
|
199
|
+
return map;
|
|
200
|
+
}
|
|
201
|
+
function scanAllTargets(opts, logger) {
|
|
202
|
+
const combined = /* @__PURE__ */ new Map();
|
|
203
|
+
for (const target of opts.targets) {
|
|
204
|
+
const targetMap = scanTarget(target, opts);
|
|
205
|
+
logger.info(
|
|
206
|
+
`Scanned ${targetMap.size} files in ${target.package} (${target.srcPath})`
|
|
207
|
+
);
|
|
208
|
+
for (const [key, filePath] of targetMap) {
|
|
209
|
+
combined.set(key, { target, filePath });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return combined;
|
|
213
|
+
}
|
|
214
|
+
function detectNamespace(filePath, overrideDir) {
|
|
215
|
+
const relative = normalizePath(path2.relative(overrideDir, filePath));
|
|
216
|
+
const scopedMatch = relative.match(/^(@[^/]+\/[^/]+)\//);
|
|
217
|
+
if (scopedMatch) return scopedMatch[1];
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
function scanOverrides(opts, logger) {
|
|
221
|
+
const flat = /* @__PURE__ */ new Map();
|
|
222
|
+
const namespaced = /* @__PURE__ */ new Map();
|
|
223
|
+
const targetPackages = new Set(opts.targets.map((t) => t.package));
|
|
224
|
+
for (const dir of opts.overrideDirs) {
|
|
225
|
+
if (!fs2.existsSync(dir)) continue;
|
|
226
|
+
for (const fullPath of collectFiles(dir, opts.extensionSet).sort()) {
|
|
227
|
+
const relative = normalizePath(path2.relative(dir, fullPath));
|
|
228
|
+
if (relative.split("/").some((part) => part.startsWith("_"))) continue;
|
|
229
|
+
const ns = detectNamespace(fullPath, dir);
|
|
230
|
+
if (ns && targetPackages.has(ns)) {
|
|
231
|
+
const nsDir = path2.join(dir, ns);
|
|
232
|
+
const key = getOverrideKey(fullPath, nsDir, opts.match);
|
|
233
|
+
if (key && key !== "index") {
|
|
234
|
+
if (!namespaced.has(ns)) namespaced.set(ns, /* @__PURE__ */ new Map());
|
|
235
|
+
namespaced.get(ns).set(key, fullPath);
|
|
236
|
+
logger.info(`Override [${ns}]: ${key} -> ${fullPath}`);
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
const key = getOverrideKey(fullPath, dir, opts.match);
|
|
240
|
+
if (key && key !== "index") {
|
|
241
|
+
flat.set(key, fullPath);
|
|
242
|
+
logger.info(`Override: ${key} -> ${fullPath}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { flat, namespaced };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/resolver.ts
|
|
251
|
+
import path3 from "path";
|
|
252
|
+
import fs3 from "fs";
|
|
253
|
+
function findImporterTarget(importer, opts) {
|
|
254
|
+
const norm = normalizePath(importer);
|
|
255
|
+
for (const target of opts.targets) {
|
|
256
|
+
const normSrc = normalizePath(target.srcPath);
|
|
257
|
+
if (norm.startsWith(normSrc + "/") || norm === normSrc) {
|
|
258
|
+
return target;
|
|
259
|
+
}
|
|
260
|
+
const pkgDir = normalizePath(path3.dirname(target.srcPath));
|
|
261
|
+
if (norm.startsWith(pkgDir + "/")) {
|
|
262
|
+
return target;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
function resolveEntryRedirect(resolvedId, opts, logger) {
|
|
268
|
+
const norm = normalizePath(resolvedId).replace(/\?.*$/, "");
|
|
269
|
+
for (const target of opts.targets) {
|
|
270
|
+
if (target.entryRedirect) {
|
|
271
|
+
const fromPattern = normalizePath(target.entryRedirect.from);
|
|
272
|
+
if (norm.endsWith(`/${fromPattern}`) || norm.includes(`/${fromPattern}`)) {
|
|
273
|
+
const pkgDir = path3.dirname(target.srcPath);
|
|
274
|
+
const srcEntry = path3.join(pkgDir, target.entryRedirect.to);
|
|
275
|
+
if (fs3.existsSync(srcEntry)) {
|
|
276
|
+
logger.info(
|
|
277
|
+
`Entry redirect: ${path3.basename(resolvedId)} -> ${target.entryRedirect.to}`
|
|
278
|
+
);
|
|
279
|
+
return srcEntry;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (!target.entryRedirect) {
|
|
284
|
+
const parts = target.package.split("/");
|
|
285
|
+
const lastPart = parts[parts.length - 1];
|
|
286
|
+
if (norm.includes(`/${lastPart}/dist/app.`)) {
|
|
287
|
+
const srcEntry = norm.replace(
|
|
288
|
+
/\/dist\/app\.(mjs|js)$/,
|
|
289
|
+
"/src/app.tsx"
|
|
290
|
+
);
|
|
291
|
+
if (fs3.existsSync(srcEntry)) {
|
|
292
|
+
logger.info(`Entry redirect (auto): dist/app -> src/app.tsx`);
|
|
293
|
+
return srcEntry;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/watcher.ts
|
|
302
|
+
import path4 from "path";
|
|
303
|
+
function setupWatcher(server, state, opts, logger) {
|
|
304
|
+
let debounceTimer = null;
|
|
305
|
+
const handleStructuralChange = (filePath) => {
|
|
306
|
+
const ext = path4.extname(filePath);
|
|
307
|
+
if (!opts.extensionSet.has(ext)) return;
|
|
308
|
+
const normFile = normalizePath(filePath);
|
|
309
|
+
const isOverrideFile = opts.overrideDirs.some(
|
|
310
|
+
(dir) => normFile.startsWith(normalizePath(dir) + "/")
|
|
311
|
+
);
|
|
312
|
+
if (!isOverrideFile) return;
|
|
313
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
314
|
+
debounceTimer = setTimeout(() => {
|
|
315
|
+
const oldFlat = new Map(state.flatOverrides);
|
|
316
|
+
const oldNamespaced = new Map(
|
|
317
|
+
[...state.namespacedOverrides].map(
|
|
318
|
+
([k, v]) => [k, new Map(v)]
|
|
319
|
+
)
|
|
320
|
+
);
|
|
321
|
+
const { flat, namespaced } = scanOverrides(opts, logger);
|
|
322
|
+
const fileOverrides = /* @__PURE__ */ new Map();
|
|
323
|
+
for (const [key, overridePath] of flat) {
|
|
324
|
+
if (state.targetFiles.has(key)) {
|
|
325
|
+
fileOverrides.set(key, overridePath);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
Object.assign(state, {
|
|
329
|
+
flatOverrides: fileOverrides,
|
|
330
|
+
namespacedOverrides: namespaced
|
|
331
|
+
});
|
|
332
|
+
let hasChanges = false;
|
|
333
|
+
const allKeys = /* @__PURE__ */ new Set([...oldFlat.keys(), ...fileOverrides.keys()]);
|
|
334
|
+
for (const key of allKeys) {
|
|
335
|
+
const wasOverride = oldFlat.has(key);
|
|
336
|
+
const isOverride = fileOverrides.has(key);
|
|
337
|
+
if (wasOverride === isOverride && oldFlat.get(key) === fileOverrides.get(key))
|
|
338
|
+
continue;
|
|
339
|
+
const action = isOverride ? wasOverride ? "changed" : "created" : "deleted";
|
|
340
|
+
logger.info(`Override "${key}" ${action}`);
|
|
341
|
+
invalidateForKey(key, isOverride, oldFlat, state, server, logger);
|
|
342
|
+
hasChanges = true;
|
|
343
|
+
}
|
|
344
|
+
const allNs = /* @__PURE__ */ new Set([
|
|
345
|
+
...oldNamespaced.keys(),
|
|
346
|
+
...namespaced.keys()
|
|
347
|
+
]);
|
|
348
|
+
for (const ns of allNs) {
|
|
349
|
+
const oldMap = oldNamespaced.get(ns) ?? /* @__PURE__ */ new Map();
|
|
350
|
+
const newMap = namespaced.get(ns) ?? /* @__PURE__ */ new Map();
|
|
351
|
+
const nsKeys = /* @__PURE__ */ new Set([...oldMap.keys(), ...newMap.keys()]);
|
|
352
|
+
for (const key of nsKeys) {
|
|
353
|
+
const was = oldMap.has(key);
|
|
354
|
+
const is = newMap.has(key);
|
|
355
|
+
if (was === is && oldMap.get(key) === newMap.get(key)) continue;
|
|
356
|
+
const action = is ? was ? "changed" : "created" : "deleted";
|
|
357
|
+
logger.info(`Override [${ns}] "${key}" ${action}`);
|
|
358
|
+
invalidateForKey(key, is, oldMap, state, server, logger);
|
|
359
|
+
hasChanges = true;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (hasChanges) {
|
|
363
|
+
logger.info("Structural change detected -> full reload");
|
|
364
|
+
const ws = server.hot ?? server.ws;
|
|
365
|
+
ws.send({ type: "full-reload" });
|
|
366
|
+
}
|
|
367
|
+
}, 50);
|
|
368
|
+
};
|
|
369
|
+
server.watcher.on("add", handleStructuralChange);
|
|
370
|
+
server.watcher.on("unlink", handleStructuralChange);
|
|
371
|
+
server.watcher.on("change", (filePath) => {
|
|
372
|
+
const ext = path4.extname(filePath);
|
|
373
|
+
if (!opts.extensionSet.has(ext)) return;
|
|
374
|
+
const normFile = normalizePath(filePath);
|
|
375
|
+
const isOverrideFile = opts.overrideDirs.some(
|
|
376
|
+
(dir) => normFile.startsWith(normalizePath(dir) + "/")
|
|
377
|
+
);
|
|
378
|
+
if (!isOverrideFile) return;
|
|
379
|
+
const basename = stripExtension(path4.basename(filePath));
|
|
380
|
+
if (!basename) return;
|
|
381
|
+
const isTrackedOverride = state.flatOverrides.has(basename) || [...state.namespacedOverrides.values()].some((m) => m.has(basename));
|
|
382
|
+
if (!isTrackedOverride) return;
|
|
383
|
+
const trackedMods = server.moduleGraph.getModulesByFile(normFile);
|
|
384
|
+
if (trackedMods && trackedMods.size > 0) return;
|
|
385
|
+
const targetInfo = state.targetFiles.get(basename);
|
|
386
|
+
if (!targetInfo) return;
|
|
387
|
+
const origMods = server.moduleGraph.getModulesByFile(
|
|
388
|
+
normalizePath(targetInfo.filePath)
|
|
389
|
+
);
|
|
390
|
+
if (origMods) {
|
|
391
|
+
for (const mod of origMods) {
|
|
392
|
+
server.moduleGraph.invalidateModule(mod);
|
|
393
|
+
}
|
|
394
|
+
logger.info(`Override content changed: ${basename} -> reload`);
|
|
395
|
+
const ws = server.hot ?? server.ws;
|
|
396
|
+
ws.send({ type: "full-reload" });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
if (opts.patches.length > 0) {
|
|
400
|
+
const handlePatchConfigStructural = (filePath) => {
|
|
401
|
+
const basename = stripExtension(path4.basename(filePath));
|
|
402
|
+
if (!basename) return;
|
|
403
|
+
const normFile = normalizePath(filePath);
|
|
404
|
+
const isOverrideFile = opts.overrideDirs.some(
|
|
405
|
+
(dir) => normFile.startsWith(normalizePath(dir) + "/")
|
|
406
|
+
);
|
|
407
|
+
if (!isOverrideFile) return;
|
|
408
|
+
for (const patch of opts.patches) {
|
|
409
|
+
if (basename !== patch.configFile) continue;
|
|
410
|
+
const newPath = findConfigFile(patch.configFile, opts.overrideDirs);
|
|
411
|
+
if (newPath === patch.configPath) continue;
|
|
412
|
+
patch.configPath = newPath;
|
|
413
|
+
logger.info(
|
|
414
|
+
`Patch config ${newPath ? "detected" : "removed"}: ${patch.configFile}`
|
|
415
|
+
);
|
|
416
|
+
invalidatePatchTarget(patch, server, logger);
|
|
417
|
+
const ws = server.hot ?? server.ws;
|
|
418
|
+
ws.send({ type: "full-reload" });
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
const handlePatchConfigContentEdit = (filePath) => {
|
|
422
|
+
const normFile = normalizePath(filePath);
|
|
423
|
+
for (const patch of opts.patches) {
|
|
424
|
+
if (!patch.configPath) continue;
|
|
425
|
+
if (normalizePath(patch.configPath) !== normFile) continue;
|
|
426
|
+
logger.info(`Patch config content changed: ${patch.configFile}`);
|
|
427
|
+
invalidatePatchTarget(patch, server, logger);
|
|
428
|
+
const ws = server.hot ?? server.ws;
|
|
429
|
+
ws.send({ type: "full-reload" });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
server.watcher.on("add", handlePatchConfigStructural);
|
|
434
|
+
server.watcher.on("unlink", handlePatchConfigStructural);
|
|
435
|
+
server.watcher.on("change", handlePatchConfigContentEdit);
|
|
436
|
+
}
|
|
437
|
+
for (const dir of opts.overrideDirs) {
|
|
438
|
+
server.watcher.add(dir);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function invalidatePatchTarget(patch, server, logger) {
|
|
442
|
+
const { moduleGraph } = server;
|
|
443
|
+
const roots = /* @__PURE__ */ new Set();
|
|
444
|
+
const timestamp = Date.now();
|
|
445
|
+
for (const mod of moduleGraph.idToModuleMap.values()) {
|
|
446
|
+
if (mod.file && patch.target.test(normalizePath(mod.file))) {
|
|
447
|
+
moduleGraph.invalidateModule(mod, /* @__PURE__ */ new Set(), timestamp, true);
|
|
448
|
+
roots.add(mod);
|
|
449
|
+
logger.info(`Invalidated patch target: ${path4.basename(mod.file)}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const seen = /* @__PURE__ */ new Set();
|
|
453
|
+
const queue = [];
|
|
454
|
+
for (const mod of roots) {
|
|
455
|
+
for (const parent of mod.importers) {
|
|
456
|
+
queue.push(parent);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
while (queue.length > 0) {
|
|
460
|
+
const mod = queue.shift();
|
|
461
|
+
if (seen.has(mod)) continue;
|
|
462
|
+
seen.add(mod);
|
|
463
|
+
moduleGraph.invalidateModule(mod, seen, timestamp, true);
|
|
464
|
+
for (const parent of mod.importers) {
|
|
465
|
+
queue.push(parent);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (roots.size > 0 && seen.size > 0) {
|
|
469
|
+
logger.info(`Invalidated ${seen.size} ancestor modules`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function invalidateForKey(key, isNowOverride, oldOverrides, state, server, logger) {
|
|
473
|
+
const { moduleGraph } = server;
|
|
474
|
+
const roots = /* @__PURE__ */ new Set();
|
|
475
|
+
const targetInfo = state.targetFiles.get(key);
|
|
476
|
+
if (targetInfo) {
|
|
477
|
+
const mods = moduleGraph.getModulesByFile(
|
|
478
|
+
normalizePath(targetInfo.filePath)
|
|
479
|
+
);
|
|
480
|
+
if (mods) for (const mod of mods) roots.add(mod);
|
|
481
|
+
}
|
|
482
|
+
const overridePath = isNowOverride ? state.flatOverrides.get(key) : oldOverrides.get(key);
|
|
483
|
+
if (overridePath) {
|
|
484
|
+
const mods = moduleGraph.getModulesByFile(normalizePath(overridePath));
|
|
485
|
+
if (mods) for (const mod of mods) roots.add(mod);
|
|
486
|
+
}
|
|
487
|
+
if (roots.size > 0) {
|
|
488
|
+
for (const mod of roots) {
|
|
489
|
+
moduleGraph.invalidateModule(mod);
|
|
490
|
+
}
|
|
491
|
+
const seen = /* @__PURE__ */ new Set();
|
|
492
|
+
const queue = [];
|
|
493
|
+
for (const mod of roots) {
|
|
494
|
+
for (const parent of mod.importers) {
|
|
495
|
+
queue.push(parent);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
while (queue.length > 0) {
|
|
499
|
+
const mod = queue.shift();
|
|
500
|
+
if (seen.has(mod)) continue;
|
|
501
|
+
seen.add(mod);
|
|
502
|
+
moduleGraph.invalidateModule(mod);
|
|
503
|
+
for (const parent of mod.importers) {
|
|
504
|
+
queue.push(parent);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
logger.info(`Invalidated "${key}" + ${seen.size} ancestor modules`);
|
|
508
|
+
} else {
|
|
509
|
+
logger.info(`Override map updated for "${key}" (module not in graph yet)`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/plugin.ts
|
|
514
|
+
function unlock(userOptions) {
|
|
515
|
+
const opts = resolveOptions(userOptions);
|
|
516
|
+
const logger = createLogger(opts.debug);
|
|
517
|
+
if (opts.targets.length === 0) {
|
|
518
|
+
logger.warn("No target packages found. Plugin will be inactive.");
|
|
519
|
+
return { name: PLUGIN_NAME };
|
|
520
|
+
}
|
|
521
|
+
const targetFiles = scanAllTargets(opts, logger);
|
|
522
|
+
const { flat, namespaced } = scanOverrides(opts, logger);
|
|
523
|
+
const fileOverrides = /* @__PURE__ */ new Map();
|
|
524
|
+
for (const [key, overridePath] of flat) {
|
|
525
|
+
if (targetFiles.has(key)) {
|
|
526
|
+
fileOverrides.set(key, overridePath);
|
|
527
|
+
} else {
|
|
528
|
+
logger.warn(
|
|
529
|
+
`Override "${key}" does not match any file in target packages \u2014 skipped`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (opts.targets.length > 1 && fileOverrides.size > 0) {
|
|
534
|
+
detectConflicts(fileOverrides, targetFiles, opts, logger, scanTarget);
|
|
535
|
+
}
|
|
536
|
+
const state = {
|
|
537
|
+
flatOverrides: fileOverrides,
|
|
538
|
+
namespacedOverrides: namespaced,
|
|
539
|
+
targetFiles
|
|
540
|
+
};
|
|
541
|
+
const totalFileOverrides = fileOverrides.size + [...namespaced.values()].reduce((sum, m) => sum + m.size, 0);
|
|
542
|
+
if (totalFileOverrides > 0) {
|
|
543
|
+
logger.info(`Active overrides: ${totalFileOverrides}`);
|
|
544
|
+
if (fileOverrides.size > 0) {
|
|
545
|
+
logger.info(` File-level: ${[...fileOverrides.keys()].join(", ")}`);
|
|
546
|
+
}
|
|
547
|
+
for (const [ns, map] of namespaced) {
|
|
548
|
+
logger.info(` [${ns}]: ${[...map.keys()].join(", ")}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
for (const patch of opts.patches) {
|
|
552
|
+
if (patch.configPath) {
|
|
553
|
+
logger.info(`Patch: ${patch.configFile} -> ${patch.configPath}`);
|
|
554
|
+
} else {
|
|
555
|
+
logger.info(`Patch: ${patch.configFile} (no config file found)`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
name: PLUGIN_NAME,
|
|
560
|
+
enforce: "pre",
|
|
561
|
+
config(config) {
|
|
562
|
+
config.server = config.server || {};
|
|
563
|
+
config.server.fs = config.server.fs || {};
|
|
564
|
+
config.server.fs.allow = config.server.fs.allow || [];
|
|
565
|
+
for (const dir of opts.overrideDirs) {
|
|
566
|
+
config.server.fs.allow.push(path5.resolve(dir));
|
|
567
|
+
}
|
|
568
|
+
config.optimizeDeps = config.optimizeDeps || {};
|
|
569
|
+
config.optimizeDeps.exclude = config.optimizeDeps.exclude || [];
|
|
570
|
+
for (const target of opts.targets) {
|
|
571
|
+
config.optimizeDeps.exclude.push(target.package);
|
|
572
|
+
if (config.optimizeDeps.include) {
|
|
573
|
+
config.optimizeDeps.include = config.optimizeDeps.include.filter(
|
|
574
|
+
(dep) => dep !== target.package
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
config.resolve = config.resolve || {};
|
|
579
|
+
config.resolve.alias = config.resolve.alias || {};
|
|
580
|
+
for (const target of opts.targets) {
|
|
581
|
+
if (Array.isArray(config.resolve.alias)) {
|
|
582
|
+
config.resolve.alias.push({
|
|
583
|
+
find: target.alias,
|
|
584
|
+
replacement: target.srcPath
|
|
585
|
+
});
|
|
586
|
+
} else {
|
|
587
|
+
;
|
|
588
|
+
config.resolve.alias[target.alias] = target.srcPath;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
for (const target of opts.targets) {
|
|
592
|
+
const entryFile = path5.join(target.srcPath, "app.tsx");
|
|
593
|
+
if (fs4.existsSync(entryFile)) {
|
|
594
|
+
const existing = config.optimizeDeps.entries;
|
|
595
|
+
if (Array.isArray(existing)) {
|
|
596
|
+
existing.push(entryFile);
|
|
597
|
+
} else if (typeof existing === "string") {
|
|
598
|
+
config.optimizeDeps.entries = [existing, entryFile];
|
|
599
|
+
} else {
|
|
600
|
+
config.optimizeDeps.entries = [entryFile];
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
async resolveId(source, importer) {
|
|
606
|
+
if (source.startsWith("\0") || !importer || importer.startsWith("\0"))
|
|
607
|
+
return null;
|
|
608
|
+
const target = findImporterTarget(importer, opts);
|
|
609
|
+
if (target) {
|
|
610
|
+
const basename = stripExtension(path5.basename(source));
|
|
611
|
+
if (basename && basename !== "index") {
|
|
612
|
+
const nsOverrides = state.namespacedOverrides.get(target.package);
|
|
613
|
+
if (nsOverrides?.has(basename)) {
|
|
614
|
+
const p = nsOverrides.get(basename);
|
|
615
|
+
logger.info(
|
|
616
|
+
`Override [${target.package}]: ${basename} -> ${path5.basename(p)}`
|
|
617
|
+
);
|
|
618
|
+
return p;
|
|
619
|
+
}
|
|
620
|
+
if (state.flatOverrides.has(basename)) {
|
|
621
|
+
const p = state.flatOverrides.get(basename);
|
|
622
|
+
logger.info(`Override: ${basename} -> ${path5.basename(p)}`);
|
|
623
|
+
return p;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (source === target.package || source.startsWith(target.package + "/")) {
|
|
627
|
+
const resolved = await this.resolve(source, importer, {
|
|
628
|
+
skipSelf: true
|
|
629
|
+
});
|
|
630
|
+
if (resolved) {
|
|
631
|
+
const redirect = resolveEntryRedirect(
|
|
632
|
+
resolved.id,
|
|
633
|
+
opts,
|
|
634
|
+
logger
|
|
635
|
+
);
|
|
636
|
+
if (redirect) return redirect;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
for (const target2 of opts.targets) {
|
|
642
|
+
if (source !== target2.package && !source.startsWith(target2.package + "/"))
|
|
643
|
+
continue;
|
|
644
|
+
const resolved = await this.resolve(source, importer, {
|
|
645
|
+
skipSelf: true
|
|
646
|
+
});
|
|
647
|
+
if (resolved) {
|
|
648
|
+
const redirect = resolveEntryRedirect(resolved.id, opts, logger);
|
|
649
|
+
if (redirect) return redirect;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
},
|
|
654
|
+
load(id) {
|
|
655
|
+
const target = findImporterTarget(id, opts);
|
|
656
|
+
if (!target) return null;
|
|
657
|
+
const basename = stripExtension(path5.basename(id));
|
|
658
|
+
if (!basename || basename === "index") return null;
|
|
659
|
+
const nsOverrides = state.namespacedOverrides.get(target.package);
|
|
660
|
+
const overridePath = nsOverrides?.get(basename) ?? state.flatOverrides.get(basename);
|
|
661
|
+
if (overridePath && fs4.existsSync(overridePath)) {
|
|
662
|
+
this.addWatchFile(overridePath);
|
|
663
|
+
const normalizedPath = normalizePath(overridePath).replace(/"/g, '\\"');
|
|
664
|
+
logger.info(
|
|
665
|
+
`Load override: ${basename} -> ${path5.basename(overridePath)}`
|
|
666
|
+
);
|
|
667
|
+
return `export { default } from "${normalizedPath}"
|
|
668
|
+
export * from "${normalizedPath}"`;
|
|
669
|
+
}
|
|
670
|
+
const normalizedId = normalizePath(id);
|
|
671
|
+
for (const patch of opts.patches) {
|
|
672
|
+
if (!patch.target.test(normalizedId)) continue;
|
|
673
|
+
if (!patch.configPath || !fs4.existsSync(patch.configPath)) continue;
|
|
674
|
+
this.addWatchFile(patch.configPath);
|
|
675
|
+
let original;
|
|
676
|
+
try {
|
|
677
|
+
original = fs4.readFileSync(id, "utf-8");
|
|
678
|
+
} catch (err) {
|
|
679
|
+
logger.error(`Failed to read file for patching: ${id}`);
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
const patched = patch.apply(original, patch.configPath);
|
|
683
|
+
logger.info(
|
|
684
|
+
`Patch applied: ${path5.basename(id)} via ${patch.configFile}`
|
|
685
|
+
);
|
|
686
|
+
return { code: patched, map: null };
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
},
|
|
690
|
+
transform(code, id) {
|
|
691
|
+
for (const target of opts.targets) {
|
|
692
|
+
if (!target.hmr || !target.entryFilePath) continue;
|
|
693
|
+
const normId = normalizePath(id);
|
|
694
|
+
const normEntry = normalizePath(target.entryFilePath);
|
|
695
|
+
if (normId !== normEntry) continue;
|
|
696
|
+
let modified = false;
|
|
697
|
+
if (target.hmr.cssRedirect) {
|
|
698
|
+
const { from, to } = target.hmr.cssRedirect;
|
|
699
|
+
const importPattern = `import "${from}"`;
|
|
700
|
+
if (code.includes(importPattern)) {
|
|
701
|
+
code = code.replace(importPattern, `import "${to}"`);
|
|
702
|
+
logger.info(`CSS rewritten: ${from} -> ${to}`);
|
|
703
|
+
modified = true;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (target.hmr.entryBoundary) {
|
|
707
|
+
code += "\nif (import.meta.hot) { import.meta.hot.accept() }";
|
|
708
|
+
logger.info(
|
|
709
|
+
`HMR boundary injected: ${target.package} entry`
|
|
710
|
+
);
|
|
711
|
+
modified = true;
|
|
712
|
+
}
|
|
713
|
+
if (modified) return { code, map: null };
|
|
714
|
+
}
|
|
715
|
+
if (opts.hmrBoundaries.length > 0) {
|
|
716
|
+
const normId = normalizePath(id);
|
|
717
|
+
if (!normId.includes("/node_modules/")) {
|
|
718
|
+
const needsBoundary = opts.hmrBoundaries.some(
|
|
719
|
+
(p) => code.includes(p)
|
|
720
|
+
);
|
|
721
|
+
if (needsBoundary) {
|
|
722
|
+
logger.info(
|
|
723
|
+
`HMR boundary injected: ${path5.basename(id)}`
|
|
724
|
+
);
|
|
725
|
+
return {
|
|
726
|
+
code: code + "\nif (import.meta.hot) { import.meta.hot.accept() }",
|
|
727
|
+
map: null
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
},
|
|
734
|
+
configureServer(server) {
|
|
735
|
+
const fsConfig = server.config.server?.fs;
|
|
736
|
+
if (fsConfig && Array.isArray(fsConfig.allow)) {
|
|
737
|
+
for (const dir of opts.overrideDirs) {
|
|
738
|
+
const resolved = path5.resolve(dir);
|
|
739
|
+
if (!fsConfig.allow.includes(resolved)) {
|
|
740
|
+
fsConfig.allow.push(resolved);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
setupWatcher(server, state, opts, logger);
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function detectConflicts(flat, _targetFiles, opts, logger, scan) {
|
|
749
|
+
const perTargetMaps = opts.targets.map((t) => ({
|
|
750
|
+
target: t,
|
|
751
|
+
files: scan(t, opts)
|
|
752
|
+
}));
|
|
753
|
+
for (const [key] of flat) {
|
|
754
|
+
const matchingTargets = perTargetMaps.filter(({ files }) => files.has(key));
|
|
755
|
+
if (matchingTargets.length > 1) {
|
|
756
|
+
const names = matchingTargets.map(({ target }) => target.package).join(", ");
|
|
757
|
+
if (opts.onConflict === "error") {
|
|
758
|
+
throw new Error(
|
|
759
|
+
`[${PLUGIN_NAME}] Override "${key}" matches files in multiple targets: ${names}. Use namespaced overrides (overrides/@scope/pkg/${key}.tsx) or set onConflict: "warn".`
|
|
760
|
+
);
|
|
761
|
+
} else if (opts.onConflict === "warn") {
|
|
762
|
+
logger.warn(
|
|
763
|
+
`Override "${key}" matches files in multiple targets: ${names}. Using first match.`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
export {
|
|
770
|
+
unlock
|
|
771
|
+
};
|
|
772
|
+
//# sourceMappingURL=index.js.map
|