@rohal12/twee-ts 1.18.1 → 2.0.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/README.md +23 -3
- package/THIRD_PARTY_NOTICES +111 -0
- package/UNLICENSE +24 -0
- package/dist/bin/twee-ts.js +689 -9335
- package/dist/bin/twee-ts.js.map +1 -1
- package/dist/diagnostic-text-BRddVulf.cjs +26463 -0
- package/dist/diagnostic-text-BRddVulf.cjs.map +1 -0
- package/dist/diagnostic-text-CebYCTPQ.js +26040 -0
- package/dist/diagnostic-text-CebYCTPQ.js.map +1 -0
- package/dist/index.cjs +86 -9026
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +192 -415
- package/dist/index.d.ts +192 -415
- package/dist/index.js +4 -9206
- package/dist/lint-VayZI_jp.js +288 -0
- package/dist/lint-VayZI_jp.js.map +1 -0
- package/dist/options-DuBkFyqi.d.cts +28 -0
- package/dist/options-nc5NpnSc.d.ts +28 -0
- package/dist/plugins/rollup.cjs +172 -0
- package/dist/plugins/rollup.cjs.map +1 -0
- package/dist/plugins/rollup.d.cts +46 -0
- package/dist/plugins/rollup.d.ts +7 -12
- package/dist/plugins/rollup.js +130 -65
- package/dist/plugins/rollup.js.map +1 -1
- package/dist/plugins/vite.cjs +1926 -0
- package/dist/plugins/vite.cjs.map +1 -0
- package/dist/plugins/vite.d.cts +15 -0
- package/dist/plugins/vite.d.ts +5 -12
- package/dist/plugins/vite.js +1771 -448
- package/dist/plugins/vite.js.map +1 -1
- package/dist/types-BvI6pg2y.d.cts +389 -0
- package/dist/types-BvI6pg2y.d.ts +389 -0
- package/dist/watch-targets-BI7jYrwa.js +486 -0
- package/dist/watch-targets-BI7jYrwa.js.map +1 -0
- package/dist/watch-targets-C0Anuw17.cjs +575 -0
- package/dist/watch-targets-C0Anuw17.cjs.map +1 -0
- package/package.json +80 -27
- package/schemas/twee-ts.config.schema.json +55 -17
- package/dist/index.js.map +0 -1
- package/dist/paths-CUv1qfz-.js +0 -6926
- package/dist/paths-CUv1qfz-.js.map +0 -1
- package/dist/types-D8Xfe0l8.d.ts +0 -71
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/*! @rohal12/twee-ts (Unlicense). Bundles third-party code under its own licence: see THIRD_PARTY_NOTICES. */
|
|
2
|
+
import { S as twine2MarkupDiagnostics, b as describeOmission, dt as isEntryPassage, ft as isInfoPassage, ht as findPassageLinks, mt as findJavaScriptPassageLinks, pt as isStoryPassage, r as compileForLint, t as formatDiagnostic, ut as hasTag, v as startPassageDiagnostics, x as passageOmission, y as storyTitleDiagnostics } from "./diagnostic-text-CebYCTPQ.js";
|
|
3
|
+
//#region src/inspect.ts
|
|
4
|
+
/**
|
|
5
|
+
* Extract the passages that passage text links to, read as SugarCube 2 reads them.
|
|
6
|
+
*
|
|
7
|
+
* Supported syntaxes:
|
|
8
|
+
* - `[[PassageName]]`
|
|
9
|
+
* - `[[Display Text|PassageName]]`
|
|
10
|
+
* - `[[Display Text->PassageName]]`
|
|
11
|
+
* - `[[PassageName<-Display Text]]`
|
|
12
|
+
* - any of those with a setter: `[[Display Text|PassageName][$x to 1]]`
|
|
13
|
+
* - `<<goto "PassageName">>` (SugarCube macro)
|
|
14
|
+
* - `<<link "Display" "PassageName">>` (SugarCube macro)
|
|
15
|
+
* - `<<button "Display" "PassageName">>` (SugarCube macro)
|
|
16
|
+
* - `<a data-passage="PassageName">` (any element SugarCube makes a link of)
|
|
17
|
+
*
|
|
18
|
+
* In link markup, the first `|`, `->` or `<-` divides the text from the passage name; image
|
|
19
|
+
* markup links to the passage of its link component (`[img[pic.png][PassageName]]`). SugarCube macro arguments may be double- or
|
|
20
|
+
* single-quoted, with backslash escapes, or bare words; a passage named by a variable or an
|
|
21
|
+
* expression is known only in play and is skipped. Links and macro calls are also read inside the
|
|
22
|
+
* quoted strings of macros' arguments, and inside the strings of `<<script>>` bodies and
|
|
23
|
+
* `<script>` elements; links and calls in comments are not read. A script passage (`isScript`) is
|
|
24
|
+
* JavaScript, so its links and calls are read only inside its strings. See `sugarcube-macros.ts`
|
|
25
|
+
* for the details and the known differences from SugarCube.
|
|
26
|
+
*/
|
|
27
|
+
function extractLinksFromText(text, isScript) {
|
|
28
|
+
const found = isScript ? findJavaScriptPassageLinks(text) : findPassageLinks(text);
|
|
29
|
+
const ordered = [
|
|
30
|
+
"markup",
|
|
31
|
+
"goto",
|
|
32
|
+
"link",
|
|
33
|
+
"button",
|
|
34
|
+
"data-passage"
|
|
35
|
+
].flatMap((via) => found.filter((link) => link.via === via).map((link) => link.passage));
|
|
36
|
+
return [...new Set(ordered)];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The passages that `p` links to. A script passage is JavaScript, read for links only in its
|
|
40
|
+
* strings. A stylesheet is CSS, which the story puts in a style element and never wikifies, so it
|
|
41
|
+
* links nowhere; a passage tagged both is a script, as Twine 2 output takes it.
|
|
42
|
+
*/
|
|
43
|
+
function passageLinks(p) {
|
|
44
|
+
if (hasTag(p, "script")) return extractLinksFromText(textAsRead(p), true);
|
|
45
|
+
if (hasTag(p, "stylesheet")) return [];
|
|
46
|
+
return extractLinksFromText(textAsRead(p), false);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether the `target` output keeps the links in `p`: always without a target, which describes
|
|
50
|
+
* every source passage. With one, a passage the output leaves out never reaches the player, so it
|
|
51
|
+
* links nowhere, except a script passage, which Twine 2 output leaves out as a passage but runs.
|
|
52
|
+
*/
|
|
53
|
+
function readsLinks(story, p, { target }) {
|
|
54
|
+
if (target === void 0) return true;
|
|
55
|
+
const omission = passageOmission(story, p, target);
|
|
56
|
+
return omission === void 0 || omission.kind === "tag" && omission.tag === "script";
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Each passage name, mapped to why the `target` output leaves that passage out, or to `undefined`
|
|
60
|
+
* when a link can reach it: always without a target, and otherwise when the output emits a
|
|
61
|
+
* passage of that name.
|
|
62
|
+
*/
|
|
63
|
+
function destinationOmissions(story, { target }) {
|
|
64
|
+
const omissions = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const p of story.passages) {
|
|
66
|
+
const omission = target === void 0 ? void 0 : passageOmission(story, p, target);
|
|
67
|
+
if (!omissions.has(p.name) || omission === void 0) omissions.set(p.name, omission);
|
|
68
|
+
}
|
|
69
|
+
return omissions;
|
|
70
|
+
}
|
|
71
|
+
/** Passages SugarCube runs from their raw text, never joining their lines. */
|
|
72
|
+
const RAW_TEXT_PASSAGES = /* @__PURE__ */ new Set([
|
|
73
|
+
"StoryInit",
|
|
74
|
+
"PassageReady",
|
|
75
|
+
"PassageDone"
|
|
76
|
+
]);
|
|
77
|
+
/**
|
|
78
|
+
* A passage's text as SugarCube reads it for its links. A passage tagged `nobr` has its line
|
|
79
|
+
* breaks joined (`Passage.processText`): leading and trailing ones removed, each run inside made
|
|
80
|
+
* one space. Script passages, `init`-tagged passages, StoryInit, PassageReady and PassageDone are
|
|
81
|
+
* run from their raw text, so they keep theirs.
|
|
82
|
+
*/
|
|
83
|
+
function textAsRead(p) {
|
|
84
|
+
return hasTag(p, "nobr") && !hasTag(p, "script") && !hasTag(p, "init") && !RAW_TEXT_PASSAGES.has(p.name) ? joinLines(p.text) : p.text;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* What SugarCube's `text.replace(/^\n+|\n+$/g, '').replace(/\n+/g, ' ')` gives, without that
|
|
88
|
+
* pattern's backtracking, which takes quadratic time on a long run of line feeds.
|
|
89
|
+
*/
|
|
90
|
+
function joinLines(text) {
|
|
91
|
+
let start = 0;
|
|
92
|
+
while (text[start] === "\n") start += 1;
|
|
93
|
+
let end = text.length;
|
|
94
|
+
while (end > start && text[end - 1] === "\n") end -= 1;
|
|
95
|
+
return text.slice(start, end).replace(/\n+/g, " ");
|
|
96
|
+
}
|
|
97
|
+
/** The passage names that `roots` and the passages their links lead to, link after link, reach. Linear. */
|
|
98
|
+
function reachable(roots, links) {
|
|
99
|
+
const reached = new Set(roots);
|
|
100
|
+
const pending = [...reached];
|
|
101
|
+
for (let name = pending.pop(); name !== void 0; name = pending.pop()) for (const target of links.get(name) ?? []) {
|
|
102
|
+
if (reached.has(target)) continue;
|
|
103
|
+
reached.add(target);
|
|
104
|
+
pending.push(target);
|
|
105
|
+
}
|
|
106
|
+
return reached;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Inspect a story and return its full structural map.
|
|
110
|
+
*
|
|
111
|
+
* Designed for story authors to use in unit tests:
|
|
112
|
+
* ```ts
|
|
113
|
+
* const result = await compile({ sources: ['./story'] });
|
|
114
|
+
* const map = storyInspect(result.story);
|
|
115
|
+
*
|
|
116
|
+
* // Assert structure
|
|
117
|
+
* expect(map.passages).toContain('Start');
|
|
118
|
+
* expect(map.tags).toContain('location');
|
|
119
|
+
* expect(map.passagesByTag.get('location')).toContain('Kitchen');
|
|
120
|
+
* expect(map.brokenLinks).toEqual([]);
|
|
121
|
+
* expect(map.deadEnds).not.toContain('Start');
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* By default, a link is broken only when no passage has its name. Pass a `target` to check links
|
|
125
|
+
* against what that output emits: `storyInspect(result.story, { target: 'twine2' })` also reports
|
|
126
|
+
* links to passages Twine 2 output leaves out, such as script, stylesheet and `Twine.private`
|
|
127
|
+
* passages. With a target, passages that output leaves out also give no links (script passages
|
|
128
|
+
* excepted, as Twine 2 output runs them), so they report no broken links and keep no passage from
|
|
129
|
+
* being an orphan.
|
|
130
|
+
*/
|
|
131
|
+
function storyInspect(story, options = {}) {
|
|
132
|
+
const destinations = destinationOmissions(story, options);
|
|
133
|
+
const passages = [];
|
|
134
|
+
const storyPassages = [];
|
|
135
|
+
const infoPassages = [];
|
|
136
|
+
const tagSet = /* @__PURE__ */ new Set();
|
|
137
|
+
const passagesByTag = /* @__PURE__ */ new Map();
|
|
138
|
+
const tagsByPassage = /* @__PURE__ */ new Map();
|
|
139
|
+
const links = /* @__PURE__ */ new Map();
|
|
140
|
+
for (const p of story.passages) {
|
|
141
|
+
passages.push(p.name);
|
|
142
|
+
if (isStoryPassage(p)) storyPassages.push(p.name);
|
|
143
|
+
if (isInfoPassage(p)) infoPassages.push(p.name);
|
|
144
|
+
tagsByPassage.set(p.name, [...p.tags]);
|
|
145
|
+
for (const tag of new Set(p.tags)) {
|
|
146
|
+
tagSet.add(tag);
|
|
147
|
+
let list = passagesByTag.get(tag);
|
|
148
|
+
if (!list) {
|
|
149
|
+
list = [];
|
|
150
|
+
passagesByTag.set(tag, list);
|
|
151
|
+
}
|
|
152
|
+
list.push(p.name);
|
|
153
|
+
}
|
|
154
|
+
links.set(p.name, readsLinks(story, p, options) ? passageLinks(p) : []);
|
|
155
|
+
}
|
|
156
|
+
const brokenLinks = [];
|
|
157
|
+
for (const [from, targets] of links) for (const to of targets) {
|
|
158
|
+
if (!destinations.has(to)) {
|
|
159
|
+
brokenLinks.push({
|
|
160
|
+
from,
|
|
161
|
+
to
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const omission = destinations.get(to);
|
|
166
|
+
if (omission !== void 0) brokenLinks.push({
|
|
167
|
+
from,
|
|
168
|
+
to,
|
|
169
|
+
omission
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const deadEnds = [];
|
|
173
|
+
for (const name of storyPassages) {
|
|
174
|
+
const targets = links.get(name);
|
|
175
|
+
if (!targets || targets.length === 0) deadEnds.push(name);
|
|
176
|
+
}
|
|
177
|
+
const start = story.twine2.start || "Start";
|
|
178
|
+
const reached = reachable([start, ...story.passages.filter(isEntryPassage).map((p) => p.name)], links);
|
|
179
|
+
const orphans = storyPassages.filter((name) => !reached.has(name));
|
|
180
|
+
return {
|
|
181
|
+
passages,
|
|
182
|
+
storyPassages,
|
|
183
|
+
infoPassages,
|
|
184
|
+
tags: [...tagSet].sort(),
|
|
185
|
+
passagesByTag,
|
|
186
|
+
tagsByPassage,
|
|
187
|
+
links,
|
|
188
|
+
brokenLinks,
|
|
189
|
+
deadEnds,
|
|
190
|
+
orphans,
|
|
191
|
+
start
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/lint.ts
|
|
196
|
+
/**
|
|
197
|
+
* Lint a story: compile without output rendering, then inspect structure.
|
|
198
|
+
* Uses JSON output mode internally to avoid format resolution, so it checks
|
|
199
|
+
* the starting passage itself, against the Twine 2 passage rules, and adds
|
|
200
|
+
* what Twine 2 HTML output reports about the story's own data. Link
|
|
201
|
+
* destinations are checked against the same rules: a link to a passage that
|
|
202
|
+
* Twine 2 output leaves out is broken, and a passage it leaves out (other than
|
|
203
|
+
* a script passage) gives no links.
|
|
204
|
+
*/
|
|
205
|
+
async function lint(options) {
|
|
206
|
+
return lintForOutputFile(options, void 0);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* lint() for a project whose builds are written to `outFile`: like a build for that file,
|
|
210
|
+
* it leaves `outFile` out of the sources and modules, so an earlier build inside a source
|
|
211
|
+
* folder is not linted as a source.
|
|
212
|
+
*
|
|
213
|
+
* Internal, for the CLI; not part of the public API.
|
|
214
|
+
*/
|
|
215
|
+
async function lintForOutputFile(options, outFile) {
|
|
216
|
+
const result = await compileForLint(options, outFile);
|
|
217
|
+
const map = storyInspect(result.story, { target: "twine2" });
|
|
218
|
+
return {
|
|
219
|
+
diagnostics: [
|
|
220
|
+
...result.diagnostics,
|
|
221
|
+
...twine2MarkupDiagnostics(result.story),
|
|
222
|
+
...startPassageDiagnostics(result.story, map.start, "twine2"),
|
|
223
|
+
...storyTitleDiagnostics(result.story, "twine2")
|
|
224
|
+
],
|
|
225
|
+
stats: result.stats,
|
|
226
|
+
formatName: result.story.twine2.format,
|
|
227
|
+
formatVersion: result.story.twine2.formatVersion,
|
|
228
|
+
start: map.start,
|
|
229
|
+
passages: map.passages.length,
|
|
230
|
+
storyPassages: map.storyPassages.length,
|
|
231
|
+
infoPassages: map.infoPassages.length,
|
|
232
|
+
brokenLinks: map.brokenLinks,
|
|
233
|
+
deadEnds: map.deadEnds,
|
|
234
|
+
orphans: map.orphans
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* `n` and a noun, plural unless `n` is 1: `1 file`, `2 files`, `1,234 words`. Digits are grouped in threes with
|
|
239
|
+
* commas whatever the system locale, so the report is the same everywhere.
|
|
240
|
+
*/
|
|
241
|
+
function counted(n, noun) {
|
|
242
|
+
const digits = String(n);
|
|
243
|
+
const groups = [];
|
|
244
|
+
for (let end = digits.length; end > 0; end -= 3) groups.unshift(digits.slice(Math.max(0, end - 3), end));
|
|
245
|
+
return `${groups.join(",")} ${noun}${n === 1 ? "" : "s"}`;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Format a lint result as a human-readable report string.
|
|
249
|
+
*/
|
|
250
|
+
function formatLintReport(result) {
|
|
251
|
+
const lines = [];
|
|
252
|
+
const formatStr = result.formatName ? `${result.formatName}${result.formatVersion ? " " + result.formatVersion : ""}` : "unknown";
|
|
253
|
+
lines.push(`Format: ${formatStr}`);
|
|
254
|
+
lines.push(`Passages: ${result.passages} total (${result.storyPassages} story, ${result.infoPassages} info), ${counted(result.stats.words, "word")}, ${counted(result.stats.files.length, "file")}`);
|
|
255
|
+
lines.push(`Start: ${result.start}`);
|
|
256
|
+
if (result.brokenLinks.length > 0) {
|
|
257
|
+
lines.push("");
|
|
258
|
+
lines.push(`Broken links (${result.brokenLinks.length}):`);
|
|
259
|
+
for (const link of result.brokenLinks) {
|
|
260
|
+
const why = link.omission === void 0 ? "does not exist" : `${describeOmission(link.omission)}, so it is left out of the story data`;
|
|
261
|
+
lines.push(` ${link.from} -> ${link.to} (passage "${link.to}" ${why})`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (result.deadEnds.length > 0) {
|
|
265
|
+
lines.push("");
|
|
266
|
+
lines.push(`Dead ends (${result.deadEnds.length}): ${result.deadEnds.join(", ")}`);
|
|
267
|
+
}
|
|
268
|
+
if (result.orphans.length > 0) {
|
|
269
|
+
lines.push("");
|
|
270
|
+
lines.push(`Orphans (${result.orphans.length}): ${result.orphans.join(", ")}`);
|
|
271
|
+
}
|
|
272
|
+
const errors = result.diagnostics.filter((d) => d.level === "error");
|
|
273
|
+
const warnings = result.diagnostics.filter((d) => d.level === "warning");
|
|
274
|
+
if (errors.length > 0 || warnings.length > 0) {
|
|
275
|
+
lines.push("");
|
|
276
|
+
lines.push(`Diagnostics: ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
277
|
+
for (const d of errors) lines.push(` error: ${formatDiagnostic(d)}`);
|
|
278
|
+
for (const d of warnings) lines.push(` warning: ${formatDiagnostic(d)}`);
|
|
279
|
+
}
|
|
280
|
+
lines.push("");
|
|
281
|
+
if (errors.length > 0 || result.brokenLinks.length > 0) lines.push("Lint failed.");
|
|
282
|
+
else lines.push("Lint passed.");
|
|
283
|
+
return lines.join("\n");
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
export { storyInspect as i, lint as n, lintForOutputFile as r, formatLintReport as t };
|
|
287
|
+
|
|
288
|
+
//# sourceMappingURL=lint-VayZI_jp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint-VayZI_jp.js","names":[],"sources":["../src/inspect.ts","../src/lint.ts"],"sourcesContent":["/**\n * Story inspection utilities for unit testing.\n *\n * Extracts structural information from a compiled story:\n * passage names, tags, link graph, broken links, dead ends, orphans.\n *\n * Usage:\n * const result = await compile({ sources: ['./story'] });\n * const map = storyInspect(result.story);\n * expect(map.passages).toContain('Start');\n * expect(map.brokenLinks).toHaveLength(0);\n */\nimport type { InspectOptions, PassageOmission, ReadonlyPassage, ReadonlyStory } from './types.js';\nimport { hasTag, isEntryPassage, isInfoPassage, isStoryPassage } from './passage.js';\nimport { passageOmission } from './passage-omission.js';\nimport { findJavaScriptPassageLinks, findPassageLinks } from './sugarcube-macros.js';\n\nexport interface StoryMap {\n /** All passage names, in source order. */\n readonly passages: readonly string[];\n /** Only story passages (excludes StoryData, StoryTitle, scripts, stylesheets, etc.). */\n readonly storyPassages: readonly string[];\n /** Only info/special passages. */\n readonly infoPassages: readonly string[];\n /** All unique tags used across the story, sorted alphabetically. */\n readonly tags: readonly string[];\n /** Map of tag → passage names that carry that tag, each listed once even if the tag is repeated. */\n readonly passagesByTag: ReadonlyMap<string, readonly string[]>;\n /** Map of passage name → tags on that passage, as written (a repeated tag appears twice). */\n readonly tagsByPassage: ReadonlyMap<string, readonly string[]>;\n /**\n * Map of passage name → passage names it links to.\n * Parses `[[target]]`, `[[display->target]]`, `[[target<-display]]`, `[[display|target]]`,\n * each with or without a setter (`[[display|target][$x to 1]]`), SugarCube's\n * `<<goto \"target\">>` / `<<link \"display\" \"target\">>` / `<<button \"display\" \"target\">>`, and\n * elements with a `data-passage=\"target\"` attribute, read as SugarCube 2 reads them\n * (see \"How links are read\" in docs/api.md). Links and calls in comments are not read. A script passage is\n * read for links only in its strings; a stylesheet passage (including a loaded `.css` file) is\n * CSS and links to nothing. With a `target` (see `InspectOptions`), a passage that output leaves\n * out links to nothing, except a script passage, which Twine 2 output runs.\n */\n readonly links: ReadonlyMap<string, readonly string[]>;\n /**\n * Broken links: `{ from, to }` pairs where `to` doesn't exist as a passage, or, with a `target`\n * (see `InspectOptions`), where that output leaves the `to` passage out (`omission` says why).\n */\n readonly brokenLinks: readonly BrokenLink[];\n /** Story passages with no outgoing links (potential dead ends). */\n readonly deadEnds: readonly string[];\n /**\n * Story passages that the player cannot reach: no chain of links leads to them from the start passage or\n * from an info passage the story format runs or shows without a link (such as StoryInit, PassageHeader, or a\n * `script` or `widget` passage). An `annotation` note, a `Twine.` passage (private or media), StoryData,\n * StorySettings and StoryIncludes are not shown, so links from them count only when a link leads to them. A\n * passage that links only to itself, or a group that links only among itself, is an orphan. Only the links\n * `links` lists count, so a passage that is shown only through a macro such as `<<include>>` is listed too.\n */\n readonly orphans: readonly string[];\n /** The configured start passage name, if any. */\n readonly start: string;\n}\n\nexport interface BrokenLink {\n readonly from: string;\n readonly to: string;\n /**\n * Set when a passage named `to` exists, but the inspected output leaves it out of the story\n * (only with a `target`): why it is left out. Absent when no passage has the name.\n */\n readonly omission?: PassageOmission;\n}\n\n/**\n * Extract the passages that passage text links to, read as SugarCube 2 reads them.\n *\n * Supported syntaxes:\n * - `[[PassageName]]`\n * - `[[Display Text|PassageName]]`\n * - `[[Display Text->PassageName]]`\n * - `[[PassageName<-Display Text]]`\n * - any of those with a setter: `[[Display Text|PassageName][$x to 1]]`\n * - `<<goto \"PassageName\">>` (SugarCube macro)\n * - `<<link \"Display\" \"PassageName\">>` (SugarCube macro)\n * - `<<button \"Display\" \"PassageName\">>` (SugarCube macro)\n * - `<a data-passage=\"PassageName\">` (any element SugarCube makes a link of)\n *\n * In link markup, the first `|`, `->` or `<-` divides the text from the passage name; image\n * markup links to the passage of its link component (`[img[pic.png][PassageName]]`). SugarCube macro arguments may be double- or\n * single-quoted, with backslash escapes, or bare words; a passage named by a variable or an\n * expression is known only in play and is skipped. Links and macro calls are also read inside the\n * quoted strings of macros' arguments, and inside the strings of `<<script>>` bodies and\n * `<script>` elements; links and calls in comments are not read. A script passage (`isScript`) is\n * JavaScript, so its links and calls are read only inside its strings. See `sugarcube-macros.ts`\n * for the details and the known differences from SugarCube.\n */\nfunction extractLinksFromText(text: string, isScript: boolean): string[] {\n const found = isScript ? findJavaScriptPassageLinks(text) : findPassageLinks(text);\n // Link markup, then gotos, then links: the order this list has always had; then buttons and elements.\n const ordered = (['markup', 'goto', 'link', 'button', 'data-passage'] as const).flatMap((via) =>\n found.filter((link) => link.via === via).map((link) => link.passage),\n );\n return [...new Set(ordered)];\n}\n\n/**\n * The passages that `p` links to. A script passage is JavaScript, read for links only in its\n * strings. A stylesheet is CSS, which the story puts in a style element and never wikifies, so it\n * links nowhere; a passage tagged both is a script, as Twine 2 output takes it.\n */\nfunction passageLinks(p: ReadonlyPassage): string[] {\n if (hasTag(p, 'script')) return extractLinksFromText(textAsRead(p), true);\n if (hasTag(p, 'stylesheet')) return [];\n return extractLinksFromText(textAsRead(p), false);\n}\n\n/**\n * Whether the `target` output keeps the links in `p`: always without a target, which describes\n * every source passage. With one, a passage the output leaves out never reaches the player, so it\n * links nowhere, except a script passage, which Twine 2 output leaves out as a passage but runs.\n */\nfunction readsLinks(story: ReadonlyStory, p: ReadonlyPassage, { target }: InspectOptions): boolean {\n if (target === undefined) return true;\n const omission = passageOmission(story, p, target);\n return omission === undefined || (omission.kind === 'tag' && omission.tag === 'script');\n}\n\n/**\n * Each passage name, mapped to why the `target` output leaves that passage out, or to `undefined`\n * when a link can reach it: always without a target, and otherwise when the output emits a\n * passage of that name.\n */\nfunction destinationOmissions(\n story: ReadonlyStory,\n { target }: InspectOptions,\n): ReadonlyMap<string, PassageOmission | undefined> {\n const omissions = new Map<string, PassageOmission | undefined>();\n for (const p of story.passages) {\n const omission = target === undefined ? undefined : passageOmission(story, p, target);\n // One emitted passage of a name is enough for a link to reach it.\n if (!omissions.has(p.name) || omission === undefined) {\n omissions.set(p.name, omission);\n }\n }\n return omissions;\n}\n\n/** Passages SugarCube runs from their raw text, never joining their lines. */\nconst RAW_TEXT_PASSAGES: ReadonlySet<string> = new Set(['StoryInit', 'PassageReady', 'PassageDone']);\n\n/**\n * A passage's text as SugarCube reads it for its links. A passage tagged `nobr` has its line\n * breaks joined (`Passage.processText`): leading and trailing ones removed, each run inside made\n * one space. Script passages, `init`-tagged passages, StoryInit, PassageReady and PassageDone are\n * run from their raw text, so they keep theirs.\n */\nfunction textAsRead(p: ReadonlyPassage): string {\n const joined = hasTag(p, 'nobr') && !hasTag(p, 'script') && !hasTag(p, 'init') && !RAW_TEXT_PASSAGES.has(p.name);\n return joined ? joinLines(p.text) : p.text;\n}\n\n/**\n * What SugarCube's `text.replace(/^\\n+|\\n+$/g, '').replace(/\\n+/g, ' ')` gives, without that\n * pattern's backtracking, which takes quadratic time on a long run of line feeds.\n */\nfunction joinLines(text: string): string {\n let start = 0;\n while (text[start] === '\\n') {\n start += 1;\n }\n let end = text.length;\n while (end > start && text[end - 1] === '\\n') {\n end -= 1;\n }\n return text.slice(start, end).replace(/\\n+/g, ' ');\n}\n\n/** The passage names that `roots` and the passages their links lead to, link after link, reach. Linear. */\nfunction reachable(roots: readonly string[], links: ReadonlyMap<string, readonly string[]>): ReadonlySet<string> {\n const reached = new Set(roots);\n const pending = [...reached];\n for (let name = pending.pop(); name !== undefined; name = pending.pop()) {\n for (const target of links.get(name) ?? []) {\n if (reached.has(target)) continue;\n reached.add(target);\n pending.push(target);\n }\n }\n return reached;\n}\n\n/**\n * Inspect a story and return its full structural map.\n *\n * Designed for story authors to use in unit tests:\n * ```ts\n * const result = await compile({ sources: ['./story'] });\n * const map = storyInspect(result.story);\n *\n * // Assert structure\n * expect(map.passages).toContain('Start');\n * expect(map.tags).toContain('location');\n * expect(map.passagesByTag.get('location')).toContain('Kitchen');\n * expect(map.brokenLinks).toEqual([]);\n * expect(map.deadEnds).not.toContain('Start');\n * ```\n *\n * By default, a link is broken only when no passage has its name. Pass a `target` to check links\n * against what that output emits: `storyInspect(result.story, { target: 'twine2' })` also reports\n * links to passages Twine 2 output leaves out, such as script, stylesheet and `Twine.private`\n * passages. With a target, passages that output leaves out also give no links (script passages\n * excepted, as Twine 2 output runs them), so they report no broken links and keep no passage from\n * being an orphan.\n */\nexport function storyInspect(story: ReadonlyStory, options: InspectOptions = {}): StoryMap {\n const destinations = destinationOmissions(story, options);\n\n const passages: string[] = [];\n const storyPassages: string[] = [];\n const infoPassages: string[] = [];\n const tagSet = new Set<string>();\n const passagesByTag = new Map<string, string[]>();\n const tagsByPassage = new Map<string, string[]>();\n const links = new Map<string, string[]>();\n\n for (const p of story.passages) {\n passages.push(p.name);\n\n if (isStoryPassage(p)) {\n storyPassages.push(p.name);\n }\n if (isInfoPassage(p)) {\n infoPassages.push(p.name);\n }\n\n // Tags\n tagsByPassage.set(p.name, [...p.tags]);\n // A tag written twice still lists the passage once.\n for (const tag of new Set(p.tags)) {\n tagSet.add(tag);\n let list = passagesByTag.get(tag);\n if (!list) {\n list = [];\n passagesByTag.set(tag, list);\n }\n list.push(p.name);\n }\n\n // Links\n links.set(p.name, readsLinks(story, p, options) ? passageLinks(p) : []);\n }\n\n // Broken links: link targets that don't exist as passages, or that the target output leaves out\n const brokenLinks: BrokenLink[] = [];\n for (const [from, targets] of links) {\n for (const to of targets) {\n if (!destinations.has(to)) {\n brokenLinks.push({ from, to });\n continue;\n }\n const omission = destinations.get(to);\n if (omission !== undefined) {\n brokenLinks.push({ from, to, omission });\n }\n }\n }\n\n // Dead ends: story passages with no outgoing links\n const deadEnds: string[] = [];\n for (const name of storyPassages) {\n const targets = links.get(name);\n if (!targets || targets.length === 0) {\n deadEnds.push(name);\n }\n }\n\n // Orphans: story passages that no chain of links reaches from the start or from a passage the format runs or\n // shows without a link. A note, private passage or story data is not one, but its links count once reached.\n const start = story.twine2.start || 'Start';\n const entries = story.passages.filter(isEntryPassage).map((p) => p.name);\n const reached = reachable([start, ...entries], links);\n const orphans = storyPassages.filter((name) => !reached.has(name));\n\n return {\n passages,\n storyPassages,\n infoPassages,\n tags: [...tagSet].sort(),\n passagesByTag,\n tagsByPassage,\n links,\n brokenLinks,\n deadEnds,\n orphans,\n start,\n };\n}\n","/**\n * Lint mode: compile + inspect story structure without producing output.\n * Surfaces broken links, dead ends, orphans, and compilation diagnostics.\n */\nimport type { CompileOptions, CompileStats, Diagnostic } from './types.js';\nimport type { BrokenLink } from './inspect.js';\nimport { compileForLint } from './compiler.js';\nimport { formatDiagnostic } from './diagnostic-text.js';\nimport { storyInspect } from './inspect.js';\nimport { describeOmission } from './passage-omission.js';\nimport { twine2MarkupDiagnostics } from './output-twine2.js';\nimport { startPassageDiagnostics, storyTitleDiagnostics } from './start-passage.js';\n\nexport interface LintResult {\n /** Compilation diagnostics (errors and warnings). */\n readonly diagnostics: readonly Diagnostic[];\n /** Compilation statistics. */\n readonly stats: CompileStats;\n /** Story format name from StoryData (e.g. \"SugarCube\"). */\n readonly formatName: string;\n /** Story format version from StoryData (e.g. \"2.37.3\"). */\n readonly formatVersion: string;\n /** The configured start passage name. */\n readonly start: string;\n /** Total passage count. */\n readonly passages: number;\n /** Story passage count (excludes StoryData, StoryTitle, scripts, etc.). */\n readonly storyPassages: number;\n /** Info/special passage count. */\n readonly infoPassages: number;\n /**\n * Broken links: link targets that don't exist as passages, or that Twine 2 output leaves out\n * (script, stylesheet and `Twine.private` passages, StoryData, StoryTitle, an empty\n * StorySettings); for those, `omission` says why.\n */\n readonly brokenLinks: readonly BrokenLink[];\n /** Story passages with no outgoing links. */\n readonly deadEnds: readonly string[];\n /**\n * Story passages the player cannot reach: no chain of links leads to them from the start passage or from\n * an info passage the story format runs or shows without a link (see `StoryMap.orphans`).\n */\n readonly orphans: readonly string[];\n}\n\n/**\n * Lint a story: compile without output rendering, then inspect structure.\n * Uses JSON output mode internally to avoid format resolution, so it checks\n * the starting passage itself, against the Twine 2 passage rules, and adds\n * what Twine 2 HTML output reports about the story's own data. Link\n * destinations are checked against the same rules: a link to a passage that\n * Twine 2 output leaves out is broken, and a passage it leaves out (other than\n * a script passage) gives no links.\n */\nexport async function lint(options: Omit<CompileOptions, 'outputMode'>): Promise<LintResult> {\n return lintForOutputFile(options, undefined);\n}\n\n/**\n * lint() for a project whose builds are written to `outFile`: like a build for that file,\n * it leaves `outFile` out of the sources and modules, so an earlier build inside a source\n * folder is not linted as a source.\n *\n * Internal, for the CLI; not part of the public API.\n */\nexport async function lintForOutputFile(\n options: Omit<CompileOptions, 'outputMode'>,\n outFile: string | undefined,\n): Promise<LintResult> {\n const result = await compileForLint(options, outFile);\n const map = storyInspect(result.story, { target: 'twine2' });\n\n return {\n diagnostics: [\n ...result.diagnostics,\n // What Twine 2 HTML output reports beyond JSON output about the story's own data.\n ...twine2MarkupDiagnostics(result.story),\n ...startPassageDiagnostics(result.story, map.start, 'twine2'),\n ...storyTitleDiagnostics(result.story, 'twine2'),\n ],\n stats: result.stats,\n formatName: result.story.twine2.format,\n formatVersion: result.story.twine2.formatVersion,\n start: map.start,\n passages: map.passages.length,\n storyPassages: map.storyPassages.length,\n infoPassages: map.infoPassages.length,\n brokenLinks: map.brokenLinks,\n deadEnds: map.deadEnds,\n orphans: map.orphans,\n };\n}\n\n/**\n * `n` and a noun, plural unless `n` is 1: `1 file`, `2 files`, `1,234 words`. Digits are grouped in threes with\n * commas whatever the system locale, so the report is the same everywhere.\n */\nfunction counted(n: number, noun: string): string {\n const digits = String(n);\n const groups: string[] = [];\n for (let end = digits.length; end > 0; end -= 3) groups.unshift(digits.slice(Math.max(0, end - 3), end));\n return `${groups.join(',')} ${noun}${n === 1 ? '' : 's'}`;\n}\n\n/**\n * Format a lint result as a human-readable report string.\n */\nexport function formatLintReport(result: LintResult): string {\n const lines: string[] = [];\n\n // Header: format and stats\n const formatStr = result.formatName\n ? `${result.formatName}${result.formatVersion ? ' ' + result.formatVersion : ''}`\n : 'unknown';\n lines.push(`Format: ${formatStr}`);\n lines.push(\n `Passages: ${result.passages} total (${result.storyPassages} story, ${result.infoPassages} info), ${counted(result.stats.words, 'word')}, ${counted(result.stats.files.length, 'file')}`,\n );\n lines.push(`Start: ${result.start}`);\n\n // Broken links (errors)\n if (result.brokenLinks.length > 0) {\n lines.push('');\n lines.push(`Broken links (${result.brokenLinks.length}):`);\n for (const link of result.brokenLinks) {\n const why =\n link.omission === undefined\n ? 'does not exist'\n : `${describeOmission(link.omission)}, so it is left out of the story data`;\n lines.push(` ${link.from} -> ${link.to} (passage \"${link.to}\" ${why})`);\n }\n }\n\n // Dead ends (warnings)\n if (result.deadEnds.length > 0) {\n lines.push('');\n lines.push(`Dead ends (${result.deadEnds.length}): ${result.deadEnds.join(', ')}`);\n }\n\n // Orphans (warnings)\n if (result.orphans.length > 0) {\n lines.push('');\n lines.push(`Orphans (${result.orphans.length}): ${result.orphans.join(', ')}`);\n }\n\n // Compilation diagnostics\n const errors = result.diagnostics.filter((d) => d.level === 'error');\n const warnings = result.diagnostics.filter((d) => d.level === 'warning');\n\n if (errors.length > 0 || warnings.length > 0) {\n lines.push('');\n lines.push(`Diagnostics: ${errors.length} error(s), ${warnings.length} warning(s)`);\n for (const d of errors) {\n lines.push(` error: ${formatDiagnostic(d)}`);\n }\n for (const d of warnings) {\n lines.push(` warning: ${formatDiagnostic(d)}`);\n }\n }\n\n // Summary line\n lines.push('');\n const hasErrors = errors.length > 0 || result.brokenLinks.length > 0;\n if (hasErrors) {\n lines.push('Lint failed.');\n } else {\n lines.push('Lint passed.');\n }\n\n return lines.join('\\n');\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
import { t as CompileOptions } from "./types-BvI6pg2y.cjs";
|
|
3
|
+
//#region src/plugins/options.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The compile options a plugin passes on to the compiler. `sources` and
|
|
6
|
+
* `formatId` come from the plugin's own `sources` and `format` options, so they
|
|
7
|
+
* are not accepted here.
|
|
8
|
+
*/
|
|
9
|
+
type PluginCompileOptions = Omit<Partial<CompileOptions>, 'sources' | 'formatId'>;
|
|
10
|
+
/** The options both plugins take. */
|
|
11
|
+
interface SharedPluginOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Source directories/files to compile, relative to the working directory. The plugins take paths only: a
|
|
14
|
+
* bundler watches files, so the inline sources `compile()` accepts are not supported here.
|
|
15
|
+
*/
|
|
16
|
+
sources: readonly string[];
|
|
17
|
+
/** Story format ID (`formatId` in the compile options). */
|
|
18
|
+
format?: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Output file name, relative to the output folder: names separated by forward
|
|
21
|
+
* slashes, without a `.` or `..` segment. Default: 'index.html'.
|
|
22
|
+
*/
|
|
23
|
+
outputFilename?: string | undefined;
|
|
24
|
+
/** Additional compile options. Set `sources` and `format` with the options above, not here. */
|
|
25
|
+
compileOptions?: PluginCompileOptions | undefined;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { SharedPluginOptions as n, PluginCompileOptions as t };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
import { t as CompileOptions } from "./types-BvI6pg2y.js";
|
|
3
|
+
//#region src/plugins/options.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The compile options a plugin passes on to the compiler. `sources` and
|
|
6
|
+
* `formatId` come from the plugin's own `sources` and `format` options, so they
|
|
7
|
+
* are not accepted here.
|
|
8
|
+
*/
|
|
9
|
+
type PluginCompileOptions = Omit<Partial<CompileOptions>, 'sources' | 'formatId'>;
|
|
10
|
+
/** The options both plugins take. */
|
|
11
|
+
interface SharedPluginOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Source directories/files to compile, relative to the working directory. The plugins take paths only: a
|
|
14
|
+
* bundler watches files, so the inline sources `compile()` accepts are not supported here.
|
|
15
|
+
*/
|
|
16
|
+
sources: readonly string[];
|
|
17
|
+
/** Story format ID (`formatId` in the compile options). */
|
|
18
|
+
format?: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Output file name, relative to the output folder: names separated by forward
|
|
21
|
+
* slashes, without a `.` or `..` segment. Default: 'index.html'.
|
|
22
|
+
*/
|
|
23
|
+
outputFilename?: string | undefined;
|
|
24
|
+
/** Additional compile options. Set `sources` and `format` with the options above, not here. */
|
|
25
|
+
compileOptions?: PluginCompileOptions | undefined;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { SharedPluginOptions as n, PluginCompileOptions as t };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/*! @rohal12/twee-ts (Unlicense). Bundles third-party code under its own licence: see THIRD_PARTY_NOTICES. */
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const require_diagnostic_text = require("../diagnostic-text-BRddVulf.cjs");
|
|
4
|
+
const require_watch_targets = require("../watch-targets-C0Anuw17.cjs");
|
|
5
|
+
let node_path = require("node:path");
|
|
6
|
+
let node_fs = require("node:fs");
|
|
7
|
+
let node_os = require("node:os");
|
|
8
|
+
//#region src/plugins/link-watch.ts
|
|
9
|
+
/**
|
|
10
|
+
* Tells a bundler's build watch that a link the story's inputs are reached through was pointed elsewhere, when the
|
|
11
|
+
* bundler's own watcher can't. Rollup's watcher (chokidar) follows a registered link and watches the file it leads to,
|
|
12
|
+
* so replacing the link, or a hop of a chain of links, raises nothing. The link's folder is watched here instead (not
|
|
13
|
+
* recursively, so what the build writes below it is not reported), and a change of any link starts a build the way an
|
|
14
|
+
* edit of a registered file does: by touching a file the plugin has registered with the bundler.
|
|
15
|
+
*/
|
|
16
|
+
/** How long a burst of file system events settles before the links are read again (a link is replaced in two steps). */
|
|
17
|
+
const SETTLE_MS = 30;
|
|
18
|
+
/** Where a link leads (as written), or nothing when it is no link or does not exist. */
|
|
19
|
+
function destination(link) {
|
|
20
|
+
try {
|
|
21
|
+
return (0, node_fs.readlinkSync)(link);
|
|
22
|
+
} catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A watch of links that touches its signal file when one of them leads elsewhere, or appears or disappears. The watch
|
|
28
|
+
* is a refinement of the build watch: without a temporary folder to write the signal file in (a missing or read-only
|
|
29
|
+
* TMPDIR), there is none, and a retargeted link is read with the next build something else starts.
|
|
30
|
+
*/
|
|
31
|
+
function createLinkWatch() {
|
|
32
|
+
let folder;
|
|
33
|
+
let signal;
|
|
34
|
+
try {
|
|
35
|
+
folder = (0, node_fs.mkdtempSync)((0, node_path.join)((0, node_os.tmpdir)(), "twee-ts-links-"));
|
|
36
|
+
signal = (0, node_path.join)(folder, "changed");
|
|
37
|
+
(0, node_fs.writeFileSync)(signal, "");
|
|
38
|
+
} catch {
|
|
39
|
+
if (folder !== void 0) (0, node_fs.rmSync)(folder, {
|
|
40
|
+
recursive: true,
|
|
41
|
+
force: true
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const signalFolder = folder;
|
|
46
|
+
let touches = 0;
|
|
47
|
+
const watchers = [];
|
|
48
|
+
let timer;
|
|
49
|
+
let known = /* @__PURE__ */ new Map();
|
|
50
|
+
const touch = () => {
|
|
51
|
+
touches += 1;
|
|
52
|
+
const at = new Date(Date.now() + touches * 1e3);
|
|
53
|
+
try {
|
|
54
|
+
(0, node_fs.writeFileSync)(signal, String(touches));
|
|
55
|
+
(0, node_fs.utimesSync)(signal, at, at);
|
|
56
|
+
} catch {}
|
|
57
|
+
};
|
|
58
|
+
const check = () => {
|
|
59
|
+
timer = void 0;
|
|
60
|
+
if ([...known].filter(([link, was]) => destination(link) !== was).length === 0) return;
|
|
61
|
+
known = new Map([...known.keys()].map((link) => [link, destination(link)]));
|
|
62
|
+
touch();
|
|
63
|
+
};
|
|
64
|
+
/** Reads the links again once the events that come together have come. */
|
|
65
|
+
const schedule = () => {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
timer = setTimeout(check, SETTLE_MS);
|
|
68
|
+
};
|
|
69
|
+
const stopWatching = () => {
|
|
70
|
+
for (const watcher of watchers.splice(0)) watcher.close();
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
timer = void 0;
|
|
73
|
+
};
|
|
74
|
+
const onExit = () => {
|
|
75
|
+
(0, node_fs.rmSync)(signalFolder, {
|
|
76
|
+
recursive: true,
|
|
77
|
+
force: true
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
process.once("exit", onExit);
|
|
81
|
+
return {
|
|
82
|
+
signal,
|
|
83
|
+
watch(links) {
|
|
84
|
+
stopWatching();
|
|
85
|
+
known = new Map(links.map((link) => [link, destination(link)]));
|
|
86
|
+
for (const parent of new Set(links.map((link) => (0, node_path.dirname)(link)))) try {
|
|
87
|
+
const watcher = (0, node_fs.watch)(parent, schedule);
|
|
88
|
+
watcher.on("error", schedule);
|
|
89
|
+
watchers.push(watcher);
|
|
90
|
+
} catch {}
|
|
91
|
+
},
|
|
92
|
+
close() {
|
|
93
|
+
stopWatching();
|
|
94
|
+
process.removeListener("exit", onExit);
|
|
95
|
+
onExit();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/plugins/rollup.ts
|
|
101
|
+
/**
|
|
102
|
+
* Ready-made Rollup plugin for twee-ts.
|
|
103
|
+
* Compiles .tw files and emits HTML as an asset. Compile errors fail the build
|
|
104
|
+
* and emit nothing; warnings go through Rollup's warnings.
|
|
105
|
+
*/
|
|
106
|
+
function tweeTsPlugin(options) {
|
|
107
|
+
const resolved = require_watch_targets.resolvePluginOptions("rollup", options);
|
|
108
|
+
const { outputFilename } = resolved;
|
|
109
|
+
const record = require_watch_targets.createOutputRecord(outputFilename);
|
|
110
|
+
let linkWatch;
|
|
111
|
+
let linkWatchUnavailable = false;
|
|
112
|
+
const followed = /* @__PURE__ */ new Map();
|
|
113
|
+
return {
|
|
114
|
+
name: "twee-ts",
|
|
115
|
+
options(inputOptions) {
|
|
116
|
+
const output = Reflect.get(inputOptions, "output");
|
|
117
|
+
for (const location of require_watch_targets.outputLocations(output)) record.addLocation(location);
|
|
118
|
+
},
|
|
119
|
+
buildStart() {
|
|
120
|
+
if (!this.meta.watchMode) return;
|
|
121
|
+
const outputs = require_diagnostic_text.outputPaths(record.outputs());
|
|
122
|
+
const plan = require_watch_targets.watchPlan(resolved.inputs, resolved.excluded, outputs, "given");
|
|
123
|
+
const targets = plan.targets.map((target) => (0, node_path.resolve)(target));
|
|
124
|
+
for (const target of targets) this.addWatchFile(target);
|
|
125
|
+
for (const [path, real] of plan.reals) {
|
|
126
|
+
const first = followed.get(path) ?? real;
|
|
127
|
+
followed.set(path, first);
|
|
128
|
+
if (first !== real) this.addWatchFile((0, node_path.resolve)(real));
|
|
129
|
+
}
|
|
130
|
+
if (plan.links.length > 0 && !linkWatchUnavailable) {
|
|
131
|
+
linkWatch ??= createLinkWatch();
|
|
132
|
+
if (linkWatch === void 0) {
|
|
133
|
+
linkWatchUnavailable = true;
|
|
134
|
+
this.warn("[twee-ts] The link watch is unavailable because no temporary folder can be written (check TMPDIR); a link pointed at another target is read with the next build something else starts.");
|
|
135
|
+
} else {
|
|
136
|
+
linkWatch.watch(plan.links);
|
|
137
|
+
this.addWatchFile(linkWatch.signal);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const unwatchable = node_path.sep === "/" ? targets.filter((target) => target.includes("\\")) : [];
|
|
141
|
+
if (unwatchable.length > 0) this.warn(`[twee-ts] Rollup's watch mode cannot watch ${unwatchable.map((target) => JSON.stringify(target)).join(", ")} because the name contains a backslash; edits to it will not rebuild the story.`);
|
|
142
|
+
},
|
|
143
|
+
closeWatcher() {
|
|
144
|
+
linkWatch?.close();
|
|
145
|
+
linkWatch = void 0;
|
|
146
|
+
},
|
|
147
|
+
renderStart(outputOptions) {
|
|
148
|
+
record.addLocation(outputOptions);
|
|
149
|
+
},
|
|
150
|
+
async generateBundle(outputOptions, bundle) {
|
|
151
|
+
if (Object.hasOwn(bundle, outputFilename)) return this.error(`twee-ts: the bundle already holds a file named ${outputFilename}; set the plugin's outputFilename to another name.`);
|
|
152
|
+
record.addLocation(outputOptions);
|
|
153
|
+
record.addFiles(outputOptions, Object.keys(bundle));
|
|
154
|
+
let story;
|
|
155
|
+
try {
|
|
156
|
+
story = await require_watch_targets.compileStory(resolved.compile(), record.outputs(), void 0, true);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return this.error(require_watch_targets.fatalError(e));
|
|
159
|
+
}
|
|
160
|
+
for (const warning of story.warnings) this.warn(warning);
|
|
161
|
+
this.emitFile({
|
|
162
|
+
type: "asset",
|
|
163
|
+
fileName: outputFilename,
|
|
164
|
+
source: story.output
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
exports.tweeTsPlugin = tweeTsPlugin;
|
|
171
|
+
|
|
172
|
+
//# sourceMappingURL=rollup.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rollup.cjs","names":["readlinkSync","mkdtempSync","join","tmpdir","dirname","watch","resolvePluginOptions","createOutputRecord","outputLocations","outputPaths","watchPlan","resolve","sep","compileStory","fatalError"],"sources":["../../src/plugins/link-watch.ts","../../src/plugins/rollup.ts"],"sourcesContent":["/**\n * Tells a bundler's build watch that a link the story's inputs are reached through was pointed elsewhere, when the\n * bundler's own watcher can't. Rollup's watcher (chokidar) follows a registered link and watches the file it leads to,\n * so replacing the link, or a hop of a chain of links, raises nothing. The link's folder is watched here instead (not\n * recursively, so what the build writes below it is not reported), and a change of any link starts a build the way an\n * edit of a registered file does: by touching a file the plugin has registered with the bundler.\n */\nimport { mkdtempSync, readlinkSync, rmSync, utimesSync, watch, writeFileSync } from 'node:fs';\nimport type { FSWatcher } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\n/** How long a burst of file system events settles before the links are read again (a link is replaced in two steps). */\nconst SETTLE_MS = 30;\n\nexport interface LinkWatch {\n /** The file to register with the bundler's watcher: written when a link changed, with the number of changes so far. */\n readonly signal: string;\n /** Watches `links` (platform paths) from now on, instead of the links watched before. */\n watch(links: readonly string[]): void;\n /** Stops watching and removes the signal file. */\n close(): void;\n}\n\n/** Where a link leads (as written), or nothing when it is no link or does not exist. */\nfunction destination(link: string): string | undefined {\n try {\n return readlinkSync(link);\n } catch {\n return undefined;\n }\n}\n\n/**\n * A watch of links that touches its signal file when one of them leads elsewhere, or appears or disappears. The watch\n * is a refinement of the build watch: without a temporary folder to write the signal file in (a missing or read-only\n * TMPDIR), there is none, and a retargeted link is read with the next build something else starts.\n */\nexport function createLinkWatch(): LinkWatch | undefined {\n let folder: string | undefined;\n let signal: string;\n try {\n folder = mkdtempSync(join(tmpdir(), 'twee-ts-links-'));\n signal = join(folder, 'changed');\n writeFileSync(signal, '');\n } catch {\n if (folder !== undefined) rmSync(folder, { recursive: true, force: true });\n return undefined;\n }\n const signalFolder = folder;\n let touches = 0;\n const watchers: FSWatcher[] = [];\n let timer: NodeJS.Timeout | undefined;\n let known = new Map<string, string | undefined>();\n\n const touch = (): void => {\n touches += 1;\n // A modification time that differs from every earlier one, whatever the clock's resolution.\n const at = new Date(Date.now() + touches * 1000);\n try {\n writeFileSync(signal, String(touches));\n utimesSync(signal, at, at);\n } catch {\n // The watch is closed.\n }\n };\n const check = (): void => {\n timer = undefined;\n const changed = [...known].filter(([link, was]) => destination(link) !== was);\n if (changed.length === 0) return;\n known = new Map([...known.keys()].map((link) => [link, destination(link)]));\n touch();\n };\n /** Reads the links again once the events that come together have come. */\n const schedule = (): void => {\n clearTimeout(timer);\n timer = setTimeout(check, SETTLE_MS);\n };\n const stopWatching = (): void => {\n for (const watcher of watchers.splice(0)) watcher.close();\n clearTimeout(timer);\n timer = undefined;\n };\n const onExit = (): void => {\n rmSync(signalFolder, { recursive: true, force: true });\n };\n process.once('exit', onExit);\n\n return {\n signal,\n watch(links) {\n stopWatching();\n known = new Map(links.map((link) => [link, destination(link)]));\n for (const parent of new Set(links.map((link) => dirname(link)))) {\n try {\n const watcher = watch(parent, schedule);\n // A folder that goes away, or a limit of the system: the links in it are read again.\n watcher.on('error', schedule);\n watchers.push(watcher);\n } catch {\n // The folder can't be watched; the links in it are read again with the next build.\n }\n }\n },\n close() {\n stopWatching();\n process.removeListener('exit', onExit);\n onExit();\n },\n };\n}\n","/**\n * Ready-made Rollup plugin for twee-ts.\n * Compiles .tw files and emits HTML as an asset. Compile errors fail the build\n * and emit nothing; warnings go through Rollup's warnings.\n */\nimport { resolve, sep } from 'node:path';\nimport { outputPaths } from '../filesystem.js';\nimport { compileStory, fatalError } from './diagnostics.js';\nimport type { CompiledStory, LocatedError } from './diagnostics.js';\nimport { resolvePluginOptions } from './options.js';\nimport type { SharedPluginOptions } from './options.js';\nimport { createOutputRecord, outputLocations } from './paths.js';\nimport type { OutputLocation } from './paths.js';\nimport { createLinkWatch } from './link-watch.js';\nimport type { LinkWatch } from './link-watch.js';\nimport { watchPlan } from './watch-targets.js';\n\nexport type { PluginCompileOptions } from './options.js';\n\nexport type TweeTsRollupPluginOptions = SharedPluginOptions;\n\nexport function tweeTsPlugin(options: TweeTsRollupPluginOptions) {\n const resolved = resolvePluginOptions('rollup', options);\n const { outputFilename } = resolved;\n // Every path this plugin's builds write: each output's folder or file, its story,\n // and the files of each bundle. A source folder may hold any of them, and none\n // may be read back as a source or rebuild under watch.\n const record = createOutputRecord(outputFilename);\n // Tells the watch that a link the inputs are reached through was pointed elsewhere (see link-watch.ts).\n let linkWatch: LinkWatch | undefined;\n // The link watch could not be set up (no temporary folder); said once, and not tried again.\n let linkWatchUnavailable = false;\n // Where each registered path led when Rollup's watcher first followed it.\n const followed = new Map<string, string>();\n\n return {\n name: 'twee-ts',\n\n // Rollup's watch mode and CLI pass the whole config here, outputs included, so\n // they are known before the first build starts and before any is written.\n // Typed `object`: a type with only optional properties would share none with\n // Rollup's InputOptions, and the plugin would no longer be assignable to Plugin.\n options(inputOptions: object): undefined {\n const output: unknown = Reflect.get(inputOptions, 'output');\n for (const location of outputLocations(output)) record.addLocation(location);\n return undefined;\n },\n\n // `rollup --watch` rebuilds for the files the build registers. The story's\n // inputs are read in generateBundle, outside the module graph, so they are\n // registered here: the sources, the head file and the modules, without the\n // files `exclude` leaves out (see watchTargets). Rollup's watcher follows a link and watches what it leads to, so\n // it doesn't see a link, or a hop of a chain of links, pointed elsewhere; the plugin watches the links itself.\n buildStart(this: {\n addWatchFile: (id: string) => void;\n warn: (message: string) => void;\n meta: { watchMode: boolean };\n }) {\n if (!this.meta.watchMode) return;\n const outputs = outputPaths(record.outputs());\n // In the platform's own form, as Rollup reports its module ids.\n const plan = watchPlan(resolved.inputs, resolved.excluded, outputs, 'given');\n const targets = plan.targets.map((target) => resolve(target));\n for (const target of targets) this.addWatchFile(target);\n // Rollup's watcher keeps watching what a path led to when it was registered, so a path that leads elsewhere now\n // (a link was pointed at another file or folder) is watched from its new location as well.\n for (const [path, real] of plan.reals) {\n const first = followed.get(path) ?? real;\n followed.set(path, first);\n if (first !== real) this.addWatchFile(resolve(real));\n }\n if (plan.links.length > 0 && !linkWatchUnavailable) {\n linkWatch ??= createLinkWatch();\n if (linkWatch === undefined) {\n // The links are a refinement: the build goes on, and says once what the watch cannot do.\n linkWatchUnavailable = true;\n this.warn(\n '[twee-ts] The link watch is unavailable because no temporary folder can be written (check TMPDIR); ' +\n 'a link pointed at another target is read with the next build something else starts.',\n );\n } else {\n linkWatch.watch(plan.links);\n this.addWatchFile(linkWatch.signal);\n }\n }\n // Rollup's file watcher (chokidar) reads every backslash as a separator, also on POSIX, where it is a\n // filename character: such a file is read by each build, but an edit to it starts none.\n const unwatchable = sep === '/' ? targets.filter((target) => target.includes('\\\\')) : [];\n if (unwatchable.length > 0) {\n this.warn(\n `[twee-ts] Rollup's watch mode cannot watch ${unwatchable.map((target) => JSON.stringify(target)).join(', ')} ` +\n 'because the name contains a backslash; edits to it will not rebuild the story.',\n );\n }\n },\n\n // The watcher is closed: the links are no longer watched.\n closeWatcher(): void {\n linkWatch?.close();\n linkWatch = undefined;\n },\n\n // Runs for every output before any of them generates its bundle, when the\n // outputs are written together (as the CLI and watch mode write them).\n renderStart(outputOptions: OutputLocation): void {\n record.addLocation(outputOptions);\n },\n\n async generateBundle(\n this: {\n emitFile: (opts: { type: 'asset'; fileName: string; source: string }) => void;\n error: (error: LocatedError | string) => never;\n warn: (message: string) => void;\n },\n outputOptions: OutputLocation,\n bundle: Readonly<Record<string, unknown>>,\n ): Promise<void> {\n if (Object.hasOwn(bundle, outputFilename)) {\n return this.error(\n `twee-ts: the bundle already holds a file named ${outputFilename}; set the plugin's outputFilename to another name.`,\n );\n }\n // Where this output writes the story and the bundle. A source folder may hold\n // them, or another output's, and no last build of them may be read back.\n record.addLocation(outputOptions);\n record.addFiles(outputOptions, Object.keys(bundle));\n let story: CompiledStory;\n try {\n story = await compileStory(resolved.compile(), record.outputs(), undefined, true);\n } catch (e) {\n return this.error(fatalError(e));\n }\n for (const warning of story.warnings) this.warn(warning);\n this.emitFile({ type: 'asset', fileName: outputFilename, source: story.output });\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
|
|
2
|
+
import { n as SharedPluginOptions, t as PluginCompileOptions } from "../options-DuBkFyqi.cjs";
|
|
3
|
+
//#region src/plugins/diagnostics.d.ts
|
|
4
|
+
/** An error carrying the file and line the bundler reports (and Vite shows in its overlay). */
|
|
5
|
+
interface LocatedError extends Error {
|
|
6
|
+
id?: string;
|
|
7
|
+
loc?: {
|
|
8
|
+
file: string;
|
|
9
|
+
line: number;
|
|
10
|
+
column: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/plugins/paths.d.ts
|
|
15
|
+
/** The output settings that say where a bundle is written, as Rollup and Vite pass them to generateBundle. */
|
|
16
|
+
interface OutputLocation {
|
|
17
|
+
readonly dir?: string | undefined;
|
|
18
|
+
readonly file?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/plugins/rollup.d.ts
|
|
22
|
+
export type TweeTsRollupPluginOptions = SharedPluginOptions;
|
|
23
|
+
export declare function tweeTsPlugin(options: TweeTsRollupPluginOptions): {
|
|
24
|
+
name: string;
|
|
25
|
+
options(inputOptions: object): undefined;
|
|
26
|
+
buildStart(this: {
|
|
27
|
+
addWatchFile: (id: string) => void;
|
|
28
|
+
warn: (message: string) => void;
|
|
29
|
+
meta: {
|
|
30
|
+
watchMode: boolean;
|
|
31
|
+
};
|
|
32
|
+
}): void;
|
|
33
|
+
closeWatcher(): void;
|
|
34
|
+
renderStart(outputOptions: OutputLocation): void;
|
|
35
|
+
generateBundle(this: {
|
|
36
|
+
emitFile: (opts: {
|
|
37
|
+
type: 'asset';
|
|
38
|
+
fileName: string;
|
|
39
|
+
source: string;
|
|
40
|
+
}) => void;
|
|
41
|
+
error: (error: LocatedError | string) => never;
|
|
42
|
+
warn: (message: string) => void;
|
|
43
|
+
}, outputOptions: OutputLocation, bundle: Readonly<Record<string, unknown>>): Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export type { PluginCompileOptions };
|