Skip to content

Commit 9a312e3

Browse files
benmccannantfu
andauthoredOct 5, 2022
feat: add update method as safer alternative to overwrite (#212)
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
1 parent ded833e commit 9a312e3

File tree

5 files changed

+176
-9
lines changed

5 files changed

+176
-9
lines changed
 

‎README.md

+15-5
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ import fs from 'fs'
4343

4444
const s = new MagicString('problems = 99');
4545

46-
s.overwrite(0, 8, 'answer');
46+
s.update(0, 8, 'answer');
4747
s.toString(); // 'answer = 99'
4848

49-
s.overwrite(11, 13, '42'); // character indices always refer to the original string
49+
s.update(11, 13, '42'); // character indices always refer to the original string
5050
s.toString(); // 'answer = 42'
5151

5252
s.prepend('var ').append(';'); // most methods are chainable
@@ -135,6 +135,10 @@ The `options` argument can have an `exclude` property, which is an array of `[st
135135

136136
**DEPRECATED** since 0.17 – use `s.prependRight(...)` instead
137137

138+
### s.isEmpty()
139+
140+
Returns true if the resulting source is empty (disregarding white space).
141+
138142
### s.locate( index )
139143

140144
**DEPRECATED** since 0.10 – see [#30](https://github.com/Rich-Harris/magic-string/pull/30)
@@ -149,10 +153,12 @@ Moves the characters from `start` and `end` to `index`. Returns `this`.
149153

150154
### s.overwrite( start, end, content[, options] )
151155

152-
Replaces the characters from `start` to `end` with `content`. The same restrictions as `s.remove()` apply. Returns `this`.
156+
Replaces the characters from `start` to `end` with `content`, along with the appended/prepended content in that range. The same restrictions as `s.remove()` apply. Returns `this`.
153157

154158
The fourth argument is optional. It can have a `storeName` property — if `true`, the original name will be stored for later inclusion in a sourcemap's `names` array — and a `contentOnly` property which determines whether only the content is overwritten, or anything that was appended/prepended to the range as well.
155159

160+
It may be preferred to use `s.update(...)` instead if you wish to avoid overwriting the appended/prepended content.
161+
156162
### s.prepend( content )
157163

158164
Prepends the string with the specified content. Returns `this`.
@@ -220,9 +226,13 @@ Trims content matching `charType` (defaults to `\s`, i.e. whitespace) from the e
220226

221227
Removes empty lines from the start and end. Returns `this`.
222228

223-
### s.isEmpty()
229+
### s.update( start, end, content[, options] )
224230

225-
Returns true if the resulting source is empty (disregarding white space).
231+
Replaces the characters from `start` to `end` with `content`. The same restrictions as `s.remove()` apply. Returns `this`.
232+
233+
The fourth argument is optional. It can have a `storeName` property — if `true`, the original name will be stored for later inclusion in a sourcemap's `names` array — and an `overwrite` property which defaults to `false` and determines whether anything that was appended/prepended to the range will be overwritten along with the original content.
234+
235+
`s.update(start, end, content)` is equivalent to `s.overwrite(start, end, content, { contentOnly: true })`.
226236

227237
## Bundling
228238

‎index.d.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ export interface OverwriteOptions {
9898
contentOnly?: boolean;
9999
}
100100

101+
export interface UpdateOptions {
102+
storeName?: boolean;
103+
overwrite?: boolean;
104+
}
105+
101106
export default class MagicString {
102107
constructor(str: string, options?: MagicStringOptions);
103108
/**
@@ -155,13 +160,24 @@ export default class MagicString {
155160
*/
156161
move(start: number, end: number, index: number): MagicString;
157162
/**
158-
* Replaces the characters from `start` to `end` with `content`. The same restrictions as `s.remove()` apply.
163+
* Replaces the characters from `start` to `end` with `content`, along with the appended/prepended content in
164+
* that range. The same restrictions as `s.remove()` apply.
159165
*
160166
* The fourth argument is optional. It can have a storeName property — if true, the original name will be stored
161167
* for later inclusion in a sourcemap's names array — and a contentOnly property which determines whether only
162168
* the content is overwritten, or anything that was appended/prepended to the range as well.
169+
*
170+
* It may be preferred to use `s.update(...)` instead if you wish to avoid overwriting the appended/prepended content.
163171
*/
164172
overwrite(start: number, end: number, content: string, options?: boolean | OverwriteOptions): MagicString;
173+
/**
174+
* Replaces the characters from `start` to `end` with `content`. The same restrictions as `s.remove()` apply.
175+
*
176+
* The fourth argument is optional. It can have a storeName property — if true, the original name will be stored
177+
* for later inclusion in a sourcemap's names array — and an overwrite property which determines whether only
178+
* the content is overwritten, or anything that was appended/prepended to the range as well.
179+
*/
180+
update(start: number, end: number, content: string, options?: boolean | UpdateOptions): MagicString;
165181
/**
166182
* Prepends the string with the specified content.
167183
*/

‎package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/MagicString.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,12 @@ export default class MagicString {
349349
}
350350

351351
overwrite(start, end, content, options) {
352+
options = options || {};
353+
options.overwrite = !options.contentOnly;
354+
return this.update(start, end, content, options);
355+
}
356+
357+
update(start, end, content, options) {
352358
if (typeof content !== 'string') throw new TypeError('replacement content must be a string');
353359

354360
while (start < 0) start += this.original.length;
@@ -376,7 +382,7 @@ export default class MagicString {
376382
options = { storeName: true };
377383
}
378384
const storeName = options !== undefined ? options.storeName : false;
379-
const contentOnly = options !== undefined ? options.contentOnly : false;
385+
const overwrite = options !== undefined ? options.overwrite : false;
380386

381387
if (storeName) {
382388
const original = this.original.slice(start, end);
@@ -400,7 +406,7 @@ export default class MagicString {
400406
chunk.edit('', false);
401407
}
402408

403-
first.edit(content, storeName, contentOnly);
409+
first.edit(content, storeName, !overwrite);
404410
} else {
405411
// must be inserting at the end
406412
const newChunk = new Chunk(start, end, '').edit(content, storeName);

‎test/MagicString.js

+135
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,141 @@ describe('MagicString', () => {
856856
});
857857
});
858858

859+
describe('update', () => {
860+
it('should replace characters', () => {
861+
const s = new MagicString('abcdefghijkl');
862+
863+
s.update(5, 8, 'FGH');
864+
assert.equal(s.toString(), 'abcdeFGHijkl');
865+
});
866+
867+
it('should throw an error if overlapping replacements are attempted', () => {
868+
const s = new MagicString('abcdefghijkl');
869+
870+
s.update(7, 11, 'xx');
871+
872+
assert.throws(() => s.update(8, 12, 'yy'), /Cannot split a chunk that has already been edited/);
873+
874+
assert.equal(s.toString(), 'abcdefgxxl');
875+
876+
s.update(6, 12, 'yes');
877+
assert.equal(s.toString(), 'abcdefyes');
878+
});
879+
880+
it('should allow contiguous but non-overlapping replacements', () => {
881+
const s = new MagicString('abcdefghijkl');
882+
883+
s.update(3, 6, 'DEF');
884+
assert.equal(s.toString(), 'abcDEFghijkl');
885+
886+
s.update(6, 9, 'GHI');
887+
assert.equal(s.toString(), 'abcDEFGHIjkl');
888+
889+
s.update(0, 3, 'ABC');
890+
assert.equal(s.toString(), 'ABCDEFGHIjkl');
891+
892+
s.update(9, 12, 'JKL');
893+
assert.equal(s.toString(), 'ABCDEFGHIJKL');
894+
});
895+
896+
it('does not replace zero-length inserts at update start location', () => {
897+
const s = new MagicString('abcdefghijkl');
898+
899+
s.remove(0, 6);
900+
s.appendLeft(6, 'DEF');
901+
s.update(6, 9, 'GHI');
902+
assert.equal(s.toString(), 'DEFGHIjkl');
903+
});
904+
905+
it('replaces zero-length inserts inside update with overwrite option', () => {
906+
const s = new MagicString('abcdefghijkl');
907+
908+
s.appendLeft(6, 'XXX');
909+
s.update(3, 9, 'DEFGHI', { overwrite: true });
910+
assert.equal(s.toString(), 'abcDEFGHIjkl');
911+
});
912+
913+
it('replaces non-zero-length inserts inside update', () => {
914+
const s = new MagicString('abcdefghijkl');
915+
916+
s.update(3, 4, 'XXX');
917+
s.update(3, 5, 'DE');
918+
assert.equal(s.toString(), 'abcDEfghijkl');
919+
920+
s.update(7, 8, 'YYY');
921+
s.update(6, 8, 'GH');
922+
assert.equal(s.toString(), 'abcDEfGHijkl');
923+
});
924+
925+
it('should return this', () => {
926+
const s = new MagicString('abcdefghijkl');
927+
assert.strictEqual(s.update(3, 4, 'D'), s);
928+
});
929+
930+
it('should disallow updating zero-length ranges', () => {
931+
const s = new MagicString('x');
932+
assert.throws(() => s.update(0, 0, 'anything'), /Cannot overwrite a zero-length range use appendLeft or prependRight instead/);
933+
});
934+
935+
it('should throw when given non-string content', () => {
936+
const s = new MagicString('');
937+
assert.throws(() => s.update(0, 1, []), TypeError);
938+
});
939+
940+
it('replaces interior inserts with overwrite option', () => {
941+
const s = new MagicString('abcdefghijkl');
942+
943+
s.appendLeft(1, '&');
944+
s.prependRight(1, '^');
945+
s.appendLeft(3, '!');
946+
s.prependRight(3, '?');
947+
s.update(1, 3, '...', { overwrite: true });
948+
assert.equal(s.toString(), 'a&...?defghijkl');
949+
});
950+
951+
it('preserves interior inserts with `contentOnly: true`', () => {
952+
const s = new MagicString('abcdefghijkl');
953+
954+
s.appendLeft(1, '&');
955+
s.prependRight(1, '^');
956+
s.appendLeft(3, '!');
957+
s.prependRight(3, '?');
958+
s.update(1, 3, '...', { contentOnly: true });
959+
assert.equal(s.toString(), 'a&^...!?defghijkl');
960+
});
961+
962+
it('disallows overwriting partially overlapping moved content', () => {
963+
const s = new MagicString('abcdefghijkl');
964+
965+
s.move(6, 9, 3);
966+
assert.throws(() => s.update(5, 7, 'XX'), /Cannot overwrite across a split point/);
967+
});
968+
969+
it('disallows overwriting fully surrounding content moved away', () => {
970+
const s = new MagicString('abcdefghijkl');
971+
972+
s.move(6, 9, 3);
973+
assert.throws(() => s.update(4, 11, 'XX'), /Cannot overwrite across a split point/);
974+
});
975+
976+
it('disallows overwriting fully surrounding content moved away even if there is another split', () => {
977+
const s = new MagicString('abcdefghijkl');
978+
979+
s.move(6, 9, 3);
980+
s.appendLeft(5, 'foo');
981+
assert.throws(() => s.update(4, 11, 'XX'), /Cannot overwrite across a split point/);
982+
});
983+
984+
it('allows later insertions at the end with overwrite option', () => {
985+
const s = new MagicString('abcdefg');
986+
987+
s.appendLeft(4, '(');
988+
s.update(2, 7, '', { overwrite: true });
989+
s.appendLeft(7, 'h');
990+
assert.equal(s.toString(), 'abh');
991+
});
992+
});
993+
859994
describe('prepend', () => {
860995
it('should prepend content', () => {
861996
const s = new MagicString('abcdefghijkl');

0 commit comments

Comments
 (0)
Please sign in to comment.