Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: Rich-Harris/magic-string
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 69336fccbb46e721d58faa5c7a0d0b7ed6ee09d2
Choose a base ref
...
head repository: Rich-Harris/magic-string
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 34935df2d22cee89f6bdadf8503d9a31602393bd
Choose a head ref

Commits on Mar 3, 2022

  1. Verified

    This commit was signed with the committer’s verified signature.
    hyoban Stephen Zhou
    Copy the full SHA
    6f09557 View commit details
  2. feat!: use .mjs for esm output (#197)

    Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
    bluwy and antfu authored Mar 3, 2022
    Copy the full SHA
    62d6e85 View commit details
  3. Unverified

    This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
    Copy the full SHA
    cd74ea2 View commit details
  4. Copy the full SHA
    5f2dba7 View commit details
  5. release v0.26.0

    antfu committed Mar 3, 2022
    Copy the full SHA
    aa33b07 View commit details
  6. chore: update changelog

    antfu committed Mar 3, 2022
    Copy the full SHA
    877b834 View commit details
  7. Copy the full SHA
    902541f View commit details
  8. chore: apply formater

    antfu committed Mar 3, 2022
    Copy the full SHA
    362e72f View commit details
  9. release v0.26.1

    antfu committed Mar 3, 2022
    Copy the full SHA
    3fa4343 View commit details
  10. chore: update readme (#206)

    btea authored Mar 3, 2022
    Copy the full SHA
    abe7e36 View commit details

Commits on Mar 4, 2022

  1. Copy the full SHA
    2348e32 View commit details

Commits on Apr 2, 2022

  1. chore: bump minimist from 1.2.5 to 1.2.6 (#213)

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Apr 2, 2022
    Copy the full SHA
    5c62a04 View commit details

Commits on May 11, 2022

  1. Copy the full SHA
    985e7b4 View commit details
  2. chore: update exports

    antfu committed May 11, 2022
    Copy the full SHA
    1dbc810 View commit details
  3. release v0.26.2

    antfu committed May 11, 2022
    Copy the full SHA
    b4603a6 View commit details

Commits on Jun 5, 2022

  1. Copy the full SHA
    69b13c7 View commit details

Commits on Aug 2, 2022

  1. Copy the full SHA
    e68f3e0 View commit details

Commits on Aug 30, 2022

  1. chore: update deps

    antfu committed Aug 30, 2022
    Copy the full SHA
    2381aca View commit details
  2. chore: move to source-map-js

    antfu committed Aug 30, 2022
    Copy the full SHA
    ff6422b View commit details
  3. chore: improve tree-shaking

    antfu committed Aug 30, 2022
    Copy the full SHA
    45eff49 View commit details
  4. chore: release v0.26.3

    antfu committed Aug 30, 2022
    Copy the full SHA
    abf373f View commit details

Commits on Sep 22, 2022

  1. Copy the full SHA
    04a05bd View commit details
  2. Copy the full SHA
    130794b View commit details
  3. chore: update deps

    antfu committed Sep 22, 2022
    Copy the full SHA
    39e8d29 View commit details
  4. chore: release v0.26.4

    antfu committed Sep 22, 2022
    Copy the full SHA
    226d381 View commit details

Commits on Sep 30, 2022

  1. Copy the full SHA
    45a4921 View commit details
  2. chore: release v0.26.5

    antfu committed Sep 30, 2022
    Copy the full SHA
    ded833e View commit details

Commits on Oct 5, 2022

  1. feat: add update method as safer alternative to overwrite (#212)

    Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
    benmccann and antfu authored Oct 5, 2022
    Copy the full SHA
    9a312e3 View commit details
  2. chore: release v0.26.6

    antfu committed Oct 5, 2022
    Copy the full SHA
    7b45b9b View commit details

Commits on Oct 9, 2022

  1. Copy the full SHA
    01d033e View commit details
  2. chore: release v0.26.7

    antfu committed Oct 9, 2022
    Copy the full SHA
    e9b2ea2 View commit details

Commits on Oct 16, 2022

  1. chore: update readme (#228)

    btea authored Oct 16, 2022
    Copy the full SHA
    634d205 View commit details

Commits on Dec 3, 2022

  1. Merge pull request #215 from sapphi-red/perf/encode-decode

    perf: use `@jridgewell/sourcemap-codec`
    Rich-Harris authored Dec 3, 2022
    Copy the full SHA
    847502c View commit details
  2. chore: release v0.27.0

    Rich-Harris committed Dec 3, 2022
    Copy the full SHA
    c87d5eb View commit details

Commits on Jan 11, 2023

  1. apply publint suggestions

    Rich-Harris committed Jan 11, 2023
    Copy the full SHA
    209dde6 View commit details

Commits on Jan 20, 2023

  1. Put types first in exports (#237)

    benmccann authored Jan 20, 2023
    Copy the full SHA
    f90bf6d View commit details
  2. fix(typings): sourcesContent may contain null (#235)

    Fixes undefined
    AriPerkkio authored Jan 20, 2023
    Copy the full SHA
    c2b652a View commit details

Commits on Feb 4, 2023

  1. feat(x_google_ignoreList): initial support for ignore lists

    This patch introduces support for the `x_google_ignoreList` field
    to the `SourceMap` class. This extension was added to the Source Map
    Revision 3 Proposal[^1] to allow build tools to provide hints to
    debuggers (e.g. browser DevTools) about which files contain library
    or framework, and should thus be ignored by default for the purpose
    of stepping, break on exceptions, and the like[^2].
    
    With this change it's possible for consumers of magic-string's
    `SourceMap` class to thread through and serialize the newly added
    `x_google_ignoreList` field (for example this is needed for rollup
    to support `x_google_ignoreList`).
    
    In a follow up change, we will also add support to the `Bundle`
    class to mark certain sources as ignore listed.
    
    [^1]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k
    [^2]: https://developer.chrome.com/blog/devtools-better-angular-debugging/
    bmeurer committed Feb 4, 2023
    Copy the full SHA
    3c711cd View commit details

Commits on Feb 7, 2023

  1. Copy the full SHA
    c1192c7 View commit details

Commits on Feb 8, 2023

  1. separate lint task

    Rich-Harris committed Feb 8, 2023
    Copy the full SHA
    e6b2866 View commit details
  2. only lint >= 14

    Rich-Harris committed Feb 8, 2023
    Copy the full SHA
    41d4f70 View commit details
  3. Copy the full SHA
    ba774cb View commit details
  4. Merge pull request #236 from Rich-Harris/publint

    apply publint suggestions
    Rich-Harris authored Feb 8, 2023
    Copy the full SHA
    a46f22a View commit details

Commits on Feb 11, 2023

  1. chore: release v0.28.0

    Rich-Harris committed Feb 11, 2023
    Copy the full SHA
    74cba9f View commit details
  2. Merge pull request #240 from bmeurer/feat/x_google_ignoreList

    feat(x_google_ignoreList): initial support for ignore lists
    Rich-Harris authored Feb 11, 2023
    Copy the full SHA
    209c42d View commit details
  3. chore: release v0.29.0

    Rich-Harris committed Feb 11, 2023
    Copy the full SHA
    6032003 View commit details

Commits on Feb 18, 2023

  1. Copy the full SHA
    d4e9c31 View commit details

Commits on Feb 22, 2023

  1. feat: add the ability to ignore-list sources (#243)

    Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
    bmeurer and antfu authored Feb 22, 2023
    Copy the full SHA
    e238f04 View commit details
  2. chore: release v0.30.0

    antfu committed Feb 22, 2023
    Copy the full SHA
    34935df View commit details
Showing with 2,750 additions and 1,294 deletions.
  1. +1 −1 .eslintrc
  2. +5 −1 .github/workflows/test.yml
  3. +135 −0 CHANGELOG.md
  4. +57 −10 README.md
  5. +12 −0 benchmark/data-min.js
  6. +1,075 −0 benchmark/data.js
  7. +87 −0 benchmark/index.mjs
  8. +47 −6 index.d.ts
  9. +753 −1,226 package-lock.json
  10. +32 −19 package.json
  11. +3 −5 rollup.config.js
  12. +12 −3 src/Bundle.js
  13. +10 −5 src/Chunk.js
  14. +130 −8 src/MagicString.js
  15. +16 −8 src/SourceMap.js
  16. +39 −1 test/MagicString.Bundle.js
  17. +36 −0 test/MagicString.SourceMap.js
  18. +300 −1 test/MagicString.js
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 6,
"ecmaVersion": 2020,
"sourceType": "module"
}
}
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ jobs:

strategy:
matrix:
node_version: [10.x, 12.x, 14.x, 16.x]
node_version: [12.x, 14.x, 16.x]
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false

@@ -34,5 +34,9 @@ jobs:
- name: Install
run: npm i

- name: Lint
if: ${{ matrix.node_version != '12.x' }}
run: npm run lint

- name: Test
run: npm test
135 changes: 135 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,138 @@
# [0.30.0](https://github.com/rich-harris/magic-string/compare/v0.29.0...v0.30.0) (2023-02-22)


### Bug Fixes

* `null` is invalid for `sources` and `file` ([#242](https://github.com/rich-harris/magic-string/issues/242)) ([d4e9c31](https://github.com/rich-harris/magic-string/commit/d4e9c31082491cfa177b31ce725c9ce39491d549))


### Features

* add the ability to ignore-list sources ([#243](https://github.com/rich-harris/magic-string/issues/243)) ([e238f04](https://github.com/rich-harris/magic-string/commit/e238f04be31ec9a3e19b18b75bb5d859f9cb2654))



# [0.29.0](https://github.com/rich-harris/magic-string/compare/v0.28.0...v0.29.0) (2023-02-11)


### Features

* **x_google_ignoreList:** initial support for ignore lists ([3c711cd](https://github.com/rich-harris/magic-string/commit/3c711cd56de6c9735f92e41e457353005c2c0d1c))



# [0.28.0](https://github.com/rich-harris/magic-string/compare/v0.27.0...v0.28.0) (2023-02-11)


### Bug Fixes

* **typings:** sourcesContent may contain null ([#235](https://github.com/rich-harris/magic-string/issues/235)) ([c2b652a](https://github.com/rich-harris/magic-string/commit/c2b652a0d353f183ca991d0b59a7ad0250a52735))



# [0.27.0](https://github.com/rich-harris/magic-string/compare/v0.26.7...v0.27.0) (2022-12-03)


### Performance Improvements

* use @jridgewell/sourcemap-codec ([e68f3e0](https://github.com/rich-harris/magic-string/commit/e68f3e05fe6d87acc1c41eddae97fc32e004320b))



## [0.26.7](https://github.com/rich-harris/magic-string/compare/v0.26.6...v0.26.7) (2022-10-09)


### Bug Fixes

* avoid mutating provided options ([#227](https://github.com/rich-harris/magic-string/issues/227)) ([01d033e](https://github.com/rich-harris/magic-string/commit/01d033e6e8630ef1d0482d9a3899f1da2bf933d5))



## [0.26.6](https://github.com/rich-harris/magic-string/compare/v0.26.5...v0.26.6) (2022-10-05)


### Features

* add `update` method as safer alternative to `overwrite` ([#212](https://github.com/rich-harris/magic-string/issues/212)) ([9a312e3](https://github.com/rich-harris/magic-string/commit/9a312e37a02629f7496c6cfcf307832e991669a3))



## [0.26.5](https://github.com/rich-harris/magic-string/compare/v0.26.4...v0.26.5) (2022-09-30)


### Bug Fixes

* update typescript definition file to contain `replaceAll()` ([#224](https://github.com/rich-harris/magic-string/issues/224)) ([45a4921](https://github.com/rich-harris/magic-string/commit/45a49214ba244b906f4d20450debc8edcc06e2a8))



## [0.26.4](https://github.com/rich-harris/magic-string/compare/v0.26.3...v0.26.4) (2022-09-22)


### Features

* fix `.replace()` when searching string, add `.replaceAll()` ([#222](https://github.com/rich-harris/magic-string/issues/222)) ([04a05bd](https://github.com/rich-harris/magic-string/commit/04a05bdc9bf56e00e616a0ae07923fbd9b63fbd0))


### Performance Improvements

* avoiding use of Object.defineProperty in Chunk constructor ([#219](https://github.com/rich-harris/magic-string/issues/219)) ([130794b](https://github.com/rich-harris/magic-string/commit/130794bb8bfd9f21eb1f50c36a1da8eb5443d256))



## [0.26.3](https://github.com/rich-harris/magic-string/compare/v0.26.2...v0.26.3) (2022-08-30)


### Performance Improvements

* delay guess encoded ([#216](https://github.com/rich-harris/magic-string/issues/216)) ([69b13c7](https://github.com/rich-harris/magic-string/commit/69b13c7a09af742e4f31cf419e8f96e6db32ab5a))



## [0.26.2](https://github.com/rich-harris/magic-string/compare/v0.26.1...v0.26.2) (2022-05-11)


### Bug Fixes

* specify types in exports ([#214](https://github.com/rich-harris/magic-string/issues/214)) ([985e7b4](https://github.com/rich-harris/magic-string/commit/985e7b4d8a6fd5911d2ad2e6524999e9198a6b9f))



## [0.26.1](https://github.com/rich-harris/magic-string/compare/v0.26.0...v0.26.1) (2022-03-03)


### Bug Fixes

* **replace:** match replacer function signature with spec ([902541f](https://github.com/rich-harris/magic-string/commit/902541fdff3998e3c957908de10769d2af1a3c70))



# [0.26.0](https://github.com/rich-harris/magic-string/compare/v0.25.9...v0.26.0) (2022-03-03)

## BREAKING CHANGES

* Support of Node.js v10 is dropped. Now `magic-string` requires Node.js v12 or higher. ([#204](https://github.com/Rich-Harris/magic-string/pull/204))
* ESM bundle is now shipped with `.mjs` extension ([#197](https://github.com/Rich-Harris/magic-string/pull/197))

```diff
- "module": "dist/magic-string.es.js",
+ "module": "dist/magic-string.es.mjs",
+ "exports": {
+ "./package.json": "./package.json",
+ ".": {
+ "import": "./dist/magic-string.es.mjs",
+ "require": "./dist/magic-string.cjs.js"
+ }
+ },
```

### Features

* new `hasChanged` method ([#202](https://github.com/rich-harris/magic-string/issues/202)) ([5f2dba7](https://github.com/rich-harris/magic-string/commit/5f2dba72774c444538ed10aa5f2096104cb0b4bb))
* support `replace` method ([#203](https://github.com/rich-harris/magic-string/issues/203)) ([cd74ea2](https://github.com/rich-harris/magic-string/commit/cd74ea2e374f526079ae1a9b9f29bc9cc2fd2ac3))



## [0.25.9](https://github.com/rich-harris/magic-string/compare/v0.25.8...v0.25.9) (2022-03-03)


67 changes: 57 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
alt="license">
</a>

Suppose you have some source code. You want to make some light modifications to it - replacing a few characters here and there, wrapping it with a header and footer, etc - and ideally you'd like to generate a source map at the end of it. You've thought about using something like [recast](https://github.com/benjamn/recast) (which allows you to generate an AST from some JavaScript, manipulate it, and reprint it with a sourcemap without losing your comments and formatting), but it seems like overkill for your needs (or maybe the source code isn't JavaScript).
Suppose you have some source code. You want to make some light modifications to it - replacing a few characters here and there, wrapping it with a header and footer, etc - and ideally you'd like to generate a [source map](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/) at the end of it. You've thought about using something like [recast](https://github.com/benjamn/recast) (which allows you to generate an AST from some JavaScript, manipulate it, and reprint it with a sourcemap without losing your comments and formatting), but it seems like overkill for your needs (or maybe the source code isn't JavaScript).

Your requirements are, frankly, rather niche. But they're requirements that I also have, and for which I made magic-string. It's a small, fast utility for manipulating strings and generating sourcemaps.

@@ -43,10 +43,10 @@ import fs from 'fs'

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

s.overwrite(0, 8, 'answer');
s.update(0, 8, 'answer');
s.toString(); // 'answer = 99'

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

s.prepend('var ').append(';'); // most methods are chainable
@@ -66,10 +66,11 @@ You can pass an options argument:

```js
const s = new MagicString(someCode, {
// both these options will be used if you later
// call `bundle.addSource( s )` - see below
// these options will be used if you later call `bundle.addSource( s )` - see below
filename: 'foo.js',
indentExclusionRanges: [/*...*/]
indentExclusionRanges: [/*...*/],
// market source as ignore in DevTools, see below #Bundling
ignoreList: false
});
```

@@ -117,6 +118,10 @@ The returned sourcemap has two (non-enumerable) methods attached for convenience
code += '\n//# sourceMappingURL=' + map.toUrl();
```

### s.hasChanged()

Indicates if the string has been changed.

### s.indent( prefix[, options] )

Prefixes each line of the string with `prefix`. If `prefix` is not supplied, the indentation will be guessed from the original content, falling back to a single tab character. Returns `this`.
@@ -131,6 +136,10 @@ The `options` argument can have an `exclude` property, which is an array of `[st

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

### s.isEmpty()

Returns true if the resulting source is empty (disregarding white space).

### s.locate( index )

**DEPRECATED** since 0.10 – see [#30](https://github.com/Rich-Harris/magic-string/pull/30)
@@ -139,16 +148,18 @@ The `options` argument can have an `exclude` property, which is an array of `[st

**DEPRECATED** since 0.10 – see [#30](https://github.com/Rich-Harris/magic-string/pull/30)

### s.move( start, end, newIndex )
### s.move( start, end, index )

Moves the characters from `start` and `end` to `index`. Returns `this`.

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

Replaces the characters from `start` to `end` with `content`. The same restrictions as `s.remove()` apply. Returns `this`.
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`.

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.

It may be preferred to use `s.update(...)` instead if you wish to avoid overwriting the appended/prepended content.

### s.prepend( content )

Prepends the string with the specified content. Returns `this`.
@@ -161,6 +172,29 @@ Same as `s.appendLeft(...)`, except that the inserted content will go *before* a

Same as `s.appendRight(...)`, except that the inserted content will go *before* any previous appends or prepends at `index`

### s.replace( regexpOrString, substitution )

String replacement with RegExp or string. When using a RegExp, replacer function is also supported. Returns `this`.

```ts
import MagicString from 'magic-string'

const s = new MagicString(source)

s.replace('foo', 'bar')
s.replace(/foo/g, 'bar')
s.replace(/(\w)(\d+)/g, (_, $1, $2) => $1.toUpperCase() + $2)
```

The differences from [`String.replace`]((https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace)):
- It will always match against the **original string**
- It mutates the magic string state (use `.clone()` to be immutable)

### s.replaceAll( regexpOrString, substitution )

Same as `s.replace`, but replace all matched strings instead of just one.
If `substitution` is a regex, then it must have the global (`g`) flag set, or a `TypeError` is thrown. Matches the behavior of the bultin [`String.property.replaceAll`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll).

### s.remove( start, end )

Removes the characters from `start` to `end` (of the original string, **not** the generated string). Removing the same content twice, or making removals that partially overlap, will cause an error. Returns `this`.
@@ -193,9 +227,13 @@ Trims content matching `charType` (defaults to `\s`, i.e. whitespace) from the e

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

### s.isEmpty()
### s.update( start, end, content[, options] )

Returns true if the resulting source is empty (disregarding white space).
Replaces the characters from `start` to `end` with `content`. The same restrictions as `s.remove()` apply. Returns `this`.

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.

`s.update(start, end, content)` is equivalent to `s.overwrite(start, end, content, { contentOnly: true })`.

## Bundling

@@ -214,6 +252,15 @@ bundle.addSource({
content: new MagicString('console.log( answer )')
});

// Sources can be marked as ignore-listed, which provides a hint to debuggers
// to not step into this code and also don't show the source files depending
// on user preferences.
bundle.addSource({
filename: 'some-3rdparty-library.js',
content: new MagicString('function myLib(){}'),
ignoreList: false // <--
})

// Advanced: a source can include an `indentExclusionRanges` property
// alongside `filename` and `content`. This will be passed to `s.indent()`
// - see documentation above
12 changes: 12 additions & 0 deletions benchmark/data-min.js

Large diffs are not rendered by default.

1,075 changes: 1,075 additions & 0 deletions benchmark/data.js

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions benchmark/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Benchmark from 'benchmark';
import MagicString from '../dist/magic-string.es.mjs';
import fs from 'fs/promises';

Benchmark.support.decompilation = false;

console.log(`node ${process.version}\n`);

function runWithInstance(name, inputs, func, setup) {
const ss = [];
new Benchmark(
name,
{
setup: () => {
for (const [i, input] of inputs.entries()) {
ss[i] = new MagicString(input);
if (setup) {
setup(ss[i]);
}
}
},
fn: () => {
for (const i of inputs.keys()) {
func(ss[i]);
}
}
}
).on('complete', (event) => {
console.log(String(event.target));
}).on('error', (event) => {
console.error(event.target.error);
}).run();
}

async function bench() {
const inputs = await Promise.all(
['data.js', 'data-min.js'].map(
(file) => fs.readFile(new URL(file, import.meta.url), 'utf-8')
)
);

new Benchmark('construct', {
fn: () => {
for (const input of inputs) {
new MagicString(input);
}
}
}).on('complete', (event) => {
console.log(String(event.target));
}).on('error', (event) => {
console.error(event.target.error);
}).run();

runWithInstance('append', inputs, s => {
s.append(';"append";');
});
runWithInstance('indent', inputs, s => {
s.indent();
});

runWithInstance('generateMap (no edit)', inputs, s => {
s.generateMap();
});
runWithInstance('generateMap (edit)', inputs, s => {
s.generateMap();
}, s => {
s.replace(/replacement/g, 'replacement\nReplacement');
});

runWithInstance('generateDecodedMap (no edit)', inputs, s => {
s.generateDecodedMap();
});
runWithInstance('generateDecodedMap (edit)', inputs, s => {
s.generateDecodedMap();
}, s => {
s.replace(/replacement/g, 'replacement\nReplacement');
});

const size = 1000000;
runWithInstance('overwrite', ['a'.repeat(size)], s => {
for (let i = 1; i < size; i+=2) {
s.overwrite(i, i+1, 'b');
}
});
}

bench();
53 changes: 47 additions & 6 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -35,9 +35,10 @@ export type SourceMapSegment =
export interface DecodedSourceMap {
file: string;
sources: string[];
sourcesContent: string[];
sourcesContent: (string | null)[];
names: string[];
mappings: SourceMapSegment[][];
x_google_ignoreList?: number[];
}

export class SourceMap {
@@ -46,9 +47,10 @@ export class SourceMap {
version: number;
file: string;
sources: string[];
sourcesContent: string[];
sourcesContent: (string | null)[];
names: string[];
mappings: string;
x_google_ignoreList?: number[];

/**
* Returns the equivalent of `JSON.stringify(map)`
@@ -63,7 +65,17 @@ export class SourceMap {

export class Bundle {
constructor(options?: BundleOptions);
addSource(source: MagicString | { filename?: string, content: MagicString }): Bundle;
/**
* Adds the specified source to the bundle, which can either be a `MagicString` object directly,
* or an options object that holds a magic string `content` property and optionally provides
* a `filename` for the source within the bundle, as well as an optional `ignoreList` hint
* (which defaults to `false`). The `filename` is used when constructing the source map for the
* bundle, to identify this `source` in the source map's `sources` field. The `ignoreList` hint
* is used to populate the `x_google_ignoreList` extension field in the source map, which is a
* mechanism for tools to signal to debuggers that certain sources should be ignored by default
* (depending on user preferences).
*/
addSource(source: MagicString | { filename?: string, content: MagicString, ignoreList?: boolean }): Bundle;
append(str: string, options?: BundleOptions): Bundle;
clone(): Bundle;
generateMap(options?: SourceMapOptions): SourceMap;
@@ -98,6 +110,11 @@ export interface OverwriteOptions {
contentOnly?: boolean;
}

export interface UpdateOptions {
storeName?: boolean;
overwrite?: boolean;
}

export default class MagicString {
constructor(str: string, options?: MagicStringOptions);
/**
@@ -110,7 +127,7 @@ export default class MagicString {
append(content: string): MagicString;
/**
* Appends the specified content at the index in the original string.
* If a range *ending* with index is subsequently moved, the insert will be moved with it.
* If a range *ending* with index is subsequently moved, the insert will be moved with it.
* See also `s.prependLeft(...)`.
*/
appendLeft(index: number, content: string): MagicString;
@@ -155,15 +172,26 @@ export default class MagicString {
*/
move(start: number, end: number, index: number): MagicString;
/**
* Replaces the characters from `start` to `end` with `content`. The same restrictions as `s.remove()` apply.
* 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.
*
* 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.
*
* It may be preferred to use `s.update(...)` instead if you wish to avoid overwriting the appended/prepended content.
*/
overwrite(start: number, end: number, content: string, options?: boolean | OverwriteOptions): MagicString;
/**
* Prepends the string with the specified content.
* Replaces the characters from `start` to `end` with `content`. The same restrictions as `s.remove()` apply.
*
* 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 determines whether only
* the content is overwritten, or anything that was appended/prepended to the range as well.
*/
update(start: number, end: number, content: string, options?: boolean | UpdateOptions): MagicString;
/**
* Prepends the string with the specified content.
*/
prepend(content: string): MagicString;
/**
@@ -204,6 +232,14 @@ export default class MagicString {
* Removes empty lines from the start and end.
*/
trimLines(): MagicString;
/**
* String replacement with RegExp or string.
*/
replace(regex: RegExp | string, replacement: string | ((substring: string, ...args: any[]) => string)): MagicString;
/**
* Same as `s.replace`, but replace all matched strings instead of just one.
*/
replaceAll(regex: RegExp | string, replacement: string | ((substring: string, ...args: any[]) => string)): MagicString;

lastChar(): string;
lastLine(): string;
@@ -213,6 +249,11 @@ export default class MagicString {
isEmpty(): boolean;
length(): number;

/**
* Indicates if the string has been changed.
*/
hasChanged(): boolean;

original: string;
/**
* Returns the generated string.
1,979 changes: 753 additions & 1,226 deletions package-lock.json

Large diffs are not rendered by default.

51 changes: 32 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "magic-string",
"version": "0.25.9",
"version": "0.30.0",
"description": "Modify strings, generate sourcemaps",
"keywords": [
"string",
@@ -12,10 +12,18 @@
"repository": "https://github.com/rich-harris/magic-string",
"license": "MIT",
"author": "Rich Harris",
"main": "dist/magic-string.cjs.js",
"module": "dist/magic-string.es.js",
"jsnext:main": "dist/magic-string.es.js",
"typings": "index.d.ts",
"main": "./dist/magic-string.cjs.js",
"module": "./dist/magic-string.es.mjs",
"jsnext:main": "./dist/magic-string.es.mjs",
"types": "./index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./index.d.ts",
"import": "./dist/magic-string.es.mjs",
"require": "./dist/magic-string.cjs.js"
}
},
"files": [
"dist/*",
"index.d.ts",
@@ -25,28 +33,33 @@
"build": "rollup -c",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"format": "prettier --single-quote --print-width 100 --use-tabs --write src/*.js src/**/*.js",
"lint": "eslint src test",
"lint": "eslint src test && publint",
"prepare": "npm run build",
"prepublishOnly": "rm -rf dist && npm test",
"prepublishOnly": "npm run lint && rm -rf dist && npm test",
"release": "bumpp -x \"npm run changelog\" --all --commit --tag --push && npm publish",
"pretest": "npm run lint && npm run build",
"pretest": "npm run build",
"test": "mocha",
"bench": "npm run build && node benchmark/index.mjs",
"watch": "rollup -cw"
},
"dependencies": {
"sourcemap-codec": "^1.4.8"
},
"devDependencies": {
"@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-node-resolve": "^14.1.0",
"@rollup/plugin-replace": "^4.0.0",
"bumpp": "^7.1.1",
"benchmark": "^2.1.4",
"bumpp": "^8.2.1",
"conventional-changelog-cli": "^2.2.2",
"eslint": "^7.32.0",
"mocha": "^9.2.1",
"prettier": "^2.5.1",
"rollup": "^2.69.0",
"source-map": "^0.6.1",
"eslint": "^8.23.1",
"mocha": "^10.0.0",
"prettier": "^2.7.1",
"publint": "^0.1.7",
"rollup": "^2.79.1",
"source-map-js": "^1.0.2",
"source-map-support": "^0.5.21"
},
"engines": {
"node": ">=12"
},
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.13"
}
}
8 changes: 3 additions & 5 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import buble from '@rollup/plugin-buble';
import nodeResolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';

const plugins = [
buble({ exclude: 'node_modules/**' }),
nodeResolve(),
replace({ DEBUG: false, preventAssignment: true })
];
@@ -12,10 +10,10 @@ export default [
/* esm */
{
input: 'src/index.js',
external: ['sourcemap-codec'],
external: ['@jridgewell/sourcemap-codec'],
plugins,
output: {
file: 'dist/magic-string.es.js',
file: 'dist/magic-string.es.mjs',
format: 'es',
exports: 'named',
sourcemap: true
@@ -25,7 +23,7 @@ export default [
/* cjs */
{
input: 'src/index-legacy.js',
external: ['sourcemap-codec'],
external: ['@jridgewell/sourcemap-codec'],
plugins,
output: {
file: 'dist/magic-string.cjs.js',
15 changes: 12 additions & 3 deletions src/Bundle.js
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ export default class Bundle {
);
}

['filename', 'indentExclusionRanges', 'separator'].forEach((option) => {
['filename', 'ignoreList', 'indentExclusionRanges', 'separator'].forEach((option) => {
if (!hasOwnProp.call(source, option)) source[option] = source.content[option];
});

@@ -84,6 +84,7 @@ export default class Bundle {

generateDecodedMap(options = {}) {
const names = [];
let x_google_ignoreList = undefined;
this.sources.forEach((source) => {
Object.keys(source.content.storedNames).forEach((name) => {
if (!~names.indexOf(name)) names.push(name);
@@ -141,10 +142,17 @@ export default class Bundle {
if (magicString.outro) {
mappings.advance(magicString.outro);
}

if (source.ignoreList && sourceIndex !== -1) {
if (x_google_ignoreList === undefined) {
x_google_ignoreList = [];
}
x_google_ignoreList.push(sourceIndex);
}
});

return {
file: options.file ? options.file.split(/[/\\]/).pop() : null,
file: options.file ? options.file.split(/[/\\]/).pop() : undefined,
sources: this.uniqueSources.map((source) => {
return options.file ? getRelativePath(options.file, source.filename) : source.filename;
}),
@@ -153,6 +161,7 @@ export default class Bundle {
}),
names,
mappings: mappings.raw,
x_google_ignoreList,
};
}

@@ -164,7 +173,7 @@ export default class Bundle {
const indentStringCounts = {};

this.sources.forEach((source) => {
const indentStr = source.content.indentStr;
const indentStr = source.content._getRawIndentString();

if (indentStr === null) return;

15 changes: 10 additions & 5 deletions src/Chunk.js
Original file line number Diff line number Diff line change
@@ -11,11 +11,16 @@ export default class Chunk {
this.storeName = false;
this.edited = false;

// we make these non-enumerable, for sanity while debugging
Object.defineProperties(this, {
previous: { writable: true, value: null },
next: { writable: true, value: null },
});
if (DEBUG) {
// we make these non-enumerable, for sanity while debugging
Object.defineProperties(this, {
previous: { writable: true, value: null },
next: { writable: true, value: null },
});
} else {
this.previous = null;
this.next = null;
}
}

appendLeft(content) {
138 changes: 130 additions & 8 deletions src/MagicString.js
Original file line number Diff line number Diff line change
@@ -33,7 +33,8 @@ export default class MagicString {
indentExclusionRanges: { writable: true, value: options.indentExclusionRanges },
sourcemapLocations: { writable: true, value: new BitSet() },
storedNames: { writable: true, value: {} },
indentStr: { writable: true, value: guessIndent(string) },
indentStr: { writable: true, value: undefined },
ignoreList: { writable: true, value: options.ignoreList },
});

if (DEBUG) {
@@ -163,19 +164,32 @@ export default class MagicString {
});

return {
file: options.file ? options.file.split(/[/\\]/).pop() : null,
sources: [options.source ? getRelativePath(options.file || '', options.source) : null],
sourcesContent: options.includeContent ? [this.original] : [null],
file: options.file ? options.file.split(/[/\\]/).pop() : undefined,
sources: [options.source ? getRelativePath(options.file || '', options.source) : (options.file || '')],
sourcesContent: options.includeContent ? [this.original] : undefined,
names,
mappings: mappings.raw,
x_google_ignoreList: this.ignoreList ? [sourceIndex] : undefined
};
}

generateMap(options) {
return new SourceMap(this.generateDecodedMap(options));
}

_ensureindentStr() {
if (this.indentStr === undefined) {
this.indentStr = guessIndent(this.original);
}
}

_getRawIndentString() {
this._ensureindentStr();
return this.indentStr;
}

getIndentString() {
this._ensureindentStr();
return this.indentStr === null ? '\t' : this.indentStr;
}

@@ -187,7 +201,10 @@ export default class MagicString {
indentStr = undefined;
}

indentStr = indentStr !== undefined ? indentStr : this.indentStr || '\t';
if (indentStr === undefined) {
this._ensureindentStr();
indentStr = this.indentStr || '\t';
}

if (indentStr === '') return this; // noop

@@ -334,6 +351,11 @@ export default class MagicString {
}

overwrite(start, end, content, options) {
options = options || {};
return this.update(start, end, content, { ...options, overwrite: !options.contentOnly });
}

update(start, end, content, options) {
if (typeof content !== 'string') throw new TypeError('replacement content must be a string');

while (start < 0) start += this.original.length;
@@ -361,11 +383,15 @@ export default class MagicString {
options = { storeName: true };
}
const storeName = options !== undefined ? options.storeName : false;
const contentOnly = options !== undefined ? options.contentOnly : false;
const overwrite = options !== undefined ? options.overwrite : false;

if (storeName) {
const original = this.original.slice(start, end);
Object.defineProperty(this.storedNames, original, { writable: true, value: true, enumerable: true });
Object.defineProperty(this.storedNames, original, {
writable: true,
value: true,
enumerable: true,
});
}

const first = this.byStart[start];
@@ -381,7 +407,7 @@ export default class MagicString {
chunk.edit('', false);
}

first.edit(content, storeName, contentOnly);
first.edit(content, storeName, !overwrite);
} else {
// must be inserting at the end
const newChunk = new Chunk(start, end, '').edit(content, storeName);
@@ -712,4 +738,100 @@ export default class MagicString {
this.trimStartAborted(charType);
return this;
}

hasChanged() {
return this.original !== this.toString();
}

_replaceRegexp(searchValue, replacement) {
function getReplacement(match, str) {
if (typeof replacement === 'string') {
return replacement.replace(/\$(\$|&|\d+)/g, (_, i) => {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_a_parameter
if (i === '$') return '$';
if (i === '&') return match[0];
const num = +i;
if (num < match.length) return match[+i];
return `$${i}`;
});
} else {
return replacement(...match, match.index, str, match.groups);
}
}
function matchAll(re, str) {
let match;
const matches = [];
while ((match = re.exec(str))) {
matches.push(match);
}
return matches;
}
if (searchValue.global) {
const matches = matchAll(searchValue, this.original);
matches.forEach((match) => {
if (match.index != null)
this.overwrite(
match.index,
match.index + match[0].length,
getReplacement(match, this.original)
);
});
} else {
const match = this.original.match(searchValue);
if (match && match.index != null)
this.overwrite(
match.index,
match.index + match[0].length,
getReplacement(match, this.original)
);
}
return this;
}

_replaceString(string, replacement) {
const { original } = this;
const index = original.indexOf(string);

if (index !== -1) {
this.overwrite(index, index + string.length, replacement);
}

return this;
}

replace(searchValue, replacement) {
if (typeof searchValue === 'string') {
return this._replaceString(searchValue, replacement);
}

return this._replaceRegexp(searchValue, replacement);
}

_replaceAllString(string, replacement) {
const { original } = this;
const stringLength = string.length;
for (
let index = original.indexOf(string);
index !== -1;
index = original.indexOf(string, index + stringLength)
) {
this.overwrite(index, index + stringLength, replacement);
}

return this;
}

replaceAll(searchValue, replacement) {
if (typeof searchValue === 'string') {
return this._replaceAllString(searchValue, replacement);
}

if (!searchValue.global) {
throw new TypeError(
'MagicString.prototype.replaceAll called with a non-global RegExp argument'
);
}

return this._replaceRegexp(searchValue, replacement);
}
}
24 changes: 16 additions & 8 deletions src/SourceMap.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { encode } from 'sourcemap-codec';
import { encode } from '@jridgewell/sourcemap-codec';

let btoa = () => {
throw new Error('Unsupported environment: `window.btoa` or `Buffer` should be supported.');
};
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
btoa = (str) => window.btoa(unescape(encodeURIComponent(str)));
} else if (typeof Buffer === 'function') {
btoa = (str) => Buffer.from(str, 'utf-8').toString('base64');
function getBtoa () {
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
return (str) => window.btoa(unescape(encodeURIComponent(str)));
} else if (typeof Buffer === 'function') {
return (str) => Buffer.from(str, 'utf-8').toString('base64');
} else {
return () => {
throw new Error('Unsupported environment: `window.btoa` or `Buffer` should be supported.');
};
}
}

const btoa = /*#__PURE__*/ getBtoa();

export default class SourceMap {
constructor(properties) {
this.version = 3;
@@ -17,6 +22,9 @@ export default class SourceMap {
this.sourcesContent = properties.sourcesContent;
this.names = properties.names;
this.mappings = encode(properties.mappings);
if (typeof properties.x_google_ignoreList !== 'undefined') {
this.x_google_ignoreList = properties.x_google_ignoreList;
}
}

toString() {
40 changes: 39 additions & 1 deletion test/MagicString.Bundle.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const assert = require('assert');
const SourceMapConsumer = require('source-map').SourceMapConsumer;
const SourceMapConsumer = require('source-map-js').SourceMapConsumer;
const MagicString = require('../');

require('source-map-support').install();
@@ -27,17 +27,32 @@ describe('MagicString.Bundle', () => {
assert.strictEqual(b.sources[0].indentExclusionRanges, array);
});

it('should accept ignore-list hint', () => {
const b = new MagicString.Bundle();
const foo = new MagicString('foo', {filename: 'foo.js'});
const bar = new MagicString('bar', {filename: 'bar.js'});

b.addSource({content: foo, ignoreList: true});
b.addSource({content: bar, ignoreList: false});
assert.strictEqual(b.sources[0].content, foo);
assert.strictEqual(b.sources[0].ignoreList, true);
assert.strictEqual(b.sources[1].content, bar);
assert.strictEqual(b.sources[1].ignoreList, false);
});

it('respects MagicString init options with { content: source }', () => {
const b = new MagicString.Bundle();
const array = [];
const source = new MagicString('abcdefghijkl', {
filename: 'foo.js',
ignoreList: false,
indentExclusionRanges: array
});

b.addSource({ content: source });
assert.strictEqual(b.sources[0].content, source);
assert.strictEqual(b.sources[0].filename, 'foo.js');
assert.strictEqual(b.sources[0].ignoreList, false);
assert.strictEqual(b.sources[0].indentExclusionRanges, array);
});
});
@@ -345,6 +360,29 @@ describe('MagicString.Bundle', () => {
assert.equal(loc.source, 'three.js');
});

it('should generate x_google_ignoreList correctly', () => {
const b = new MagicString.Bundle();

const one = new MagicString('function one () {}', { filename: 'one.js' });
const two = new MagicString('function two () {}', { filename: 'two.js' });
const three = new MagicString('function three () {}', { filename: 'three.js' });
const four = new MagicString('function four () {}', { filename: 'four.js' });

b.addSource({ content: one, ignoreList: false });
b.addSource({ content: two, ignoreList: true });
b.addSource({ content: three, ignoreList: true });
b.addSource({ content: four });

const map = b.generateMap({
file: 'output.js'
});

assert.deepEqual(map.x_google_ignoreList, [
map.sources.indexOf('two.js'),
map.sources.indexOf('three.js')
]);
});

it('handles prepended content', () => {
const b = new MagicString.Bundle();

36 changes: 36 additions & 0 deletions test/MagicString.SourceMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const assert = require('assert');
const MagicString = require('../');

require('source-map-support').install();

describe('MagicString.SourceMap', () => {
describe('options', () => {
it('preserves ignore list information', () => {
const map = new MagicString.SourceMap({
file: 'foo.min.js',
sources: ['foo.js'],
sourcesContent: ['42'],
names: [],
mappings: [[0, 0]],
x_google_ignoreList: [0]
});

assert.deepEqual(map.x_google_ignoreList, [0]);
});
});

describe('toString', () => {
it('serializes ignore list information', () => {
const map = new MagicString.SourceMap({
file: 'foo.min.js',
sources: ['foo.js'],
sourcesContent: ['42'],
names: [],
mappings: [[0, 0]],
x_google_ignoreList: [0]
});

assert.equal(map.toString(), '{"version":3,"file":"foo.min.js","sources":["foo.js"],"sourcesContent":["42"],"names":[],"mappings":"AAAAA,AAAAA","x_google_ignoreList":[0]}');
});
});
});
301 changes: 300 additions & 1 deletion test/MagicString.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const assert = require('assert');
const SourceMapConsumer = require('source-map').SourceMapConsumer;
const SourceMapConsumer = require('source-map-js').SourceMapConsumer;
const MagicString = require('./utils/IntegrityCheckingMagicString');

require('source-map-support').install();
@@ -13,6 +13,12 @@ describe('MagicString', () => {

assert.equal(s.filename, 'foo.js');
});

it('stores ignore-list hint', () => {
const s = new MagicString('abc', { ignoreList: true });

assert.equal(s.ignoreList, true);
});
});

describe('append', () => {
@@ -418,6 +424,16 @@ describe('MagicString', () => {
const map = s.generateMap();
assert.equal(map.mappings, 'IAAA');
});

it('generates x_google_ignoreList', () => {
const s = new MagicString('function foo(){}', {
ignoreList: true
});

const map = s.generateMap({ source: 'foo.js' });
assert.deepEqual(map.sources, ['foo.js']);
assert.deepEqual(map.x_google_ignoreList, [0]);
});
});

describe('getIndentString', () => {
@@ -856,6 +872,141 @@ describe('MagicString', () => {
});
});

describe('update', () => {
it('should replace characters', () => {
const s = new MagicString('abcdefghijkl');

s.update(5, 8, 'FGH');
assert.equal(s.toString(), 'abcdeFGHijkl');
});

it('should throw an error if overlapping replacements are attempted', () => {
const s = new MagicString('abcdefghijkl');

s.update(7, 11, 'xx');

assert.throws(() => s.update(8, 12, 'yy'), /Cannot split a chunk that has already been edited/);

assert.equal(s.toString(), 'abcdefgxxl');

s.update(6, 12, 'yes');
assert.equal(s.toString(), 'abcdefyes');
});

it('should allow contiguous but non-overlapping replacements', () => {
const s = new MagicString('abcdefghijkl');

s.update(3, 6, 'DEF');
assert.equal(s.toString(), 'abcDEFghijkl');

s.update(6, 9, 'GHI');
assert.equal(s.toString(), 'abcDEFGHIjkl');

s.update(0, 3, 'ABC');
assert.equal(s.toString(), 'ABCDEFGHIjkl');

s.update(9, 12, 'JKL');
assert.equal(s.toString(), 'ABCDEFGHIJKL');
});

it('does not replace zero-length inserts at update start location', () => {
const s = new MagicString('abcdefghijkl');

s.remove(0, 6);
s.appendLeft(6, 'DEF');
s.update(6, 9, 'GHI');
assert.equal(s.toString(), 'DEFGHIjkl');
});

it('replaces zero-length inserts inside update with overwrite option', () => {
const s = new MagicString('abcdefghijkl');

s.appendLeft(6, 'XXX');
s.update(3, 9, 'DEFGHI', { overwrite: true });
assert.equal(s.toString(), 'abcDEFGHIjkl');
});

it('replaces non-zero-length inserts inside update', () => {
const s = new MagicString('abcdefghijkl');

s.update(3, 4, 'XXX');
s.update(3, 5, 'DE');
assert.equal(s.toString(), 'abcDEfghijkl');

s.update(7, 8, 'YYY');
s.update(6, 8, 'GH');
assert.equal(s.toString(), 'abcDEfGHijkl');
});

it('should return this', () => {
const s = new MagicString('abcdefghijkl');
assert.strictEqual(s.update(3, 4, 'D'), s);
});

it('should disallow updating zero-length ranges', () => {
const s = new MagicString('x');
assert.throws(() => s.update(0, 0, 'anything'), /Cannot overwrite a zero-length range use appendLeft or prependRight instead/);
});

it('should throw when given non-string content', () => {
const s = new MagicString('');
assert.throws(() => s.update(0, 1, []), TypeError);
});

it('replaces interior inserts with overwrite option', () => {
const s = new MagicString('abcdefghijkl');

s.appendLeft(1, '&');
s.prependRight(1, '^');
s.appendLeft(3, '!');
s.prependRight(3, '?');
s.update(1, 3, '...', { overwrite: true });
assert.equal(s.toString(), 'a&...?defghijkl');
});

it('preserves interior inserts with `contentOnly: true`', () => {
const s = new MagicString('abcdefghijkl');

s.appendLeft(1, '&');
s.prependRight(1, '^');
s.appendLeft(3, '!');
s.prependRight(3, '?');
s.update(1, 3, '...', { contentOnly: true });
assert.equal(s.toString(), 'a&^...!?defghijkl');
});

it('disallows overwriting partially overlapping moved content', () => {
const s = new MagicString('abcdefghijkl');

s.move(6, 9, 3);
assert.throws(() => s.update(5, 7, 'XX'), /Cannot overwrite across a split point/);
});

it('disallows overwriting fully surrounding content moved away', () => {
const s = new MagicString('abcdefghijkl');

s.move(6, 9, 3);
assert.throws(() => s.update(4, 11, 'XX'), /Cannot overwrite across a split point/);
});

it('disallows overwriting fully surrounding content moved away even if there is another split', () => {
const s = new MagicString('abcdefghijkl');

s.move(6, 9, 3);
s.appendLeft(5, 'foo');
assert.throws(() => s.update(4, 11, 'XX'), /Cannot overwrite across a split point/);
});

it('allows later insertions at the end with overwrite option', () => {
const s = new MagicString('abcdefg');

s.appendLeft(4, '(');
s.update(2, 7, '', { overwrite: true });
s.appendLeft(7, 'h');
assert.equal(s.toString(), 'abh');
});
});

describe('prepend', () => {
it('should prepend content', () => {
const s = new MagicString('abcdefghijkl');
@@ -1277,4 +1428,152 @@ describe('MagicString', () => {
assert.equal(s.lastLine(), '//lastline');
});
});

describe('hasChanged', () => {
it('should works', () => {
const s = new MagicString(' abcde fghijkl ');

assert.ok(!s.hasChanged());

assert.ok(s.clone().prepend(' ').hasChanged());
assert.ok(s.clone().overwrite(1, 2, 'b').hasChanged());
assert.ok(s.clone().remove(1, 6).hasChanged());

s.trim();

assert.ok(s.hasChanged());

const clone = s.clone();

assert.ok(clone.hasChanged());
});
});

describe('replace', () => {
it('works with string replace', () => {
const code = '1 2 1 2';
const s = new MagicString(code);

s.replace('2', '3');

assert.strictEqual(s.toString(), '1 3 1 2');
});

it('Should not treat string as regexp', () => {
assert.strictEqual(
new MagicString('1234').replace('.', '*').toString(),
'1234'
);
});

it('Should use substitution directly', () => {
assert.strictEqual(
new MagicString('11').replace('1', '$0$1').toString(),
'$0$11'
);
});

it('Should not search back', () => {
assert.strictEqual(
new MagicString('122121').replace('12', '21').toString(),
'212121'
);
});

it('works with global regex replace', () => {
const s = new MagicString('1 2 3 4 a b c');

s.replace(/(\d)/g, 'xx$1$10');

assert.strictEqual(s.toString(), 'xx1$10 xx2$10 xx3$10 xx4$10 a b c');
});

it('works with global regex replace $$', () => {
const s = new MagicString('1 2 3 4 a b c');

s.replace(/(\d)/g, '$$');

assert.strictEqual(s.toString(),'$ $ $ $ a b c');
});

it('works with global regex replace function', () => {
const code = 'hey this is magic';
const s = new MagicString(code);

s.replace(/(\w)(\w+)/g, (_, $1, $2) => `${$1.toUpperCase()}${$2}`);

assert.strictEqual(s.toString(),'Hey This Is Magic');
});

it('replace function offset', () => {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_a_parameter
function replacer(match, p1, p2, p3, offset, string, groups) {
// p1 is nondigits, p2 digits, and p3 non-alphanumerics
return [match, p1, p2, p3, offset, string, groups].join(' - ');
}
const code = 'abc12345#$*%';
const regex = /([^\d]*)(\d*)([^\w]*)/;
assert.strictEqual(
code.replace(regex, replacer),
new MagicString(code).replace(regex, replacer).toString()
);
});
});

describe('replaceAll', () => {
it('works with string replace', () => {
assert.strictEqual(
new MagicString('1212').replaceAll('2', '3').toString(),
'1313',
);
});

it('Should not treat string as regexp', () => {
assert.strictEqual(
new MagicString('1234').replaceAll('.', '*').toString(),
'1234'
);
});

it('Should use substitution directly', () => {
assert.strictEqual(
new MagicString('11').replaceAll('1', '$0$1').toString(),
'$0$1$0$1'
);
});

it('Should not search back', () => {
assert.strictEqual(
new MagicString('121212').replaceAll('12', '21').toString(),
'212121'
);
});

it('global regex result the same as .replace', () => {
assert.strictEqual(
new MagicString('1 2 3 4 a b c').replaceAll(/(\d)/g, 'xx$1$10').toString(),
new MagicString('1 2 3 4 a b c').replace(/(\d)/g, 'xx$1$10').toString(),
);

assert.strictEqual(
new MagicString('1 2 3 4 a b c').replaceAll(/(\d)/g, '$$').toString(),
new MagicString('1 2 3 4 a b c').replace(/(\d)/g, '$$').toString(),
);

assert.strictEqual(
new MagicString('hey this is magic').replaceAll(/(\w)(\w+)/g, (_, $1, $2) => `${$1.toUpperCase()}${$2}`).toString(),
new MagicString('hey this is magic').replace(/(\w)(\w+)/g, (_, $1, $2) => `${$1.toUpperCase()}${$2}`).toString(),
);
});

it('rejects with non-global regexp', () => {
assert.throws(
() => new MagicString('123').replaceAll(/./, ''),
{
name: 'TypeError',
message: 'MagicString.prototype.replaceAll called with a non-global RegExp argument',
},
);
});
});
});