@rohal12/twee-ts 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,9 +30,11 @@ __export(src_exports, {
30
30
  discoverFormats: () => discoverFormats,
31
31
  fetchAndCacheFormat: () => fetchAndCacheFormat,
32
32
  fetchDirectFormat: () => fetchDirectFormat,
33
+ formatLintReport: () => formatLintReport,
33
34
  generateIFID: () => generateIFID,
34
35
  getCacheDir: () => getCacheDir,
35
36
  getFormatSearchDirs: () => getFormatSearchDirs,
37
+ lint: () => lint,
36
38
  loadConfig: () => loadConfig,
37
39
  loadConfigFile: () => loadConfigFile,
38
40
  parseFormatJSON: () => parseFormatJSON,
@@ -50,7 +52,7 @@ __export(src_exports, {
50
52
  module.exports = __toCommonJS(src_exports);
51
53
 
52
54
  // src/compiler.ts
53
- var import_node_fs6 = require("fs");
55
+ var import_node_fs7 = require("fs");
54
56
 
55
57
  // src/ifid.ts
56
58
  var import_node_crypto = require("crypto");
@@ -309,8 +311,21 @@ function quote(s) {
309
311
  }
310
312
 
311
313
  // src/story.ts
314
+ var passageIndex = /* @__PURE__ */ new WeakMap();
315
+ function getIndex(story) {
316
+ let index = passageIndex.get(story);
317
+ if (!index) {
318
+ index = /* @__PURE__ */ new Map();
319
+ for (let i = 0; i < story.passages.length; i++) {
320
+ const passage = story.passages[i];
321
+ if (passage) index.set(passage.name, i);
322
+ }
323
+ passageIndex.set(story, index);
324
+ }
325
+ return index;
326
+ }
312
327
  function createStory() {
313
- return {
328
+ const story = {
314
329
  name: "",
315
330
  ifid: "",
316
331
  passages: [],
@@ -326,16 +341,17 @@ function createStory() {
326
341
  zoom: 1
327
342
  }
328
343
  };
344
+ passageIndex.set(story, /* @__PURE__ */ new Map());
345
+ return story;
329
346
  }
330
347
  function storyHas(story, name) {
331
- return story.passages.some((p) => p.name === name);
332
- }
333
- function storyIndex(story, name) {
334
- return story.passages.findIndex((p) => p.name === name);
348
+ return getIndex(story).has(name);
335
349
  }
336
350
  function storyAppend(story, p, diagnostics) {
337
- const i = storyIndex(story, p.name);
351
+ const index = getIndex(story);
352
+ const i = index.get(p.name) ?? -1;
338
353
  if (i === -1) {
354
+ index.set(p.name, story.passages.length);
339
355
  story.passages.push(p);
340
356
  } else {
341
357
  diagnostics.push({
@@ -346,9 +362,15 @@ function storyAppend(story, p, diagnostics) {
346
362
  }
347
363
  }
348
364
  function storyPrepend(story, p, diagnostics) {
349
- const i = storyIndex(story, p.name);
365
+ const index = getIndex(story);
366
+ const i = index.get(p.name) ?? -1;
350
367
  if (i === -1) {
351
368
  story.passages.unshift(p);
369
+ index.clear();
370
+ for (let j = 0; j < story.passages.length; j++) {
371
+ const passage = story.passages[j];
372
+ if (passage) index.set(passage.name, j);
373
+ }
352
374
  } else {
353
375
  diagnostics.push({
354
376
  level: "warning",
@@ -638,11 +660,15 @@ function watchFilesystem(pathnames, outFilename, callback) {
638
660
  const watchers = [];
639
661
  let buildTimer = null;
640
662
  const BUILD_DEBOUNCE = 500;
641
- function scheduleBuild() {
663
+ const pendingFiles = /* @__PURE__ */ new Set();
664
+ function scheduleBuild(changedFile) {
665
+ if (changedFile) pendingFiles.add(changedFile);
642
666
  if (buildTimer) clearTimeout(buildTimer);
643
667
  buildTimer = setTimeout(() => {
644
668
  buildTimer = null;
645
- callback();
669
+ const files = pendingFiles.size > 0 ? new Set(pendingFiles) : void 0;
670
+ pendingFiles.clear();
671
+ callback(files);
646
672
  }, BUILD_DEBOUNCE);
647
673
  }
648
674
  for (const pathname of pathnames) {
@@ -652,7 +678,8 @@ function watchFilesystem(pathnames, outFilename, callback) {
652
678
  const abs = (0, import_node_path2.resolve)(pathname, filename);
653
679
  if (abs === absOutFile) return;
654
680
  if (isKnownFileType(filename)) {
655
- scheduleBuild();
681
+ const rel = (0, import_node_path2.relative)(process.cwd(), abs);
682
+ scheduleBuild(rel || abs);
656
683
  }
657
684
  });
658
685
  watchers.push(watcher);
@@ -911,6 +938,7 @@ function readFormatSource(format) {
911
938
 
912
939
  // src/loader.ts
913
940
  var import_node_path5 = require("path");
941
+ var import_node_fs4 = require("fs");
914
942
 
915
943
  // src/lexer.ts
916
944
  var EOF = -1;
@@ -1434,54 +1462,182 @@ function loadInlineSources(story, sources, opts, diagnostics) {
1434
1462
  }
1435
1463
  }
1436
1464
  }
1437
- function loadTwee(story, filename, opts, diagnostics) {
1465
+ function parseTweeFile(filename, opts) {
1438
1466
  const source = readUTF8(filename);
1439
1467
  const result = parseTwee(source, {
1440
1468
  filename,
1441
1469
  trim: opts.trim ?? true,
1442
1470
  twee2Compat: opts.twee2Compat ?? false
1443
1471
  });
1444
- diagnostics.push(...result.diagnostics);
1445
- for (const p of result.passages) {
1446
- storyAdd(story, p, diagnostics);
1447
- }
1472
+ return { passages: result.passages, diagnostics: [...result.diagnostics] };
1448
1473
  }
1449
- function loadTagged(story, tag, filename, diagnostics) {
1474
+ function parseTaggedFile(tag, filename) {
1450
1475
  const source = readUTF8(filename);
1451
- storyAdd(story, { name: (0, import_node_path5.basename)(filename), tags: [tag], text: source }, diagnostics);
1476
+ return { passages: [{ name: (0, import_node_path5.basename)(filename), tags: [tag], text: source }], diagnostics: [] };
1452
1477
  }
1453
- function loadMedia(story, tag, filename, diagnostics) {
1478
+ function parseMediaFile(tag, filename) {
1454
1479
  const source = readBase64(filename);
1455
1480
  const name = baseNameWithoutExt(filename);
1456
- storyAdd(
1457
- story,
1458
- {
1459
- name,
1460
- tags: [tag],
1461
- text: `data:${mediaTypeFromFilename(filename)};base64,${source}`
1462
- },
1463
- diagnostics
1464
- );
1481
+ return {
1482
+ passages: [{ name, tags: [tag], text: `data:${mediaTypeFromFilename(filename)};base64,${source}` }],
1483
+ diagnostics: []
1484
+ };
1465
1485
  }
1466
- function loadFont(story, filename, diagnostics) {
1486
+ function parseFontFile(filename) {
1467
1487
  const source = readBase64(filename);
1468
1488
  const name = (0, import_node_path5.basename)(filename);
1469
1489
  const family = baseNameWithoutExt(filename);
1470
1490
  const ext = normalizedFileExt(filename);
1471
1491
  const mediaType = mediaTypeFromExt(ext);
1472
1492
  const hint = fontFormatHint(ext);
1473
- storyAdd(
1474
- story,
1475
- {
1476
- name,
1477
- tags: ["stylesheet"],
1478
- text: `@font-face {
1493
+ return {
1494
+ passages: [
1495
+ {
1496
+ name,
1497
+ tags: ["stylesheet"],
1498
+ text: `@font-face {
1479
1499
  font-family: "${family}";
1480
1500
  src: url("data:${mediaType};base64,${source}") format("${hint}");
1481
1501
  }`
1482
- },
1483
- diagnostics
1484
- );
1502
+ }
1503
+ ],
1504
+ diagnostics: []
1505
+ };
1506
+ }
1507
+ function parseFile(filename, opts) {
1508
+ const ext = normalizedFileExt(filename);
1509
+ switch (ext) {
1510
+ case "tw":
1511
+ case "twee":
1512
+ return parseTweeFile(filename, opts);
1513
+ case "tw2":
1514
+ case "twee2":
1515
+ return parseTweeFile(filename, { ...opts, twee2Compat: true });
1516
+ case "css":
1517
+ return parseTaggedFile("stylesheet", filename);
1518
+ case "js":
1519
+ return parseTaggedFile("script", filename);
1520
+ case "otf":
1521
+ case "ttf":
1522
+ case "woff":
1523
+ case "woff2":
1524
+ return parseFontFile(filename);
1525
+ case "gif":
1526
+ case "jpeg":
1527
+ case "jpg":
1528
+ case "png":
1529
+ case "svg":
1530
+ case "tif":
1531
+ case "tiff":
1532
+ case "webp":
1533
+ return parseMediaFile("Twine.image", filename);
1534
+ case "aac":
1535
+ case "flac":
1536
+ case "m4a":
1537
+ case "mp3":
1538
+ case "oga":
1539
+ case "ogg":
1540
+ case "opus":
1541
+ case "wav":
1542
+ case "wave":
1543
+ case "weba":
1544
+ return parseMediaFile("Twine.audio", filename);
1545
+ case "mp4":
1546
+ case "ogv":
1547
+ case "webm":
1548
+ return parseMediaFile("Twine.video", filename);
1549
+ case "vtt":
1550
+ return parseMediaFile("Twine.vtt", filename);
1551
+ default:
1552
+ return void 0;
1553
+ }
1554
+ }
1555
+ function loadTwee(story, filename, opts, diagnostics) {
1556
+ const result = parseTweeFile(filename, opts);
1557
+ diagnostics.push(...result.diagnostics);
1558
+ for (const p of result.passages) {
1559
+ storyAdd(story, p, diagnostics);
1560
+ }
1561
+ }
1562
+ function loadTagged(story, tag, filename, diagnostics) {
1563
+ const result = parseTaggedFile(tag, filename);
1564
+ for (const p of result.passages) {
1565
+ storyAdd(story, p, diagnostics);
1566
+ }
1567
+ }
1568
+ function loadMedia(story, tag, filename, diagnostics) {
1569
+ const result = parseMediaFile(tag, filename);
1570
+ for (const p of result.passages) {
1571
+ storyAdd(story, p, diagnostics);
1572
+ }
1573
+ }
1574
+ function loadFont(story, filename, diagnostics) {
1575
+ const result = parseFontFile(filename);
1576
+ for (const p of result.passages) {
1577
+ storyAdd(story, p, diagnostics);
1578
+ }
1579
+ }
1580
+ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles, cache, changedFiles) {
1581
+ const currentFiles = /* @__PURE__ */ new Set();
1582
+ for (const filename of filenames) {
1583
+ if (processedFiles.has(filename)) {
1584
+ diagnostics.push({ level: "warning", message: `load ${filename}: Skipping duplicate.` });
1585
+ continue;
1586
+ }
1587
+ currentFiles.add(filename);
1588
+ const cached = cache.get(filename);
1589
+ if (changedFiles && cached && !changedFiles.has(filename)) {
1590
+ diagnostics.push(...cached.diagnostics);
1591
+ for (const p of cached.passages) {
1592
+ storyAdd(story, p, diagnostics);
1593
+ }
1594
+ processedFiles.add(filename);
1595
+ continue;
1596
+ }
1597
+ let mtimeMs;
1598
+ try {
1599
+ mtimeMs = (0, import_node_fs4.statSync)(filename).mtimeMs;
1600
+ } catch {
1601
+ continue;
1602
+ }
1603
+ if (cached && cached.mtimeMs === mtimeMs) {
1604
+ diagnostics.push(...cached.diagnostics);
1605
+ for (const p of cached.passages) {
1606
+ storyAdd(story, p, diagnostics);
1607
+ }
1608
+ processedFiles.add(filename);
1609
+ continue;
1610
+ }
1611
+ try {
1612
+ const result = parseFile(filename, opts);
1613
+ if (!result) continue;
1614
+ cache.set(filename, {
1615
+ mtimeMs,
1616
+ passages: result.passages,
1617
+ diagnostics: result.diagnostics
1618
+ });
1619
+ diagnostics.push(...result.diagnostics);
1620
+ for (const p of result.passages) {
1621
+ storyAdd(story, p, diagnostics);
1622
+ }
1623
+ } catch (e) {
1624
+ diagnostics.push({
1625
+ level: "error",
1626
+ message: `load ${filename}: ${e instanceof Error ? e.message : String(e)}`,
1627
+ file: filename
1628
+ });
1629
+ continue;
1630
+ }
1631
+ processedFiles.add(filename);
1632
+ }
1633
+ for (const key of cache.keys()) {
1634
+ if (!currentFiles.has(key)) {
1635
+ cache.delete(key);
1636
+ }
1637
+ }
1638
+ if (story.name !== "" && !story.passages.some((p) => p.name === "StoryTitle")) {
1639
+ storyPrepend(story, { name: "StoryTitle", tags: [], text: story.name }, diagnostics);
1640
+ }
1485
1641
  }
1486
1642
 
1487
1643
  // src/version.ts
@@ -1591,7 +1747,7 @@ function ensureIFID(story, diagnostics) {
1591
1747
  }
1592
1748
 
1593
1749
  // src/output-twine1.ts
1594
- var import_node_fs4 = require("fs");
1750
+ var import_node_fs5 = require("fs");
1595
1751
  var import_node_path6 = require("path");
1596
1752
  var CREATOR_NAME2 = "twee-ts";
1597
1753
  function toTwine1Archive(story, _startName) {
@@ -1624,7 +1780,7 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
1624
1780
  } else {
1625
1781
  let footer;
1626
1782
  try {
1627
- footer = (0, import_node_fs4.readFileSync)((0, import_node_path6.join)(formatDir, "footer.html"), "utf-8");
1783
+ footer = (0, import_node_fs5.readFileSync)((0, import_node_path6.join)(formatDir, "footer.html"), "utf-8");
1628
1784
  } catch {
1629
1785
  footer = "</div>\n</body>\n</html>\n";
1630
1786
  }
@@ -1654,7 +1810,7 @@ function getTwine1PassageChunk(story) {
1654
1810
  function tryReplaceComponent(template, placeholder, componentPath, required) {
1655
1811
  if (!template.includes(placeholder)) return template;
1656
1812
  try {
1657
- let content = (0, import_node_fs4.readFileSync)(componentPath, "utf-8");
1813
+ let content = (0, import_node_fs5.readFileSync)(componentPath, "utf-8");
1658
1814
  if (content.charCodeAt(0) === 65279) content = content.slice(1);
1659
1815
  return template.replace(placeholder, content);
1660
1816
  } catch (e) {
@@ -1750,7 +1906,7 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
1750
1906
  }
1751
1907
 
1752
1908
  // src/remote-formats.ts
1753
- var import_node_fs5 = require("fs");
1909
+ var import_node_fs6 = require("fs");
1754
1910
  var import_node_path7 = require("path");
1755
1911
  var import_node_os2 = require("os");
1756
1912
  var DEFAULT_SFA_INDICES = [
@@ -1851,9 +2007,9 @@ async function fetchAndCacheFormat(entry, downloadUrl) {
1851
2007
  }
1852
2008
  const cacheDir = getCacheDir();
1853
2009
  const formatDir = (0, import_node_path7.join)(cacheDir, entry.name, entry.version);
1854
- (0, import_node_fs5.mkdirSync)(formatDir, { recursive: true });
2010
+ (0, import_node_fs6.mkdirSync)(formatDir, { recursive: true });
1855
2011
  const formatPath = (0, import_node_path7.join)(formatDir, "format.js");
1856
- (0, import_node_fs5.writeFileSync)(formatPath, text, "utf-8");
2012
+ (0, import_node_fs6.writeFileSync)(formatPath, text, "utf-8");
1857
2013
  return {
1858
2014
  id,
1859
2015
  filename: formatPath,
@@ -1877,9 +2033,9 @@ async function fetchDirectFormat(url) {
1877
2033
  const id = makeFormatId(data.name, data.version);
1878
2034
  const cacheDir = getCacheDir();
1879
2035
  const formatDir = (0, import_node_path7.join)(cacheDir, data.name, data.version);
1880
- (0, import_node_fs5.mkdirSync)(formatDir, { recursive: true });
2036
+ (0, import_node_fs6.mkdirSync)(formatDir, { recursive: true });
1881
2037
  const formatPath = (0, import_node_path7.join)(formatDir, "format.js");
1882
- (0, import_node_fs5.writeFileSync)(formatPath, text, "utf-8");
2038
+ (0, import_node_fs6.writeFileSync)(formatPath, text, "utf-8");
1883
2039
  return {
1884
2040
  id,
1885
2041
  filename: formatPath,
@@ -1928,8 +2084,8 @@ async function resolveRemoteFormat(name, version, indices, urls) {
1928
2084
  function getCachedFormat(name, version) {
1929
2085
  const formatPath = (0, import_node_path7.join)(getCacheDir(), name, version, "format.js");
1930
2086
  try {
1931
- if (!(0, import_node_fs5.existsSync)(formatPath)) return void 0;
1932
- const source = (0, import_node_fs5.readFileSync)(formatPath, "utf-8");
2087
+ if (!(0, import_node_fs6.existsSync)(formatPath)) return void 0;
2088
+ const source = (0, import_node_fs6.readFileSync)(formatPath, "utf-8");
1933
2089
  const id = makeFormatId(name, version);
1934
2090
  const data = parseFormatJSON(source, id);
1935
2091
  if (!data) return void 0;
@@ -1949,26 +2105,26 @@ function discoverCachedFormats() {
1949
2105
  const formats = /* @__PURE__ */ new Map();
1950
2106
  const cacheDir = getCacheDir();
1951
2107
  try {
1952
- if (!(0, import_node_fs5.existsSync)(cacheDir)) return formats;
2108
+ if (!(0, import_node_fs6.existsSync)(cacheDir)) return formats;
1953
2109
  } catch {
1954
2110
  return formats;
1955
2111
  }
1956
2112
  let names;
1957
2113
  try {
1958
- names = (0, import_node_fs5.readdirSync)(cacheDir);
2114
+ names = (0, import_node_fs6.readdirSync)(cacheDir);
1959
2115
  } catch {
1960
2116
  return formats;
1961
2117
  }
1962
2118
  for (const name of names) {
1963
2119
  const nameDir = (0, import_node_path7.join)(cacheDir, name);
1964
2120
  try {
1965
- if (!(0, import_node_fs5.statSync)(nameDir).isDirectory()) continue;
2121
+ if (!(0, import_node_fs6.statSync)(nameDir).isDirectory()) continue;
1966
2122
  } catch {
1967
2123
  continue;
1968
2124
  }
1969
2125
  let versions;
1970
2126
  try {
1971
- versions = (0, import_node_fs5.readdirSync)(nameDir);
2127
+ versions = (0, import_node_fs6.readdirSync)(nameDir);
1972
2128
  } catch {
1973
2129
  continue;
1974
2130
  }
@@ -1998,17 +2154,18 @@ async function compile(options) {
1998
2154
  }
1999
2155
  async function compileToFile(options) {
2000
2156
  const result = await compile(options);
2001
- (0, import_node_fs6.writeFileSync)(options.outFile, result.output, "utf-8");
2157
+ (0, import_node_fs7.writeFileSync)(options.outFile, result.output, "utf-8");
2002
2158
  return result;
2003
2159
  }
2004
2160
  async function watch(options) {
2005
2161
  const controller = new AbortController();
2162
+ const cache = /* @__PURE__ */ new Map();
2006
2163
  const filePaths = options.sources.filter((s) => typeof s === "string");
2007
2164
  const modulePaths = options.modules ?? [];
2008
2165
  const allPaths = [...filePaths, ...modulePaths];
2009
- const handle = watchFilesystem(allPaths, options.outFile, () => {
2010
- buildOutput(options).then((result) => {
2011
- (0, import_node_fs6.writeFileSync)(options.outFile, result.output, "utf-8");
2166
+ const handle = watchFilesystem(allPaths, options.outFile, (changedFiles) => {
2167
+ buildOutput(options, cache, changedFiles).then((result) => {
2168
+ (0, import_node_fs7.writeFileSync)(options.outFile, result.output, "utf-8");
2012
2169
  options.onBuild?.(result);
2013
2170
  }).catch((e) => {
2014
2171
  options.onError?.(e instanceof Error ? e : new Error(String(e)));
@@ -2017,7 +2174,7 @@ async function watch(options) {
2017
2174
  controller.signal.addEventListener("abort", () => handle.close());
2018
2175
  return controller;
2019
2176
  }
2020
- async function buildOutput(options) {
2177
+ async function buildOutput(options, cache, changedFiles) {
2021
2178
  const diagnostics = [];
2022
2179
  const outputMode = options.outputMode ?? "html";
2023
2180
  const trim = options.trim ?? true;
@@ -2038,7 +2195,11 @@ async function buildOutput(options) {
2038
2195
  const sourceFilenames = getFilenames(filePaths);
2039
2196
  const story = createStory();
2040
2197
  const processedFiles = /* @__PURE__ */ new Set();
2041
- loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
2198
+ if (cache) {
2199
+ loadSourcesCached(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles, cache, changedFiles);
2200
+ } else {
2201
+ loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
2202
+ }
2042
2203
  loadInlineSources(story, inlineSources, { trim, twee2Compat }, diagnostics);
2043
2204
  if (options.tagAliases) {
2044
2205
  story.passages = applyTagAliases(story.passages, options.tagAliases);
@@ -2278,18 +2439,81 @@ function storyInspect(story) {
2278
2439
  };
2279
2440
  }
2280
2441
 
2442
+ // src/lint.ts
2443
+ async function lint(options) {
2444
+ const result = await compile({ ...options, outputMode: "json" });
2445
+ const map = storyInspect(result.story);
2446
+ return {
2447
+ diagnostics: result.diagnostics,
2448
+ stats: result.stats,
2449
+ formatName: result.story.twine2.format,
2450
+ formatVersion: result.story.twine2.formatVersion,
2451
+ start: map.start,
2452
+ passages: map.passages.length,
2453
+ storyPassages: map.storyPassages.length,
2454
+ infoPassages: map.infoPassages.length,
2455
+ brokenLinks: map.brokenLinks,
2456
+ deadEnds: map.deadEnds,
2457
+ orphans: map.orphans
2458
+ };
2459
+ }
2460
+ function formatLintReport(result) {
2461
+ const lines = [];
2462
+ const formatStr = result.formatName ? `${result.formatName}${result.formatVersion ? " " + result.formatVersion : ""}` : "unknown";
2463
+ lines.push(`Format: ${formatStr}`);
2464
+ lines.push(
2465
+ `Passages: ${result.passages} total (${result.storyPassages} story, ${result.infoPassages} info), ${result.stats.words.toLocaleString()} words, ${result.stats.files.length} files`
2466
+ );
2467
+ lines.push(`Start: ${result.start}`);
2468
+ if (result.brokenLinks.length > 0) {
2469
+ lines.push("");
2470
+ lines.push(`Broken links (${result.brokenLinks.length}):`);
2471
+ for (const link of result.brokenLinks) {
2472
+ lines.push(` ${link.from} -> ${link.to} (passage "${link.to}" does not exist)`);
2473
+ }
2474
+ }
2475
+ if (result.deadEnds.length > 0) {
2476
+ lines.push("");
2477
+ lines.push(`Dead ends (${result.deadEnds.length}): ${result.deadEnds.join(", ")}`);
2478
+ }
2479
+ if (result.orphans.length > 0) {
2480
+ lines.push("");
2481
+ lines.push(`Orphans (${result.orphans.length}): ${result.orphans.join(", ")}`);
2482
+ }
2483
+ const errors = result.diagnostics.filter((d) => d.level === "error");
2484
+ const warnings = result.diagnostics.filter((d) => d.level === "warning");
2485
+ if (errors.length > 0 || warnings.length > 0) {
2486
+ lines.push("");
2487
+ lines.push(`Diagnostics: ${errors.length} error(s), ${warnings.length} warning(s)`);
2488
+ for (const d of errors) {
2489
+ lines.push(` error: ${d.message}`);
2490
+ }
2491
+ for (const d of warnings) {
2492
+ lines.push(` warning: ${d.message}`);
2493
+ }
2494
+ }
2495
+ lines.push("");
2496
+ const hasErrors = errors.length > 0 || result.brokenLinks.length > 0;
2497
+ if (hasErrors) {
2498
+ lines.push("Lint failed.");
2499
+ } else {
2500
+ lines.push("Lint passed.");
2501
+ }
2502
+ return lines.join("\n");
2503
+ }
2504
+
2281
2505
  // src/config.ts
2282
- var import_node_fs7 = require("fs");
2506
+ var import_node_fs8 = require("fs");
2283
2507
  var import_node_path8 = require("path");
2284
2508
  var CONFIG_FILENAME = "twee-ts.config.json";
2285
2509
  var VALID_OUTPUT_MODES = ["html", "twee3", "twee1", "twine2-archive", "twine1-archive", "json"];
2286
2510
  function loadConfig(dir) {
2287
2511
  const base = dir ?? process.cwd();
2288
2512
  const configPath = (0, import_node_path8.join)(base, CONFIG_FILENAME);
2289
- if (!(0, import_node_fs7.existsSync)(configPath)) return null;
2513
+ if (!(0, import_node_fs8.existsSync)(configPath)) return null;
2290
2514
  let raw;
2291
2515
  try {
2292
- raw = (0, import_node_fs7.readFileSync)(configPath, "utf-8");
2516
+ raw = (0, import_node_fs8.readFileSync)(configPath, "utf-8");
2293
2517
  } catch {
2294
2518
  return null;
2295
2519
  }
@@ -2309,7 +2533,7 @@ function loadConfig(dir) {
2309
2533
  function loadConfigFile(filePath) {
2310
2534
  let raw;
2311
2535
  try {
2312
- raw = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
2536
+ raw = (0, import_node_fs8.readFileSync)(filePath, "utf-8");
2313
2537
  } catch (e) {
2314
2538
  throw new Error(`Cannot read config file ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
2315
2539
  }
@@ -2390,9 +2614,11 @@ function scaffoldConfig() {
2390
2614
  discoverFormats,
2391
2615
  fetchAndCacheFormat,
2392
2616
  fetchDirectFormat,
2617
+ formatLintReport,
2393
2618
  generateIFID,
2394
2619
  getCacheDir,
2395
2620
  getFormatSearchDirs,
2621
+ lint,
2396
2622
  loadConfig,
2397
2623
  loadConfigFile,
2398
2624
  parseFormatJSON,