@rohal12/twee-ts 1.18.1 → 1.18.2

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.
@@ -1,4 +1,4 @@
1
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
1
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import * as nodePath from "node:path";
3
3
  import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
4
4
  import { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -1344,6 +1344,89 @@ function stripSetupFunction(chunk) {
1344
1344
  for (const match of chunk.matchAll(SETUP_FUNCTION_PROPERTY)) lastIndex = match.index;
1345
1345
  return lastIndex === -1 ? void 0 : chunk.slice(0, lastIndex) + "}";
1346
1346
  }
1347
+ /** Index after the whitespace and `//` or block comments starting at `i`. */
1348
+ function skipTrivia(text, i) {
1349
+ let pos = i;
1350
+ for (;;) {
1351
+ const ch = text[pos];
1352
+ if (ch !== void 0 && /\s/.test(ch)) pos++;
1353
+ else if (text.startsWith("//", pos)) {
1354
+ const newline = text.indexOf("\n", pos);
1355
+ pos = newline === -1 ? text.length : newline + 1;
1356
+ } else if (text.startsWith("/*", pos)) {
1357
+ const close = text.indexOf("*/", pos + 2);
1358
+ pos = close === -1 ? text.length : close + 2;
1359
+ } else return pos;
1360
+ }
1361
+ }
1362
+ /** Index after the quoted string starting at `i` (a quote character); template literals nest `${…}`. */
1363
+ function skipString(text, i) {
1364
+ const quote = text[i];
1365
+ let pos = i + 1;
1366
+ while (pos < text.length) {
1367
+ const ch = text[pos];
1368
+ if (ch === "\\") pos += 2;
1369
+ else if (ch === quote) return pos + 1;
1370
+ else if (quote === "`" && ch === "$" && text[pos + 1] === "{") {
1371
+ const close = findClosingBrace(text, pos + 1);
1372
+ if (close === -1) return text.length;
1373
+ pos = close + 1;
1374
+ } else pos++;
1375
+ }
1376
+ return text.length;
1377
+ }
1378
+ function startsComment(text, pos) {
1379
+ return text[pos] === "/" && (text[pos + 1] === "/" || text[pos + 1] === "*");
1380
+ }
1381
+ function startsString(text, pos) {
1382
+ const ch = text[pos];
1383
+ return ch === "\"" || ch === "'" || ch === "`";
1384
+ }
1385
+ /** Index of the `}` matching the `{` at `open`, skipping comments and strings; -1 when unbalanced. */
1386
+ function findClosingBrace(text, open) {
1387
+ let depth = 0;
1388
+ let pos = open;
1389
+ while (pos < text.length) if (startsComment(text, pos)) pos = skipTrivia(text, pos);
1390
+ else if (startsString(text, pos)) pos = skipString(text, pos);
1391
+ else {
1392
+ if (text[pos] === "{") depth++;
1393
+ else if (text[pos] === "}" && --depth === 0) return pos;
1394
+ pos++;
1395
+ }
1396
+ return -1;
1397
+ }
1398
+ const STORY_FORMAT = "storyFormat";
1399
+ const IDENTIFIER_CHAR = /[\w$]/;
1400
+ /**
1401
+ * Locate the object literal passed to `storyFormat(` as `[start, end)` offsets, ignoring comments and
1402
+ * strings outside it and never evaluating anything. Without such a call, the first `{` outside comments
1403
+ * and strings starts the object. When its closing brace cannot be matched (a Harlowe setup function can
1404
+ * hold regular expression literals), the last `}` of the file ends it.
1405
+ */
1406
+ function locateFormatObject(text) {
1407
+ let start = -1;
1408
+ let firstBrace = -1;
1409
+ let pos = 0;
1410
+ while (pos < text.length && start === -1) if (startsComment(text, pos)) pos = skipTrivia(text, pos);
1411
+ else if (startsString(text, pos)) pos = skipString(text, pos);
1412
+ else if (text[pos] === "{") {
1413
+ if (firstBrace === -1) firstBrace = pos;
1414
+ pos++;
1415
+ } else if (text.startsWith(STORY_FORMAT, pos) && !IDENTIFIER_CHAR.test(text[pos - 1] ?? "")) {
1416
+ const paren = skipTrivia(text, pos + 11);
1417
+ const brace = text[paren] === "(" ? skipTrivia(text, paren + 1) : -1;
1418
+ if (text[brace] === "{") start = brace;
1419
+ pos += 11;
1420
+ } else pos++;
1421
+ if (start === -1) start = firstBrace;
1422
+ if (start === -1) return void 0;
1423
+ const close = findClosingBrace(text, start);
1424
+ const end = close === -1 ? text.lastIndexOf("}") : close;
1425
+ return end < start ? void 0 : {
1426
+ start,
1427
+ end: end + 1
1428
+ };
1429
+ }
1347
1430
  /**
1348
1431
  * Parse the format object at `start`–`end` of `text`: strict JSON first (fast), then the
1349
1432
  * JavaScript literal subset formats use. Error positions point into `text`.
@@ -1414,15 +1497,14 @@ function toFormatJSON(raw) {
1414
1497
  * `harlowe-3` directory or a direct download.
1415
1498
  */
1416
1499
  function decodeFormatJSON(source) {
1417
- const first = source.indexOf("{");
1418
- const last = source.lastIndexOf("}");
1419
- if (first === -1 || last < first) return {
1500
+ const located = locateFormatObject(source);
1501
+ if (located === void 0) return {
1420
1502
  ok: false,
1421
1503
  reason: "Could not find Twine 2 style story format JSON chunk."
1422
1504
  };
1423
- const parsed = parseFormatObject(source, first, last + 1);
1505
+ const parsed = parseFormatObject(source, located.start, located.end);
1424
1506
  if (parsed.ok) return toFormatJSON(parsed.value);
1425
- const stripped = stripSetupFunction(source.slice(first, last + 1));
1507
+ const stripped = stripSetupFunction(source.slice(located.start, located.end));
1426
1508
  const retried = stripped === void 0 ? void 0 : parseFormatObject(stripped, 0, stripped.length);
1427
1509
  if (retried?.ok) return toFormatJSON(retried.value);
1428
1510
  return {
@@ -1709,16 +1791,17 @@ function readFormatSource(format, diagnostics) {
1709
1791
  /**
1710
1792
  * Writes `data` to `path` so that a reader sees either the previous file or the complete new
1711
1793
  * one, never a partly written file. The data goes to a temporary file in the same folder,
1712
- * which is then renamed over `path`. When `path` is a symlink, the file it points to is
1713
- * replaced, and an existing file keeps its permissions.
1794
+ * which is then renamed over `path`. When `path` is a symlink (even a dangling one, or a chain of links), the file it
1795
+ * finally points to is replaced or created and the link stays, and an existing file keeps its permissions.
1714
1796
  *
1715
1797
  * On failure (a full disk, a missing folder) the temporary file is removed, `path` is left
1716
1798
  * as it was, and the error is thrown with `path` in its message and its original `code`.
1717
1799
  */
1718
1800
  function writeFileAtomic(path, data) {
1719
- const target = followSymlink(path);
1720
- const temp = join(dirname(target), `.${basename(target)}.${process.pid}-${randomBytes(6).toString("hex")}.tmp`);
1801
+ let temp;
1721
1802
  try {
1803
+ const target = resolveWriteTarget(path);
1804
+ temp = join(dirname(target), `.${basename(target)}.${process.pid}-${randomBytes(6).toString("hex")}.tmp`);
1722
1805
  writeFileSync(temp, data, {
1723
1806
  encoding: "utf-8",
1724
1807
  flag: "wx"
@@ -1726,17 +1809,25 @@ function writeFileAtomic(path, data) {
1726
1809
  keepPermissions(target, temp);
1727
1810
  renameSync(temp, target);
1728
1811
  } catch (e) {
1729
- rmSync(temp, { force: true });
1812
+ if (temp !== void 0) rmSync(temp, { force: true });
1730
1813
  throw writeError(path, e);
1731
1814
  }
1732
1815
  }
1733
- /** The file a symlink at `path` points to, or `path` itself (also when it doesn't exist yet). */
1734
- function followSymlink(path) {
1735
- try {
1736
- return realpathSync(path);
1737
- } catch {
1738
- return path;
1816
+ /** Links followed before a path counts as a cycle (Linux stops at 40 as well). */
1817
+ const MAX_LINK_DEPTH = 40;
1818
+ /**
1819
+ * The file a symlink at `path` finally points to, or `path` itself when it is no link. Links are
1820
+ * followed one by one, so a link whose target does not exist yet (absolute, relative, or a chain
1821
+ * of them) still resolves to the file that should be created. A cycle or an unreadable link throws.
1822
+ */
1823
+ function resolveWriteTarget(path) {
1824
+ let current = path;
1825
+ for (let depth = 0; depth <= MAX_LINK_DEPTH; depth++) {
1826
+ const stat = lstatSync(current, { throwIfNoEntry: false });
1827
+ if (stat === void 0 || !stat.isSymbolicLink()) return current;
1828
+ current = resolve(dirname(current), readlinkSync(current));
1739
1829
  }
1830
+ throw Object.assign(/* @__PURE__ */ new Error(`ELOOP: too many levels of symbolic links, ${path}`), { code: "ELOOP" });
1740
1831
  }
1741
1832
  /**
1742
1833
  * Gives `temp` the permission bits of `target`, when `target` exists. Best effort: on a file
@@ -2078,6 +2169,16 @@ async function resolveFormatUrls(request, urls, options = {}) {
2078
2169
  }
2079
2170
  if (lastError) throw lastError;
2080
2171
  }
2172
+ /**
2173
+ * Find the best format among the cached copies of direct format URLs, without network access.
2174
+ * Matching follows {@link selectFormatCandidate}, so `allowOlder` admits a same-major older version.
2175
+ * Each URL keeps its own copy; the shared name and version downloads are never consulted.
2176
+ *
2177
+ * Internal, for format resolution; not part of the public API.
2178
+ */
2179
+ function findCachedUrlFormat(request, urls, options = {}) {
2180
+ return selectFormatCandidate(request, urls.flatMap((url) => getCachedDirectFormat(url) ?? []), (f) => f, options);
2181
+ }
2081
2182
  /** Whether `info` is a format the request accepts. */
2082
2183
  function answers(request, info) {
2083
2184
  return selectFormatCandidate(request, [info], (f) => f) !== void 0;
@@ -2213,8 +2314,8 @@ function findLocalFormat(all, pruned, request) {
2213
2314
  * then format indices (unless `noRemote`). Names and IDs match the same way in each (see
2214
2315
  * `selectFormatCandidate`), so a request that a local format answers never reaches the cache or the
2215
2316
  * network. Local formats that cannot be used are reported as warnings. When a StoryData request
2216
- * names a version that none of them has, an older version of
2217
- * the same format and major version is used with a warning. Anything else is an error diagnostic,
2317
+ * names a version that none of them has, an older version of the same format and major version
2318
+ * is used with a warning, from local formats, the project's cached format URLs, or the shared cache. Anything else is an error diagnostic,
2218
2319
  * and the result is `undefined`.
2219
2320
  */
2220
2321
  async function resolveStoryFormat(request, options, diagnostics) {
@@ -2252,7 +2353,7 @@ async function resolveStoryFormat(request, options, diagnostics) {
2252
2353
  if (remote) return remote;
2253
2354
  }
2254
2355
  if (request.kind === "name") {
2255
- const older = selectFormatCandidate(request, rankedTwine2Formats(pruned), (f) => f, { allowOlder: true }) ?? findCachedFormat(request, { allowOlder: true });
2356
+ const older = selectFormatCandidate(request, rankedTwine2Formats(pruned), (f) => f, { allowOlder: true }) ?? findCachedUrlFormat(request, options.formatUrls ?? [], { allowOlder: true }) ?? findCachedFormat(request, { allowOlder: true });
2256
2357
  if (older) {
2257
2358
  diagnostics.push({
2258
2359
  level: "warning",
@@ -6100,8 +6201,23 @@ const isLetter = (c) => c !== void 0 && /^[A-Za-z]$/.test(c);
6100
6201
  * a full HTML tokenizer; it needs no more than the tags and where they end.
6101
6202
  */
6102
6203
  function scanHeadTags(html) {
6204
+ const { closingHead, bodyStart } = scanTags(html);
6205
+ return {
6206
+ closingHead,
6207
+ bodyStart
6208
+ };
6209
+ }
6210
+ /**
6211
+ * The offset just after the first real head start tag, where content goes to come first in the head; found like
6212
+ * the tags of `scanHeadTags()`, so not one in a comment, a script or an attribute value. `undefined` without one.
6213
+ */
6214
+ function findHeadStartEnd(html) {
6215
+ return scanTags(html).headStartEnd;
6216
+ }
6217
+ function scanTags(html) {
6103
6218
  let closingHead;
6104
6219
  let bodyStart;
6220
+ let headStartEnd;
6105
6221
  let i = 0;
6106
6222
  while (i < html.length) {
6107
6223
  const lt = html.indexOf("<", i);
@@ -6120,13 +6236,15 @@ function scanHeadTags(html) {
6120
6236
  } else if (isLetter(next)) {
6121
6237
  const { name, end } = readTag(html, lt + 1);
6122
6238
  if (name === "body" && bodyStart === void 0) bodyStart = lt;
6239
+ if (name === "head" && headStartEnd === void 0) headStartEnd = end;
6123
6240
  i = RAW_TEXT_ELEMENTS.has(name) ? rawTextEnd(html, name, end) : end;
6124
6241
  } else i = lt + 1;
6125
6242
  if (closingHead !== void 0 && bodyStart !== void 0) break;
6126
6243
  }
6127
6244
  return {
6128
6245
  closingHead,
6129
- bodyStart
6246
+ bodyStart,
6247
+ headStartEnd
6130
6248
  };
6131
6249
  }
6132
6250
  /** Read a tag whose name starts at `from`: its lowercase name, and the offset after its `>` (or the end). */
@@ -6921,6 +7039,6 @@ function isViteConfigTemp(file) {
6921
7039
  return /\.timestamp-\d+-[0-9a-f]*\.mjs$/.test(file);
6922
7040
  }
6923
7041
  //#endregion
6924
- export { toPosix as a, splitDiagnostics as c, isExcluded as d, outputPaths as f, mediaTypeFromFilename as h, outputLocations as i, compileForOutputFile as l, walkedEntry as m, isInside as n, fatalError as o, realPathOf as p, isViteConfigTemp as r, formatDiagnostic as s, createOutputRecord as t, getFilenames as u };
7042
+ export { toPosix as a, splitDiagnostics as c, getFilenames as d, isExcluded as f, mediaTypeFromFilename as g, walkedEntry as h, outputLocations as i, compileForOutputFile as l, realPathOf as m, isInside as n, fatalError as o, outputPaths as p, isViteConfigTemp as r, formatDiagnostic as s, createOutputRecord as t, findHeadStartEnd as u };
6925
7043
 
6926
- //# sourceMappingURL=paths-CUv1qfz-.js.map
7044
+ //# sourceMappingURL=paths-BuTAg2av.js.map