vite-plugin-asset-manager 0.0.1
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/README.md +132 -0
- package/dist/client/assets/figtree-latin-ext-wght-normal-DCwSJGxG.woff2 +0 -0
- package/dist/client/assets/figtree-latin-wght-normal-D_ZTVpCC.woff2 +0 -0
- package/dist/client/assets/index-BGQcqNrG.js +144 -0
- package/dist/client/assets/index-u18B5UIt.css +1 -0
- package/dist/client/index.html +27 -0
- package/dist/index.cjs +1837 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +51 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +1796 -0
- package/dist/index.js.map +1 -0
- package/package.json +115 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1837 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
assetManager: () => assetManager,
|
|
34
|
+
default: () => assetManager
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
|
|
39
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
40
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
41
|
+
|
|
42
|
+
// src/plugin.ts
|
|
43
|
+
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
44
|
+
|
|
45
|
+
// src/server/index.ts
|
|
46
|
+
var import_sirv = __toESM(require("sirv"), 1);
|
|
47
|
+
var import_path3 = __toESM(require("path"), 1);
|
|
48
|
+
var import_fs2 = __toESM(require("fs"), 1);
|
|
49
|
+
var import_url2 = require("url");
|
|
50
|
+
|
|
51
|
+
// src/server/api.ts
|
|
52
|
+
var import_url = require("url");
|
|
53
|
+
var import_path2 = __toESM(require("path"), 1);
|
|
54
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
55
|
+
var import_archiver = __toESM(require("archiver"), 1);
|
|
56
|
+
|
|
57
|
+
// src/server/editor-launcher.ts
|
|
58
|
+
var import_launch_editor = __toESM(require("launch-editor"), 1);
|
|
59
|
+
function launchEditor(absolutePath, line, column, editor) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const fileSpec = `${absolutePath}:${line}:${column}`;
|
|
62
|
+
(0, import_launch_editor.default)(fileSpec, editor, (_fileName, errorMsg) => {
|
|
63
|
+
if (errorMsg) {
|
|
64
|
+
reject(new Error(errorMsg));
|
|
65
|
+
} else {
|
|
66
|
+
resolve();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/server/file-revealer.ts
|
|
73
|
+
var import_child_process = require("child_process");
|
|
74
|
+
var import_path = __toESM(require("path"), 1);
|
|
75
|
+
async function revealInFileExplorer(absolutePath) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const platform = process.platform;
|
|
78
|
+
let command;
|
|
79
|
+
let args;
|
|
80
|
+
switch (platform) {
|
|
81
|
+
case "darwin":
|
|
82
|
+
command = "open";
|
|
83
|
+
args = ["-R", absolutePath];
|
|
84
|
+
break;
|
|
85
|
+
case "win32":
|
|
86
|
+
command = "explorer";
|
|
87
|
+
args = ["/select,", absolutePath];
|
|
88
|
+
break;
|
|
89
|
+
case "linux": {
|
|
90
|
+
const directory = import_path.default.dirname(absolutePath);
|
|
91
|
+
command = "xdg-open";
|
|
92
|
+
args = [directory];
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
default:
|
|
96
|
+
return reject(new Error(`Unsupported platform: ${platform}`));
|
|
97
|
+
}
|
|
98
|
+
const child = (0, import_child_process.spawn)(command, args);
|
|
99
|
+
child.on("error", (error) => {
|
|
100
|
+
reject(new Error(`Failed to reveal file: ${error.message}`));
|
|
101
|
+
});
|
|
102
|
+
child.on("close", (code) => {
|
|
103
|
+
if (code === 0) {
|
|
104
|
+
resolve();
|
|
105
|
+
} else {
|
|
106
|
+
reject(new Error(`Reveal command exited with code ${code}`));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/server/api.ts
|
|
113
|
+
var sseClients = /* @__PURE__ */ new Set();
|
|
114
|
+
var MIME_TYPES = {
|
|
115
|
+
/**
|
|
116
|
+
* Images:
|
|
117
|
+
*/
|
|
118
|
+
".png": "image/png",
|
|
119
|
+
".jpg": "image/jpeg",
|
|
120
|
+
".jpeg": "image/jpeg",
|
|
121
|
+
".gif": "image/gif",
|
|
122
|
+
".svg": "image/svg+xml",
|
|
123
|
+
".webp": "image/webp",
|
|
124
|
+
".avif": "image/avif",
|
|
125
|
+
".ico": "image/x-icon",
|
|
126
|
+
".bmp": "image/bmp",
|
|
127
|
+
".tiff": "image/tiff",
|
|
128
|
+
".tif": "image/tiff",
|
|
129
|
+
".heic": "image/heic",
|
|
130
|
+
".heif": "image/heif",
|
|
131
|
+
/**
|
|
132
|
+
* Videos:
|
|
133
|
+
*/
|
|
134
|
+
".mp4": "video/mp4",
|
|
135
|
+
".webm": "video/webm",
|
|
136
|
+
".ogg": "video/ogg",
|
|
137
|
+
".mov": "video/quicktime",
|
|
138
|
+
/**
|
|
139
|
+
* Audio:
|
|
140
|
+
*/
|
|
141
|
+
".mp3": "audio/mpeg",
|
|
142
|
+
".wav": "audio/wav",
|
|
143
|
+
/**
|
|
144
|
+
* Documents:
|
|
145
|
+
*/
|
|
146
|
+
".pdf": "application/pdf",
|
|
147
|
+
".json": "application/json",
|
|
148
|
+
".md": "text/markdown",
|
|
149
|
+
".txt": "text/plain",
|
|
150
|
+
".csv": "text/csv",
|
|
151
|
+
/**
|
|
152
|
+
* Config files:
|
|
153
|
+
*/
|
|
154
|
+
".yml": "text/yaml",
|
|
155
|
+
".yaml": "text/yaml",
|
|
156
|
+
".toml": "application/toml",
|
|
157
|
+
".xml": "application/xml",
|
|
158
|
+
/**
|
|
159
|
+
* Fonts:
|
|
160
|
+
*/
|
|
161
|
+
".woff": "font/woff",
|
|
162
|
+
".woff2": "font/woff2",
|
|
163
|
+
".ttf": "font/ttf",
|
|
164
|
+
".otf": "font/otf",
|
|
165
|
+
".eot": "application/vnd.ms-fontobject"
|
|
166
|
+
};
|
|
167
|
+
function createApiRouter(scanner, importerScanner, duplicateScanner, thumbnailService, root, basePath, editor) {
|
|
168
|
+
return async (req, res, next) => {
|
|
169
|
+
const { pathname, query } = (0, import_url.parse)(req.url || "", true);
|
|
170
|
+
const apiPath = pathname?.replace(`${basePath}/api`, "") || "";
|
|
171
|
+
try {
|
|
172
|
+
switch (apiPath) {
|
|
173
|
+
case "/assets":
|
|
174
|
+
return handleGetAssets(res, scanner, query);
|
|
175
|
+
case "/assets/grouped":
|
|
176
|
+
return handleGetGroupedAssets(res, scanner, query);
|
|
177
|
+
case "/search":
|
|
178
|
+
return handleSearch(res, scanner, query);
|
|
179
|
+
case "/thumbnail":
|
|
180
|
+
return handleThumbnail(res, thumbnailService, root, query);
|
|
181
|
+
case "/file":
|
|
182
|
+
return handleServeFile(res, root, query);
|
|
183
|
+
case "/stats":
|
|
184
|
+
return handleGetStats(res, scanner, duplicateScanner);
|
|
185
|
+
case "/duplicates":
|
|
186
|
+
return handleGetDuplicates(res, scanner, duplicateScanner, query);
|
|
187
|
+
case "/importers":
|
|
188
|
+
return handleGetImporters(res, importerScanner, query);
|
|
189
|
+
case "/open-in-editor":
|
|
190
|
+
return handleOpenInEditor(req, res, root, editor, query);
|
|
191
|
+
case "/reveal-in-finder":
|
|
192
|
+
return handleRevealInFinder(req, res, root, query);
|
|
193
|
+
case "/bulk-download":
|
|
194
|
+
return handleBulkDownload(req, res, root);
|
|
195
|
+
case "/bulk-delete":
|
|
196
|
+
return handleBulkDelete(req, res, root);
|
|
197
|
+
case "/events":
|
|
198
|
+
return handleSSE(res);
|
|
199
|
+
default:
|
|
200
|
+
next();
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error("[asset-manager] API error:", error);
|
|
204
|
+
res.statusCode = 500;
|
|
205
|
+
res.setHeader("Content-Type", "application/json");
|
|
206
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async function handleGetAssets(res, scanner, query) {
|
|
211
|
+
const assets = scanner.getAssets();
|
|
212
|
+
let filtered = assets;
|
|
213
|
+
const directory = query.directory;
|
|
214
|
+
if (directory) {
|
|
215
|
+
filtered = filtered.filter(
|
|
216
|
+
(a) => a.directory === directory || a.directory.startsWith(directory + "/")
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const type = query.type;
|
|
220
|
+
if (type) {
|
|
221
|
+
filtered = filtered.filter((a) => a.type === type);
|
|
222
|
+
}
|
|
223
|
+
sendJson(res, { assets: filtered, total: filtered.length });
|
|
224
|
+
}
|
|
225
|
+
async function handleGetGroupedAssets(res, scanner, query) {
|
|
226
|
+
let groups = scanner.getGroupedAssets();
|
|
227
|
+
const type = query.type;
|
|
228
|
+
if (type) {
|
|
229
|
+
groups = groups.map((group) => ({
|
|
230
|
+
...group,
|
|
231
|
+
assets: group.assets.filter((a) => a.type === type),
|
|
232
|
+
count: group.assets.filter((a) => a.type === type).length
|
|
233
|
+
})).filter((group) => group.count > 0);
|
|
234
|
+
}
|
|
235
|
+
const unused = query.unused === "true";
|
|
236
|
+
if (unused) {
|
|
237
|
+
groups = groups.map((group) => ({
|
|
238
|
+
...group,
|
|
239
|
+
assets: group.assets.filter((a) => a.importersCount === 0),
|
|
240
|
+
count: group.assets.filter((a) => a.importersCount === 0).length
|
|
241
|
+
})).filter((group) => group.count > 0);
|
|
242
|
+
}
|
|
243
|
+
const duplicates = query.duplicates === "true";
|
|
244
|
+
if (duplicates) {
|
|
245
|
+
groups = groups.map((group) => ({
|
|
246
|
+
...group,
|
|
247
|
+
assets: group.assets.filter((a) => (a.duplicatesCount ?? 0) > 0),
|
|
248
|
+
count: group.assets.filter((a) => (a.duplicatesCount ?? 0) > 0).length
|
|
249
|
+
})).filter((group) => group.count > 0);
|
|
250
|
+
}
|
|
251
|
+
const total = groups.reduce((sum, g) => sum + g.count, 0);
|
|
252
|
+
sendJson(res, { groups, total });
|
|
253
|
+
}
|
|
254
|
+
async function handleSearch(res, scanner, query) {
|
|
255
|
+
const q = query.q || "";
|
|
256
|
+
const results = scanner.search(q);
|
|
257
|
+
sendJson(res, { assets: results, total: results.length, query: q });
|
|
258
|
+
}
|
|
259
|
+
async function handleThumbnail(res, thumbnailService, root, query) {
|
|
260
|
+
const relativePath = query.path;
|
|
261
|
+
if (!relativePath) {
|
|
262
|
+
res.statusCode = 400;
|
|
263
|
+
res.end("Missing path parameter");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const absolutePath = import_path2.default.resolve(root, relativePath);
|
|
267
|
+
if (!absolutePath.startsWith(root)) {
|
|
268
|
+
res.statusCode = 403;
|
|
269
|
+
res.end("Forbidden");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (relativePath.endsWith(".svg")) {
|
|
273
|
+
res.setHeader("Content-Type", "image/svg+xml");
|
|
274
|
+
res.setHeader("Cache-Control", "public, max-age=31536000");
|
|
275
|
+
import_fs.default.createReadStream(absolutePath).pipe(res);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const thumbnail = await thumbnailService.getThumbnail(absolutePath);
|
|
279
|
+
if (thumbnail) {
|
|
280
|
+
res.setHeader("Content-Type", "image/jpeg");
|
|
281
|
+
res.setHeader("Cache-Control", "public, max-age=31536000");
|
|
282
|
+
res.end(thumbnail);
|
|
283
|
+
} else {
|
|
284
|
+
const ext = import_path2.default.extname(relativePath).toLowerCase();
|
|
285
|
+
res.setHeader("Content-Type", MIME_TYPES[ext] || "application/octet-stream");
|
|
286
|
+
import_fs.default.createReadStream(absolutePath).pipe(res);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function handleServeFile(res, root, query) {
|
|
290
|
+
const relativePath = query.path;
|
|
291
|
+
if (!relativePath) {
|
|
292
|
+
res.statusCode = 400;
|
|
293
|
+
res.end("Missing path parameter");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const absolutePath = import_path2.default.resolve(root, relativePath);
|
|
297
|
+
if (!absolutePath.startsWith(root)) {
|
|
298
|
+
res.statusCode = 403;
|
|
299
|
+
res.end("Forbidden");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
await import_fs.default.promises.access(absolutePath, import_fs.default.constants.R_OK);
|
|
304
|
+
} catch {
|
|
305
|
+
res.statusCode = 404;
|
|
306
|
+
res.end("File not found");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const ext = import_path2.default.extname(relativePath).toLowerCase();
|
|
310
|
+
res.setHeader("Content-Type", MIME_TYPES[ext] || "application/octet-stream");
|
|
311
|
+
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
312
|
+
import_fs.default.createReadStream(absolutePath).pipe(res);
|
|
313
|
+
}
|
|
314
|
+
async function handleGetStats(res, scanner, duplicateScanner) {
|
|
315
|
+
const assets = scanner.getAssets();
|
|
316
|
+
const dupStats = duplicateScanner.getStats();
|
|
317
|
+
const stats = {
|
|
318
|
+
total: assets.length,
|
|
319
|
+
byType: {
|
|
320
|
+
image: assets.filter((a) => a.type === "image").length,
|
|
321
|
+
video: assets.filter((a) => a.type === "video").length,
|
|
322
|
+
audio: assets.filter((a) => a.type === "audio").length,
|
|
323
|
+
document: assets.filter((a) => a.type === "document").length,
|
|
324
|
+
font: assets.filter((a) => a.type === "font").length,
|
|
325
|
+
data: assets.filter((a) => a.type === "data").length,
|
|
326
|
+
text: assets.filter((a) => a.type === "text").length,
|
|
327
|
+
other: assets.filter((a) => a.type === "other").length
|
|
328
|
+
},
|
|
329
|
+
totalSize: assets.reduce((sum, a) => sum + a.size, 0),
|
|
330
|
+
directories: [...new Set(assets.map((a) => a.directory))].length,
|
|
331
|
+
unused: assets.filter((a) => a.importersCount === 0).length,
|
|
332
|
+
duplicateGroups: dupStats.duplicateGroups,
|
|
333
|
+
duplicateFiles: dupStats.duplicateFiles
|
|
334
|
+
};
|
|
335
|
+
sendJson(res, stats);
|
|
336
|
+
}
|
|
337
|
+
async function handleGetDuplicates(res, scanner, duplicateScanner, query) {
|
|
338
|
+
const hash = query.hash;
|
|
339
|
+
if (hash) {
|
|
340
|
+
const paths = duplicateScanner.getDuplicatesByHash(hash);
|
|
341
|
+
const assets = scanner.getAssets().filter((a) => paths.includes(a.path));
|
|
342
|
+
sendJson(res, { duplicates: assets, total: assets.length, hash });
|
|
343
|
+
} else {
|
|
344
|
+
const assets = scanner.getAssets().filter((a) => (a.duplicatesCount ?? 0) > 0);
|
|
345
|
+
sendJson(res, { duplicates: assets, total: assets.length });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function sendJson(res, data) {
|
|
349
|
+
res.setHeader("Content-Type", "application/json");
|
|
350
|
+
res.end(JSON.stringify(data));
|
|
351
|
+
}
|
|
352
|
+
function handleSSE(res) {
|
|
353
|
+
res.writeHead(200, {
|
|
354
|
+
"Content-Type": "text/event-stream",
|
|
355
|
+
"Cache-Control": "no-cache",
|
|
356
|
+
Connection: "keep-alive",
|
|
357
|
+
"Access-Control-Allow-Origin": "*"
|
|
358
|
+
});
|
|
359
|
+
res.write(`data: ${JSON.stringify({ type: "connected" })}
|
|
360
|
+
|
|
361
|
+
`);
|
|
362
|
+
sseClients.add(res);
|
|
363
|
+
res.on("close", () => {
|
|
364
|
+
sseClients.delete(res);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function broadcastSSE(event, data) {
|
|
368
|
+
const message = JSON.stringify({ event, data });
|
|
369
|
+
for (const client of sseClients) {
|
|
370
|
+
client.write(`data: ${message}
|
|
371
|
+
|
|
372
|
+
`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function handleGetImporters(res, importerScanner, query) {
|
|
376
|
+
const assetPath = query.path;
|
|
377
|
+
if (!assetPath) {
|
|
378
|
+
res.statusCode = 400;
|
|
379
|
+
sendJson(res, { error: "Missing path parameter" });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const importers = importerScanner.getImporters(assetPath);
|
|
383
|
+
sendJson(res, { importers, total: importers.length });
|
|
384
|
+
}
|
|
385
|
+
async function handleOpenInEditor(req, res, root, editor, query) {
|
|
386
|
+
if (req.method !== "POST") {
|
|
387
|
+
res.statusCode = 405;
|
|
388
|
+
sendJson(res, { error: "Method not allowed" });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const filePath = query.file;
|
|
392
|
+
const line = parseInt(query.line) || 1;
|
|
393
|
+
const column = parseInt(query.column) || 1;
|
|
394
|
+
if (!filePath) {
|
|
395
|
+
res.statusCode = 400;
|
|
396
|
+
sendJson(res, { error: "Missing file parameter" });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const absolutePath = import_path2.default.resolve(root, filePath);
|
|
400
|
+
if (!absolutePath.startsWith(root)) {
|
|
401
|
+
res.statusCode = 403;
|
|
402
|
+
sendJson(res, { error: "Forbidden" });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
await import_fs.default.promises.access(absolutePath, import_fs.default.constants.R_OK);
|
|
407
|
+
} catch {
|
|
408
|
+
res.statusCode = 404;
|
|
409
|
+
sendJson(res, { error: "File not found" });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
await launchEditor(absolutePath, line, column, editor);
|
|
414
|
+
sendJson(res, { success: true });
|
|
415
|
+
} catch (error) {
|
|
416
|
+
res.statusCode = 500;
|
|
417
|
+
sendJson(res, { error: error instanceof Error ? error.message : "Failed to open editor" });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function handleRevealInFinder(req, res, root, query) {
|
|
421
|
+
if (req.method !== "POST") {
|
|
422
|
+
res.statusCode = 405;
|
|
423
|
+
sendJson(res, { error: "Method not allowed" });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const filePath = query.path;
|
|
427
|
+
if (!filePath) {
|
|
428
|
+
res.statusCode = 400;
|
|
429
|
+
sendJson(res, { error: "Missing path parameter" });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const absolutePath = import_path2.default.resolve(root, filePath);
|
|
433
|
+
if (!absolutePath.startsWith(root)) {
|
|
434
|
+
res.statusCode = 403;
|
|
435
|
+
sendJson(res, { error: "Invalid path" });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
await import_fs.default.promises.access(absolutePath, import_fs.default.constants.R_OK);
|
|
440
|
+
} catch {
|
|
441
|
+
res.statusCode = 404;
|
|
442
|
+
sendJson(res, { error: "File not found" });
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
await revealInFileExplorer(absolutePath);
|
|
447
|
+
sendJson(res, { success: true });
|
|
448
|
+
} catch (error) {
|
|
449
|
+
res.statusCode = 500;
|
|
450
|
+
sendJson(res, {
|
|
451
|
+
error: error instanceof Error ? error.message : "Failed to reveal file"
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async function parseJsonBody(req) {
|
|
456
|
+
return new Promise((resolve, reject) => {
|
|
457
|
+
let body = "";
|
|
458
|
+
req.on("data", (chunk) => {
|
|
459
|
+
body += chunk.toString();
|
|
460
|
+
});
|
|
461
|
+
req.on("end", () => {
|
|
462
|
+
try {
|
|
463
|
+
resolve(JSON.parse(body));
|
|
464
|
+
} catch {
|
|
465
|
+
reject(new Error("Invalid JSON"));
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
req.on("error", reject);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
async function handleBulkDownload(req, res, root) {
|
|
472
|
+
if (req.method !== "POST") {
|
|
473
|
+
res.statusCode = 405;
|
|
474
|
+
sendJson(res, { error: "Method not allowed" });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
let body;
|
|
478
|
+
try {
|
|
479
|
+
body = await parseJsonBody(req);
|
|
480
|
+
} catch {
|
|
481
|
+
res.statusCode = 400;
|
|
482
|
+
sendJson(res, { error: "Invalid JSON body" });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const paths = body.paths;
|
|
486
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
487
|
+
res.statusCode = 400;
|
|
488
|
+
sendJson(res, { error: "Missing or invalid paths array" });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const validatedPaths = [];
|
|
492
|
+
for (const relativePath of paths) {
|
|
493
|
+
const absolutePath = import_path2.default.resolve(root, relativePath);
|
|
494
|
+
if (!absolutePath.startsWith(root)) {
|
|
495
|
+
res.statusCode = 403;
|
|
496
|
+
sendJson(res, { error: `Forbidden path: ${relativePath}` });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
await import_fs.default.promises.access(absolutePath, import_fs.default.constants.R_OK);
|
|
501
|
+
validatedPaths.push({ relativePath, absolutePath });
|
|
502
|
+
} catch {
|
|
503
|
+
console.warn(`[asset-manager] Bulk download skipping missing file: ${relativePath}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (validatedPaths.length === 0) {
|
|
507
|
+
res.statusCode = 404;
|
|
508
|
+
sendJson(res, { error: "No valid files found" });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
res.setHeader("Content-Type", "application/zip");
|
|
512
|
+
res.setHeader("Content-Disposition", `attachment; filename="assets-${Date.now()}.zip"`);
|
|
513
|
+
const archive = (0, import_archiver.default)("zip", { zlib: { level: 6 } });
|
|
514
|
+
archive.on("error", (err) => {
|
|
515
|
+
console.error("[asset-manager] ZIP creation error:", err);
|
|
516
|
+
if (!res.headersSent) {
|
|
517
|
+
res.statusCode = 500;
|
|
518
|
+
res.end("ZIP creation failed");
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
archive.pipe(res);
|
|
522
|
+
for (const { relativePath, absolutePath } of validatedPaths) {
|
|
523
|
+
archive.file(absolutePath, { name: relativePath });
|
|
524
|
+
}
|
|
525
|
+
await archive.finalize();
|
|
526
|
+
}
|
|
527
|
+
async function handleBulkDelete(req, res, root) {
|
|
528
|
+
if (req.method !== "POST") {
|
|
529
|
+
res.statusCode = 405;
|
|
530
|
+
sendJson(res, { error: "Method not allowed" });
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
let body;
|
|
534
|
+
try {
|
|
535
|
+
body = await parseJsonBody(req);
|
|
536
|
+
} catch {
|
|
537
|
+
res.statusCode = 400;
|
|
538
|
+
sendJson(res, { error: "Invalid JSON body" });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const paths = body.paths;
|
|
542
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
543
|
+
res.statusCode = 400;
|
|
544
|
+
sendJson(res, { error: "Missing or invalid paths array" });
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const results = {
|
|
548
|
+
deleted: 0,
|
|
549
|
+
failed: [],
|
|
550
|
+
errors: []
|
|
551
|
+
};
|
|
552
|
+
for (const relativePath of paths) {
|
|
553
|
+
const absolutePath = import_path2.default.resolve(root, relativePath);
|
|
554
|
+
if (!absolutePath.startsWith(root)) {
|
|
555
|
+
results.failed.push(relativePath);
|
|
556
|
+
results.errors.push(`Forbidden path: ${relativePath}`);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
await import_fs.default.promises.unlink(absolutePath);
|
|
561
|
+
results.deleted++;
|
|
562
|
+
} catch (error) {
|
|
563
|
+
results.failed.push(relativePath);
|
|
564
|
+
results.errors.push(error instanceof Error ? error.message : "Unknown error");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
sendJson(res, {
|
|
568
|
+
deleted: results.deleted,
|
|
569
|
+
failed: results.failed.length,
|
|
570
|
+
errors: results.errors
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/server/index.ts
|
|
575
|
+
var __dirname = import_path3.default.dirname((0, import_url2.fileURLToPath)(importMetaUrl));
|
|
576
|
+
function findClientDir() {
|
|
577
|
+
const fromDist = import_path3.default.join(__dirname, "client");
|
|
578
|
+
if (import_fs2.default.existsSync(fromDist)) {
|
|
579
|
+
return fromDist;
|
|
580
|
+
}
|
|
581
|
+
const fromSource = import_path3.default.resolve(__dirname, "../../dist/client");
|
|
582
|
+
if (import_fs2.default.existsSync(fromSource)) {
|
|
583
|
+
return fromSource;
|
|
584
|
+
}
|
|
585
|
+
return fromDist;
|
|
586
|
+
}
|
|
587
|
+
function setupMiddleware(server, context) {
|
|
588
|
+
const { base, scanner, importerScanner, duplicateScanner, thumbnailService, root, launchEditor: launchEditor2 } = context;
|
|
589
|
+
const apiRouter = createApiRouter(
|
|
590
|
+
scanner,
|
|
591
|
+
importerScanner,
|
|
592
|
+
duplicateScanner,
|
|
593
|
+
thumbnailService,
|
|
594
|
+
root,
|
|
595
|
+
base,
|
|
596
|
+
launchEditor2
|
|
597
|
+
);
|
|
598
|
+
const clientDir = findClientDir();
|
|
599
|
+
server.middlewares.use((req, res, next) => {
|
|
600
|
+
const url = req.url || "";
|
|
601
|
+
if (url.startsWith(`${base}/api/`)) {
|
|
602
|
+
return apiRouter(req, res, next);
|
|
603
|
+
}
|
|
604
|
+
if (url === base || url.startsWith(`${base}/`)) {
|
|
605
|
+
const serve = (0, import_sirv.default)(clientDir, {
|
|
606
|
+
single: true,
|
|
607
|
+
dev: true
|
|
608
|
+
});
|
|
609
|
+
req.url = url.slice(base.length) || "/";
|
|
610
|
+
return serve(req, res, next);
|
|
611
|
+
}
|
|
612
|
+
next();
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/server/scanner.ts
|
|
617
|
+
var import_events = require("events");
|
|
618
|
+
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
619
|
+
var import_path4 = __toESM(require("path"), 1);
|
|
620
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
621
|
+
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
622
|
+
var AssetScanner = class extends import_events.EventEmitter {
|
|
623
|
+
root;
|
|
624
|
+
options;
|
|
625
|
+
cache = /* @__PURE__ */ new Map();
|
|
626
|
+
watcher;
|
|
627
|
+
scanPromise;
|
|
628
|
+
constructor(root, options) {
|
|
629
|
+
super();
|
|
630
|
+
this.root = root;
|
|
631
|
+
this.options = options;
|
|
632
|
+
}
|
|
633
|
+
async init() {
|
|
634
|
+
await this.scan();
|
|
635
|
+
if (this.options.watch) {
|
|
636
|
+
this.initWatcher();
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async scan() {
|
|
640
|
+
if (this.scanPromise) {
|
|
641
|
+
await this.scanPromise;
|
|
642
|
+
return this.getAssets();
|
|
643
|
+
}
|
|
644
|
+
this.scanPromise = this.performScan();
|
|
645
|
+
await this.scanPromise;
|
|
646
|
+
this.scanPromise = void 0;
|
|
647
|
+
return this.getAssets();
|
|
648
|
+
}
|
|
649
|
+
async performScan() {
|
|
650
|
+
const extensionPattern = this.options.extensions.map((ext) => ext.replace(".", "")).join(",");
|
|
651
|
+
const patterns = this.options.include.map((dir) => `${dir}/**/*.{${extensionPattern}}`);
|
|
652
|
+
const entries = await (0, import_fast_glob.default)(patterns, {
|
|
653
|
+
cwd: this.root,
|
|
654
|
+
ignore: this.options.exclude.map((p) => `**/${p}/**`),
|
|
655
|
+
absolute: false,
|
|
656
|
+
stats: true,
|
|
657
|
+
onlyFiles: true,
|
|
658
|
+
dot: false
|
|
659
|
+
});
|
|
660
|
+
this.cache.clear();
|
|
661
|
+
for (const entry of entries) {
|
|
662
|
+
const asset = this.createAsset(entry);
|
|
663
|
+
this.cache.set(asset.path, asset);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
createAsset(entry) {
|
|
667
|
+
const relativePath = entry.path;
|
|
668
|
+
const absolutePath = import_path4.default.join(this.root, relativePath);
|
|
669
|
+
const extension = import_path4.default.extname(relativePath).toLowerCase();
|
|
670
|
+
const name = import_path4.default.basename(relativePath);
|
|
671
|
+
const directory = import_path4.default.dirname(relativePath);
|
|
672
|
+
return {
|
|
673
|
+
id: Buffer.from(relativePath).toString("base64url"),
|
|
674
|
+
name,
|
|
675
|
+
path: relativePath,
|
|
676
|
+
absolutePath,
|
|
677
|
+
extension,
|
|
678
|
+
type: this.getAssetType(extension),
|
|
679
|
+
size: entry.stats?.size || 0,
|
|
680
|
+
mtime: entry.stats?.mtimeMs || Date.now(),
|
|
681
|
+
directory: directory === "." ? "/" : directory
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
getAssetType(extension) {
|
|
685
|
+
const imageExts = [
|
|
686
|
+
".png",
|
|
687
|
+
".jpg",
|
|
688
|
+
".jpeg",
|
|
689
|
+
".gif",
|
|
690
|
+
".svg",
|
|
691
|
+
".webp",
|
|
692
|
+
".avif",
|
|
693
|
+
".ico",
|
|
694
|
+
".bmp",
|
|
695
|
+
".tiff",
|
|
696
|
+
".tif",
|
|
697
|
+
".heic",
|
|
698
|
+
".heif"
|
|
699
|
+
];
|
|
700
|
+
const videoExts = [".mp4", ".webm", ".ogg", ".mov", ".avi"];
|
|
701
|
+
const audioExts = [".mp3", ".wav", ".flac", ".aac"];
|
|
702
|
+
const docExts = [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"];
|
|
703
|
+
const fontExts = [".woff", ".woff2", ".ttf", ".otf", ".eot"];
|
|
704
|
+
const dataExts = [".json", ".csv", ".xml", ".yml", ".yaml", ".toml"];
|
|
705
|
+
const textExts = [".md", ".txt"];
|
|
706
|
+
if (imageExts.includes(extension)) return "image";
|
|
707
|
+
if (videoExts.includes(extension)) return "video";
|
|
708
|
+
if (audioExts.includes(extension)) return "audio";
|
|
709
|
+
if (docExts.includes(extension)) return "document";
|
|
710
|
+
if (fontExts.includes(extension)) return "font";
|
|
711
|
+
if (dataExts.includes(extension)) return "data";
|
|
712
|
+
if (textExts.includes(extension)) return "text";
|
|
713
|
+
return "other";
|
|
714
|
+
}
|
|
715
|
+
getAssets() {
|
|
716
|
+
return Array.from(this.cache.values());
|
|
717
|
+
}
|
|
718
|
+
getGroupedAssets() {
|
|
719
|
+
const groups = /* @__PURE__ */ new Map();
|
|
720
|
+
for (const asset of this.cache.values()) {
|
|
721
|
+
const dir = asset.directory;
|
|
722
|
+
if (!groups.has(dir)) {
|
|
723
|
+
groups.set(dir, []);
|
|
724
|
+
}
|
|
725
|
+
groups.get(dir).push(asset);
|
|
726
|
+
}
|
|
727
|
+
return Array.from(groups.entries()).map(([directory, assets]) => ({
|
|
728
|
+
directory,
|
|
729
|
+
assets: assets.sort((a, b) => a.name.localeCompare(b.name)),
|
|
730
|
+
count: assets.length
|
|
731
|
+
})).sort((a, b) => a.directory.localeCompare(b.directory));
|
|
732
|
+
}
|
|
733
|
+
search(query) {
|
|
734
|
+
const normalizedQuery = query.toLowerCase().trim();
|
|
735
|
+
if (!normalizedQuery) return this.getAssets();
|
|
736
|
+
return this.getAssets().filter(
|
|
737
|
+
(asset) => asset.name.toLowerCase().includes(normalizedQuery) || asset.path.toLowerCase().includes(normalizedQuery)
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
getAsset(relativePath) {
|
|
741
|
+
return this.cache.get(relativePath);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Enrich assets with importer count metadata.
|
|
745
|
+
* Should be called after scanning completes and when importers change.
|
|
746
|
+
*/
|
|
747
|
+
enrichWithImporterCounts(importerScanner) {
|
|
748
|
+
for (const asset of this.cache.values()) {
|
|
749
|
+
const importers = importerScanner.getImporters(asset.path);
|
|
750
|
+
asset.importersCount = importers.length;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Enrich assets with duplicate detection metadata.
|
|
755
|
+
* Should be called after scanning completes and when file content changes.
|
|
756
|
+
*/
|
|
757
|
+
enrichWithDuplicateInfo(duplicateScanner) {
|
|
758
|
+
for (const asset of this.cache.values()) {
|
|
759
|
+
const info = duplicateScanner.getDuplicateInfo(asset.path);
|
|
760
|
+
asset.contentHash = info.hash;
|
|
761
|
+
asset.duplicatesCount = info.duplicatesCount;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
initWatcher() {
|
|
765
|
+
const watchPaths = this.options.include.map((dir) => import_path4.default.join(this.root, dir));
|
|
766
|
+
this.watcher = import_chokidar.default.watch(watchPaths, {
|
|
767
|
+
ignored: this.options.exclude.map((p) => `**/${p}/**`),
|
|
768
|
+
persistent: true,
|
|
769
|
+
ignoreInitial: true,
|
|
770
|
+
awaitWriteFinish: {
|
|
771
|
+
stabilityThreshold: 100,
|
|
772
|
+
pollInterval: 50
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
this.watcher.on("add", (filePath) => this.handleFileChange("add", filePath));
|
|
776
|
+
this.watcher.on("unlink", (filePath) => this.handleFileChange("unlink", filePath));
|
|
777
|
+
this.watcher.on("change", (filePath) => this.handleFileChange("change", filePath));
|
|
778
|
+
}
|
|
779
|
+
async handleFileChange(event, absolutePath) {
|
|
780
|
+
const relativePath = import_path4.default.relative(this.root, absolutePath);
|
|
781
|
+
const extension = import_path4.default.extname(relativePath).toLowerCase();
|
|
782
|
+
if (!this.options.extensions.includes(extension)) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (event === "unlink") {
|
|
786
|
+
this.cache.delete(relativePath);
|
|
787
|
+
} else {
|
|
788
|
+
try {
|
|
789
|
+
const stats = await import_promises.default.stat(absolutePath);
|
|
790
|
+
const asset = {
|
|
791
|
+
id: Buffer.from(relativePath).toString("base64url"),
|
|
792
|
+
name: import_path4.default.basename(relativePath),
|
|
793
|
+
path: relativePath,
|
|
794
|
+
absolutePath,
|
|
795
|
+
extension,
|
|
796
|
+
type: this.getAssetType(extension),
|
|
797
|
+
size: stats.size,
|
|
798
|
+
mtime: stats.mtimeMs,
|
|
799
|
+
directory: import_path4.default.dirname(relativePath)
|
|
800
|
+
};
|
|
801
|
+
this.cache.set(relativePath, asset);
|
|
802
|
+
} catch {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
this.emit("change", { event, path: relativePath });
|
|
807
|
+
}
|
|
808
|
+
destroy() {
|
|
809
|
+
this.watcher?.close();
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// src/server/importer-scanner.ts
|
|
814
|
+
var import_events2 = require("events");
|
|
815
|
+
var import_fast_glob2 = __toESM(require("fast-glob"), 1);
|
|
816
|
+
var import_path5 = __toESM(require("path"), 1);
|
|
817
|
+
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
818
|
+
var import_chokidar2 = __toESM(require("chokidar"), 1);
|
|
819
|
+
var SOURCE_EXTENSIONS = ["js", "jsx", "ts", "tsx", "vue", "svelte", "css", "scss", "less", "html"];
|
|
820
|
+
var ASSET_EXTENSIONS = [
|
|
821
|
+
"png",
|
|
822
|
+
"jpg",
|
|
823
|
+
"jpeg",
|
|
824
|
+
"gif",
|
|
825
|
+
"svg",
|
|
826
|
+
"webp",
|
|
827
|
+
"avif",
|
|
828
|
+
"ico",
|
|
829
|
+
"bmp",
|
|
830
|
+
"tiff",
|
|
831
|
+
"tif",
|
|
832
|
+
"heic",
|
|
833
|
+
"heif",
|
|
834
|
+
"mp4",
|
|
835
|
+
"webm",
|
|
836
|
+
"ogg",
|
|
837
|
+
"mov",
|
|
838
|
+
"avi",
|
|
839
|
+
"mp3",
|
|
840
|
+
"wav",
|
|
841
|
+
"flac",
|
|
842
|
+
"aac",
|
|
843
|
+
"woff",
|
|
844
|
+
"woff2",
|
|
845
|
+
"ttf",
|
|
846
|
+
"otf",
|
|
847
|
+
"eot",
|
|
848
|
+
"pdf",
|
|
849
|
+
"json",
|
|
850
|
+
"md",
|
|
851
|
+
"txt",
|
|
852
|
+
"csv",
|
|
853
|
+
"xml",
|
|
854
|
+
"yml",
|
|
855
|
+
"yaml",
|
|
856
|
+
"toml"
|
|
857
|
+
];
|
|
858
|
+
var ASSET_EXT_PATTERN = ASSET_EXTENSIONS.join("|");
|
|
859
|
+
var IMPORT_PATTERNS = [
|
|
860
|
+
{
|
|
861
|
+
type: "es-import",
|
|
862
|
+
pattern: new RegExp(
|
|
863
|
+
`import\\s+(?:[\\w\\s{},*]+\\s+from\\s+)?['"]([^'"]*\\.(?:${ASSET_EXT_PATTERN}))['"]`,
|
|
864
|
+
"gi"
|
|
865
|
+
)
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
type: "dynamic-import",
|
|
869
|
+
pattern: new RegExp(`import\\s*\\(\\s*['"]([^'"]*\\.(?:${ASSET_EXT_PATTERN}))['"]\\s*\\)`, "gi")
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
type: "require",
|
|
873
|
+
pattern: new RegExp(
|
|
874
|
+
`require\\s*\\(\\s*['"]([^'"]*\\.(?:${ASSET_EXT_PATTERN}))['"]\\s*\\)`,
|
|
875
|
+
"gi"
|
|
876
|
+
)
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
type: "css-url",
|
|
880
|
+
pattern: new RegExp(
|
|
881
|
+
`url\\s*\\(\\s*['"]?([^'")\\s]+\\.(?:${ASSET_EXT_PATTERN}))['"]?\\s*\\)`,
|
|
882
|
+
"gi"
|
|
883
|
+
)
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
type: "html-src",
|
|
887
|
+
pattern: new RegExp(`\\bsrc\\s*=\\s*['"]([^'"]*\\.(?:${ASSET_EXT_PATTERN}))['"]`, "gi")
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
type: "html-href",
|
|
891
|
+
pattern: new RegExp(`\\bhref\\s*=\\s*['"]([^'"]*\\.(?:${ASSET_EXT_PATTERN}))['"]`, "gi")
|
|
892
|
+
}
|
|
893
|
+
];
|
|
894
|
+
var ImporterScanner = class extends import_events2.EventEmitter {
|
|
895
|
+
root;
|
|
896
|
+
options;
|
|
897
|
+
/** Maps asset path -> array of importers */
|
|
898
|
+
cache = /* @__PURE__ */ new Map();
|
|
899
|
+
/** Reverse index: source file -> set of asset paths it imports */
|
|
900
|
+
reverseIndex = /* @__PURE__ */ new Map();
|
|
901
|
+
watcher;
|
|
902
|
+
scanPromise;
|
|
903
|
+
initialized = false;
|
|
904
|
+
constructor(root, options) {
|
|
905
|
+
super();
|
|
906
|
+
this.root = root;
|
|
907
|
+
this.options = options;
|
|
908
|
+
}
|
|
909
|
+
async init() {
|
|
910
|
+
if (this.initialized) return;
|
|
911
|
+
await this.scan();
|
|
912
|
+
if (this.options.watch) {
|
|
913
|
+
this.initWatcher();
|
|
914
|
+
}
|
|
915
|
+
this.initialized = true;
|
|
916
|
+
}
|
|
917
|
+
async scan() {
|
|
918
|
+
if (this.scanPromise) {
|
|
919
|
+
await this.scanPromise;
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
this.scanPromise = this.performScan();
|
|
923
|
+
await this.scanPromise;
|
|
924
|
+
this.scanPromise = void 0;
|
|
925
|
+
}
|
|
926
|
+
async performScan() {
|
|
927
|
+
const patterns = this.options.include.map((dir) => `${dir}/**/*.{${SOURCE_EXTENSIONS.join(",")}}`);
|
|
928
|
+
const entries = await (0, import_fast_glob2.default)(patterns, {
|
|
929
|
+
cwd: this.root,
|
|
930
|
+
ignore: this.options.exclude.map((p) => `**/${p}/**`),
|
|
931
|
+
absolute: false,
|
|
932
|
+
onlyFiles: true,
|
|
933
|
+
dot: false
|
|
934
|
+
});
|
|
935
|
+
this.cache.clear();
|
|
936
|
+
this.reverseIndex.clear();
|
|
937
|
+
const BATCH_SIZE = 50;
|
|
938
|
+
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
|
939
|
+
const batch = entries.slice(i, i + BATCH_SIZE);
|
|
940
|
+
await Promise.all(batch.map((filePath) => this.scanFile(filePath)));
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async scanFile(relativePath) {
|
|
944
|
+
const absolutePath = import_path5.default.join(this.root, relativePath);
|
|
945
|
+
try {
|
|
946
|
+
const content = await import_promises2.default.readFile(absolutePath, "utf-8");
|
|
947
|
+
const importers = this.findImportsInFile(content, relativePath, absolutePath);
|
|
948
|
+
const previousAssets = this.reverseIndex.get(relativePath);
|
|
949
|
+
if (previousAssets) {
|
|
950
|
+
for (const assetPath of previousAssets) {
|
|
951
|
+
const assetImporters = this.cache.get(assetPath);
|
|
952
|
+
if (assetImporters) {
|
|
953
|
+
const filtered = assetImporters.filter((i) => i.filePath !== relativePath);
|
|
954
|
+
if (filtered.length > 0) {
|
|
955
|
+
this.cache.set(assetPath, filtered);
|
|
956
|
+
} else {
|
|
957
|
+
this.cache.delete(assetPath);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const newAssets = /* @__PURE__ */ new Set();
|
|
963
|
+
for (const importer of importers) {
|
|
964
|
+
const assetPath = this.resolveAssetPath(importer.filePath, importer.snippet);
|
|
965
|
+
if (assetPath) {
|
|
966
|
+
newAssets.add(assetPath);
|
|
967
|
+
const existing = this.cache.get(assetPath) || [];
|
|
968
|
+
existing.push({ ...importer, filePath: relativePath, absolutePath });
|
|
969
|
+
this.cache.set(assetPath, existing);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
this.reverseIndex.set(relativePath, newAssets);
|
|
973
|
+
} catch {
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
findImportsInFile(content, relativePath, absolutePath) {
|
|
977
|
+
const importers = [];
|
|
978
|
+
const lines = content.split("\n");
|
|
979
|
+
const fileDir = import_path5.default.dirname(relativePath);
|
|
980
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
981
|
+
const line = lines[lineIndex];
|
|
982
|
+
for (const { type, pattern } of IMPORT_PATTERNS) {
|
|
983
|
+
pattern.lastIndex = 0;
|
|
984
|
+
let match;
|
|
985
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
986
|
+
const importPath = match[1];
|
|
987
|
+
const resolvedAssetPath = this.resolveImportPath(importPath, fileDir);
|
|
988
|
+
if (resolvedAssetPath) {
|
|
989
|
+
importers.push({
|
|
990
|
+
filePath: relativePath,
|
|
991
|
+
absolutePath,
|
|
992
|
+
line: lineIndex + 1,
|
|
993
|
+
column: match.index + 1,
|
|
994
|
+
importType: type,
|
|
995
|
+
snippet: line.trim().slice(0, 100)
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return importers;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Resolves an import path relative to the file directory.
|
|
1005
|
+
* Returns the normalized asset path relative to project root, or null if invalid.
|
|
1006
|
+
*/
|
|
1007
|
+
resolveImportPath(importPath, fileDir) {
|
|
1008
|
+
if (importPath.startsWith("http://") || importPath.startsWith("https://") || importPath.startsWith("//")) {
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("/") && !importPath.startsWith("@/")) {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
let resolvedPath;
|
|
1015
|
+
if (importPath.startsWith("/")) {
|
|
1016
|
+
resolvedPath = importPath.slice(1);
|
|
1017
|
+
if (!resolvedPath.startsWith("public/")) {
|
|
1018
|
+
resolvedPath = "public" + importPath;
|
|
1019
|
+
}
|
|
1020
|
+
} else if (importPath.startsWith("@/")) {
|
|
1021
|
+
resolvedPath = "src/" + importPath.slice(2);
|
|
1022
|
+
} else {
|
|
1023
|
+
resolvedPath = import_path5.default.normalize(import_path5.default.join(fileDir, importPath));
|
|
1024
|
+
}
|
|
1025
|
+
resolvedPath = resolvedPath.split(import_path5.default.sep).join("/");
|
|
1026
|
+
return resolvedPath;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Extract asset path from importer snippet (for reverse lookup)
|
|
1030
|
+
*/
|
|
1031
|
+
resolveAssetPath(sourceFile, snippet) {
|
|
1032
|
+
const fileDir = import_path5.default.dirname(sourceFile);
|
|
1033
|
+
for (const { pattern } of IMPORT_PATTERNS) {
|
|
1034
|
+
pattern.lastIndex = 0;
|
|
1035
|
+
const match = pattern.exec(snippet);
|
|
1036
|
+
if (match) {
|
|
1037
|
+
return this.resolveImportPath(match[1], fileDir);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Get all importers for a specific asset.
|
|
1044
|
+
* @param assetPath - Relative path to the asset from project root
|
|
1045
|
+
*/
|
|
1046
|
+
getImporters(assetPath) {
|
|
1047
|
+
const normalizedPath = assetPath.split(import_path5.default.sep).join("/");
|
|
1048
|
+
let importers = this.cache.get(normalizedPath);
|
|
1049
|
+
if (!importers) {
|
|
1050
|
+
if (normalizedPath.startsWith("public/")) {
|
|
1051
|
+
importers = this.cache.get(normalizedPath.slice(7));
|
|
1052
|
+
} else {
|
|
1053
|
+
importers = this.cache.get("public/" + normalizedPath);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return importers || [];
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Get assets affected when a source file changes.
|
|
1060
|
+
*/
|
|
1061
|
+
getAffectedAssets(sourceFile) {
|
|
1062
|
+
return Array.from(this.reverseIndex.get(sourceFile) || []);
|
|
1063
|
+
}
|
|
1064
|
+
initWatcher() {
|
|
1065
|
+
const watchPaths = this.options.include.map((dir) => import_path5.default.join(this.root, dir));
|
|
1066
|
+
this.watcher = import_chokidar2.default.watch(watchPaths, {
|
|
1067
|
+
ignored: [
|
|
1068
|
+
...this.options.exclude.map((p) => `**/${p}/**`),
|
|
1069
|
+
(filePath) => {
|
|
1070
|
+
const ext = import_path5.default.extname(filePath).slice(1);
|
|
1071
|
+
return ext !== "" && !SOURCE_EXTENSIONS.includes(ext);
|
|
1072
|
+
}
|
|
1073
|
+
],
|
|
1074
|
+
persistent: true,
|
|
1075
|
+
ignoreInitial: true,
|
|
1076
|
+
awaitWriteFinish: {
|
|
1077
|
+
stabilityThreshold: 200,
|
|
1078
|
+
pollInterval: 50
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
this.watcher.on("add", (filePath) => this.handleFileChange("add", filePath));
|
|
1082
|
+
this.watcher.on("unlink", (filePath) => this.handleFileChange("unlink", filePath));
|
|
1083
|
+
this.watcher.on("change", (filePath) => this.handleFileChange("change", filePath));
|
|
1084
|
+
}
|
|
1085
|
+
async handleFileChange(event, absolutePath) {
|
|
1086
|
+
const relativePath = import_path5.default.relative(this.root, absolutePath);
|
|
1087
|
+
const extension = import_path5.default.extname(relativePath).slice(1);
|
|
1088
|
+
if (!SOURCE_EXTENSIONS.includes(extension)) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const previousAssets = this.getAffectedAssets(relativePath);
|
|
1092
|
+
if (event === "unlink") {
|
|
1093
|
+
for (const assetPath of previousAssets) {
|
|
1094
|
+
const assetImporters = this.cache.get(assetPath);
|
|
1095
|
+
if (assetImporters) {
|
|
1096
|
+
const filtered = assetImporters.filter((i) => i.filePath !== relativePath);
|
|
1097
|
+
if (filtered.length > 0) {
|
|
1098
|
+
this.cache.set(assetPath, filtered);
|
|
1099
|
+
} else {
|
|
1100
|
+
this.cache.delete(assetPath);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
this.reverseIndex.delete(relativePath);
|
|
1105
|
+
} else {
|
|
1106
|
+
await this.scanFile(relativePath);
|
|
1107
|
+
}
|
|
1108
|
+
const currentAssets = this.getAffectedAssets(relativePath);
|
|
1109
|
+
const allAffectedAssets = [.../* @__PURE__ */ new Set([...previousAssets, ...currentAssets])];
|
|
1110
|
+
if (allAffectedAssets.length > 0) {
|
|
1111
|
+
this.emit("change", {
|
|
1112
|
+
event,
|
|
1113
|
+
path: relativePath,
|
|
1114
|
+
affectedAssets: allAffectedAssets
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
destroy() {
|
|
1119
|
+
this.watcher?.close();
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
// src/server/duplicate-scanner.ts
|
|
1124
|
+
var import_events3 = require("events");
|
|
1125
|
+
var import_crypto = __toESM(require("crypto"), 1);
|
|
1126
|
+
var import_fs3 = __toESM(require("fs"), 1);
|
|
1127
|
+
var import_path6 = __toESM(require("path"), 1);
|
|
1128
|
+
var import_chokidar3 = __toESM(require("chokidar"), 1);
|
|
1129
|
+
var STREAMING_THRESHOLD = 1024 * 1024;
|
|
1130
|
+
var DuplicateScanner = class extends import_events3.EventEmitter {
|
|
1131
|
+
root;
|
|
1132
|
+
options;
|
|
1133
|
+
/** Maps relative path -> { hash, mtime, size } for cache validation */
|
|
1134
|
+
hashCache = /* @__PURE__ */ new Map();
|
|
1135
|
+
/** Maps hash -> set of relative paths (for grouping duplicates) */
|
|
1136
|
+
duplicateGroups = /* @__PURE__ */ new Map();
|
|
1137
|
+
/** Reverse index: path -> hash (for quick lookups) */
|
|
1138
|
+
pathToHash = /* @__PURE__ */ new Map();
|
|
1139
|
+
watcher;
|
|
1140
|
+
scanPromise;
|
|
1141
|
+
initialized = false;
|
|
1142
|
+
constructor(root, options) {
|
|
1143
|
+
super();
|
|
1144
|
+
this.root = root;
|
|
1145
|
+
this.options = options;
|
|
1146
|
+
}
|
|
1147
|
+
async init() {
|
|
1148
|
+
if (this.initialized) return;
|
|
1149
|
+
this.initialized = true;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Scan all assets and compute hashes.
|
|
1153
|
+
* Called after AssetScanner has discovered assets.
|
|
1154
|
+
*/
|
|
1155
|
+
async scanAssets(assets) {
|
|
1156
|
+
if (this.scanPromise) {
|
|
1157
|
+
await this.scanPromise;
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
this.scanPromise = this.performScan(assets);
|
|
1161
|
+
await this.scanPromise;
|
|
1162
|
+
this.scanPromise = void 0;
|
|
1163
|
+
}
|
|
1164
|
+
async performScan(assets) {
|
|
1165
|
+
this.duplicateGroups.clear();
|
|
1166
|
+
this.pathToHash.clear();
|
|
1167
|
+
const BATCH_SIZE = 20;
|
|
1168
|
+
for (let i = 0; i < assets.length; i += BATCH_SIZE) {
|
|
1169
|
+
const batch = assets.slice(i, i + BATCH_SIZE);
|
|
1170
|
+
await Promise.all(batch.map((asset) => this.processAsset(asset)));
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
async processAsset(asset) {
|
|
1174
|
+
try {
|
|
1175
|
+
const hash = await this.getOrComputeHash(asset.path, asset.absolutePath);
|
|
1176
|
+
if (hash) {
|
|
1177
|
+
this.pathToHash.set(asset.path, hash);
|
|
1178
|
+
if (!this.duplicateGroups.has(hash)) {
|
|
1179
|
+
this.duplicateGroups.set(hash, /* @__PURE__ */ new Set());
|
|
1180
|
+
}
|
|
1181
|
+
this.duplicateGroups.get(hash).add(asset.path);
|
|
1182
|
+
}
|
|
1183
|
+
} catch {
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Get cached hash or compute new one if cache is invalid.
|
|
1188
|
+
*/
|
|
1189
|
+
async getOrComputeHash(relativePath, absolutePath) {
|
|
1190
|
+
try {
|
|
1191
|
+
const stats = await import_fs3.default.promises.stat(absolutePath);
|
|
1192
|
+
const cached = this.hashCache.get(relativePath);
|
|
1193
|
+
if (cached && cached.mtime === stats.mtimeMs && cached.size === stats.size) {
|
|
1194
|
+
return cached.hash;
|
|
1195
|
+
}
|
|
1196
|
+
const hash = await this.computeFileHash(absolutePath, stats.size);
|
|
1197
|
+
this.hashCache.set(relativePath, {
|
|
1198
|
+
hash,
|
|
1199
|
+
mtime: stats.mtimeMs,
|
|
1200
|
+
size: stats.size
|
|
1201
|
+
});
|
|
1202
|
+
return hash;
|
|
1203
|
+
} catch {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Compute MD5 hash of file contents.
|
|
1209
|
+
* Uses streaming for large files to avoid memory issues.
|
|
1210
|
+
*/
|
|
1211
|
+
async computeFileHash(absolutePath, size) {
|
|
1212
|
+
if (size > STREAMING_THRESHOLD) {
|
|
1213
|
+
return new Promise((resolve, reject) => {
|
|
1214
|
+
const hash = import_crypto.default.createHash("md5");
|
|
1215
|
+
const stream = import_fs3.default.createReadStream(absolutePath);
|
|
1216
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
1217
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
1218
|
+
stream.on("error", reject);
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
const content = await import_fs3.default.promises.readFile(absolutePath);
|
|
1222
|
+
return import_crypto.default.createHash("md5").update(content).digest("hex");
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Get duplicate info for a specific asset.
|
|
1226
|
+
*/
|
|
1227
|
+
getDuplicateInfo(assetPath) {
|
|
1228
|
+
const normalizedPath = assetPath.split(import_path6.default.sep).join("/");
|
|
1229
|
+
const hash = this.pathToHash.get(normalizedPath);
|
|
1230
|
+
if (!hash) {
|
|
1231
|
+
return { hash: "", duplicatesCount: 0 };
|
|
1232
|
+
}
|
|
1233
|
+
const group = this.duplicateGroups.get(hash);
|
|
1234
|
+
const duplicatesCount = group ? group.size - 1 : 0;
|
|
1235
|
+
return { hash, duplicatesCount };
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Get all assets in a duplicate group by hash.
|
|
1239
|
+
*/
|
|
1240
|
+
getDuplicatesByHash(hash) {
|
|
1241
|
+
const group = this.duplicateGroups.get(hash);
|
|
1242
|
+
return group ? Array.from(group).sort() : [];
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Get duplicate statistics.
|
|
1246
|
+
*/
|
|
1247
|
+
getStats() {
|
|
1248
|
+
let duplicateGroups = 0;
|
|
1249
|
+
let duplicateFiles = 0;
|
|
1250
|
+
for (const [, paths] of this.duplicateGroups) {
|
|
1251
|
+
if (paths.size > 1) {
|
|
1252
|
+
duplicateGroups++;
|
|
1253
|
+
duplicateFiles += paths.size;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return { duplicateGroups, duplicateFiles };
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Enrich assets with duplicate detection metadata.
|
|
1260
|
+
*/
|
|
1261
|
+
enrichAssetsWithDuplicateInfo(assets) {
|
|
1262
|
+
for (const asset of assets) {
|
|
1263
|
+
const info = this.getDuplicateInfo(asset.path);
|
|
1264
|
+
asset.contentHash = info.hash;
|
|
1265
|
+
asset.duplicatesCount = info.duplicatesCount;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Handle file change event from watcher.
|
|
1270
|
+
* Recalculates hash and updates duplicate groups.
|
|
1271
|
+
*/
|
|
1272
|
+
async handleAssetChange(event, relativePath, absolutePath) {
|
|
1273
|
+
const normalizedPath = relativePath.split(import_path6.default.sep).join("/");
|
|
1274
|
+
const previousHash = this.pathToHash.get(normalizedPath);
|
|
1275
|
+
const affectedHashes = [];
|
|
1276
|
+
if (previousHash) {
|
|
1277
|
+
affectedHashes.push(previousHash);
|
|
1278
|
+
const oldGroup = this.duplicateGroups.get(previousHash);
|
|
1279
|
+
if (oldGroup) {
|
|
1280
|
+
oldGroup.delete(normalizedPath);
|
|
1281
|
+
if (oldGroup.size === 0) {
|
|
1282
|
+
this.duplicateGroups.delete(previousHash);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
this.pathToHash.delete(normalizedPath);
|
|
1286
|
+
}
|
|
1287
|
+
if (event === "unlink") {
|
|
1288
|
+
this.hashCache.delete(normalizedPath);
|
|
1289
|
+
} else {
|
|
1290
|
+
try {
|
|
1291
|
+
const stats = await import_fs3.default.promises.stat(absolutePath);
|
|
1292
|
+
const hash = await this.computeFileHash(absolutePath, stats.size);
|
|
1293
|
+
this.hashCache.set(normalizedPath, {
|
|
1294
|
+
hash,
|
|
1295
|
+
mtime: stats.mtimeMs,
|
|
1296
|
+
size: stats.size
|
|
1297
|
+
});
|
|
1298
|
+
this.pathToHash.set(normalizedPath, hash);
|
|
1299
|
+
if (!this.duplicateGroups.has(hash)) {
|
|
1300
|
+
this.duplicateGroups.set(hash, /* @__PURE__ */ new Set());
|
|
1301
|
+
}
|
|
1302
|
+
this.duplicateGroups.get(hash).add(normalizedPath);
|
|
1303
|
+
if (!affectedHashes.includes(hash)) {
|
|
1304
|
+
affectedHashes.push(hash);
|
|
1305
|
+
}
|
|
1306
|
+
} catch {
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (affectedHashes.length > 0) {
|
|
1310
|
+
this.emit("change", { event, affectedHashes });
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Initialize file watcher for real-time updates.
|
|
1315
|
+
* Note: This watches asset files, not source files.
|
|
1316
|
+
*/
|
|
1317
|
+
initWatcher() {
|
|
1318
|
+
if (this.watcher) return;
|
|
1319
|
+
const watchPaths = this.options.include.map((dir) => import_path6.default.join(this.root, dir));
|
|
1320
|
+
this.watcher = import_chokidar3.default.watch(watchPaths, {
|
|
1321
|
+
ignored: [
|
|
1322
|
+
...this.options.exclude.map((p) => `**/${p}/**`),
|
|
1323
|
+
(filePath) => {
|
|
1324
|
+
const ext = import_path6.default.extname(filePath).toLowerCase();
|
|
1325
|
+
return ext !== "" && !this.options.extensions.includes(ext);
|
|
1326
|
+
}
|
|
1327
|
+
],
|
|
1328
|
+
persistent: true,
|
|
1329
|
+
ignoreInitial: true,
|
|
1330
|
+
awaitWriteFinish: {
|
|
1331
|
+
stabilityThreshold: 200,
|
|
1332
|
+
pollInterval: 50
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
this.watcher.on("add", async (filePath) => {
|
|
1336
|
+
const relativePath = import_path6.default.relative(this.root, filePath);
|
|
1337
|
+
await this.handleAssetChange("add", relativePath, filePath);
|
|
1338
|
+
});
|
|
1339
|
+
this.watcher.on("change", async (filePath) => {
|
|
1340
|
+
const relativePath = import_path6.default.relative(this.root, filePath);
|
|
1341
|
+
await this.handleAssetChange("change", relativePath, filePath);
|
|
1342
|
+
});
|
|
1343
|
+
this.watcher.on("unlink", async (filePath) => {
|
|
1344
|
+
const relativePath = import_path6.default.relative(this.root, filePath);
|
|
1345
|
+
await this.handleAssetChange("unlink", relativePath, filePath);
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
destroy() {
|
|
1349
|
+
this.watcher?.close();
|
|
1350
|
+
this.hashCache.clear();
|
|
1351
|
+
this.duplicateGroups.clear();
|
|
1352
|
+
this.pathToHash.clear();
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
// src/server/thumbnail.ts
|
|
1357
|
+
var import_sharp = __toESM(require("sharp"), 1);
|
|
1358
|
+
var import_path7 = __toESM(require("path"), 1);
|
|
1359
|
+
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
1360
|
+
var import_crypto2 = require("crypto");
|
|
1361
|
+
var import_os = __toESM(require("os"), 1);
|
|
1362
|
+
var ThumbnailService = class {
|
|
1363
|
+
size;
|
|
1364
|
+
cache = /* @__PURE__ */ new Map();
|
|
1365
|
+
cacheDir;
|
|
1366
|
+
supportedFormats = [".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif", ".tiff"];
|
|
1367
|
+
constructor(size = 200) {
|
|
1368
|
+
this.size = size;
|
|
1369
|
+
this.cacheDir = import_path7.default.join(import_os.default.tmpdir(), "vite-asset-manager-thumbnails");
|
|
1370
|
+
}
|
|
1371
|
+
async getThumbnail(absolutePath) {
|
|
1372
|
+
const extension = import_path7.default.extname(absolutePath).toLowerCase();
|
|
1373
|
+
if (!this.supportedFormats.includes(extension)) {
|
|
1374
|
+
return null;
|
|
1375
|
+
}
|
|
1376
|
+
const cacheKey = await this.getCacheKey(absolutePath);
|
|
1377
|
+
const cached = this.cache.get(cacheKey);
|
|
1378
|
+
if (cached) {
|
|
1379
|
+
return cached;
|
|
1380
|
+
}
|
|
1381
|
+
const diskCached = await this.loadFromDiskCache(cacheKey);
|
|
1382
|
+
if (diskCached) {
|
|
1383
|
+
this.cache.set(cacheKey, diskCached);
|
|
1384
|
+
return diskCached;
|
|
1385
|
+
}
|
|
1386
|
+
try {
|
|
1387
|
+
const thumbnail = await this.generateThumbnail(absolutePath);
|
|
1388
|
+
this.cache.set(cacheKey, thumbnail);
|
|
1389
|
+
await this.saveToDiskCache(cacheKey, thumbnail);
|
|
1390
|
+
return thumbnail;
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
console.warn(`[asset-manager] Failed to generate thumbnail for ${absolutePath}:`, error);
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
async generateThumbnail(absolutePath) {
|
|
1397
|
+
return (0, import_sharp.default)(absolutePath).resize(this.size, this.size, {
|
|
1398
|
+
fit: "cover",
|
|
1399
|
+
position: "center"
|
|
1400
|
+
}).jpeg({ quality: 80 }).toBuffer();
|
|
1401
|
+
}
|
|
1402
|
+
async getCacheKey(absolutePath) {
|
|
1403
|
+
const hash = (0, import_crypto2.createHash)("md5");
|
|
1404
|
+
hash.update(absolutePath);
|
|
1405
|
+
hash.update(this.size.toString());
|
|
1406
|
+
try {
|
|
1407
|
+
const stats = await import_promises3.default.stat(absolutePath);
|
|
1408
|
+
hash.update(stats.mtimeMs.toString());
|
|
1409
|
+
} catch {
|
|
1410
|
+
}
|
|
1411
|
+
return hash.digest("hex");
|
|
1412
|
+
}
|
|
1413
|
+
async loadFromDiskCache(key) {
|
|
1414
|
+
try {
|
|
1415
|
+
const cachePath = import_path7.default.join(this.cacheDir, `${key}.jpg`);
|
|
1416
|
+
return await import_promises3.default.readFile(cachePath);
|
|
1417
|
+
} catch {
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
async saveToDiskCache(key, data) {
|
|
1422
|
+
try {
|
|
1423
|
+
await import_promises3.default.mkdir(this.cacheDir, { recursive: true });
|
|
1424
|
+
const cachePath = import_path7.default.join(this.cacheDir, `${key}.jpg`);
|
|
1425
|
+
await import_promises3.default.writeFile(cachePath, data);
|
|
1426
|
+
} catch {
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
invalidate(absolutePath) {
|
|
1430
|
+
this.getCacheKey(absolutePath).then((key) => {
|
|
1431
|
+
this.cache.delete(key);
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
isSupportedFormat(extension) {
|
|
1435
|
+
return this.supportedFormats.includes(extension.toLowerCase());
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
// src/shared/types.ts
|
|
1440
|
+
var DEFAULT_OPTIONS = {
|
|
1441
|
+
base: "/__asset_manager__",
|
|
1442
|
+
include: ["src", "public"],
|
|
1443
|
+
exclude: ["node_modules", ".git", "dist", ".cache", "coverage"],
|
|
1444
|
+
extensions: [
|
|
1445
|
+
/**
|
|
1446
|
+
* Images:
|
|
1447
|
+
*/
|
|
1448
|
+
".png",
|
|
1449
|
+
".jpg",
|
|
1450
|
+
".jpeg",
|
|
1451
|
+
".gif",
|
|
1452
|
+
".svg",
|
|
1453
|
+
".webp",
|
|
1454
|
+
".avif",
|
|
1455
|
+
".ico",
|
|
1456
|
+
".bmp",
|
|
1457
|
+
".tiff",
|
|
1458
|
+
".tif",
|
|
1459
|
+
".heic",
|
|
1460
|
+
".heif",
|
|
1461
|
+
/**
|
|
1462
|
+
* Videos:
|
|
1463
|
+
*/
|
|
1464
|
+
".mp4",
|
|
1465
|
+
".webm",
|
|
1466
|
+
".ogg",
|
|
1467
|
+
".mov",
|
|
1468
|
+
".avi",
|
|
1469
|
+
/**
|
|
1470
|
+
* Audio:
|
|
1471
|
+
*/
|
|
1472
|
+
".mp3",
|
|
1473
|
+
".wav",
|
|
1474
|
+
".flac",
|
|
1475
|
+
".aac",
|
|
1476
|
+
/**
|
|
1477
|
+
* Documents:
|
|
1478
|
+
*/
|
|
1479
|
+
".pdf",
|
|
1480
|
+
".doc",
|
|
1481
|
+
".docx",
|
|
1482
|
+
".xls",
|
|
1483
|
+
".xlsx",
|
|
1484
|
+
".ppt",
|
|
1485
|
+
".pptx",
|
|
1486
|
+
/**
|
|
1487
|
+
* Text/Config:
|
|
1488
|
+
*/
|
|
1489
|
+
".json",
|
|
1490
|
+
".md",
|
|
1491
|
+
".txt",
|
|
1492
|
+
".csv",
|
|
1493
|
+
".yml",
|
|
1494
|
+
".yaml",
|
|
1495
|
+
".toml",
|
|
1496
|
+
".xml",
|
|
1497
|
+
/**
|
|
1498
|
+
* Fonts:
|
|
1499
|
+
*/
|
|
1500
|
+
".woff",
|
|
1501
|
+
".woff2",
|
|
1502
|
+
".ttf",
|
|
1503
|
+
".otf",
|
|
1504
|
+
".eot"
|
|
1505
|
+
],
|
|
1506
|
+
thumbnails: true,
|
|
1507
|
+
thumbnailSize: 200,
|
|
1508
|
+
watch: true,
|
|
1509
|
+
floatingIcon: true,
|
|
1510
|
+
launchEditor: "code"
|
|
1511
|
+
};
|
|
1512
|
+
function resolveOptions(options) {
|
|
1513
|
+
return {
|
|
1514
|
+
...DEFAULT_OPTIONS,
|
|
1515
|
+
...options
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// src/plugin.ts
|
|
1520
|
+
var FLOATING_ICON_SCRIPT = (base) => `
|
|
1521
|
+
<script type="module">
|
|
1522
|
+
(function() {
|
|
1523
|
+
const BASE_URL = '${base}';
|
|
1524
|
+
const STORAGE_KEY = 'vite-asset-manager-open';
|
|
1525
|
+
|
|
1526
|
+
// Styles for the floating button and overlay
|
|
1527
|
+
const styles = document.createElement('style');
|
|
1528
|
+
styles.textContent = \`
|
|
1529
|
+
#vam-trigger {
|
|
1530
|
+
position: fixed;
|
|
1531
|
+
bottom: 20px;
|
|
1532
|
+
right: 20px;
|
|
1533
|
+
width: 48px;
|
|
1534
|
+
height: 48px;
|
|
1535
|
+
border-radius: 14px;
|
|
1536
|
+
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
|
1537
|
+
border: none;
|
|
1538
|
+
cursor: pointer;
|
|
1539
|
+
display: flex;
|
|
1540
|
+
align-items: center;
|
|
1541
|
+
justify-content: center;
|
|
1542
|
+
box-shadow: 0 4px 24px -4px rgba(139, 92, 246, 0.5), 0 0 0 1px rgba(255,255,255,0.1) inset;
|
|
1543
|
+
transition: all 0.2s ease;
|
|
1544
|
+
z-index: 99998;
|
|
1545
|
+
}
|
|
1546
|
+
#vam-trigger:hover {
|
|
1547
|
+
transform: translateY(-2px) scale(1.05);
|
|
1548
|
+
box-shadow: 0 8px 32px -4px rgba(139, 92, 246, 0.6), 0 0 0 1px rgba(255,255,255,0.15) inset;
|
|
1549
|
+
}
|
|
1550
|
+
#vam-trigger:active {
|
|
1551
|
+
transform: translateY(0) scale(0.98);
|
|
1552
|
+
}
|
|
1553
|
+
#vam-trigger svg {
|
|
1554
|
+
width: 24px;
|
|
1555
|
+
height: 24px;
|
|
1556
|
+
color: white;
|
|
1557
|
+
}
|
|
1558
|
+
#vam-overlay {
|
|
1559
|
+
position: fixed;
|
|
1560
|
+
inset: 0;
|
|
1561
|
+
background: rgba(0, 0, 0, 0.6);
|
|
1562
|
+
backdrop-filter: blur(4px);
|
|
1563
|
+
z-index: 99999;
|
|
1564
|
+
opacity: 0;
|
|
1565
|
+
visibility: hidden;
|
|
1566
|
+
transition: opacity 0.3s ease, visibility 0.3s ease;
|
|
1567
|
+
}
|
|
1568
|
+
#vam-overlay.open {
|
|
1569
|
+
opacity: 1;
|
|
1570
|
+
visibility: visible;
|
|
1571
|
+
}
|
|
1572
|
+
#vam-panel {
|
|
1573
|
+
position: fixed;
|
|
1574
|
+
top: 0;
|
|
1575
|
+
right: 0;
|
|
1576
|
+
bottom: 0;
|
|
1577
|
+
width: min(90vw, 1200px);
|
|
1578
|
+
background: #09090b;
|
|
1579
|
+
border-left: 1px solid rgba(255,255,255,0.08);
|
|
1580
|
+
transform: translateX(100%);
|
|
1581
|
+
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
|
|
1582
|
+
z-index: 100000;
|
|
1583
|
+
display: flex;
|
|
1584
|
+
flex-direction: column;
|
|
1585
|
+
}
|
|
1586
|
+
#vam-overlay.open #vam-panel {
|
|
1587
|
+
transform: translateX(0);
|
|
1588
|
+
}
|
|
1589
|
+
#vam-panel-header {
|
|
1590
|
+
display: flex;
|
|
1591
|
+
align-items: center;
|
|
1592
|
+
justify-content: space-between;
|
|
1593
|
+
padding: 12px 16px;
|
|
1594
|
+
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
1595
|
+
background: #0f0f11;
|
|
1596
|
+
}
|
|
1597
|
+
#vam-panel-title {
|
|
1598
|
+
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
1599
|
+
font-size: 12px;
|
|
1600
|
+
font-weight: 600;
|
|
1601
|
+
color: #fafafa;
|
|
1602
|
+
letter-spacing: 0.05em;
|
|
1603
|
+
display: flex;
|
|
1604
|
+
align-items: center;
|
|
1605
|
+
gap: 8px;
|
|
1606
|
+
}
|
|
1607
|
+
#vam-panel-title svg {
|
|
1608
|
+
width: 18px;
|
|
1609
|
+
height: 18px;
|
|
1610
|
+
color: #8b5cf6;
|
|
1611
|
+
}
|
|
1612
|
+
#vam-close-btn {
|
|
1613
|
+
width: 32px;
|
|
1614
|
+
height: 32px;
|
|
1615
|
+
border-radius: 8px;
|
|
1616
|
+
border: none;
|
|
1617
|
+
background: transparent;
|
|
1618
|
+
cursor: pointer;
|
|
1619
|
+
display: flex;
|
|
1620
|
+
align-items: center;
|
|
1621
|
+
justify-content: center;
|
|
1622
|
+
color: #71717a;
|
|
1623
|
+
transition: all 0.15s ease;
|
|
1624
|
+
}
|
|
1625
|
+
#vam-close-btn:hover {
|
|
1626
|
+
background: rgba(255,255,255,0.1);
|
|
1627
|
+
color: #fafafa;
|
|
1628
|
+
}
|
|
1629
|
+
#vam-iframe {
|
|
1630
|
+
flex: 1;
|
|
1631
|
+
border: none;
|
|
1632
|
+
width: 100%;
|
|
1633
|
+
height: 100%;
|
|
1634
|
+
}
|
|
1635
|
+
\`;
|
|
1636
|
+
document.head.appendChild(styles);
|
|
1637
|
+
|
|
1638
|
+
// Create trigger button
|
|
1639
|
+
const trigger = document.createElement('button');
|
|
1640
|
+
trigger.id = 'vam-trigger';
|
|
1641
|
+
trigger.title = 'Open Asset Manager (\u2325\u21E7A)';
|
|
1642
|
+
trigger.innerHTML = \`
|
|
1643
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor">
|
|
1644
|
+
<path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,16V88H40V56Zm0,144H40V104H216v96ZM64,128a8,8,0,0,1,8-8h80a8,8,0,0,1,0,16H72A8,8,0,0,1,64,128Zm0,32a8,8,0,0,1,8-8h80a8,8,0,0,1,0,16H72A8,8,0,0,1,64,160Z"/>
|
|
1645
|
+
</svg>
|
|
1646
|
+
\`;
|
|
1647
|
+
|
|
1648
|
+
// Create overlay and panel
|
|
1649
|
+
const overlay = document.createElement('div');
|
|
1650
|
+
overlay.id = 'vam-overlay';
|
|
1651
|
+
overlay.innerHTML = \`
|
|
1652
|
+
<div id="vam-panel">
|
|
1653
|
+
<div id="vam-panel-header">
|
|
1654
|
+
<div id="vam-panel-title">
|
|
1655
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor">
|
|
1656
|
+
<path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,160H40V56H216V200ZM176,88a48,48,0,1,0-96,0,48,48,0,0,0,96,0Zm-48,32a32,32,0,1,1,32-32A32,32,0,0,1,128,120Zm80,56a8,8,0,0,1-8,8H56a8,8,0,0,1-6.65-12.44l24-36a8,8,0,0,1,13.3,0l15.18,22.77,24.89-41.48a8,8,0,0,1,13.72.18l40,64A8,8,0,0,1,208,176Z"/>
|
|
1657
|
+
</svg>
|
|
1658
|
+
ASSET MANAGER
|
|
1659
|
+
</div>
|
|
1660
|
+
<button id="vam-close-btn" title="Close (Esc)">
|
|
1661
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor">
|
|
1662
|
+
<path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/>
|
|
1663
|
+
</svg>
|
|
1664
|
+
</button>
|
|
1665
|
+
</div>
|
|
1666
|
+
<iframe id="vam-iframe" src="\${BASE_URL}?embedded=true"></iframe>
|
|
1667
|
+
</div>
|
|
1668
|
+
\`;
|
|
1669
|
+
|
|
1670
|
+
document.body.appendChild(trigger);
|
|
1671
|
+
document.body.appendChild(overlay);
|
|
1672
|
+
|
|
1673
|
+
// State management
|
|
1674
|
+
let isOpen = sessionStorage.getItem(STORAGE_KEY) === 'true';
|
|
1675
|
+
|
|
1676
|
+
function open() {
|
|
1677
|
+
isOpen = true;
|
|
1678
|
+
overlay.classList.add('open');
|
|
1679
|
+
sessionStorage.setItem(STORAGE_KEY, 'true');
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function close() {
|
|
1683
|
+
isOpen = false;
|
|
1684
|
+
overlay.classList.remove('open');
|
|
1685
|
+
sessionStorage.setItem(STORAGE_KEY, 'false');
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// Restore state
|
|
1689
|
+
if (isOpen) {
|
|
1690
|
+
requestAnimationFrame(() => open());
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// Event listeners
|
|
1694
|
+
trigger.addEventListener('click', () => {
|
|
1695
|
+
if (isOpen) close();
|
|
1696
|
+
else open();
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
overlay.addEventListener('click', (e) => {
|
|
1700
|
+
if (e.target === overlay) close();
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
document.getElementById('vam-close-btn').addEventListener('click', close);
|
|
1704
|
+
|
|
1705
|
+
document.addEventListener('keydown', (e) => {
|
|
1706
|
+
// Close on Escape
|
|
1707
|
+
if (e.key === 'Escape' && isOpen) {
|
|
1708
|
+
close();
|
|
1709
|
+
}
|
|
1710
|
+
// Toggle on Option/Alt + Shift + A
|
|
1711
|
+
if (e.altKey && e.shiftKey && e.code === 'KeyA') {
|
|
1712
|
+
e.preventDefault();
|
|
1713
|
+
if (isOpen) close();
|
|
1714
|
+
else open();
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
})();
|
|
1718
|
+
</script>
|
|
1719
|
+
`;
|
|
1720
|
+
function createAssetManagerPlugin(options = {}) {
|
|
1721
|
+
let config;
|
|
1722
|
+
let scanner;
|
|
1723
|
+
let importerScanner;
|
|
1724
|
+
let duplicateScanner;
|
|
1725
|
+
let thumbnailService;
|
|
1726
|
+
const resolvedOptions = resolveOptions(options);
|
|
1727
|
+
return {
|
|
1728
|
+
name: "vite-plugin-asset-manager",
|
|
1729
|
+
apply: "serve",
|
|
1730
|
+
configResolved(resolvedConfig) {
|
|
1731
|
+
config = resolvedConfig;
|
|
1732
|
+
},
|
|
1733
|
+
configureServer(server) {
|
|
1734
|
+
scanner = new AssetScanner(config.root, resolvedOptions);
|
|
1735
|
+
importerScanner = new ImporterScanner(config.root, resolvedOptions);
|
|
1736
|
+
duplicateScanner = new DuplicateScanner(config.root, resolvedOptions);
|
|
1737
|
+
thumbnailService = new ThumbnailService(resolvedOptions.thumbnailSize);
|
|
1738
|
+
setupMiddleware(server, {
|
|
1739
|
+
base: resolvedOptions.base,
|
|
1740
|
+
scanner,
|
|
1741
|
+
importerScanner,
|
|
1742
|
+
duplicateScanner,
|
|
1743
|
+
thumbnailService,
|
|
1744
|
+
root: config.root,
|
|
1745
|
+
launchEditor: resolvedOptions.launchEditor
|
|
1746
|
+
});
|
|
1747
|
+
scanner.init().then(async () => {
|
|
1748
|
+
await importerScanner.init();
|
|
1749
|
+
scanner.enrichWithImporterCounts(importerScanner);
|
|
1750
|
+
await duplicateScanner.init();
|
|
1751
|
+
await duplicateScanner.scanAssets(scanner.getAssets());
|
|
1752
|
+
scanner.enrichWithDuplicateInfo(duplicateScanner);
|
|
1753
|
+
if (resolvedOptions.watch) {
|
|
1754
|
+
duplicateScanner.initWatcher();
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
const _printUrls = server.printUrls;
|
|
1758
|
+
server.printUrls = () => {
|
|
1759
|
+
_printUrls();
|
|
1760
|
+
const colorUrl = (url2) => import_picocolors.default.cyan(url2.replace(/:(\d+)\//, (_, port) => `:${import_picocolors.default.bold(port)}/`));
|
|
1761
|
+
let host = `${server.config.server.https ? "https" : "http"}://localhost:${server.config.server.port || "80"}`;
|
|
1762
|
+
const url = server.resolvedUrls?.local[0];
|
|
1763
|
+
if (url) {
|
|
1764
|
+
try {
|
|
1765
|
+
const u = new URL(url);
|
|
1766
|
+
host = `${u.protocol}//${u.host}`;
|
|
1767
|
+
} catch {
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
const base = server.config.base || "/";
|
|
1771
|
+
const fullUrl = `${host}${base}${resolvedOptions.base.replace(/^\//, "")}/`;
|
|
1772
|
+
server.config.logger.info(
|
|
1773
|
+
` ${import_picocolors.default.magenta("\u279C")} ${import_picocolors.default.bold("Asset Manager")}: Open ${colorUrl(fullUrl)} as a separate window`
|
|
1774
|
+
);
|
|
1775
|
+
server.config.logger.info(
|
|
1776
|
+
` ${import_picocolors.default.magenta("\u279C")} ${import_picocolors.default.bold("Asset Manager")}: Press ${import_picocolors.default.yellow("Option(\u2325)+Shift(\u21E7)+A")} in App to toggle the Asset Manager`
|
|
1777
|
+
);
|
|
1778
|
+
};
|
|
1779
|
+
if (resolvedOptions.watch) {
|
|
1780
|
+
scanner.on("change", async (event) => {
|
|
1781
|
+
await duplicateScanner.scanAssets(scanner.getAssets());
|
|
1782
|
+
scanner.enrichWithDuplicateInfo(duplicateScanner);
|
|
1783
|
+
broadcastSSE("asset-manager:update", event);
|
|
1784
|
+
});
|
|
1785
|
+
importerScanner.on("change", (event) => {
|
|
1786
|
+
scanner.enrichWithImporterCounts(importerScanner);
|
|
1787
|
+
broadcastSSE("asset-manager:importers-update", event);
|
|
1788
|
+
});
|
|
1789
|
+
duplicateScanner.on("change", (event) => {
|
|
1790
|
+
scanner.enrichWithDuplicateInfo(duplicateScanner);
|
|
1791
|
+
broadcastSSE("asset-manager:duplicates-update", event);
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
},
|
|
1795
|
+
transformIndexHtml() {
|
|
1796
|
+
if (!resolvedOptions.floatingIcon) {
|
|
1797
|
+
return [];
|
|
1798
|
+
}
|
|
1799
|
+
return [
|
|
1800
|
+
{
|
|
1801
|
+
tag: "script",
|
|
1802
|
+
attrs: { type: "module" },
|
|
1803
|
+
children: FLOATING_ICON_SCRIPT(resolvedOptions.base).replace(/<\/?script[^>]*>/g, "").trim(),
|
|
1804
|
+
injectTo: "body"
|
|
1805
|
+
}
|
|
1806
|
+
];
|
|
1807
|
+
},
|
|
1808
|
+
resolveId(id) {
|
|
1809
|
+
if (id === "virtual:asset-manager-config") {
|
|
1810
|
+
return "\0virtual:asset-manager-config";
|
|
1811
|
+
}
|
|
1812
|
+
},
|
|
1813
|
+
load(id) {
|
|
1814
|
+
if (id === "\0virtual:asset-manager-config") {
|
|
1815
|
+
return `export default ${JSON.stringify({
|
|
1816
|
+
base: resolvedOptions.base,
|
|
1817
|
+
extensions: resolvedOptions.extensions
|
|
1818
|
+
})}`;
|
|
1819
|
+
}
|
|
1820
|
+
},
|
|
1821
|
+
buildEnd() {
|
|
1822
|
+
scanner?.destroy();
|
|
1823
|
+
importerScanner?.destroy();
|
|
1824
|
+
duplicateScanner?.destroy();
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// src/index.ts
|
|
1830
|
+
function assetManager(options = {}) {
|
|
1831
|
+
return createAssetManagerPlugin(options);
|
|
1832
|
+
}
|
|
1833
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1834
|
+
0 && (module.exports = {
|
|
1835
|
+
assetManager
|
|
1836
|
+
});
|
|
1837
|
+
//# sourceMappingURL=index.cjs.map
|