@bendyline/docblocks-cli 2.6.2 → 2.7.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 +27 -15
- package/THIRD_PARTY_NOTICES.txt +3858 -3403
- package/dist/bin.js +787 -116
- package/dist/index.d.ts +56 -1
- package/dist/index.js +706 -46
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// src/commands/build.ts
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { mkdir, opendir, readFile, stat as stat2, writeFile } from "fs/promises";
|
|
4
|
-
import
|
|
4
|
+
import path5 from "path";
|
|
5
5
|
|
|
6
6
|
// src/render-html.ts
|
|
7
7
|
import { realpath, stat } from "fs/promises";
|
|
8
8
|
import path3 from "path";
|
|
9
9
|
import { markdownToDoc } from "@bendyline/squisq/doc";
|
|
10
|
-
import { parseMarkdown } from "@bendyline/squisq/markdown";
|
|
10
|
+
import { parseMarkdown, readFrontmatterThemeId } from "@bendyline/squisq/markdown";
|
|
11
11
|
import { collectImagePaths, docToHtml } from "@bendyline/squisq-formats/html";
|
|
12
12
|
import { PLAYER_BUNDLE } from "@bendyline/squisq-react/standalone-source";
|
|
13
13
|
|
|
@@ -123,25 +123,35 @@ var DEFAULT_MAX_ASSET_BYTES = 50 * 1024 * 1024;
|
|
|
123
123
|
var MAX_SINGLE_ASSET_BYTES = 20 * 1024 * 1024;
|
|
124
124
|
async function renderMarkdownHtml(markdown, options) {
|
|
125
125
|
await assertKnownThemeId(options.themeId);
|
|
126
|
+
const warn = options.onWarning ?? ((message) => console.warn(message));
|
|
126
127
|
const markdownDoc = parseMarkdown(markdown);
|
|
127
128
|
const doc = markdownToDoc(markdownDoc);
|
|
129
|
+
const themeId = options.themeId ?? (readFrontmatterThemeId(markdownDoc.frontmatter) ? void 0 : await usableFallbackThemeId(options.fallbackThemeId, warn));
|
|
128
130
|
const images = options.sourcePath && options.assetRoot ? await readReferencedImages(
|
|
129
131
|
doc,
|
|
130
132
|
options.sourcePath,
|
|
131
133
|
options.assetRoot,
|
|
132
134
|
options.maxAssetBytes ?? DEFAULT_MAX_ASSET_BYTES,
|
|
133
135
|
options.allowReferencedAsset,
|
|
134
|
-
|
|
136
|
+
warn
|
|
135
137
|
) : void 0;
|
|
136
138
|
return docToHtml(doc, {
|
|
137
139
|
playerScript: PLAYER_BUNDLE,
|
|
138
140
|
images,
|
|
139
141
|
title: options.title,
|
|
140
142
|
mode: options.mode ?? "static",
|
|
141
|
-
themeId
|
|
143
|
+
themeId,
|
|
142
144
|
showCodeCopyButton: true
|
|
143
145
|
});
|
|
144
146
|
}
|
|
147
|
+
async function usableFallbackThemeId(fallbackThemeId, warn) {
|
|
148
|
+
if (!fallbackThemeId) return void 0;
|
|
149
|
+
if ((await getAvailableThemeIds()).includes(fallbackThemeId)) return fallbackThemeId;
|
|
150
|
+
warn(
|
|
151
|
+
`Ignoring unknown fallback theme "${fallbackThemeId}"; the document's default theme applies.`
|
|
152
|
+
);
|
|
153
|
+
return void 0;
|
|
154
|
+
}
|
|
145
155
|
async function readReferencedImages(doc, sourcePath, assetRoot, maxAssetBytes, allowReferencedAsset, warn) {
|
|
146
156
|
const images = /* @__PURE__ */ new Map();
|
|
147
157
|
const root = await realpathIfPresent(path3.resolve(assetRoot));
|
|
@@ -237,8 +247,575 @@ function positiveLimit(value, fallback, label) {
|
|
|
237
247
|
return selected;
|
|
238
248
|
}
|
|
239
249
|
|
|
250
|
+
// src/internal/workspace-settings.ts
|
|
251
|
+
import { decodeUtf8Text, parseWorkspacePath as parseWorkspacePath2 } from "@bendyline/docblocks/filesystem";
|
|
252
|
+
import {
|
|
253
|
+
WORKSPACE_SETTINGS_LIMITS,
|
|
254
|
+
WORKSPACE_SETTINGS_PATH,
|
|
255
|
+
parseWorkspaceSettingsText,
|
|
256
|
+
resolveWorkspaceDefaultThemeId
|
|
257
|
+
} from "@bendyline/docblocks/workspace-settings";
|
|
258
|
+
|
|
259
|
+
// src/internal/workspace-io.ts
|
|
260
|
+
import { constants as fsConstants } from "fs";
|
|
261
|
+
import fs2 from "fs/promises";
|
|
262
|
+
import { randomUUID } from "crypto";
|
|
263
|
+
import path4 from "path";
|
|
264
|
+
import {
|
|
265
|
+
FsError,
|
|
266
|
+
mapNodeErrorCodeToFsErrorCode,
|
|
267
|
+
parseFileSystemVersion,
|
|
268
|
+
parseWorkspacePath,
|
|
269
|
+
tryParseWorkspacePath
|
|
270
|
+
} from "@bendyline/docblocks/filesystem";
|
|
271
|
+
|
|
272
|
+
// src/internal/link-support.ts
|
|
273
|
+
var LINK_UNSUPPORTED_CODES = [
|
|
274
|
+
"EPERM",
|
|
275
|
+
"ENOSYS",
|
|
276
|
+
"ENOTSUP",
|
|
277
|
+
"EOPNOTSUPP",
|
|
278
|
+
"EXDEV",
|
|
279
|
+
"EMLINK"
|
|
280
|
+
];
|
|
281
|
+
function isLinkUnsupportedError(error) {
|
|
282
|
+
return isNodeErrorCode(error, LINK_UNSUPPORTED_CODES);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/internal/workspace-io.ts
|
|
286
|
+
var NODE_WORKSPACE_IO_LIMITS = {
|
|
287
|
+
maxFileBytes: 256 * 1024 * 1024,
|
|
288
|
+
maxDirectoryEntries: 1e5
|
|
289
|
+
};
|
|
290
|
+
var LISTING_STAT_CONCURRENCY = 16;
|
|
291
|
+
var TEMPORARY_PREFIX = ".docblocks-tmp-";
|
|
292
|
+
async function createNodeWorkspaceIO(rootDir, options = {}) {
|
|
293
|
+
const requested = path4.resolve(rootDir);
|
|
294
|
+
let root;
|
|
295
|
+
try {
|
|
296
|
+
root = await fs2.realpath(requested);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (isNodeErrorCode(error, ["ENOENT", "ENOTDIR"])) {
|
|
299
|
+
throw new FsError("not-found", `Workspace folder not found: ${requested}`, {
|
|
300
|
+
operation: "stat"
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
throw toFsError(error, "stat", "");
|
|
304
|
+
}
|
|
305
|
+
let info;
|
|
306
|
+
try {
|
|
307
|
+
info = await fs2.stat(root);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw toFsError(error, "stat", "");
|
|
310
|
+
}
|
|
311
|
+
if (!info.isDirectory()) {
|
|
312
|
+
throw new FsError("type-mismatch", `Workspace root is not a folder: ${requested}`, {
|
|
313
|
+
operation: "stat"
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return new NodeWorkspaceIO(root, {
|
|
317
|
+
maxFileBytes: positiveLimit(
|
|
318
|
+
options.maxFileBytes,
|
|
319
|
+
NODE_WORKSPACE_IO_LIMITS.maxFileBytes,
|
|
320
|
+
"workspace file byte"
|
|
321
|
+
),
|
|
322
|
+
maxDirectoryEntries: positiveLimit(
|
|
323
|
+
options.maxDirectoryEntries,
|
|
324
|
+
NODE_WORKSPACE_IO_LIMITS.maxDirectoryEntries,
|
|
325
|
+
"workspace folder entry"
|
|
326
|
+
)
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
var NodeWorkspaceIO = class {
|
|
330
|
+
constructor(root, limits) {
|
|
331
|
+
this.root = root;
|
|
332
|
+
this.limits = limits;
|
|
333
|
+
}
|
|
334
|
+
async stat(input) {
|
|
335
|
+
const workspacePath = canonical(input, "stat");
|
|
336
|
+
try {
|
|
337
|
+
const located = await this.locate(workspacePath, "stat");
|
|
338
|
+
if (!located) return null;
|
|
339
|
+
const info = await lstatOrNull(located);
|
|
340
|
+
if (!info) return null;
|
|
341
|
+
return snapshotFor(workspacePath, assertSupportedKind(info, workspacePath, "stat"), info);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
throw toFsError(error, "stat", workspacePath);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async readFile(input) {
|
|
347
|
+
const workspacePath = canonical(input, "read");
|
|
348
|
+
try {
|
|
349
|
+
const located = await this.locate(workspacePath, "read");
|
|
350
|
+
if (!located) return null;
|
|
351
|
+
const before = await lstatOrNull(located);
|
|
352
|
+
if (!before) return null;
|
|
353
|
+
if (assertSupportedKind(before, workspacePath, "read") !== "file") {
|
|
354
|
+
throw new FsError("type-mismatch", `"${workspacePath}" is a folder, not a file.`, {
|
|
355
|
+
operation: "read",
|
|
356
|
+
path: workspacePath
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
this.assertFileSize(before.size, workspacePath, "read");
|
|
360
|
+
return await this.readLocatedFile(located, before, workspacePath);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (isNodeErrorCode(error, ["ENOENT", "ENOTDIR"])) return null;
|
|
363
|
+
throw toFsError(error, "read", workspacePath);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async readDirectory(input) {
|
|
367
|
+
const workspacePath = canonical(input, "list");
|
|
368
|
+
try {
|
|
369
|
+
let directory = this.root;
|
|
370
|
+
if (workspacePath) {
|
|
371
|
+
const located = await this.locate(workspacePath, "list");
|
|
372
|
+
const info = located ? await lstatOrNull(located) : null;
|
|
373
|
+
if (!located || !info) {
|
|
374
|
+
throw new FsError("not-found", `Folder "${workspacePath}" does not exist.`, {
|
|
375
|
+
operation: "list",
|
|
376
|
+
path: workspacePath
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (assertSupportedKind(info, workspacePath, "list") !== "directory") {
|
|
380
|
+
throw new FsError("type-mismatch", `"${workspacePath}" is a file, not a folder.`, {
|
|
381
|
+
operation: "list",
|
|
382
|
+
path: workspacePath
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
directory = located;
|
|
386
|
+
}
|
|
387
|
+
const names = await this.listNames(directory, workspacePath);
|
|
388
|
+
const entries = [];
|
|
389
|
+
for (let index = 0; index < names.length; index += LISTING_STAT_CONCURRENCY) {
|
|
390
|
+
const batch = names.slice(index, index + LISTING_STAT_CONCURRENCY);
|
|
391
|
+
const snapshots = await Promise.all(
|
|
392
|
+
batch.map(async (name) => {
|
|
393
|
+
const info = await lstatOrNull(path4.join(directory, name));
|
|
394
|
+
if (!info || info.isSymbolicLink()) return null;
|
|
395
|
+
const kind = info.isFile() ? "file" : info.isDirectory() ? "directory" : null;
|
|
396
|
+
if (!kind) return null;
|
|
397
|
+
const childPath = parseWorkspacePath(workspacePath ? `${workspacePath}/${name}` : name);
|
|
398
|
+
return snapshotFor(childPath, kind, info);
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
for (const snapshot of snapshots) if (snapshot) entries.push(snapshot);
|
|
402
|
+
}
|
|
403
|
+
return Object.freeze(entries.sort(compareSnapshots));
|
|
404
|
+
} catch (error) {
|
|
405
|
+
throw toFsError(error, "list", workspacePath);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async writeFile(input, data, options = {}) {
|
|
409
|
+
const workspacePath = canonical(input, "write");
|
|
410
|
+
if (!workspacePath) {
|
|
411
|
+
throw new FsError("invalid-path", "The workspace root cannot be written.", {
|
|
412
|
+
operation: "write",
|
|
413
|
+
path: workspacePath
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const bytes = ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : new Uint8Array(data);
|
|
417
|
+
const mode = options.mode ?? "upsert";
|
|
418
|
+
const expectedVersion = options.expectedVersion;
|
|
419
|
+
let temporary = null;
|
|
420
|
+
try {
|
|
421
|
+
this.assertFileSize(BigInt(bytes.byteLength), workspacePath, "write");
|
|
422
|
+
const parent = await this.ensureParent(workspacePath, options.createParents === true);
|
|
423
|
+
const target = path4.join(parent, basename(workspacePath));
|
|
424
|
+
let current = await lstatOrNull(target);
|
|
425
|
+
assertWritable(current, workspacePath, mode, expectedVersion);
|
|
426
|
+
temporary = path4.join(parent, `${TEMPORARY_PREFIX}${randomUUID()}`);
|
|
427
|
+
const handle = await fs2.open(temporary, "wx", 438);
|
|
428
|
+
try {
|
|
429
|
+
await handle.writeFile(bytes);
|
|
430
|
+
if (current?.isFile()) await handle.chmod(Number(current.mode & 0o7777n));
|
|
431
|
+
} finally {
|
|
432
|
+
await handle.close();
|
|
433
|
+
}
|
|
434
|
+
current = await lstatOrNull(target);
|
|
435
|
+
assertWritable(current, workspacePath, mode, expectedVersion);
|
|
436
|
+
if (!current && (mode === "create" || expectedVersion === null)) {
|
|
437
|
+
await publishExclusive(temporary, target, bytes, workspacePath, mode);
|
|
438
|
+
} else {
|
|
439
|
+
await fs2.rename(temporary, target);
|
|
440
|
+
temporary = null;
|
|
441
|
+
}
|
|
442
|
+
const written = await fs2.lstat(target, { bigint: true });
|
|
443
|
+
const snapshot = snapshotFor(workspacePath, "file", written);
|
|
444
|
+
if (snapshot.kind !== "file") throw new Error("Unreachable: wrote a non-file entry.");
|
|
445
|
+
return snapshot;
|
|
446
|
+
} catch (error) {
|
|
447
|
+
throw toFsError(error, "write", workspacePath);
|
|
448
|
+
} finally {
|
|
449
|
+
if (temporary) await fs2.rm(temporary, { force: true }).catch(() => void 0);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Physical absolute path for `workspacePath`, with its parent proven to be
|
|
454
|
+
* inside the root, or null when a parent is missing or is a file.
|
|
455
|
+
*/
|
|
456
|
+
async locate(workspacePath, operation) {
|
|
457
|
+
if (!workspacePath) return this.root;
|
|
458
|
+
const segments = workspacePath.split("/");
|
|
459
|
+
const name = segments.pop();
|
|
460
|
+
let parent;
|
|
461
|
+
try {
|
|
462
|
+
parent = await fs2.realpath(path4.join(this.root, ...segments));
|
|
463
|
+
} catch (error) {
|
|
464
|
+
if (isNodeErrorCode(error, ["ENOENT", "ENOTDIR"])) return null;
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
if (!isPathInside(this.root, parent)) throw escapeError(workspacePath, operation);
|
|
468
|
+
return path4.join(parent, name);
|
|
469
|
+
}
|
|
470
|
+
/** Resolve (and optionally create) the physical parent folder of a write. */
|
|
471
|
+
async ensureParent(workspacePath, createParents) {
|
|
472
|
+
const segments = workspacePath.split("/").slice(0, -1);
|
|
473
|
+
let current = this.root;
|
|
474
|
+
let walked = "";
|
|
475
|
+
for (const segment of segments) {
|
|
476
|
+
walked = walked ? `${walked}/${segment}` : segment;
|
|
477
|
+
const next = path4.join(current, segment);
|
|
478
|
+
let info = await lstatOrNull(next);
|
|
479
|
+
if (!info) {
|
|
480
|
+
if (!createParents) {
|
|
481
|
+
throw new FsError("not-found", `Parent folder "${walked}" does not exist.`, {
|
|
482
|
+
operation: "write",
|
|
483
|
+
path: workspacePath
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
await fs2.mkdir(next);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (!isNodeErrorCode(error, "EEXIST")) throw error;
|
|
490
|
+
}
|
|
491
|
+
info = await fs2.lstat(next, { bigint: true });
|
|
492
|
+
}
|
|
493
|
+
if (info.isSymbolicLink()) {
|
|
494
|
+
const physical = await fs2.realpath(next);
|
|
495
|
+
if (!isPathInside(this.root, physical)) throw escapeError(workspacePath, "write");
|
|
496
|
+
if (!(await fs2.stat(physical)).isDirectory()) throw parentFileError(walked, workspacePath);
|
|
497
|
+
current = physical;
|
|
498
|
+
} else if (info.isDirectory()) {
|
|
499
|
+
current = next;
|
|
500
|
+
} else {
|
|
501
|
+
throw parentFileError(walked, workspacePath);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const physicalParent = await fs2.realpath(current);
|
|
505
|
+
if (!isPathInside(this.root, physicalParent)) throw escapeError(workspacePath, "write");
|
|
506
|
+
return physicalParent;
|
|
507
|
+
}
|
|
508
|
+
async listNames(directory, workspacePath) {
|
|
509
|
+
const names = [];
|
|
510
|
+
let seen = 0;
|
|
511
|
+
const handle = await fs2.opendir(directory);
|
|
512
|
+
for await (const dirent of handle) {
|
|
513
|
+
seen += 1;
|
|
514
|
+
if (seen > this.limits.maxDirectoryEntries) {
|
|
515
|
+
throw new FsError(
|
|
516
|
+
"not-supported",
|
|
517
|
+
`Folder "${workspacePath || "."}" has more than ${this.limits.maxDirectoryEntries} entries.`,
|
|
518
|
+
{ operation: "list", path: workspacePath }
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
if (dirent.isSymbolicLink()) continue;
|
|
522
|
+
if (tryParseWorkspacePath(dirent.name) !== dirent.name) continue;
|
|
523
|
+
names.push(dirent.name);
|
|
524
|
+
}
|
|
525
|
+
return names;
|
|
526
|
+
}
|
|
527
|
+
async readLocatedFile(located, before, workspacePath) {
|
|
528
|
+
const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
529
|
+
const handle = await fs2.open(located, fsConstants.O_RDONLY | noFollow);
|
|
530
|
+
try {
|
|
531
|
+
const opened = await handle.stat({ bigint: true });
|
|
532
|
+
if (!opened.isFile() || opened.ino !== before.ino) throw changedError(workspacePath);
|
|
533
|
+
const physical = await fs2.realpath(located);
|
|
534
|
+
if (!isPathInside(this.root, physical)) throw escapeError(workspacePath, "read");
|
|
535
|
+
this.assertFileSize(opened.size, workspacePath, "read");
|
|
536
|
+
const size = Number(opened.size);
|
|
537
|
+
const buffer = new Uint8Array(size);
|
|
538
|
+
let offset = 0;
|
|
539
|
+
while (offset < size) {
|
|
540
|
+
const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
|
|
541
|
+
if (bytesRead === 0) break;
|
|
542
|
+
offset += bytesRead;
|
|
543
|
+
}
|
|
544
|
+
const probe = new Uint8Array(1);
|
|
545
|
+
const grew = (await handle.read(probe, 0, 1, offset)).bytesRead > 0;
|
|
546
|
+
const after = await handle.stat({ bigint: true });
|
|
547
|
+
if (grew || offset !== size || !sameFileState(opened, after)) {
|
|
548
|
+
throw changedError(workspacePath);
|
|
549
|
+
}
|
|
550
|
+
const entry = snapshotFor(workspacePath, "file", opened);
|
|
551
|
+
if (entry.kind !== "file") throw new Error("Unreachable: read a non-file entry.");
|
|
552
|
+
return Object.freeze({ entry, data: buffer.buffer });
|
|
553
|
+
} finally {
|
|
554
|
+
await handle.close();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
assertFileSize(size, workspacePath, operation) {
|
|
558
|
+
if (size > BigInt(this.limits.maxFileBytes)) {
|
|
559
|
+
throw new FsError(
|
|
560
|
+
"not-supported",
|
|
561
|
+
`"${workspacePath}" is larger than the ${this.limits.maxFileBytes}-byte workspace file limit.`,
|
|
562
|
+
{ operation, path: workspacePath }
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
function canonical(input, operation) {
|
|
568
|
+
try {
|
|
569
|
+
return parseWorkspacePath(input);
|
|
570
|
+
} catch (error) {
|
|
571
|
+
throw toFsError(error, operation, input);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function basename(workspacePath) {
|
|
575
|
+
const slash = workspacePath.lastIndexOf("/");
|
|
576
|
+
return slash < 0 ? workspacePath : workspacePath.slice(slash + 1);
|
|
577
|
+
}
|
|
578
|
+
async function lstatOrNull(absolutePath) {
|
|
579
|
+
try {
|
|
580
|
+
return await fs2.lstat(absolutePath, { bigint: true });
|
|
581
|
+
} catch (error) {
|
|
582
|
+
if (isNodeErrorCode(error, ["ENOENT", "ENOTDIR"])) return null;
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function assertSupportedKind(info, workspacePath, operation) {
|
|
587
|
+
if (info.isSymbolicLink()) {
|
|
588
|
+
throw new FsError(
|
|
589
|
+
"not-supported",
|
|
590
|
+
`"${workspacePath}" is a symbolic link; workspace outputs never follow links.`,
|
|
591
|
+
{ operation, path: workspacePath }
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
if (info.isFile()) return "file";
|
|
595
|
+
if (info.isDirectory()) return "directory";
|
|
596
|
+
throw new FsError("not-supported", `"${workspacePath}" is not a regular file or folder.`, {
|
|
597
|
+
operation,
|
|
598
|
+
path: workspacePath
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
function versionFor(kind, info) {
|
|
602
|
+
return parseFileSystemVersion(
|
|
603
|
+
`node-v1:${kind}:ino=${info.ino}:size=${kind === "file" ? info.size : 0n}:mtime-ns=${info.mtimeNs}`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
function snapshotFor(workspacePath, kind, info) {
|
|
607
|
+
const base = {
|
|
608
|
+
path: workspacePath,
|
|
609
|
+
name: basename(workspacePath),
|
|
610
|
+
version: versionFor(kind, info),
|
|
611
|
+
lastModified: info.mtime.toISOString()
|
|
612
|
+
};
|
|
613
|
+
if (kind === "directory") return Object.freeze({ ...base, kind, size: null });
|
|
614
|
+
const size = Number(info.size);
|
|
615
|
+
if (!Number.isSafeInteger(size)) {
|
|
616
|
+
throw new FsError("not-supported", `"${workspacePath}" is too large to describe.`, {
|
|
617
|
+
path: workspacePath
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
return Object.freeze({ ...base, kind, size });
|
|
621
|
+
}
|
|
622
|
+
function sameFileState(left, right) {
|
|
623
|
+
return left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs;
|
|
624
|
+
}
|
|
625
|
+
function assertWritable(current, workspacePath, mode, expectedVersion) {
|
|
626
|
+
if (current) {
|
|
627
|
+
const kind = assertSupportedKind(current, workspacePath, "write");
|
|
628
|
+
if (kind === "directory") {
|
|
629
|
+
throw new FsError("type-mismatch", `"${workspacePath}" is a folder; it cannot be replaced.`, {
|
|
630
|
+
operation: "write",
|
|
631
|
+
path: workspacePath
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
if (mode === "create") {
|
|
635
|
+
throw new FsError("already-exists", `"${workspacePath}" already exists.`, {
|
|
636
|
+
operation: "write",
|
|
637
|
+
path: workspacePath
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
} else if (mode === "replace") {
|
|
641
|
+
throw new FsError("not-found", `"${workspacePath}" does not exist.`, {
|
|
642
|
+
operation: "write",
|
|
643
|
+
path: workspacePath
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
if (expectedVersion === void 0) return;
|
|
647
|
+
const matches = expectedVersion === null ? !current : !!current && versionFor("file", current) === expectedVersion;
|
|
648
|
+
if (!matches) {
|
|
649
|
+
throw new FsError("conflict", `"${workspacePath}" changed since it was read.`, {
|
|
650
|
+
operation: "write",
|
|
651
|
+
path: workspacePath,
|
|
652
|
+
retryable: true
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function publishExclusive(temporary, target, bytes, workspacePath, mode) {
|
|
657
|
+
try {
|
|
658
|
+
await fs2.link(temporary, target);
|
|
659
|
+
return;
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (isNodeErrorCode(error, "EEXIST")) throw existsRaceError(workspacePath, mode);
|
|
662
|
+
if (!isLinkUnsupportedError(error)) throw error;
|
|
663
|
+
}
|
|
664
|
+
let handle;
|
|
665
|
+
try {
|
|
666
|
+
handle = await fs2.open(target, "wx", 438);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
if (isNodeErrorCode(error, "EEXIST")) throw existsRaceError(workspacePath, mode);
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
await handle.writeFile(bytes);
|
|
673
|
+
} finally {
|
|
674
|
+
await handle.close();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function existsRaceError(workspacePath, mode) {
|
|
678
|
+
return mode === "create" ? new FsError("already-exists", `"${workspacePath}" already exists.`, {
|
|
679
|
+
operation: "write",
|
|
680
|
+
path: workspacePath
|
|
681
|
+
}) : new FsError("conflict", `"${workspacePath}" was created by another writer.`, {
|
|
682
|
+
operation: "write",
|
|
683
|
+
path: workspacePath,
|
|
684
|
+
retryable: true
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
function escapeError(workspacePath, operation) {
|
|
688
|
+
return new FsError(
|
|
689
|
+
"path-escape",
|
|
690
|
+
`"${workspacePath}" resolves outside the workspace folder through a symbolic link.`,
|
|
691
|
+
{ operation, path: workspacePath }
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
function parentFileError(parent, workspacePath) {
|
|
695
|
+
return new FsError("type-mismatch", `"${parent}" is a file, not a folder.`, {
|
|
696
|
+
operation: "write",
|
|
697
|
+
path: workspacePath
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
function changedError(workspacePath) {
|
|
701
|
+
return new FsError("conflict", `"${workspacePath}" changed while it was being read.`, {
|
|
702
|
+
operation: "read",
|
|
703
|
+
path: workspacePath,
|
|
704
|
+
retryable: true
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
function compareSnapshots(left, right) {
|
|
708
|
+
if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1;
|
|
709
|
+
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
710
|
+
}
|
|
711
|
+
function toFsError(error, operation, workspacePath) {
|
|
712
|
+
if (error instanceof FsError) return error;
|
|
713
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
714
|
+
const code = nodeErrorCode(error);
|
|
715
|
+
if (code === "ELOOP") {
|
|
716
|
+
return new FsError(
|
|
717
|
+
"not-supported",
|
|
718
|
+
`"${workspacePath}" is a symbolic link; workspace outputs never follow links.`,
|
|
719
|
+
{ operation, path: workspacePath }
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
if (code) {
|
|
723
|
+
return new FsError(
|
|
724
|
+
mapNodeErrorCodeToFsErrorCode(code),
|
|
725
|
+
`Could not ${OPERATION_VERBS[operation]} "${workspacePath || "."}": ${message}`,
|
|
726
|
+
{ operation, path: workspacePath }
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
return new FsError("io", message, { operation, path: workspacePath });
|
|
730
|
+
}
|
|
731
|
+
function nodeErrorCode(error) {
|
|
732
|
+
if (!(error instanceof Error) || !("code" in error)) return null;
|
|
733
|
+
const { code } = error;
|
|
734
|
+
return typeof code === "string" ? code : null;
|
|
735
|
+
}
|
|
736
|
+
var OPERATION_VERBS = {
|
|
737
|
+
"parse-path": "parse",
|
|
738
|
+
read: "read",
|
|
739
|
+
stat: "inspect",
|
|
740
|
+
list: "list",
|
|
741
|
+
write: "write",
|
|
742
|
+
"create-directory": "create",
|
|
743
|
+
remove: "remove",
|
|
744
|
+
move: "move",
|
|
745
|
+
snapshot: "snapshot",
|
|
746
|
+
watch: "watch",
|
|
747
|
+
dispose: "close"
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// src/internal/workspace-settings.ts
|
|
751
|
+
var MAX_SETTINGS_FILE_BYTES = WORKSPACE_SETTINGS_LIMITS.maxFileBytes + 3;
|
|
752
|
+
async function readWorkspaceSettings(io) {
|
|
753
|
+
const settingsPath = parseWorkspacePath2(WORKSPACE_SETTINGS_PATH);
|
|
754
|
+
const info = await io.stat(settingsPath);
|
|
755
|
+
if (!info) return { status: "missing" };
|
|
756
|
+
if (info.kind !== "file") {
|
|
757
|
+
return { status: "invalid", message: `${WORKSPACE_SETTINGS_PATH} is a folder, not a file.` };
|
|
758
|
+
}
|
|
759
|
+
if (info.size > MAX_SETTINGS_FILE_BYTES) return tooLarge();
|
|
760
|
+
const read = await io.readFile(settingsPath);
|
|
761
|
+
if (!read) return { status: "missing" };
|
|
762
|
+
if (read.data.byteLength > MAX_SETTINGS_FILE_BYTES) return tooLarge();
|
|
763
|
+
let text;
|
|
764
|
+
try {
|
|
765
|
+
text = decodeUtf8Text(read.data, {
|
|
766
|
+
label: "Workspace settings",
|
|
767
|
+
path: WORKSPACE_SETTINGS_PATH
|
|
768
|
+
});
|
|
769
|
+
} catch (error) {
|
|
770
|
+
return {
|
|
771
|
+
status: "invalid",
|
|
772
|
+
message: error instanceof Error ? error.message : `${WORKSPACE_SETTINGS_PATH} is not UTF-8.`
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
return parseWorkspaceSettingsText(text);
|
|
776
|
+
}
|
|
777
|
+
function describeWorkspaceSettingsProblem(message) {
|
|
778
|
+
return message.includes(WORKSPACE_SETTINGS_PATH) ? message : `${WORKSPACE_SETTINGS_PATH}: ${message}`;
|
|
779
|
+
}
|
|
780
|
+
function tooLarge() {
|
|
781
|
+
return {
|
|
782
|
+
status: "invalid",
|
|
783
|
+
message: `${WORKSPACE_SETTINGS_PATH} is larger than ${WORKSPACE_SETTINGS_LIMITS.maxFileBytes / 1024} KiB.`
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
async function createWorkspaceThemeFallback(rootDir, warn) {
|
|
787
|
+
const io = await createNodeWorkspaceIO(rootDir);
|
|
788
|
+
let lastWarning = null;
|
|
789
|
+
const report = (message) => {
|
|
790
|
+
if (message !== null && message !== lastWarning) warn(message);
|
|
791
|
+
lastWarning = message;
|
|
792
|
+
};
|
|
793
|
+
return {
|
|
794
|
+
async resolve() {
|
|
795
|
+
const result = await readWorkspaceSettings(io);
|
|
796
|
+
if (result.status === "missing") {
|
|
797
|
+
report(null);
|
|
798
|
+
return void 0;
|
|
799
|
+
}
|
|
800
|
+
if (result.status !== "ok") {
|
|
801
|
+
report(`Ignoring workspace settings. ${describeWorkspaceSettingsProblem(result.message)}`);
|
|
802
|
+
return void 0;
|
|
803
|
+
}
|
|
804
|
+
const themeId = resolveWorkspaceDefaultThemeId(result.settings);
|
|
805
|
+
if (themeId && !(await getAvailableThemeIds()).includes(themeId)) {
|
|
806
|
+
report(
|
|
807
|
+
`Ignoring workspace default theme "${themeId}" from ${WORKSPACE_SETTINGS_PATH}: it is not a known theme.`
|
|
808
|
+
);
|
|
809
|
+
return void 0;
|
|
810
|
+
}
|
|
811
|
+
report(null);
|
|
812
|
+
return themeId;
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
240
817
|
// src/commands/build.ts
|
|
241
|
-
import { decodeUtf8Text } from "@bendyline/docblocks/filesystem";
|
|
818
|
+
import { decodeUtf8Text as decodeUtf8Text2 } from "@bendyline/docblocks/filesystem";
|
|
242
819
|
var DEFAULT_MAX_BUILD_ENTRIES = 1e5;
|
|
243
820
|
var DEFAULT_MAX_BUILD_DEPTH = 64;
|
|
244
821
|
var DEFAULT_MAX_BUILD_INPUT_BYTES = 20 * 1024 * 1024;
|
|
@@ -247,8 +824,8 @@ var DEFAULT_MAX_BUILD_OUTPUT_BYTES = 128 * 1024 * 1024;
|
|
|
247
824
|
var DEFAULT_MAX_BUILD_TOTAL_OUTPUT_BYTES = 1024 * 1024 * 1024;
|
|
248
825
|
async function runBuild(opts) {
|
|
249
826
|
await assertKnownThemeId(opts.theme);
|
|
250
|
-
const inputDir =
|
|
251
|
-
const outputDir =
|
|
827
|
+
const inputDir = path5.resolve(opts.input);
|
|
828
|
+
const outputDir = path5.resolve(opts.output);
|
|
252
829
|
let inputStat;
|
|
253
830
|
try {
|
|
254
831
|
inputStat = await stat2(inputDir);
|
|
@@ -257,6 +834,8 @@ async function runBuild(opts) {
|
|
|
257
834
|
throw new Error(`Input directory not found: ${inputDir}`);
|
|
258
835
|
}
|
|
259
836
|
if (!inputStat.isDirectory()) throw new Error(`Input is not a directory: ${inputDir}`);
|
|
837
|
+
const warn = opts.onWarning ?? ((message) => console.warn(message));
|
|
838
|
+
const fallbackThemeId = opts.theme || opts.ignoreWorkspaceSettings ? void 0 : await (await createWorkspaceThemeFallback(inputDir, warn)).resolve();
|
|
260
839
|
const markdownFiles = await listMarkdownFiles(inputDir, {
|
|
261
840
|
maxEntries: positiveLimit(opts.maxEntries, DEFAULT_MAX_BUILD_ENTRIES, "build entry"),
|
|
262
841
|
maxDepth: positiveLimit(opts.maxDepth, DEFAULT_MAX_BUILD_DEPTH, "build depth")
|
|
@@ -271,8 +850,8 @@ async function runBuild(opts) {
|
|
|
271
850
|
let totalInputBytes = 0;
|
|
272
851
|
let totalOutputBytes = 0;
|
|
273
852
|
for (const sourcePath of markdownFiles) {
|
|
274
|
-
const relativePath =
|
|
275
|
-
const outputPath =
|
|
853
|
+
const relativePath = path5.relative(inputDir, sourcePath);
|
|
854
|
+
const outputPath = path5.join(outputDir, replaceMarkdownExtension(relativePath));
|
|
276
855
|
const sourceBytes = await readFile(sourcePath);
|
|
277
856
|
if (sourceBytes.byteLength > byteLimits.maxInputBytes) {
|
|
278
857
|
throw new Error(
|
|
@@ -285,13 +864,15 @@ async function runBuild(opts) {
|
|
|
285
864
|
`Build inputs exceed the ${byteLimits.maxTotalInputBytes}-byte aggregate limit.`
|
|
286
865
|
);
|
|
287
866
|
}
|
|
288
|
-
const source =
|
|
867
|
+
const source = decodeUtf8Text2(sourceBytes, { label: "Build input", path: sourcePath });
|
|
289
868
|
const html = await renderMarkdownHtml(source, {
|
|
290
869
|
title: titleFromPath(relativePath),
|
|
291
870
|
sourcePath,
|
|
292
871
|
assetRoot: inputDir,
|
|
293
872
|
themeId: opts.theme,
|
|
294
|
-
|
|
873
|
+
fallbackThemeId,
|
|
874
|
+
mode: "static",
|
|
875
|
+
onWarning: warn
|
|
295
876
|
});
|
|
296
877
|
const outputBytes = Buffer.byteLength(html, "utf8");
|
|
297
878
|
if (outputBytes > byteLimits.maxOutputBytes) {
|
|
@@ -305,18 +886,22 @@ async function runBuild(opts) {
|
|
|
305
886
|
`Build outputs exceed the ${byteLimits.maxTotalOutputBytes}-byte aggregate limit.`
|
|
306
887
|
);
|
|
307
888
|
}
|
|
308
|
-
await mkdir(
|
|
889
|
+
await mkdir(path5.dirname(outputPath), { recursive: true });
|
|
309
890
|
await writeFile(outputPath, html, "utf-8");
|
|
310
891
|
builtFiles.push(outputPath);
|
|
311
892
|
}
|
|
312
893
|
return { inputDir, outputDir, builtFiles };
|
|
313
894
|
}
|
|
314
|
-
var buildCommand = new Command("build").description("Build markdown files into HTML output").option("-i, --input <dir>", "input directory", ".").option("-o, --output <dir>", "output directory", "dist").option("-t, --theme <id>", "Squisq theme ID to apply").option(
|
|
895
|
+
var buildCommand = new Command("build").description("Build markdown files into HTML output").option("-i, --input <dir>", "input directory", ".").option("-o, --output <dir>", "output directory", "dist").option("-t, --theme <id>", "Squisq theme ID to apply").option(
|
|
896
|
+
"--ignore-workspace-settings",
|
|
897
|
+
"do not apply the default theme from <input>/.docblocks/workspace.json"
|
|
898
|
+
).option("--max-input-bytes <bytes>", "maximum bytes in one Markdown input").option("--max-total-input-bytes <bytes>", "maximum aggregate Markdown input bytes").option("--max-output-bytes <bytes>", "maximum bytes in one generated HTML file").option("--max-total-output-bytes <bytes>", "maximum aggregate generated HTML bytes").option("--allow-large-build", "disable default byte budgets for a trusted bulk build").action(async (opts) => {
|
|
315
899
|
try {
|
|
316
900
|
const result = await runBuild({
|
|
317
901
|
input: opts.input,
|
|
318
902
|
output: opts.output,
|
|
319
903
|
theme: opts.theme,
|
|
904
|
+
ignoreWorkspaceSettings: opts.ignoreWorkspaceSettings,
|
|
320
905
|
maxInputBytes: parseOptionalByteLimit("--max-input-bytes", opts.maxInputBytes),
|
|
321
906
|
maxTotalInputBytes: parseOptionalByteLimit(
|
|
322
907
|
"--max-total-input-bytes",
|
|
@@ -400,7 +985,7 @@ async function listMarkdownFiles(root, limits) {
|
|
|
400
985
|
if (entryCount > limits.maxEntries) {
|
|
401
986
|
throw new Error(`Build traversal exceeded ${limits.maxEntries} filesystem entries.`);
|
|
402
987
|
}
|
|
403
|
-
const absolutePath =
|
|
988
|
+
const absolutePath = path5.join(current.directory, entry.name);
|
|
404
989
|
if (entry.isDirectory()) {
|
|
405
990
|
if (current.depth >= limits.maxDepth) {
|
|
406
991
|
throw new Error(`Build traversal exceeded a depth of ${limits.maxDepth}.`);
|
|
@@ -420,7 +1005,7 @@ function replaceMarkdownExtension(filePath) {
|
|
|
420
1005
|
return filePath.replace(/\.(md|markdown)$/i, ".html");
|
|
421
1006
|
}
|
|
422
1007
|
function titleFromPath(filePath) {
|
|
423
|
-
const baseName =
|
|
1008
|
+
const baseName = path5.basename(filePath).replace(/\.(md|markdown)$/i, "");
|
|
424
1009
|
return baseName || "DocBlocks Document";
|
|
425
1010
|
}
|
|
426
1011
|
|
|
@@ -437,23 +1022,10 @@ import {
|
|
|
437
1022
|
rm,
|
|
438
1023
|
stat as stat3
|
|
439
1024
|
} from "fs/promises";
|
|
440
|
-
import { randomUUID } from "crypto";
|
|
441
|
-
import { dirname, basename, extname, join, resolve } from "path";
|
|
1025
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1026
|
+
import { dirname, basename as basename2, extname, join, resolve } from "path";
|
|
442
1027
|
import { Command as Command2 } from "commander";
|
|
443
1028
|
|
|
444
|
-
// src/internal/link-support.ts
|
|
445
|
-
var LINK_UNSUPPORTED_CODES = [
|
|
446
|
-
"EPERM",
|
|
447
|
-
"ENOSYS",
|
|
448
|
-
"ENOTSUP",
|
|
449
|
-
"EOPNOTSUPP",
|
|
450
|
-
"EXDEV",
|
|
451
|
-
"EMLINK"
|
|
452
|
-
];
|
|
453
|
-
function isLinkUnsupportedError(error) {
|
|
454
|
-
return isNodeErrorCode(error, LINK_UNSUPPORTED_CODES);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
1029
|
// src/internal/cancellation.ts
|
|
458
1030
|
function cancellationError(message) {
|
|
459
1031
|
const error = new Error(message);
|
|
@@ -536,7 +1108,7 @@ async function runConvert(inputPath, opts) {
|
|
|
536
1108
|
throwIfAborted2(opts.signal);
|
|
537
1109
|
const resolvedInput = resolve(inputPath);
|
|
538
1110
|
const outputDir = opts.outputDir ? resolve(opts.outputDir) : dirname(resolvedInput);
|
|
539
|
-
const inputBasename =
|
|
1111
|
+
const inputBasename = basename2(resolvedInput);
|
|
540
1112
|
const inputExt = extname(inputBasename);
|
|
541
1113
|
const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;
|
|
542
1114
|
const maxInputBytes = positiveLimit(
|
|
@@ -678,7 +1250,7 @@ async function runConvert(inputPath, opts) {
|
|
|
678
1250
|
if (totalOutputBytes > maxTotalOutputBytes) {
|
|
679
1251
|
throw new Error(`Conversion outputs exceed the ${maxTotalOutputBytes}-byte aggregate limit.`);
|
|
680
1252
|
}
|
|
681
|
-
const suggestedFilename =
|
|
1253
|
+
const suggestedFilename = basename2(conversion.suggestedFilename);
|
|
682
1254
|
const outPath = join(outputDir, suggestedFilename);
|
|
683
1255
|
const outputPathKey = process.platform === "win32" ? outPath.toLowerCase() : outPath;
|
|
684
1256
|
if (outputPathKeys.has(outputPathKey)) {
|
|
@@ -794,7 +1366,7 @@ async function publishConversionBatch(outputs, outputDir, allowOverwrite, signal
|
|
|
794
1366
|
throwIfAborted2(signal);
|
|
795
1367
|
const output = outputs[index];
|
|
796
1368
|
if (!output) continue;
|
|
797
|
-
const stagedPath = join(stagingDirectory, `${index}-${
|
|
1369
|
+
const stagedPath = join(stagingDirectory, `${index}-${randomUUID2()}.stage`);
|
|
798
1370
|
await writePrivateFile(stagedPath, output.bytes);
|
|
799
1371
|
stagedOutputs.push({ outputPath: output.path, stagedPath, bytes: output.bytes });
|
|
800
1372
|
}
|
|
@@ -824,7 +1396,7 @@ async function publishConversionBatch(outputs, outputDir, allowOverwrite, signal
|
|
|
824
1396
|
throw new Error(`Refusing to replace a non-file destination: ${output.outputPath}`);
|
|
825
1397
|
}
|
|
826
1398
|
if (existing) {
|
|
827
|
-
state.backupPath = join(stagingDirectory, `${index}-${
|
|
1399
|
+
state.backupPath = join(stagingDirectory, `${index}-${randomUUID2()}.backup`);
|
|
828
1400
|
await rename(output.outputPath, state.backupPath);
|
|
829
1401
|
}
|
|
830
1402
|
await rename(output.stagedPath, output.outputPath);
|
|
@@ -852,8 +1424,8 @@ async function publishConversionBatch(outputs, outputDir, allowOverwrite, signal
|
|
|
852
1424
|
}
|
|
853
1425
|
}
|
|
854
1426
|
}
|
|
855
|
-
async function writePrivateFile(
|
|
856
|
-
const handle = await open2(
|
|
1427
|
+
async function writePrivateFile(path7, bytes) {
|
|
1428
|
+
const handle = await open2(path7, "wx", 384);
|
|
857
1429
|
try {
|
|
858
1430
|
await handle.writeFile(bytes);
|
|
859
1431
|
await handle.sync();
|
|
@@ -912,9 +1484,9 @@ async function assertReplaceableOutputs(outputPaths) {
|
|
|
912
1484
|
}
|
|
913
1485
|
}
|
|
914
1486
|
}
|
|
915
|
-
async function lstatIfPresent(
|
|
1487
|
+
async function lstatIfPresent(path7) {
|
|
916
1488
|
try {
|
|
917
|
-
return await lstat(
|
|
1489
|
+
return await lstat(path7);
|
|
918
1490
|
} catch (error) {
|
|
919
1491
|
if (isNodeErrorCode(error, ["ENOENT", "ENOTDIR"])) return null;
|
|
920
1492
|
throw error;
|
|
@@ -933,11 +1505,11 @@ function throwIfAborted2(signal) {
|
|
|
933
1505
|
}
|
|
934
1506
|
async function applyTransformToMarkdown(markdownDoc, container, transformStyle, themeId) {
|
|
935
1507
|
const { markdownToDoc: markdownToDoc2, docToMarkdown } = await import("@bendyline/squisq/doc");
|
|
936
|
-
const { readFrontmatterThemeId } = await import("@bendyline/squisq/markdown");
|
|
1508
|
+
const { readFrontmatterThemeId: readFrontmatterThemeId2 } = await import("@bendyline/squisq/markdown");
|
|
937
1509
|
const { applyTransform, extractDocImages } = await import("@bendyline/squisq/transform");
|
|
938
1510
|
const doc = markdownToDoc2(markdownDoc);
|
|
939
1511
|
const images = extractDocImages(doc.blocks);
|
|
940
|
-
const effectiveTheme = themeId ??
|
|
1512
|
+
const effectiveTheme = themeId ?? readFrontmatterThemeId2(markdownDoc.frontmatter);
|
|
941
1513
|
const result = applyTransform(doc, transformStyle, { themeId: effectiveTheme, images });
|
|
942
1514
|
const projected = projectTransformOntoMarkdownSource(doc, result.doc);
|
|
943
1515
|
const transformedMarkdown = docToMarkdown(projected);
|
|
@@ -1127,7 +1699,7 @@ function isConversionErrorMetadata(error) {
|
|
|
1127
1699
|
|
|
1128
1700
|
// src/commands/video.ts
|
|
1129
1701
|
import { lstat as lstat2, mkdir as mkdir3 } from "fs/promises";
|
|
1130
|
-
import { dirname as dirname2, basename as
|
|
1702
|
+
import { dirname as dirname2, basename as basename3, extname as extname2, resolve as resolve2 } from "path";
|
|
1131
1703
|
import { Command as Command3 } from "commander";
|
|
1132
1704
|
import {
|
|
1133
1705
|
resolveDimensions,
|
|
@@ -1169,7 +1741,7 @@ async function runVideo(inputPath, opts, onProgress, dependencies = {}) {
|
|
|
1169
1741
|
width: opts.width,
|
|
1170
1742
|
height: opts.height
|
|
1171
1743
|
});
|
|
1172
|
-
const inputBasename =
|
|
1744
|
+
const inputBasename = basename3(resolvedInput);
|
|
1173
1745
|
const inputExt = extname2(inputBasename);
|
|
1174
1746
|
const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;
|
|
1175
1747
|
const outputPath = opts.output ? resolve2(opts.output) : resolve2(dirname2(resolvedInput), `${baseName}.mp4`);
|
|
@@ -1307,7 +1879,7 @@ function selectVideoOutput(positional, option) {
|
|
|
1307
1879
|
import { open as open3 } from "fs/promises";
|
|
1308
1880
|
import { resolve as resolve3 } from "path";
|
|
1309
1881
|
import { Command as Command4 } from "commander";
|
|
1310
|
-
import { decodeUtf8Text as
|
|
1882
|
+
import { decodeUtf8Text as decodeUtf8Text3 } from "@bendyline/docblocks/filesystem";
|
|
1311
1883
|
var DEFAULT_MAX_PARSE_INPUT_BYTES = 20 * 1024 * 1024;
|
|
1312
1884
|
var DEFAULT_MAX_PARSE_OUTPUT_BYTES = 128 * 1024 * 1024;
|
|
1313
1885
|
async function runParse(inputPath, options = {}) {
|
|
@@ -1323,7 +1895,7 @@ async function runParse(inputPath, options = {}) {
|
|
|
1323
1895
|
"parse output byte"
|
|
1324
1896
|
);
|
|
1325
1897
|
const bytes = await readBoundedFile(resolvedInput, maxInputBytes);
|
|
1326
|
-
const content =
|
|
1898
|
+
const content = decodeUtf8Text3(bytes, { label: "Parse input", path: resolvedInput });
|
|
1327
1899
|
const { parseMarkdown: parseMarkdown2 } = await import("@bendyline/squisq/markdown");
|
|
1328
1900
|
const markdownDoc = parseMarkdown2(content);
|
|
1329
1901
|
const stats = {
|
|
@@ -1386,12 +1958,98 @@ var parseCommand = new Command4("parse").description("Parse a markdown file and
|
|
|
1386
1958
|
}
|
|
1387
1959
|
});
|
|
1388
1960
|
|
|
1961
|
+
// src/commands/workspace.ts
|
|
1962
|
+
import { Command as Command5 } from "commander";
|
|
1963
|
+
import { FsError as FsError2 } from "@bendyline/docblocks/filesystem";
|
|
1964
|
+
import {
|
|
1965
|
+
WORKSPACE_SETTINGS_PATH as WORKSPACE_SETTINGS_PATH2,
|
|
1966
|
+
refreshWorkspaceOutputs,
|
|
1967
|
+
resolveCatalogOutputs
|
|
1968
|
+
} from "@bendyline/docblocks/workspace-settings";
|
|
1969
|
+
import { htmlToMarkdownDocSync, markdownDocToPlainHtml } from "@bendyline/squisq-formats/html";
|
|
1970
|
+
async function runWorkspaceRefresh(options = {}) {
|
|
1971
|
+
const io = await createNodeWorkspaceIO(options.dir ?? ".");
|
|
1972
|
+
const dryRun = options.dryRun === true;
|
|
1973
|
+
const settingsRead = await readWorkspaceSettings(io);
|
|
1974
|
+
if (settingsRead.status === "missing") {
|
|
1975
|
+
return { root: io.root, status: "missing", documentCount: 0, outputs: [], dryRun };
|
|
1976
|
+
}
|
|
1977
|
+
if (settingsRead.status !== "ok") {
|
|
1978
|
+
throw new Error(describeWorkspaceSettingsProblem(settingsRead.message));
|
|
1979
|
+
}
|
|
1980
|
+
const settings = settingsRead.settings;
|
|
1981
|
+
const resolved = resolveCatalogOutputs(settings);
|
|
1982
|
+
if (!resolved) {
|
|
1983
|
+
return { root: io.root, status: "disabled", documentCount: 0, outputs: [], dryRun };
|
|
1984
|
+
}
|
|
1985
|
+
const warn = options.onWarning ?? ((message) => console.warn(message));
|
|
1986
|
+
const catalogThemeId = resolved.html?.themeId;
|
|
1987
|
+
if (catalogThemeId && !(await getAvailableThemeIds()).includes(catalogThemeId)) {
|
|
1988
|
+
warn(
|
|
1989
|
+
`Catalog theme "${catalogThemeId}" from ${WORKSPACE_SETTINGS_PATH2} is not a known theme; the page renders with the standard theme.`
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
const refresh = () => refreshWorkspaceOutputs(io, settings, renderer, {
|
|
1993
|
+
signal: options.signal,
|
|
1994
|
+
force: options.force === true,
|
|
1995
|
+
dryRun,
|
|
1996
|
+
limits: options.limits
|
|
1997
|
+
});
|
|
1998
|
+
let result;
|
|
1999
|
+
try {
|
|
2000
|
+
result = await refresh();
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
if (!(error instanceof FsError2) || error.code !== "conflict") throw error;
|
|
2003
|
+
result = await refresh();
|
|
2004
|
+
}
|
|
2005
|
+
return {
|
|
2006
|
+
root: io.root,
|
|
2007
|
+
status: "refreshed",
|
|
2008
|
+
documentCount: result.documentCount,
|
|
2009
|
+
outputs: result.outputs,
|
|
2010
|
+
dryRun
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
var renderer = {
|
|
2014
|
+
renderCatalogHtml: (document, { title, themeId }) => markdownDocToPlainHtml(document, { title, themeId }),
|
|
2015
|
+
importHtml: (html) => htmlToMarkdownDocSync(html)
|
|
2016
|
+
};
|
|
2017
|
+
function describeWorkspaceOutput(output) {
|
|
2018
|
+
const status = output.status === "would-write" ? "would write" : output.status;
|
|
2019
|
+
return output.message ? `${status} ${output.path}: ${output.message}` : `${status} ${output.path}`;
|
|
2020
|
+
}
|
|
2021
|
+
var refreshCommand = new Command5("refresh").description(`Regenerate the catalog outputs enabled in <dir>/${WORKSPACE_SETTINGS_PATH2}`).argument("[dir]", "workspace root", ".").option("--force", "rewrite the catalog page even when its inputs are unchanged").option("--dry-run", "report what would change without writing anything").action(async (dir, opts) => {
|
|
2022
|
+
try {
|
|
2023
|
+
const result = await runWorkspaceRefresh({ dir, force: opts.force, dryRun: opts.dryRun });
|
|
2024
|
+
if (result.status === "missing") {
|
|
2025
|
+
console.error(
|
|
2026
|
+
`No workspace settings found at ${WORKSPACE_SETTINGS_PATH2} in ${result.root}; no outputs are enabled.`
|
|
2027
|
+
);
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
if (result.status === "disabled") {
|
|
2031
|
+
console.error(`${WORKSPACE_SETTINGS_PATH2} enables no catalog outputs; nothing to refresh.`);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
for (const output of result.outputs) console.error(describeWorkspaceOutput(output));
|
|
2035
|
+
console.error(
|
|
2036
|
+
`${result.dryRun ? "Dry run: " : ""}${result.documentCount} document(s) cataloged in ${result.root}.`
|
|
2037
|
+
);
|
|
2038
|
+
if (result.outputs.some((output) => output.status === "blocked")) process.exitCode = 1;
|
|
2039
|
+
} catch (err) {
|
|
2040
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2041
|
+
console.error(`Error: ${message}`);
|
|
2042
|
+
process.exitCode = 1;
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
var workspaceCommand = new Command5("workspace").description(`Maintain workspace outputs configured in ${WORKSPACE_SETTINGS_PATH2}`).addCommand(refreshCommand);
|
|
2046
|
+
|
|
1389
2047
|
// src/version.ts
|
|
1390
2048
|
import { createHash } from "crypto";
|
|
1391
2049
|
import { createRequire } from "module";
|
|
1392
2050
|
import { readFileSync } from "fs";
|
|
1393
2051
|
import { readdir, readFile as readFile2, stat as stat4 } from "fs/promises";
|
|
1394
|
-
import
|
|
2052
|
+
import path6 from "path";
|
|
1395
2053
|
var require2 = createRequire(import.meta.url);
|
|
1396
2054
|
function getPackageVersion() {
|
|
1397
2055
|
const pkg = readPackageJson();
|
|
@@ -1411,9 +2069,11 @@ function readPackageJson() {
|
|
|
1411
2069
|
export {
|
|
1412
2070
|
applyTransformToMarkdown,
|
|
1413
2071
|
assertCliVideoRenderBudget,
|
|
2072
|
+
describeWorkspaceOutput,
|
|
1414
2073
|
getPackageVersion,
|
|
1415
2074
|
runBuild,
|
|
1416
2075
|
runConvert,
|
|
1417
2076
|
runParse,
|
|
1418
|
-
runVideo
|
|
2077
|
+
runVideo,
|
|
2078
|
+
runWorkspaceRefresh
|
|
1419
2079
|
};
|