@imjp/writenex-astro 0.1.0 → 1.3.6
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 +13 -13
- package/dist/{chunk-CF2XXJFF.js → chunk-4H63L4YO.js} +436 -436
- package/dist/chunk-4H63L4YO.js.map +1 -0
- package/dist/{chunk-AAOQHQPU.js → chunk-GYAFIVVI.js} +6 -6
- package/dist/chunk-GYAFIVVI.js.map +1 -0
- package/dist/{chunk-XNTQTTJU.js → chunk-JFQQJPDF.js} +2 -2
- package/dist/{chunk-XNTQTTJU.js.map → chunk-JFQQJPDF.js.map} +1 -1
- package/dist/{chunk-CYLDJ3HZ.js → chunk-JMNCPNQX.js} +4 -4
- package/dist/{chunk-CYLDJ3HZ.js.map → chunk-JMNCPNQX.js.map} +1 -1
- package/dist/{chunk-5PM6EQE5.js → chunk-N37EPLKG.js} +13 -5
- package/dist/chunk-N37EPLKG.js.map +1 -0
- package/dist/{chunk-7XU5X6CW.js → chunk-NSW7AIVF.js} +12 -12
- package/dist/chunk-NSW7AIVF.js.map +1 -0
- package/dist/{chunk-CRPZUUDU.js → chunk-YBCPOLMY.js} +1 -1
- package/dist/{chunk-CRPZUUDU.js.map → chunk-YBCPOLMY.js.map} +1 -1
- package/dist/client/index.css +1 -1
- package/dist/client/index.css.map +1 -1
- package/dist/client/index.d.ts +19 -0
- package/dist/client/index.js +159 -147
- package/dist/client/index.js.map +1 -1
- package/dist/client/styles.css +2 -8
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +2 -2
- package/dist/{config-BmEdBDo_.d.ts → config-CliL0CoN.d.ts} +1 -1
- package/dist/{content-BWR52vD-.d.ts → content-TuL3GT66.d.ts} +1 -1
- package/dist/discovery/index.d.ts +2 -2
- package/dist/discovery/index.js +3 -3
- package/dist/filesystem/index.d.ts +703 -703
- package/dist/filesystem/index.js +4 -4
- package/dist/filesystem/index.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/{loader-55LWCXHA.js → loader-53VVP2IN.js} +3 -3
- package/dist/schema-DDJyoVkj.d.ts +189 -0
- package/dist/server/index.d.ts +37 -37
- package/dist/server/index.js +5 -5
- package/package.json +17 -18
- package/src/client/App.tsx +18 -18
- package/src/client/components/ConfigPanel/ConfigPanel.tsx +14 -13
- package/src/client/components/CreateContentModal/CreateContentModal.tsx +1 -1
- package/src/client/components/Editor/Editor.tsx +27 -27
- package/src/client/components/Editor/ImageDialog.tsx +4 -3
- package/src/client/components/Editor/LinkDialog.tsx +7 -6
- package/src/client/components/Editor/index.ts +1 -1
- package/src/client/components/FrontmatterForm/FrontmatterForm.css +1 -1
- package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +1 -1
- package/src/client/components/Header/Header.tsx +8 -8
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +1 -1
- package/src/client/components/LazyEditor.tsx +1 -1
- package/src/client/components/LiveRegion/index.ts +1 -1
- package/src/client/components/SearchReplace/SearchReplacePanel.tsx +5 -5
- package/src/client/components/SearchReplace/index.ts +1 -1
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +2 -2
- package/src/client/components/Sidebar/Sidebar.tsx +6 -6
- package/src/client/components/SkipLink/index.ts +1 -1
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +1 -1
- package/src/client/components/VersionHistory/DiffViewer.tsx +18 -11
- package/src/client/components/VersionHistory/VersionActions.tsx +6 -6
- package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +10 -10
- package/src/client/components/VersionHistory/index.ts +2 -2
- package/src/client/context/ApiContext.tsx +2 -2
- package/src/client/context/ThemeContext.tsx +2 -2
- package/src/client/hooks/useApi.ts +1 -1
- package/src/client/hooks/useFocusTrap.ts +1 -1
- package/src/client/hooks/useSearch.ts +1 -1
- package/src/client/hooks/useVersionHistory.ts +2 -2
- package/src/client/index.tsx +1 -1
- package/src/client/styles.css +2 -8
- package/src/config/defaults.ts +4 -4
- package/src/config/index.ts +14 -16
- package/src/config/loader.ts +24 -4
- package/src/config/schema.ts +8 -4
- package/src/core/index.ts +1 -1
- package/src/discovery/collections.ts +3 -3
- package/src/discovery/index.ts +9 -11
- package/src/discovery/patterns.ts +2 -2
- package/src/discovery/schema.ts +1 -1
- package/src/filesystem/images.ts +3 -3
- package/src/filesystem/index.ts +74 -79
- package/src/filesystem/reader.ts +5 -3
- package/src/filesystem/version-config.ts +10 -10
- package/src/filesystem/versions.ts +9 -9
- package/src/filesystem/watcher.ts +1 -1
- package/src/filesystem/writer.ts +6 -6
- package/src/global.d.ts +39 -0
- package/src/index.ts +10 -10
- package/src/integration.ts +6 -3
- package/src/server/assets.ts +3 -3
- package/src/server/cache.ts +1 -1
- package/src/server/index.ts +12 -15
- package/src/server/middleware.ts +3 -3
- package/src/server/routes.ts +28 -28
- package/src/types/index.ts +24 -28
- package/dist/chunk-5PM6EQE5.js.map +0 -1
- package/dist/chunk-7XU5X6CW.js.map +0 -1
- package/dist/chunk-AAOQHQPU.js.map +0 -1
- package/dist/chunk-CF2XXJFF.js.map +0 -1
- package/dist/loader-CrdnaAWR.d.ts +0 -327
- /package/dist/{loader-55LWCXHA.js.map → loader-53VVP2IN.js.map} +0 -0
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
isValidPattern,
|
|
5
5
|
readContentFile,
|
|
6
6
|
resolvePatternTokens
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-GYAFIVVI.js";
|
|
8
8
|
|
|
9
9
|
// src/core/errors.ts
|
|
10
10
|
var WritenexErrorCode = /* @__PURE__ */ ((WritenexErrorCode2) => {
|
|
@@ -260,17 +260,330 @@ function wrapError(error, defaultCode = "UNKNOWN_ERROR" /* UNKNOWN_ERROR */) {
|
|
|
260
260
|
return new WritenexError(defaultCode, message, { cause });
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
// src/filesystem/images.ts
|
|
264
|
+
import { existsSync } from "fs";
|
|
265
|
+
import { mkdir, readdir, stat, writeFile } from "fs/promises";
|
|
266
|
+
import { basename, dirname, extname, join, relative } from "path";
|
|
267
|
+
var DEFAULT_IMAGE_CONFIG = {
|
|
268
|
+
strategy: "colocated",
|
|
269
|
+
publicPath: "/images",
|
|
270
|
+
storagePath: "public/images"
|
|
271
|
+
};
|
|
272
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
273
|
+
".jpg",
|
|
274
|
+
".jpeg",
|
|
275
|
+
".png",
|
|
276
|
+
".gif",
|
|
277
|
+
".webp",
|
|
278
|
+
".avif",
|
|
279
|
+
".svg"
|
|
280
|
+
]);
|
|
281
|
+
function isValidImageFile(filename) {
|
|
282
|
+
const ext = extname(filename).toLowerCase();
|
|
283
|
+
return SUPPORTED_EXTENSIONS.has(ext);
|
|
284
|
+
}
|
|
285
|
+
function generateUniqueFilename(originalName, _contentId) {
|
|
286
|
+
const ext = extname(originalName).toLowerCase();
|
|
287
|
+
const baseName = basename(originalName, ext).toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").substring(0, 50);
|
|
288
|
+
const timestamp = Date.now().toString(36);
|
|
289
|
+
return `${baseName}-${timestamp}${ext}`;
|
|
290
|
+
}
|
|
291
|
+
function getColocatedPath(projectRoot, collection, contentId, filename) {
|
|
292
|
+
const collectionPath = join(projectRoot, "src/content", collection);
|
|
293
|
+
const imageDir = join(collectionPath, contentId);
|
|
294
|
+
const storagePath = join(imageDir, filename);
|
|
295
|
+
const indexMdPath = join(collectionPath, contentId, "index.md");
|
|
296
|
+
const indexMdxPath = join(collectionPath, contentId, "index.mdx");
|
|
297
|
+
const isFolderBased = existsSync(indexMdPath) || existsSync(indexMdxPath);
|
|
298
|
+
const markdownPath = isFolderBased ? `./${filename}` : `./${contentId}/${filename}`;
|
|
299
|
+
return { storagePath, markdownPath };
|
|
300
|
+
}
|
|
301
|
+
function getPublicPath(projectRoot, collection, filename, config) {
|
|
302
|
+
const storagePath = join(
|
|
303
|
+
projectRoot,
|
|
304
|
+
config.storagePath ?? "public/images",
|
|
305
|
+
collection,
|
|
306
|
+
filename
|
|
307
|
+
);
|
|
308
|
+
const publicPath = config.publicPath ?? "/images";
|
|
309
|
+
const url = `${publicPath}/${collection}/${filename}`;
|
|
310
|
+
return { storagePath, markdownPath: url, url };
|
|
311
|
+
}
|
|
312
|
+
async function uploadImage(options) {
|
|
313
|
+
const {
|
|
314
|
+
filename,
|
|
315
|
+
data,
|
|
316
|
+
collection,
|
|
317
|
+
contentId,
|
|
318
|
+
projectRoot,
|
|
319
|
+
config = DEFAULT_IMAGE_CONFIG
|
|
320
|
+
} = options;
|
|
321
|
+
if (!isValidImageFile(filename)) {
|
|
322
|
+
return {
|
|
323
|
+
success: false,
|
|
324
|
+
error: `Invalid image file type. Supported: ${[...SUPPORTED_EXTENSIONS].join(", ")}`
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const uniqueFilename = generateUniqueFilename(filename, contentId);
|
|
328
|
+
try {
|
|
329
|
+
let storagePath;
|
|
330
|
+
let markdownPath;
|
|
331
|
+
let url;
|
|
332
|
+
switch (config.strategy) {
|
|
333
|
+
case "public": {
|
|
334
|
+
const paths = getPublicPath(
|
|
335
|
+
projectRoot,
|
|
336
|
+
collection,
|
|
337
|
+
uniqueFilename,
|
|
338
|
+
config
|
|
339
|
+
);
|
|
340
|
+
storagePath = paths.storagePath;
|
|
341
|
+
markdownPath = paths.markdownPath;
|
|
342
|
+
url = paths.url;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case "colocated":
|
|
346
|
+
default: {
|
|
347
|
+
const paths = getColocatedPath(
|
|
348
|
+
projectRoot,
|
|
349
|
+
collection,
|
|
350
|
+
contentId,
|
|
351
|
+
uniqueFilename
|
|
352
|
+
);
|
|
353
|
+
storagePath = paths.storagePath;
|
|
354
|
+
markdownPath = paths.markdownPath;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const dir = dirname(storagePath);
|
|
359
|
+
if (!existsSync(dir)) {
|
|
360
|
+
await mkdir(dir, { recursive: true });
|
|
361
|
+
}
|
|
362
|
+
await writeFile(storagePath, data);
|
|
363
|
+
return {
|
|
364
|
+
success: true,
|
|
365
|
+
path: markdownPath,
|
|
366
|
+
url: url ?? markdownPath
|
|
367
|
+
};
|
|
368
|
+
} catch (error) {
|
|
369
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
error: `Failed to upload image: ${message}`
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function parseMultipartFormData(body, contentType) {
|
|
377
|
+
const result = { fields: {} };
|
|
378
|
+
const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/);
|
|
379
|
+
if (!boundaryMatch) {
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
382
|
+
const boundary = boundaryMatch[1] ?? boundaryMatch[2];
|
|
383
|
+
if (!boundary) {
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
const boundaryBuffer = Buffer.from(`--${boundary}`);
|
|
387
|
+
const parts = splitBuffer(body, boundaryBuffer);
|
|
388
|
+
for (const part of parts) {
|
|
389
|
+
if (part.length < 10) continue;
|
|
390
|
+
const separatorIndex = part.indexOf("\r\n\r\n");
|
|
391
|
+
if (separatorIndex === -1) continue;
|
|
392
|
+
const headerSection = part.slice(0, separatorIndex).toString("utf-8");
|
|
393
|
+
const bodySection = part.slice(separatorIndex + 4);
|
|
394
|
+
const bodyEnd = bodySection.length - 2;
|
|
395
|
+
const cleanBody = bodyEnd > 0 ? bodySection.slice(0, bodyEnd) : bodySection;
|
|
396
|
+
const headers = parseHeaders(headerSection);
|
|
397
|
+
const disposition = headers["content-disposition"];
|
|
398
|
+
if (!disposition) continue;
|
|
399
|
+
const nameMatch = disposition.match(/name="([^"]+)"/);
|
|
400
|
+
const filenameMatch = disposition.match(/filename="([^"]+)"/);
|
|
401
|
+
if (filenameMatch) {
|
|
402
|
+
result.file = {
|
|
403
|
+
filename: filenameMatch[1] ?? "unknown",
|
|
404
|
+
data: cleanBody,
|
|
405
|
+
contentType: headers["content-type"] ?? "application/octet-stream"
|
|
406
|
+
};
|
|
407
|
+
} else if (nameMatch) {
|
|
408
|
+
result.fields[nameMatch[1] ?? ""] = cleanBody.toString("utf-8");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
function splitBuffer(buffer, delimiter) {
|
|
414
|
+
const parts = [];
|
|
415
|
+
let start = 0;
|
|
416
|
+
let index;
|
|
417
|
+
while ((index = buffer.indexOf(delimiter, start)) !== -1) {
|
|
418
|
+
if (index > start) {
|
|
419
|
+
parts.push(buffer.slice(start, index));
|
|
420
|
+
}
|
|
421
|
+
start = index + delimiter.length;
|
|
422
|
+
}
|
|
423
|
+
if (start < buffer.length) {
|
|
424
|
+
parts.push(buffer.slice(start));
|
|
425
|
+
}
|
|
426
|
+
return parts;
|
|
427
|
+
}
|
|
428
|
+
function parseHeaders(headerSection) {
|
|
429
|
+
const headers = {};
|
|
430
|
+
const lines = headerSection.split("\r\n");
|
|
431
|
+
for (const line of lines) {
|
|
432
|
+
const colonIndex = line.indexOf(":");
|
|
433
|
+
if (colonIndex > 0) {
|
|
434
|
+
const key = line.slice(0, colonIndex).trim().toLowerCase();
|
|
435
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
436
|
+
headers[key] = value;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return headers;
|
|
440
|
+
}
|
|
441
|
+
var DATE_PREFIX_PATTERN = /^\d{4}-\d{2}-\d{2}-/;
|
|
442
|
+
function getContentImageFolder(collectionPath, contentId, contentFilePath) {
|
|
443
|
+
const filename = basename(contentFilePath);
|
|
444
|
+
const contentDir = dirname(contentFilePath);
|
|
445
|
+
if (filename === "index.md" || filename === "index.mdx") {
|
|
446
|
+
return contentDir;
|
|
447
|
+
}
|
|
448
|
+
const siblingFolderPath = join(collectionPath, contentId);
|
|
449
|
+
if (existsSync(siblingFolderPath)) {
|
|
450
|
+
return siblingFolderPath;
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
function detectContentStructure(contentFilePath) {
|
|
455
|
+
const filename = basename(contentFilePath);
|
|
456
|
+
if (filename === "index.md" || filename === "index.mdx") {
|
|
457
|
+
return "folder-based";
|
|
458
|
+
}
|
|
459
|
+
const nameWithoutExt = filename.replace(/\.(md|mdx)$/, "");
|
|
460
|
+
if (DATE_PREFIX_PATTERN.test(nameWithoutExt)) {
|
|
461
|
+
return "date-prefixed";
|
|
462
|
+
}
|
|
463
|
+
return "flat";
|
|
464
|
+
}
|
|
465
|
+
function shouldSkipDirectory(dirName) {
|
|
466
|
+
return dirName.startsWith(".") || dirName.startsWith("_");
|
|
467
|
+
}
|
|
468
|
+
async function scanDirectoryForImages(dirPath, basePath, options) {
|
|
469
|
+
const { maxDepth, currentDepth } = options;
|
|
470
|
+
if (currentDepth >= maxDepth) {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
if (!existsSync(dirPath)) {
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
const images = [];
|
|
477
|
+
try {
|
|
478
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
479
|
+
for (const entry of entries) {
|
|
480
|
+
const entryPath = join(dirPath, entry.name);
|
|
481
|
+
if (entry.isDirectory()) {
|
|
482
|
+
if (shouldSkipDirectory(entry.name)) {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const subImages = await scanDirectoryForImages(entryPath, basePath, {
|
|
486
|
+
maxDepth,
|
|
487
|
+
currentDepth: currentDepth + 1,
|
|
488
|
+
basePath
|
|
489
|
+
});
|
|
490
|
+
images.push(...subImages);
|
|
491
|
+
} else if (entry.isFile()) {
|
|
492
|
+
if (isValidImageFile(entry.name)) {
|
|
493
|
+
const fileStat = await stat(entryPath);
|
|
494
|
+
const extension = extname(entry.name).toLowerCase();
|
|
495
|
+
const relativePath = calculateRelativePathFromBase(
|
|
496
|
+
basePath,
|
|
497
|
+
entryPath
|
|
498
|
+
);
|
|
499
|
+
images.push({
|
|
500
|
+
filename: entry.name,
|
|
501
|
+
relativePath,
|
|
502
|
+
absolutePath: entryPath,
|
|
503
|
+
size: fileStat.size,
|
|
504
|
+
extension
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} catch {
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
return images;
|
|
513
|
+
}
|
|
514
|
+
function calculateRelativePathFromBase(basePath, targetPath) {
|
|
515
|
+
const relPath = relative(basePath, targetPath);
|
|
516
|
+
return `./${relPath}`;
|
|
517
|
+
}
|
|
518
|
+
function calculateRelativePath(contentFilePath, imagePath) {
|
|
519
|
+
const contentDir = dirname(contentFilePath);
|
|
520
|
+
const relPath = relative(contentDir, imagePath);
|
|
521
|
+
if (relPath.startsWith("..")) {
|
|
522
|
+
return relPath;
|
|
523
|
+
}
|
|
524
|
+
return `./${relPath}`;
|
|
525
|
+
}
|
|
526
|
+
var DEFAULT_MAX_DEPTH = 5;
|
|
527
|
+
async function discoverContentImages(collectionPath, contentId, options) {
|
|
528
|
+
const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
529
|
+
try {
|
|
530
|
+
const contentFilePath = getContentFilePath(collectionPath, contentId);
|
|
531
|
+
if (!contentFilePath) {
|
|
532
|
+
return {
|
|
533
|
+
success: false,
|
|
534
|
+
images: [],
|
|
535
|
+
error: `Content '${contentId}' not found in collection`
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const imageFolderPath = getContentImageFolder(
|
|
539
|
+
collectionPath,
|
|
540
|
+
contentId,
|
|
541
|
+
contentFilePath
|
|
542
|
+
);
|
|
543
|
+
if (!imageFolderPath) {
|
|
544
|
+
return {
|
|
545
|
+
success: true,
|
|
546
|
+
images: []
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
const scannedImages = await scanDirectoryForImages(
|
|
550
|
+
imageFolderPath,
|
|
551
|
+
imageFolderPath,
|
|
552
|
+
{
|
|
553
|
+
maxDepth,
|
|
554
|
+
currentDepth: 0,
|
|
555
|
+
basePath: imageFolderPath
|
|
556
|
+
}
|
|
557
|
+
);
|
|
558
|
+
const images = scannedImages.map((img) => ({
|
|
559
|
+
...img,
|
|
560
|
+
relativePath: calculateRelativePath(contentFilePath, img.absolutePath)
|
|
561
|
+
}));
|
|
562
|
+
return {
|
|
563
|
+
success: true,
|
|
564
|
+
images
|
|
565
|
+
};
|
|
566
|
+
} catch (error) {
|
|
567
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
568
|
+
return {
|
|
569
|
+
success: false,
|
|
570
|
+
images: [],
|
|
571
|
+
error: `Failed to discover images: ${message}`
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
263
576
|
// src/filesystem/versions.ts
|
|
577
|
+
import { existsSync as existsSync2 } from "fs";
|
|
264
578
|
import {
|
|
579
|
+
mkdir as mkdir2,
|
|
580
|
+
readdir as readdir2,
|
|
265
581
|
readFile,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
stat,
|
|
270
|
-
unlink
|
|
582
|
+
stat as stat2,
|
|
583
|
+
unlink,
|
|
584
|
+
writeFile as writeFile2
|
|
271
585
|
} from "fs/promises";
|
|
272
|
-
import {
|
|
273
|
-
import { join, basename } from "path";
|
|
586
|
+
import { basename as basename2, join as join2 } from "path";
|
|
274
587
|
import matter from "gray-matter";
|
|
275
588
|
var PREVIEW_MAX_LENGTH = 100;
|
|
276
589
|
var GITIGNORE_CONTENT = "*\n";
|
|
@@ -338,13 +651,13 @@ function parseVersionId(versionId) {
|
|
|
338
651
|
return isNaN(date.getTime()) ? null : date;
|
|
339
652
|
}
|
|
340
653
|
function getVersionStoragePath(projectRoot, collection, contentId, config) {
|
|
341
|
-
return
|
|
654
|
+
return join2(projectRoot, config.storagePath, collection, contentId);
|
|
342
655
|
}
|
|
343
656
|
function getVersionFilePath(storagePath, versionId) {
|
|
344
|
-
return
|
|
657
|
+
return join2(storagePath, `${versionId}.md`);
|
|
345
658
|
}
|
|
346
659
|
function getManifestPath(storagePath) {
|
|
347
|
-
return
|
|
660
|
+
return join2(storagePath, "manifest.json");
|
|
348
661
|
}
|
|
349
662
|
function generatePreview(content) {
|
|
350
663
|
try {
|
|
@@ -397,23 +710,23 @@ function stripLabelFromContent(content) {
|
|
|
397
710
|
}
|
|
398
711
|
}
|
|
399
712
|
async function ensureGitignore(projectRoot, config) {
|
|
400
|
-
const storageRoot =
|
|
401
|
-
const gitignorePath =
|
|
402
|
-
if (!
|
|
403
|
-
await
|
|
713
|
+
const storageRoot = join2(projectRoot, config.storagePath);
|
|
714
|
+
const gitignorePath = join2(storageRoot, ".gitignore");
|
|
715
|
+
if (!existsSync2(storageRoot)) {
|
|
716
|
+
await mkdir2(storageRoot, { recursive: true });
|
|
404
717
|
}
|
|
405
|
-
if (!
|
|
406
|
-
await
|
|
718
|
+
if (!existsSync2(gitignorePath)) {
|
|
719
|
+
await writeFile2(gitignorePath, GITIGNORE_CONTENT, "utf-8");
|
|
407
720
|
}
|
|
408
721
|
}
|
|
409
722
|
async function ensureStorageDirectory(storagePath) {
|
|
410
|
-
if (!
|
|
411
|
-
await
|
|
723
|
+
if (!existsSync2(storagePath)) {
|
|
724
|
+
await mkdir2(storagePath, { recursive: true });
|
|
412
725
|
}
|
|
413
726
|
}
|
|
414
727
|
async function readManifest(storagePath) {
|
|
415
728
|
const manifestPath = getManifestPath(storagePath);
|
|
416
|
-
if (!
|
|
729
|
+
if (!existsSync2(manifestPath)) {
|
|
417
730
|
return null;
|
|
418
731
|
}
|
|
419
732
|
try {
|
|
@@ -436,7 +749,7 @@ async function writeManifest(storagePath, manifest) {
|
|
|
436
749
|
await ensureStorageDirectory(storagePath);
|
|
437
750
|
const manifestPath = getManifestPath(storagePath);
|
|
438
751
|
const content = JSON.stringify(manifest, null, 2);
|
|
439
|
-
await
|
|
752
|
+
await writeFile2(manifestPath, content, "utf-8");
|
|
440
753
|
}
|
|
441
754
|
function createEmptyManifest(collection, contentId) {
|
|
442
755
|
return {
|
|
@@ -448,20 +761,20 @@ function createEmptyManifest(collection, contentId) {
|
|
|
448
761
|
}
|
|
449
762
|
async function recoverManifest(storagePath, collection, contentId) {
|
|
450
763
|
const manifest = createEmptyManifest(collection, contentId);
|
|
451
|
-
if (!
|
|
764
|
+
if (!existsSync2(storagePath)) {
|
|
452
765
|
return manifest;
|
|
453
766
|
}
|
|
454
767
|
try {
|
|
455
|
-
const files = await
|
|
768
|
+
const files = await readdir2(storagePath);
|
|
456
769
|
const versionFiles = files.filter(
|
|
457
770
|
(f) => f.endsWith(".md") && f !== "manifest.json"
|
|
458
771
|
);
|
|
459
772
|
for (const file of versionFiles) {
|
|
460
|
-
const versionId =
|
|
461
|
-
const filePath =
|
|
773
|
+
const versionId = basename2(file, ".md");
|
|
774
|
+
const filePath = join2(storagePath, file);
|
|
462
775
|
try {
|
|
463
776
|
const content = await readFile(filePath, "utf-8");
|
|
464
|
-
const stats = await
|
|
777
|
+
const stats = await stat2(filePath);
|
|
465
778
|
const timestamp = parseVersionId(versionId);
|
|
466
779
|
if (timestamp) {
|
|
467
780
|
const label = extractLabelFromContent(content);
|
|
@@ -523,7 +836,7 @@ async function saveVersion(projectRoot, collection, contentId, content, config,
|
|
|
523
836
|
storagePath,
|
|
524
837
|
lastVersion.id
|
|
525
838
|
);
|
|
526
|
-
if (
|
|
839
|
+
if (existsSync2(lastVersionPath)) {
|
|
527
840
|
try {
|
|
528
841
|
const lastContent = await readFile(lastVersionPath, "utf-8");
|
|
529
842
|
if (lastContent === content) {
|
|
@@ -538,8 +851,8 @@ async function saveVersion(projectRoot, collection, contentId, content, config,
|
|
|
538
851
|
const versionId = now.toISOString().replace(/:/g, "-");
|
|
539
852
|
const versionPath = getVersionFilePath(storagePath, versionId);
|
|
540
853
|
const contentToSave = label ? injectLabelIntoContent(content, label) : content;
|
|
541
|
-
await
|
|
542
|
-
const stats = await
|
|
854
|
+
await writeFile2(versionPath, contentToSave, "utf-8");
|
|
855
|
+
const stats = await stat2(versionPath);
|
|
543
856
|
const entry = {
|
|
544
857
|
id: versionId,
|
|
545
858
|
timestamp: now.toISOString(),
|
|
@@ -570,7 +883,7 @@ async function getVersions(projectRoot, collection, contentId, config) {
|
|
|
570
883
|
contentId,
|
|
571
884
|
config
|
|
572
885
|
);
|
|
573
|
-
if (!
|
|
886
|
+
if (!existsSync2(storagePath)) {
|
|
574
887
|
return [];
|
|
575
888
|
}
|
|
576
889
|
const manifest = await getOrRecoverManifest(
|
|
@@ -598,11 +911,11 @@ async function getVersion(projectRoot, collection, contentId, versionId, config)
|
|
|
598
911
|
config
|
|
599
912
|
);
|
|
600
913
|
const versionPath = getVersionFilePath(storagePath, versionId);
|
|
601
|
-
if (!
|
|
914
|
+
if (!existsSync2(versionPath)) {
|
|
602
915
|
return null;
|
|
603
916
|
}
|
|
604
917
|
const rawContent = await readFile(versionPath, "utf-8");
|
|
605
|
-
const stats = await
|
|
918
|
+
const stats = await stat2(versionPath);
|
|
606
919
|
const labelFromContent = extractLabelFromContent(rawContent);
|
|
607
920
|
const content = stripLabelFromContent(rawContent);
|
|
608
921
|
const { data: frontmatter, content: body } = matter(content);
|
|
@@ -638,7 +951,7 @@ async function deleteVersion(projectRoot, collection, contentId, versionId, conf
|
|
|
638
951
|
return withLock(storagePath, async () => {
|
|
639
952
|
try {
|
|
640
953
|
const versionPath = getVersionFilePath(storagePath, versionId);
|
|
641
|
-
if (!
|
|
954
|
+
if (!existsSync2(versionPath)) {
|
|
642
955
|
return { success: false, error: `Version not found: ${versionId}` };
|
|
643
956
|
}
|
|
644
957
|
const manifest = await getOrRecoverManifest(
|
|
@@ -669,17 +982,17 @@ async function clearVersions(projectRoot, collection, contentId, config) {
|
|
|
669
982
|
contentId,
|
|
670
983
|
config
|
|
671
984
|
);
|
|
672
|
-
if (!
|
|
985
|
+
if (!existsSync2(storagePath)) {
|
|
673
986
|
return { success: true };
|
|
674
987
|
}
|
|
675
988
|
return withLock(storagePath, async () => {
|
|
676
989
|
try {
|
|
677
|
-
const files = await
|
|
990
|
+
const files = await readdir2(storagePath);
|
|
678
991
|
const versionFiles = files.filter(
|
|
679
992
|
(f) => f.endsWith(".md") && f !== "manifest.json"
|
|
680
993
|
);
|
|
681
994
|
for (const file of versionFiles) {
|
|
682
|
-
const filePath =
|
|
995
|
+
const filePath = join2(storagePath, file);
|
|
683
996
|
try {
|
|
684
997
|
await unlink(filePath);
|
|
685
998
|
} catch {
|
|
@@ -697,7 +1010,7 @@ async function clearVersions(projectRoot, collection, contentId, config) {
|
|
|
697
1010
|
}
|
|
698
1011
|
async function pruneVersionsInternal(storagePath, config) {
|
|
699
1012
|
try {
|
|
700
|
-
if (!
|
|
1013
|
+
if (!existsSync2(storagePath)) {
|
|
701
1014
|
return { success: true };
|
|
702
1015
|
}
|
|
703
1016
|
const manifest = await readManifest(storagePath);
|
|
@@ -719,7 +1032,7 @@ async function pruneVersionsInternal(storagePath, config) {
|
|
|
719
1032
|
for (const version of toDelete) {
|
|
720
1033
|
const versionPath = getVersionFilePath(storagePath, version.id);
|
|
721
1034
|
try {
|
|
722
|
-
if (
|
|
1035
|
+
if (existsSync2(versionPath)) {
|
|
723
1036
|
await unlink(versionPath);
|
|
724
1037
|
}
|
|
725
1038
|
} catch {
|
|
@@ -747,7 +1060,7 @@ async function pruneVersions(projectRoot, collection, contentId, config) {
|
|
|
747
1060
|
contentId,
|
|
748
1061
|
config
|
|
749
1062
|
);
|
|
750
|
-
if (!
|
|
1063
|
+
if (!existsSync2(storagePath)) {
|
|
751
1064
|
return { success: true };
|
|
752
1065
|
}
|
|
753
1066
|
return withLock(
|
|
@@ -772,7 +1085,7 @@ async function restoreVersion(projectRoot, collection, contentId, versionId, con
|
|
|
772
1085
|
};
|
|
773
1086
|
}
|
|
774
1087
|
let safetySnapshot;
|
|
775
|
-
if (!skipSafetySnapshot &&
|
|
1088
|
+
if (!skipSafetySnapshot && existsSync2(contentFilePath)) {
|
|
776
1089
|
try {
|
|
777
1090
|
const currentContent = await readFile(contentFilePath, "utf-8");
|
|
778
1091
|
const snapshotResult = await saveVersion(
|
|
@@ -793,7 +1106,7 @@ async function restoreVersion(projectRoot, collection, contentId, versionId, con
|
|
|
793
1106
|
);
|
|
794
1107
|
}
|
|
795
1108
|
}
|
|
796
|
-
await
|
|
1109
|
+
await writeFile2(contentFilePath, versionToRestore.content, "utf-8");
|
|
797
1110
|
return {
|
|
798
1111
|
success: true,
|
|
799
1112
|
version: {
|
|
@@ -817,9 +1130,9 @@ async function restoreVersion(projectRoot, collection, contentId, versionId, con
|
|
|
817
1130
|
}
|
|
818
1131
|
|
|
819
1132
|
// src/filesystem/writer.ts
|
|
820
|
-
import {
|
|
821
|
-
import {
|
|
822
|
-
import {
|
|
1133
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1134
|
+
import { mkdir as mkdir3, readFile as readFile2, stat as stat3, unlink as unlink2, writeFile as writeFile3 } from "fs/promises";
|
|
1135
|
+
import { basename as basename3, dirname as dirname2, join as join3 } from "path";
|
|
823
1136
|
import slugify from "slugify";
|
|
824
1137
|
function generateSlug(text) {
|
|
825
1138
|
return slugify(text, {
|
|
@@ -830,12 +1143,12 @@ function generateSlug(text) {
|
|
|
830
1143
|
}
|
|
831
1144
|
function contentExists(slug, collectionPath, filePattern) {
|
|
832
1145
|
const relativePath = generatePathFromPattern(filePattern, { slug });
|
|
833
|
-
const fullPath =
|
|
1146
|
+
const fullPath = join3(collectionPath, relativePath);
|
|
834
1147
|
if (filePattern.includes("/index.")) {
|
|
835
|
-
const folderPath =
|
|
836
|
-
return
|
|
1148
|
+
const folderPath = join3(collectionPath, slug);
|
|
1149
|
+
return existsSync3(folderPath);
|
|
837
1150
|
}
|
|
838
|
-
return
|
|
1151
|
+
return existsSync3(fullPath);
|
|
839
1152
|
}
|
|
840
1153
|
async function generateUniqueSlug(baseSlug, collectionPath, filePattern = "{slug}.md") {
|
|
841
1154
|
let slug = baseSlug;
|
|
@@ -917,13 +1230,13 @@ async function createContent(collectionPath, options) {
|
|
|
917
1230
|
customTokens
|
|
918
1231
|
});
|
|
919
1232
|
const relativePath = generatePathFromPattern(filePattern, tokens);
|
|
920
|
-
const filePath =
|
|
921
|
-
const parentDir =
|
|
922
|
-
if (!
|
|
923
|
-
await
|
|
1233
|
+
const filePath = join3(collectionPath, relativePath);
|
|
1234
|
+
const parentDir = dirname2(filePath);
|
|
1235
|
+
if (!existsSync3(parentDir)) {
|
|
1236
|
+
await mkdir3(parentDir, { recursive: true });
|
|
924
1237
|
}
|
|
925
1238
|
const content = createFileContent(frontmatter, body);
|
|
926
|
-
await
|
|
1239
|
+
await writeFile3(filePath, content, "utf-8");
|
|
927
1240
|
return {
|
|
928
1241
|
success: true,
|
|
929
1242
|
id: slug,
|
|
@@ -955,405 +1268,92 @@ async function updateContent(filePath, collectionPath, options) {
|
|
|
955
1268
|
existing.content.id,
|
|
956
1269
|
existing.content.raw,
|
|
957
1270
|
existing.content.mtime,
|
|
958
|
-
expectedMtime
|
|
959
|
-
);
|
|
960
|
-
return {
|
|
961
|
-
success: false,
|
|
962
|
-
error: conflictError.message,
|
|
963
|
-
conflict: conflictError
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
const frontmatter = options.frontmatter ? { ...existing.content.frontmatter, ...options.frontmatter } : existing.content.frontmatter;
|
|
968
|
-
const body = options.body ?? existing.content.body;
|
|
969
|
-
const newContent = createFileContent(frontmatter, body);
|
|
970
|
-
const currentContent = existsSync2(filePath) ? await readFile2(filePath, "utf-8") : "";
|
|
971
|
-
if (newContent === currentContent) {
|
|
972
|
-
return {
|
|
973
|
-
success: true,
|
|
974
|
-
id: existing.content.id,
|
|
975
|
-
path: filePath,
|
|
976
|
-
mtime: existing.content.mtime
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
if (projectRoot && collection && versionHistoryConfig && versionHistoryConfig.enabled && currentContent) {
|
|
980
|
-
try {
|
|
981
|
-
const fileName = basename2(filePath);
|
|
982
|
-
const contentId = fileName === "index.md" || fileName === "index.mdx" ? basename2(dirname(filePath)) : fileName.replace(/\.(md|mdx)$/, "");
|
|
983
|
-
const versionResult = await saveVersion(
|
|
984
|
-
projectRoot,
|
|
985
|
-
collection,
|
|
986
|
-
contentId,
|
|
987
|
-
currentContent,
|
|
988
|
-
versionHistoryConfig,
|
|
989
|
-
{ skipIfIdentical: true }
|
|
990
|
-
);
|
|
991
|
-
if (!versionResult.success) {
|
|
992
|
-
console.warn(
|
|
993
|
-
`[writenex] Failed to create version snapshot: ${versionResult.error}`
|
|
994
|
-
);
|
|
995
|
-
}
|
|
996
|
-
} catch (versionError) {
|
|
997
|
-
console.warn(
|
|
998
|
-
`[writenex] Version creation error (save will continue):`,
|
|
999
|
-
versionError
|
|
1000
|
-
);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
await writeFile2(filePath, newContent, "utf-8");
|
|
1004
|
-
const newStats = await stat2(filePath);
|
|
1005
|
-
return {
|
|
1006
|
-
success: true,
|
|
1007
|
-
id: existing.content.id,
|
|
1008
|
-
path: filePath,
|
|
1009
|
-
mtime: newStats.mtimeMs
|
|
1010
|
-
};
|
|
1011
|
-
} catch (error) {
|
|
1012
|
-
if (error instanceof ContentConflictError) {
|
|
1013
|
-
return {
|
|
1014
|
-
success: false,
|
|
1015
|
-
error: error.message,
|
|
1016
|
-
conflict: error
|
|
1017
|
-
};
|
|
1018
|
-
}
|
|
1019
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1020
|
-
return {
|
|
1021
|
-
success: false,
|
|
1022
|
-
error: `Failed to update content: ${message}`
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
async function deleteContent(filePath) {
|
|
1027
|
-
try {
|
|
1028
|
-
if (!existsSync2(filePath)) {
|
|
1029
|
-
return {
|
|
1030
|
-
success: false,
|
|
1031
|
-
error: "Content file not found"
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
await unlink2(filePath);
|
|
1035
|
-
return {
|
|
1036
|
-
success: true,
|
|
1037
|
-
path: filePath
|
|
1038
|
-
};
|
|
1039
|
-
} catch (error) {
|
|
1040
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1041
|
-
return {
|
|
1042
|
-
success: false,
|
|
1043
|
-
error: `Failed to delete content: ${message}`
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
// src/filesystem/images.ts
|
|
1049
|
-
import { writeFile as writeFile3, mkdir as mkdir3, readdir as readdir2, stat as stat3 } from "fs/promises";
|
|
1050
|
-
import { existsSync as existsSync3 } from "fs";
|
|
1051
|
-
import { join as join3, dirname as dirname2, basename as basename3, extname, relative } from "path";
|
|
1052
|
-
var DEFAULT_IMAGE_CONFIG = {
|
|
1053
|
-
strategy: "colocated",
|
|
1054
|
-
publicPath: "/images",
|
|
1055
|
-
storagePath: "public/images"
|
|
1056
|
-
};
|
|
1057
|
-
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1058
|
-
".jpg",
|
|
1059
|
-
".jpeg",
|
|
1060
|
-
".png",
|
|
1061
|
-
".gif",
|
|
1062
|
-
".webp",
|
|
1063
|
-
".avif",
|
|
1064
|
-
".svg"
|
|
1065
|
-
]);
|
|
1066
|
-
function isValidImageFile(filename) {
|
|
1067
|
-
const ext = extname(filename).toLowerCase();
|
|
1068
|
-
return SUPPORTED_EXTENSIONS.has(ext);
|
|
1069
|
-
}
|
|
1070
|
-
function generateUniqueFilename(originalName, _contentId) {
|
|
1071
|
-
const ext = extname(originalName).toLowerCase();
|
|
1072
|
-
const baseName = basename3(originalName, ext).toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").substring(0, 50);
|
|
1073
|
-
const timestamp = Date.now().toString(36);
|
|
1074
|
-
return `${baseName}-${timestamp}${ext}`;
|
|
1075
|
-
}
|
|
1076
|
-
function getColocatedPath(projectRoot, collection, contentId, filename) {
|
|
1077
|
-
const collectionPath = join3(projectRoot, "src/content", collection);
|
|
1078
|
-
const imageDir = join3(collectionPath, contentId);
|
|
1079
|
-
const storagePath = join3(imageDir, filename);
|
|
1080
|
-
const indexMdPath = join3(collectionPath, contentId, "index.md");
|
|
1081
|
-
const indexMdxPath = join3(collectionPath, contentId, "index.mdx");
|
|
1082
|
-
const isFolderBased = existsSync3(indexMdPath) || existsSync3(indexMdxPath);
|
|
1083
|
-
const markdownPath = isFolderBased ? `./${filename}` : `./${contentId}/${filename}`;
|
|
1084
|
-
return { storagePath, markdownPath };
|
|
1085
|
-
}
|
|
1086
|
-
function getPublicPath(projectRoot, collection, filename, config) {
|
|
1087
|
-
const storagePath = join3(
|
|
1088
|
-
projectRoot,
|
|
1089
|
-
config.storagePath ?? "public/images",
|
|
1090
|
-
collection,
|
|
1091
|
-
filename
|
|
1092
|
-
);
|
|
1093
|
-
const publicPath = config.publicPath ?? "/images";
|
|
1094
|
-
const url = `${publicPath}/${collection}/${filename}`;
|
|
1095
|
-
return { storagePath, markdownPath: url, url };
|
|
1096
|
-
}
|
|
1097
|
-
async function uploadImage(options) {
|
|
1098
|
-
const {
|
|
1099
|
-
filename,
|
|
1100
|
-
data,
|
|
1101
|
-
collection,
|
|
1102
|
-
contentId,
|
|
1103
|
-
projectRoot,
|
|
1104
|
-
config = DEFAULT_IMAGE_CONFIG
|
|
1105
|
-
} = options;
|
|
1106
|
-
if (!isValidImageFile(filename)) {
|
|
1107
|
-
return {
|
|
1108
|
-
success: false,
|
|
1109
|
-
error: `Invalid image file type. Supported: ${[...SUPPORTED_EXTENSIONS].join(", ")}`
|
|
1110
|
-
};
|
|
1111
|
-
}
|
|
1112
|
-
const uniqueFilename = generateUniqueFilename(filename, contentId);
|
|
1113
|
-
try {
|
|
1114
|
-
let storagePath;
|
|
1115
|
-
let markdownPath;
|
|
1116
|
-
let url;
|
|
1117
|
-
switch (config.strategy) {
|
|
1118
|
-
case "public": {
|
|
1119
|
-
const paths = getPublicPath(
|
|
1120
|
-
projectRoot,
|
|
1121
|
-
collection,
|
|
1122
|
-
uniqueFilename,
|
|
1123
|
-
config
|
|
1124
|
-
);
|
|
1125
|
-
storagePath = paths.storagePath;
|
|
1126
|
-
markdownPath = paths.markdownPath;
|
|
1127
|
-
url = paths.url;
|
|
1128
|
-
break;
|
|
1271
|
+
expectedMtime
|
|
1272
|
+
);
|
|
1273
|
+
return {
|
|
1274
|
+
success: false,
|
|
1275
|
+
error: conflictError.message,
|
|
1276
|
+
conflict: conflictError
|
|
1277
|
+
};
|
|
1129
1278
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1279
|
+
}
|
|
1280
|
+
const frontmatter = options.frontmatter ? { ...existing.content.frontmatter, ...options.frontmatter } : existing.content.frontmatter;
|
|
1281
|
+
const body = options.body ?? existing.content.body;
|
|
1282
|
+
const newContent = createFileContent(frontmatter, body);
|
|
1283
|
+
const currentContent = existsSync3(filePath) ? await readFile2(filePath, "utf-8") : "";
|
|
1284
|
+
if (newContent === currentContent) {
|
|
1285
|
+
return {
|
|
1286
|
+
success: true,
|
|
1287
|
+
id: existing.content.id,
|
|
1288
|
+
path: filePath,
|
|
1289
|
+
mtime: existing.content.mtime
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
if (projectRoot && collection && versionHistoryConfig && versionHistoryConfig.enabled && currentContent) {
|
|
1293
|
+
try {
|
|
1294
|
+
const fileName = basename3(filePath);
|
|
1295
|
+
const contentId = fileName === "index.md" || fileName === "index.mdx" ? basename3(dirname2(filePath)) : fileName.replace(/\.(md|mdx)$/, "");
|
|
1296
|
+
const versionResult = await saveVersion(
|
|
1133
1297
|
projectRoot,
|
|
1134
1298
|
collection,
|
|
1135
1299
|
contentId,
|
|
1136
|
-
|
|
1300
|
+
currentContent,
|
|
1301
|
+
versionHistoryConfig,
|
|
1302
|
+
{ skipIfIdentical: true }
|
|
1303
|
+
);
|
|
1304
|
+
if (!versionResult.success) {
|
|
1305
|
+
console.warn(
|
|
1306
|
+
`[writenex] Failed to create version snapshot: ${versionResult.error}`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
} catch (versionError) {
|
|
1310
|
+
console.warn(
|
|
1311
|
+
`[writenex] Version creation error (save will continue):`,
|
|
1312
|
+
versionError
|
|
1137
1313
|
);
|
|
1138
|
-
storagePath = paths.storagePath;
|
|
1139
|
-
markdownPath = paths.markdownPath;
|
|
1140
|
-
break;
|
|
1141
1314
|
}
|
|
1142
1315
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
await mkdir3(dir, { recursive: true });
|
|
1146
|
-
}
|
|
1147
|
-
await writeFile3(storagePath, data);
|
|
1316
|
+
await writeFile3(filePath, newContent, "utf-8");
|
|
1317
|
+
const newStats = await stat3(filePath);
|
|
1148
1318
|
return {
|
|
1149
1319
|
success: true,
|
|
1150
|
-
|
|
1151
|
-
|
|
1320
|
+
id: existing.content.id,
|
|
1321
|
+
path: filePath,
|
|
1322
|
+
mtime: newStats.mtimeMs
|
|
1152
1323
|
};
|
|
1153
1324
|
} catch (error) {
|
|
1154
|
-
|
|
1325
|
+
if (error instanceof ContentConflictError) {
|
|
1326
|
+
return {
|
|
1327
|
+
success: false,
|
|
1328
|
+
error: error.message,
|
|
1329
|
+
conflict: error
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1155
1333
|
return {
|
|
1156
1334
|
success: false,
|
|
1157
|
-
error: `Failed to
|
|
1335
|
+
error: `Failed to update content: ${message}`
|
|
1158
1336
|
};
|
|
1159
1337
|
}
|
|
1160
1338
|
}
|
|
1161
|
-
function
|
|
1162
|
-
const result = { fields: {} };
|
|
1163
|
-
const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/);
|
|
1164
|
-
if (!boundaryMatch) {
|
|
1165
|
-
return result;
|
|
1166
|
-
}
|
|
1167
|
-
const boundary = boundaryMatch[1] ?? boundaryMatch[2];
|
|
1168
|
-
if (!boundary) {
|
|
1169
|
-
return result;
|
|
1170
|
-
}
|
|
1171
|
-
const boundaryBuffer = Buffer.from(`--${boundary}`);
|
|
1172
|
-
const parts = splitBuffer(body, boundaryBuffer);
|
|
1173
|
-
for (const part of parts) {
|
|
1174
|
-
if (part.length < 10) continue;
|
|
1175
|
-
const separatorIndex = part.indexOf("\r\n\r\n");
|
|
1176
|
-
if (separatorIndex === -1) continue;
|
|
1177
|
-
const headerSection = part.slice(0, separatorIndex).toString("utf-8");
|
|
1178
|
-
const bodySection = part.slice(separatorIndex + 4);
|
|
1179
|
-
const bodyEnd = bodySection.length - 2;
|
|
1180
|
-
const cleanBody = bodyEnd > 0 ? bodySection.slice(0, bodyEnd) : bodySection;
|
|
1181
|
-
const headers = parseHeaders(headerSection);
|
|
1182
|
-
const disposition = headers["content-disposition"];
|
|
1183
|
-
if (!disposition) continue;
|
|
1184
|
-
const nameMatch = disposition.match(/name="([^"]+)"/);
|
|
1185
|
-
const filenameMatch = disposition.match(/filename="([^"]+)"/);
|
|
1186
|
-
if (filenameMatch) {
|
|
1187
|
-
result.file = {
|
|
1188
|
-
filename: filenameMatch[1] ?? "unknown",
|
|
1189
|
-
data: cleanBody,
|
|
1190
|
-
contentType: headers["content-type"] ?? "application/octet-stream"
|
|
1191
|
-
};
|
|
1192
|
-
} else if (nameMatch) {
|
|
1193
|
-
result.fields[nameMatch[1] ?? ""] = cleanBody.toString("utf-8");
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
return result;
|
|
1197
|
-
}
|
|
1198
|
-
function splitBuffer(buffer, delimiter) {
|
|
1199
|
-
const parts = [];
|
|
1200
|
-
let start = 0;
|
|
1201
|
-
let index;
|
|
1202
|
-
while ((index = buffer.indexOf(delimiter, start)) !== -1) {
|
|
1203
|
-
if (index > start) {
|
|
1204
|
-
parts.push(buffer.slice(start, index));
|
|
1205
|
-
}
|
|
1206
|
-
start = index + delimiter.length;
|
|
1207
|
-
}
|
|
1208
|
-
if (start < buffer.length) {
|
|
1209
|
-
parts.push(buffer.slice(start));
|
|
1210
|
-
}
|
|
1211
|
-
return parts;
|
|
1212
|
-
}
|
|
1213
|
-
function parseHeaders(headerSection) {
|
|
1214
|
-
const headers = {};
|
|
1215
|
-
const lines = headerSection.split("\r\n");
|
|
1216
|
-
for (const line of lines) {
|
|
1217
|
-
const colonIndex = line.indexOf(":");
|
|
1218
|
-
if (colonIndex > 0) {
|
|
1219
|
-
const key = line.slice(0, colonIndex).trim().toLowerCase();
|
|
1220
|
-
const value = line.slice(colonIndex + 1).trim();
|
|
1221
|
-
headers[key] = value;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
return headers;
|
|
1225
|
-
}
|
|
1226
|
-
var DATE_PREFIX_PATTERN = /^\d{4}-\d{2}-\d{2}-/;
|
|
1227
|
-
function getContentImageFolder(collectionPath, contentId, contentFilePath) {
|
|
1228
|
-
const filename = basename3(contentFilePath);
|
|
1229
|
-
const contentDir = dirname2(contentFilePath);
|
|
1230
|
-
if (filename === "index.md" || filename === "index.mdx") {
|
|
1231
|
-
return contentDir;
|
|
1232
|
-
}
|
|
1233
|
-
const siblingFolderPath = join3(collectionPath, contentId);
|
|
1234
|
-
if (existsSync3(siblingFolderPath)) {
|
|
1235
|
-
return siblingFolderPath;
|
|
1236
|
-
}
|
|
1237
|
-
return null;
|
|
1238
|
-
}
|
|
1239
|
-
function detectContentStructure(contentFilePath) {
|
|
1240
|
-
const filename = basename3(contentFilePath);
|
|
1241
|
-
if (filename === "index.md" || filename === "index.mdx") {
|
|
1242
|
-
return "folder-based";
|
|
1243
|
-
}
|
|
1244
|
-
const nameWithoutExt = filename.replace(/\.(md|mdx)$/, "");
|
|
1245
|
-
if (DATE_PREFIX_PATTERN.test(nameWithoutExt)) {
|
|
1246
|
-
return "date-prefixed";
|
|
1247
|
-
}
|
|
1248
|
-
return "flat";
|
|
1249
|
-
}
|
|
1250
|
-
function shouldSkipDirectory(dirName) {
|
|
1251
|
-
return dirName.startsWith(".") || dirName.startsWith("_");
|
|
1252
|
-
}
|
|
1253
|
-
async function scanDirectoryForImages(dirPath, basePath, options) {
|
|
1254
|
-
const { maxDepth, currentDepth } = options;
|
|
1255
|
-
if (currentDepth >= maxDepth) {
|
|
1256
|
-
return [];
|
|
1257
|
-
}
|
|
1258
|
-
if (!existsSync3(dirPath)) {
|
|
1259
|
-
return [];
|
|
1260
|
-
}
|
|
1261
|
-
const images = [];
|
|
1262
|
-
try {
|
|
1263
|
-
const entries = await readdir2(dirPath, { withFileTypes: true });
|
|
1264
|
-
for (const entry of entries) {
|
|
1265
|
-
const entryPath = join3(dirPath, entry.name);
|
|
1266
|
-
if (entry.isDirectory()) {
|
|
1267
|
-
if (shouldSkipDirectory(entry.name)) {
|
|
1268
|
-
continue;
|
|
1269
|
-
}
|
|
1270
|
-
const subImages = await scanDirectoryForImages(entryPath, basePath, {
|
|
1271
|
-
maxDepth,
|
|
1272
|
-
currentDepth: currentDepth + 1,
|
|
1273
|
-
basePath
|
|
1274
|
-
});
|
|
1275
|
-
images.push(...subImages);
|
|
1276
|
-
} else if (entry.isFile()) {
|
|
1277
|
-
if (isValidImageFile(entry.name)) {
|
|
1278
|
-
const fileStat = await stat3(entryPath);
|
|
1279
|
-
const extension = extname(entry.name).toLowerCase();
|
|
1280
|
-
const relativePath = calculateRelativePathFromBase(
|
|
1281
|
-
basePath,
|
|
1282
|
-
entryPath
|
|
1283
|
-
);
|
|
1284
|
-
images.push({
|
|
1285
|
-
filename: entry.name,
|
|
1286
|
-
relativePath,
|
|
1287
|
-
absolutePath: entryPath,
|
|
1288
|
-
size: fileStat.size,
|
|
1289
|
-
extension
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
} catch {
|
|
1295
|
-
return [];
|
|
1296
|
-
}
|
|
1297
|
-
return images;
|
|
1298
|
-
}
|
|
1299
|
-
function calculateRelativePathFromBase(basePath, targetPath) {
|
|
1300
|
-
const relPath = relative(basePath, targetPath);
|
|
1301
|
-
return `./${relPath}`;
|
|
1302
|
-
}
|
|
1303
|
-
function calculateRelativePath(contentFilePath, imagePath) {
|
|
1304
|
-
const contentDir = dirname2(contentFilePath);
|
|
1305
|
-
const relPath = relative(contentDir, imagePath);
|
|
1306
|
-
if (relPath.startsWith("..")) {
|
|
1307
|
-
return relPath;
|
|
1308
|
-
}
|
|
1309
|
-
return `./${relPath}`;
|
|
1310
|
-
}
|
|
1311
|
-
var DEFAULT_MAX_DEPTH = 5;
|
|
1312
|
-
async function discoverContentImages(collectionPath, contentId, options) {
|
|
1313
|
-
const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
1339
|
+
async function deleteContent(filePath) {
|
|
1314
1340
|
try {
|
|
1315
|
-
|
|
1316
|
-
if (!contentFilePath) {
|
|
1341
|
+
if (!existsSync3(filePath)) {
|
|
1317
1342
|
return {
|
|
1318
1343
|
success: false,
|
|
1319
|
-
|
|
1320
|
-
error: `Content '${contentId}' not found in collection`
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
const imageFolderPath = getContentImageFolder(
|
|
1324
|
-
collectionPath,
|
|
1325
|
-
contentId,
|
|
1326
|
-
contentFilePath
|
|
1327
|
-
);
|
|
1328
|
-
if (!imageFolderPath) {
|
|
1329
|
-
return {
|
|
1330
|
-
success: true,
|
|
1331
|
-
images: []
|
|
1344
|
+
error: "Content file not found"
|
|
1332
1345
|
};
|
|
1333
1346
|
}
|
|
1334
|
-
|
|
1335
|
-
imageFolderPath,
|
|
1336
|
-
imageFolderPath,
|
|
1337
|
-
{
|
|
1338
|
-
maxDepth,
|
|
1339
|
-
currentDepth: 0,
|
|
1340
|
-
basePath: imageFolderPath
|
|
1341
|
-
}
|
|
1342
|
-
);
|
|
1343
|
-
const images = scannedImages.map((img) => ({
|
|
1344
|
-
...img,
|
|
1345
|
-
relativePath: calculateRelativePath(contentFilePath, img.absolutePath)
|
|
1346
|
-
}));
|
|
1347
|
+
await unlink2(filePath);
|
|
1347
1348
|
return {
|
|
1348
1349
|
success: true,
|
|
1349
|
-
|
|
1350
|
+
path: filePath
|
|
1350
1351
|
};
|
|
1351
1352
|
} catch (error) {
|
|
1352
|
-
const message = error instanceof Error ? error.message :
|
|
1353
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1353
1354
|
return {
|
|
1354
1355
|
success: false,
|
|
1355
|
-
|
|
1356
|
-
error: `Failed to discover images: ${message}`
|
|
1356
|
+
error: `Failed to delete content: ${message}`
|
|
1357
1357
|
};
|
|
1358
1358
|
}
|
|
1359
1359
|
}
|
|
@@ -1372,6 +1372,15 @@ export {
|
|
|
1372
1372
|
VersionNotFoundError,
|
|
1373
1373
|
isWritenexError,
|
|
1374
1374
|
wrapError,
|
|
1375
|
+
DEFAULT_IMAGE_CONFIG,
|
|
1376
|
+
isValidImageFile,
|
|
1377
|
+
uploadImage,
|
|
1378
|
+
parseMultipartFormData,
|
|
1379
|
+
getContentImageFolder,
|
|
1380
|
+
detectContentStructure,
|
|
1381
|
+
scanDirectoryForImages,
|
|
1382
|
+
calculateRelativePath,
|
|
1383
|
+
discoverContentImages,
|
|
1375
1384
|
generateVersionId,
|
|
1376
1385
|
parseVersionId,
|
|
1377
1386
|
getVersionStoragePath,
|
|
@@ -1396,15 +1405,6 @@ export {
|
|
|
1396
1405
|
generateUniqueSlug,
|
|
1397
1406
|
createContent,
|
|
1398
1407
|
updateContent,
|
|
1399
|
-
deleteContent
|
|
1400
|
-
DEFAULT_IMAGE_CONFIG,
|
|
1401
|
-
isValidImageFile,
|
|
1402
|
-
uploadImage,
|
|
1403
|
-
parseMultipartFormData,
|
|
1404
|
-
getContentImageFolder,
|
|
1405
|
-
detectContentStructure,
|
|
1406
|
-
scanDirectoryForImages,
|
|
1407
|
-
calculateRelativePath,
|
|
1408
|
-
discoverContentImages
|
|
1408
|
+
deleteContent
|
|
1409
1409
|
};
|
|
1410
|
-
//# sourceMappingURL=chunk-
|
|
1410
|
+
//# sourceMappingURL=chunk-4H63L4YO.js.map
|