Skip to content

Commit f16b690

Browse files
authored
release: v1.13.1 (#47)
## Summary - Fix `String.replace` `$&` corruption when inlining JS modules — `$&`, `` $` ``, `$'`, and `$<digits>` in module content were silently replaced by special replacement patterns, corrupting bundled JavaScript (#46) - Fix same corruption pattern in Twine 2 and Twine 1 HTML output renderers - All `.replace()` calls with user-controlled replacement strings now use function replacements to bypass special pattern interpretation ## Changed files - `src/modules.ts` — `modifyHead()` replacement - `src/output-twine1.ts` — `toTwine1HTML()` and `tryReplaceComponent()` replacements - `src/output-twine2.ts` — `toTwine2HTML()` replacements - `test/modules.test.ts` — regression tests for `$&`, `` $` ``, and `$'` patterns - `CHANGELOG.md` — v1.13.1 entry ## Test plan - [x] Added regression test: JS module with `$&$&` is preserved verbatim in output - [x] Added regression test: JS module with `` $` `` and `$'` is preserved verbatim - [x] Verified both tests fail on unaltered code (confirmed `$&` becomes `</head>`) - [x] All 1216 tests pass after fix - [x] Typecheck, format, and build all pass release-npm 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 9b1cb8b + 72da0ed commit f16b690

5 files changed

Lines changed: 33 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.13.1] - 2026-03-19
9+
10+
### Fixed
11+
12+
- `String.replace` `$&` corruption when inlining JS modules — `$&`, `` $` ``, `$'`, and `$<digits>` in module content were silently replaced by `String.replace()` special patterns, corrupting bundled JavaScript ([#46](https://github.com/rohal12/twee-ts/issues/46))
13+
- Same `String.replace` corruption in Twine 2 and Twine 1 HTML output renderers when story content contains replacement patterns
14+
815
## [1.13.0] - 2026-03-06
916

1017
### Added

src/modules.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ export function modifyHead(html: string, modulePaths: string[], headFile?: strin
9090

9191
if (parts.length > 0) {
9292
parts.push('</head>');
93-
return html.replace('</head>', parts.join('\n'));
93+
const replacement = parts.join('\n');
94+
return html.replace('</head>', () => replacement);
9495
}
9596
return html;
9697
}

src/output-twine1.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ export function toTwine1HTML(story: ReadonlyStory, format: StoryFormatInfo, star
4040
const displayStart = startName === 'Start' ? '' : startName;
4141
template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME}, ${VERSION}`);
4242
template = template.replace('"TIME"', `Built on ${new Date().toUTCString()}`);
43-
template = template.replace('"START_AT"', `"${jsStringEscape(displayStart)}"`);
43+
const startAtValue = `"${jsStringEscape(displayStart)}"`;
44+
template = template.replace('"START_AT"', () => startAtValue);
4445
template = template.replace('"STORY_SIZE"', `"${count}"`);
4546

4647
if (template.includes('"STORY"')) {
47-
template = template.replace('"STORY"', data);
48+
template = template.replace('"STORY"', () => data);
4849
} else {
4950
// Pre-1.4 format: append data + footer
5051
let footer: string;
@@ -88,7 +89,7 @@ function tryReplaceComponent(template: string, placeholder: string, componentPat
8889
try {
8990
let content = readFileSync(componentPath, 'utf-8');
9091
if (content.charCodeAt(0) === 0xfeff) content = content.slice(1);
91-
return template.replace(placeholder, content);
92+
return template.replace(placeholder, () => content);
9293
} catch (e) {
9394
if (required) {
9495
throw new Error(

src/output-twine2.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ export function toTwine2HTML(
2727
let template = readFormatSource(format);
2828

2929
if (template.includes('{{STORY_NAME}}')) {
30-
template = template.replaceAll('{{STORY_NAME}}', htmlEscape(story.name));
30+
const name = htmlEscape(story.name);
31+
template = template.replaceAll('{{STORY_NAME}}', () => name);
3132
}
3233
if (template.includes('{{STORY_DATA}}')) {
33-
template = template.replace('{{STORY_DATA}}', getTwine2DataChunk(story, startName, options));
34+
const data = getTwine2DataChunk(story, startName, options);
35+
template = template.replace('{{STORY_DATA}}', () => data);
3436
}
3537

3638
return template;

test/modules.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ describe('modifyHead', () => {
9393
expect(modifyHead(baseHtml, [])).toBe(baseHtml);
9494
});
9595

96+
it('preserves $& and other replacement patterns in module content', () => {
97+
const file = join(TMP_DIR, 'regex-lib.js');
98+
writeFileSync(file, 'var x = "test".replace(/t/, "$&$&");');
99+
const result = modifyHead(baseHtml, [file]);
100+
expect(result).toContain('$&$&');
101+
expect(result).not.toContain('</head></head>');
102+
});
103+
104+
it("preserves $` and $' replacement patterns in module content", () => {
105+
const file = join(TMP_DIR, 'patterns.js');
106+
writeFileSync(file, 'var a = "$`"; var b = "$\'";');
107+
const result = modifyHead(baseHtml, [file]);
108+
expect(result).toContain('$`');
109+
expect(result).toContain("$'");
110+
});
111+
96112
it('collects diagnostics for missing head file', () => {
97113
const diagnostics: Diagnostic[] = [];
98114
const result = modifyHead(baseHtml, [], join(TMP_DIR, 'missing.html'), diagnostics);

0 commit comments

Comments
 (0)