Git Product home page Git Product logo

magic-string's Introduction

magic-string

build status npm version license

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 (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.

Installation

magic-string works in both node.js and browser environments. For node, install with npm:

npm i magic-string

To use in browser, grab the magic-string.umd.js file and add it to your page:

<script src='magic-string.umd.js'></script>

(It also works with various module systems, if you prefer that sort of thing - it has a dependency on vlq.)

Usage

These examples assume you're in node.js, or something similar:

import MagicString from 'magic-string';
import fs from 'fs'

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

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

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
s.toString(); // 'var answer = 42;'

const map = s.generateMap({
  source: 'source.js',
  file: 'converted.js.map',
  includeContent: true
}); // generates a v3 sourcemap

fs.writeFileSync('converted.js', s.toString());
fs.writeFileSync('converted.js.map', map.toString());

You can pass an options argument:

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

Methods

s.addSourcemapLocation( index )

Adds the specified character index (with respect to the original string) to sourcemap mappings, if hires is false (see below).

s.append( content )

Appends the specified content to the end of the string. Returns this.

s.appendLeft( index, content )

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. Returns this. See also s.prependLeft(...).

s.appendRight( index, content )

Appends the specified content at the index in the original string. If a range starting with index is subsequently moved, the insert will be moved with it. Returns this. See also s.prependRight(...).

s.clone()

Does what you'd expect.

s.generateDecodedMap( options )

Generates a sourcemap object with raw mappings in array form, rather than encoded as a string. See generateMap documentation below for options details. Useful if you need to manipulate the sourcemap further, but most of the time you will use generateMap instead.

s.generateMap( options )

Generates a version 3 sourcemap. All options are, well, optional:

  • file - the filename where you plan to write the sourcemap
  • source - the filename of the file containing the original source
  • includeContent - whether to include the original content in the map's sourcesContent array
  • hires - whether the mapping should be high-resolution. Hi-res mappings map every single character, meaning (for example) your devtools will always be able to pinpoint the exact location of function calls and so on. With lo-res mappings, devtools may only be able to identify the correct line - but they're quicker to generate and less bulky. You can also set "boundary" to generate a semi-hi-res mappings segmented per word boundary instead of per character, suitable for string semantics that are separated by words. If sourcemap locations have been specified with s.addSourcemapLocation(), they will be used here.

The returned sourcemap has two (non-enumerable) methods attached for convenience:

  • toString - returns the equivalent of JSON.stringify(map)
  • toUrl - returns a DataURI containing the sourcemap. Useful for doing this sort of thing:
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.

The options argument can have an exclude property, which is an array of [start, end] character ranges. These ranges will be excluded from the indentation - useful for (e.g.) multiline strings.

s.insertLeft( index, content )

DEPRECATED since 0.17 – use s.appendLeft(...) instead

s.insertRight( index, content )

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

s.locateOrigin( index )

DEPRECATED since 0.10 – see #30

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, 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.

s.prependLeft ( index, content )

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

s.prependRight ( index, content )

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.

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:

  • 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 builtin String.property.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.

s.reset( start, end )

Resets the characters from start to end (of the original string, not the generated string). It can be used to restore previously removed characters and discard unwanted changes.

s.slice( start, end )

Returns the content of the generated string that corresponds to the slice between start and end of the original string. Throws error if the indices are for characters that were already removed.

s.snip( start, end )

Returns a clone of s, with all content before the start and end characters of the original string removed.

s.toString()

Returns the generated string.

s.trim([ charType ])

Trims content matching charType (defaults to \s, i.e. whitespace) from the start and end. Returns this.

s.trimStart([ charType ])

Trims content matching charType (defaults to \s, i.e. whitespace) from the start. Returns this.

s.trimEnd([ charType ])

Trims content matching charType (defaults to \s, i.e. whitespace) from the end. Returns this.

s.trimLines()

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

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

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

To concatenate several sources, use MagicString.Bundle:

const bundle = new MagicString.Bundle();

bundle.addSource({
  filename: 'foo.js',
  content: new MagicString('var answer = 42;')
});

bundle.addSource({
  filename: 'bar.js',
  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

bundle.indent() // optionally, pass an indent string, otherwise it will be guessed
  .prepend('(function () {\n')
  .append('}());');

bundle.toString();
// (function () {
//   var answer = 42;
//   console.log( answer );
// }());

// options are as per `s.generateMap()` above
const map = bundle.generateMap({
  file: 'bundle.js',
  includeContent: true,
  hires: true
});

As an alternative syntax, if you a) don't have filename or indentExclusionRanges options, or b) passed those in when you used new MagicString(...), you can simply pass the MagicString instance itself:

const bundle = new MagicString.Bundle();
const source = new MagicString(someCode, {
  filename: 'foo.js'
});

bundle.addSource(source);

License

MIT

magic-string's People

Contributors

alangpierce avatar andarist avatar antfu avatar avivahl avatar benmccann avatar bluwy avatar bmeurer avatar btea avatar dependabot[bot] avatar dummdidumm avatar eventualbuddha avatar greenkeeperio-bot avatar guybedford avatar kermanx avatar kristoferbaxter avatar kzc avatar leebyron avatar lemmabit avatar marvinhagemeister avatar mattiasbuelens avatar mihaip avatar mourner avatar mvolfik avatar poyoho avatar pravi avatar rich-harris avatar sapphi-red avatar tal500 avatar tricknotes avatar trysound avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

magic-string's Issues

feat: Allow to edit sourcemap to move caret to a correct place.

Originally evanw/esbuild#1886

Abstract
Sourcemap is about strings, it is not strictly bound to some language. However, tools that processing sourcemaps are expecting some mapping rules to be followed. If we can move the corresponding caret of source code in the generated code, we may be able to follow those rules.

Reproduction

  1. Consider we have a file written: (the | represents our caret).

    |<div>hello</div>

  2. We edit that source into the form export default templ(…).

    export default templ(`|<div>hello</div>`)

  3. Now something bad happens: if templ throws any error or just print something to the console, Chrome, Firefox and node won't generate correct location.

    This is easy to explain: the runtime captures an error thrown from templ, it then look for the source location from the sourcemap. However, it can not find a location because it did not find a caret around the templ token, either at left ( |templ ) or at right ( templ| ).

  4. The solution is moving that caret to the left of templ, or even more left to export. Because the runtime has the whole AST, it knows where to stop searching.

API Advice
I currently have no good design to this feature. Ideally the carets may increase if we split the input into parts. It's not obvious to find a way to manipulate them.

incorrect insert order for `s.insertRight(s.original.length, str)`

Observed when researching #109:

version: [email protected]

$ cat test_insertRight.js

var magic = require('magic-string');

var s = new magic('0123456789');

var index = 0;
s.insertRight(index, 'a');
s.insertRight(index, 'b');
s.insertRight(index, 'c');

index = 5;
s.insertRight(index, 'd');
s.insertRight(index, 'e');
s.insertRight(index, 'f');

index = 10;
s.insertRight(index, 'g');
s.insertRight(index, 'h');
s.insertRight(index, 'i');

console.log(s.toString());

Produces incorrect result:

$ node ./test_insertRight.js

cba01234fed56789ghi

Should be:

cba01234fed56789ihg

Fix:

--- a/src/MagicString.js
+++ b/src/MagicString.js
@@ -234,7 +234,7 @@ MagicString.prototype = {
                if ( chunk ) {
                        chunk.prepend( content );
                } else {
-                       this.outro += content;
+                       this.outro = content + this.outro;
                }
 
                if ( DEBUG ) this.stats.timeEnd( 'insertRight' );

This would make the behavior consistent with chunk.prepend( content );.

The fix probably should result in a major version change as code in the wild may rely on this inconsistent behavior.

ChainAlert: new npm maintainer has published version 0.25.8 of package magic-string

Dear magic-string maintainers,
Thank you for your contribution to the open-source community.

We've noticed that antfu, a new maintainer, just published version 0.25.8 of magic-string to npm.

As part of our efforts to fight software supply chain attacks, we would like to verify this release is known and intended, and not a result of an unauthorized activity.

This issue was automatically created by ChainAlert.
If you find this behavior legitimate, kindly close and ignore this issue. Read more

badge

test/mocha.opts seems wrong

While packaging version 0.25.7 for Debian, I had to patch out test/mocha.opts, as it's using a deprecated switch.

Without that file, tests run.

Bundle mappings and source mutation

Does the generateMap method of MagicString.Bundle ignore changes made to any of the MagicString instances added via addSource? In other words, do I need to rewrap sources before passing them?

const source = new MagicString('...');
source.append('...');

// Can I do this?
bundle.addSource({ filename: 'foo.js', content: source });

// Or do I need to do this?
bundle.addSource({
  filename: 'foo.js',
  content: new MagicString(source.toString()),
});

The first way would be ideal. :)

Also, the bundle's sourcemap should map back to the original source, not the mutated source.

content being set to '"undefined"' in rollup context

I'm sorry I don't have a small reproducable case, but when when using rollup with rollup-plugin-commonjs and importing graphql, there is some weird things going on this line:

if (chunk.edited && chunk.content.length) {

See the screenshot here:

rollup/rollup#1859 (comment)

Essentially, in some previous code magicstring set content to the string 'undefined' (maybe here?):

chunk.content = chunk.content.replace(pattern, replacer);

How do I exchange two ranges?

For example, I want to transform foo-bar to bar-foo. I've tried using .move but the bar chunk is lost:

const MagicString = require("magic-string");
const s = new MagicString("foo-bar");
s.move(0, 3, 7);
s.move(4, 7, 0);
console.log(s.toString()); // "-foo"

Using move() does work when two ranges are concatenated:

const MagicString = require("magic-string");
const s = new MagicString("foobar");
s.move(0, 3, 6);
s.move(3, 6, 0);
console.log(s.toString()); // "barfoo"

But then another problem is that I can't insert text between two ranges. It would be moved together.

const MagicString = require("magic-string");
const s = new MagicString("foobar");
s.move(0, 3, 6);
s.move(3, 6, 0);
s.appendRight(3, "-");
console.log(s.toString()); // "-barfoo"

Unexpected `.replace` behavior

> new (require('magic-string'))('foo+bar').replace('foo\\.bar', 'replaced').toString()
'foo+bar'
> new (require('magic-string'))('foo+bar').replace('foo.bar', 'replaced').toString()
'replaced'

This behavior is unexpected to me. Why don't we use String#indexOf instead of String#match?

`move` needs an affinity (prepend/append ; left/right)

Consider the following example: (https://jsfiddle.net/0eojnd8f/1/)

let s;
s = new MagicString("ABABAB"); // "ABABAB"
s.move(2,3,1); // "AABBAB"
s.move(4,5,1); // "AAABBB"
s.move(3,4,2); // "ABAABB"
s.move(5,6,2); // "ABBAAB"
s = new MagicString("142536"); // "142536"
s.move(2,3,1); // "124536"
s.move(4,5,1); // "123456"
s.move(3,4,2); // "152346"
s.move(5,6,2); // "156234"

I want to group all the As and Bs together, by moving the 2nd/3rd occurence after the first one. However this does not really work as intended.

The other way around, when I move the 1st/2nd before the 3rd one, things work:

s = new MagicString("142536"); // "142536"
s.move(0,1,4); // "425136"
s.move(2,3,4); // "451236"
s.move(1,2,5); // "512346"
s.move(3,4,5); // "123456"

This is a bit weird and unintuitive :-(

overwrite method can cause state corruption

Follow-up from #113, where an optimization commit was causing the decaffeinate build to fail.

I tracked down the issue and found two problems with the code change:

  • In the first.next = last.next; line, it might end up causing first.next to point to null. In that case, this.lastChunk needs to be updated. In the test case below, this.lastChunk ends up pointing to an object that isn't actually the last chunk.
  • The byStart and byEnd maps are never updated to remove the old chunks, so later operations will find those unreferenced chunks and insert into them. This basically means that later insertions at certain points are just lost.

Here's a test case that worked before and fails now:

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

			s.appendLeft(4, '(');
			s.overwrite( 2, 7, '' );
			s.appendLeft(7, 'h');
			console.log(`s.toString is ${s.toString()}`);
			assert.equal( s.toString(), 'abh' );
		});

I was thinking of writing up a fix, but the code seems a bit delicate, so probably @Rich-Harris is better-qualified to come up with a confident fix that updates all relevant state. It might be best to just revert the optimization if the added complexity/risk isn't worth it.

Feature request: inputSourceMap option

As far as I understand, for now it's not possible to pass an input sourcemap to the constructor in order to get it modified by the library and return the modified version.

Best practice for controlling order of multiple insertions at one position

In building decaffeinate I've run into an issue where the order of strings inserted at the same position is not always what I'd like it to be. For example, given the CoffeeScript string a b: c I'd like to turn it into the JavaScript string a({b: c});. This might be accomplished by visiting various AST node types in turn and inserting or replacing characters as required. Here's an example:

REPLACE 1...2 "("
INSERT 6 ")"
INSERT 2 "{"
INSERT 6 "}"
INSERT 6 ";"

This would be represented using MagicString's API like so:

> new MagicString('a b: c').replace(1, 2, '(').insert(6, ')').insert(2, '{').insert(6, '}').insert(6, ';').toString()
'a({b: c)};'

As you can see, the resulting string is not correct. Do you have any suggestions for achieving the desired result here? I'm not tied to the sequence of API calls above.

[question] How to translate variables to sourcemaps?

Hi,

I'm trying to create the source maps for an xml type of language I'm converting to javascript and it works okay. The only thing I can't seem to be able to do is to be able to inspect the variables in the original source.
I use .overwrite to replace the variable name and, when I step through the code, I can't inspect the content of the variable.

Exemple:

Original:
<whatever value="1">

After MagicStings:
const whateverVariable0 = 1;

The process is:
remove '<'
overwrite 'whatever' with 'whateverVariable0'
remove value="
prepend = before 1
remove ">

The sourcemaps work, I can step through it on Chrome, but when I hover "<whatever" to inspect it, nothing happens.

Any idea what I'm doing wrong?

Thanks.

Import existing sourcemap

Author of gobble-include here.

My workflow with gobble-include is set up in such a way that the files used by gobble-include as input already have sourcemaps. Right now these must be ignored (which is not a big deal, really) because magic-string doesn't handle them.

It would be nice if magic-string would have some way of importing an existing sourcemap, preferably in the constructor.

This would mean some extra complexity due to the current assumption of just one source file.

Wrong columns on source indented with spaces

When the source string passed to magic string is indented with spaces the generated mappings will map back to column 0. I would expect to be able to map back to the right column (in example below column 3)

import { createRequire } from "node:module"
import MagicString from "magic-string"

const require = createRequire(import.meta.url)
const { SourceMapConsumer } = require("source-map")

const source = `
  a
`
const magicString = new MagicString(source)
magicString.overwrite(source.indexOf("a"), source.indexOf("a") + 1, "b")
const output = magicString.toString()
const map = magicString.generateMap({ hires: true })
const sourceMapConsumer = await new SourceMapConsumer(map)
const originalPosition = sourceMapConsumer.originalPositionFor({
  line: 1,
  column: 3,
})
console.log({ output, originalPosition })

Executing code above logs the following:

{
  output: '\n  b\n',
  originalPosition: { source: 'null', line: 1, column: 0, name: null }
}

I am surprised to get column: 0. I would expect column: 3. Is it a "bug" in magic-string?

Error: Character is out of bounds

Hi, I got this error and I have no idea how to solve it. Below is the code I want to handle with MagicString.remove.

const ES6CurriedFn = (param1) => (param2) => param1

// const ES6CurriedFn = (param1) => {
//   return param1
// }

function ES5CurriedFn (param1) {
  return function (param2) {
    return param1
  }
}

export {
  ES6CurriedFn,
  ES5CurriedFn
}

I guess there is no special character here at all, but it just doesn't work. How should I fix this?

source mapping bug with multiple sources

The fix in #159 wasn't general enough - see rollup/rollup#3001 (comment).

Using [email protected]:

$ cat bug.js
const MagicString = require('./dist/magic-string.umd.js')
const SourceMapConsumer = require( 'source-map' ).SourceMapConsumer;

const s1 = 'ABCDE';
const ms1 = new MagicString(s1, { filename: 'first' });

const s2 = 'VWXYZ';
const ms2 = new MagicString(s2, { filename: 'second' });

const bundle = new MagicString.Bundle();
bundle.addSource(ms1);
bundle.addSource(ms2);

ms1.remove(1,4);   // AE
ms1.move(0, 1, 5); // EA

ms2.remove(2,4);   // VWZ
ms2.move(0, 1, 5); // WZV

const map = bundle.generateMap({file: 'result', hires: true, includeContent: true});
const smc = new SourceMapConsumer(map);

let output = '';
output += bundle.toString() + '\n';;

const result1 = ms1.toString();
const result2 = ms2.toString();

var line = 1;
for (let i = 0; i < result1.length; i++) {
    let loc = smc.originalPositionFor({ line: line, column: i });
    output += `${s1[loc.column]} = ${result1[i]}\n`;
}

var line = 2;
for (let i = 0; i < result2.length; i++) {
    let loc = smc.originalPositionFor({ line: line, column: i });
    output += `${s2[loc.column]} = ${result2[i]}\n`;
}

output += map.toString();
console.log(output);

Incorrect result:

$ cat bug.js | node
EA
WZV
E = E
A = A
W = W
X = Z
V = V
{"version":3,"file":"result","sources":["first","second"],"sourcesContent":["ABCDE","VWXYZ"],"names":[],"mappings":"AAAI,CAAJ;ACAC,CAAC,AAAE,CAAJ"}

Here's the fix:

--- a/src/utils/Mappings.js
+++ b/src/utils/Mappings.js
@@ -48,9 +48,7 @@ export default class Mappings {
                        originalCharIndex += 1;
                }
 
-               this.pending = sourceIndex > 0
-                       ? [this.generatedCodeColumn, sourceIndex, loc.line, loc.column]
-                       : null;
+               this.pending = null;
        }
 
        advance(str) {

Correct result with fix:

$ cat bug.js | node
EA
WZV
E = E
A = A
W = W
Z = Z
V = V
{"version":3,"file":"result","sources":["first","second"],"sourcesContent":["ABCDE","VWXYZ"],"names":[],"mappings":"AAAI,CAAJ;ACAC,CAAG,CAAJ"}

[Feature request] in replace and replaceAll, allow the replacer function to return "no-op" value

The replace and replaceAll functions are mimicking the standard method of the string prototype in JS.

If you enter a replacer function on standard JS, and you see some match you don't want to change, you're simply return the original match in the function body.
This strategy allows the replacer function to do a "no-op" replacement.

Today, you might think you can do the same on magic-string, but in the sourcemap I think it will be shown that the substring was changed(e.g. "blabla" was replaced by "blabla"), but this was not the intention.

Can we allow for the replacement function to return sometimes a value of undefined? This way, whenever this value is being returned, magic-string will know not to replace this incident.

It might make sense in some cases to mark a substring as "replaced" in the sourcemap, even though it didn't change at all. This is why I'm not suggesting to check if the replacer return value was different, but rather suggest to check if it was returning undefined.

no hires source map where the appendLeft inserted

We use this library to transform svelte code to power editor features in language-tools. And use the source map to map character to character back to the original code.

When using appendLeft and prependLeft with hires: true the position where the appendLeft inserted is not included in the mapping. So it is mapped to the previous character. In our use case, it causes the error or warning to be highlighted one character less than it should be, for example like this:

圖片

Here is a test case for the situation

it('should correctly map content before append', () => {
    const s = new MagicString('function Foo () {}');

    s.appendLeft(8, '*'); // test will pass if this line is removed

    const map = s.generateMap({
        hires: true,
        file: 'output.js',
        source: 'input.js',
        includeContent: true
    });

    const smc = new SourceMapConsumer(map);
    
    const loc = smc.originalPositionFor({ line: 1, column: 8 });
    assert.strictEqual(loc.line, 1);
    assert.strictEqual(loc.column, 8); // failed here 7 !== 8
});

Cannot split a chunk that has already been edited

calling s.replace() after previously replacing a section in a magic string throws the following error:

 ERROR  Cannot split a chunk that has already been edited (0:63 – "const base = '/__NUXT_BASE__/'")    09:37:01

  at MagicString._splitChunk (node_modules/magic-string/dist/magic-string.cjs.js:893:10)
  at MagicString._split (node_modules/magic-string/dist/magic-string.cjs.js:883:43)
  at MagicString.overwrite (node_modules/magic-string/dist/magic-string.cjs.js:670:8)
  at node_modules/magic-string/dist/magic-string.cjs.js:1054:11
  at Array.forEach (<anonymous>)
  at MagicString.replace (node_modules/magic-string/dist/magic-string.cjs.js:1052:12)
  at Object.transform (src/plugins/dynamic-base.ts:89:11)
  at node_modules/rollup/dist/shared/rollup.js:22795:37
  at processTicksAndRejections (internal/process/task_queues.js:95:5)

cc: @antfu
related: nuxt/nuxt#13439

Incorrect output when magicString.appendLeft > magicString.overwrite

I want to show you an example. Rather than tell.

This is correct output normally.

const magicString = new MagicString(`a + b;`)
magicString.overwrite(4, 5, "c")
console.log(magicString.toString()) // a + c;

But this is incorrect output.

const magicString = new MagicString(`a + b`)
magicString.appendLeft(5, ";")
console.log(magicString.toString()) // a + b;
console.log("-------------------------------")
magicString.overwrite(4, 5, "c") // a + c (where is the ; character)
console.log(magicString.toString())

Problem in second example the last character is eaten.

Would you be open to convert this package to TypeScript?

Hello,

The Angular-CLI team uses this extensively to do refactoring of TypeScript. I notice you don't have typings and, although we don't need them, it would be nice to have on our side.

I actually propose to convert your project to TypeScript, myself. It wouldn't be much work as I've been doing it for other projects as well, and your code is already well structured. Normally for bigger projects it's a big deal but yours seems to be mainly maintained by yourself.

So no work from you except telling me that a PR is welcome :) I'll also include build steps so you would only need to do npm run build or similar, before publishing.

Let me know!

Support characters outside of the Latin1 range in Browser

I came across this issue where if the source code passed into magic-string contains characters outside of the Latin1 range, e.g. Cyrillic, and the execution context is a browser (works fine in Node), then an error is thrown:

InvalidCharacterError: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.

I did some research and found a solution (in the context of a Rollup use case):

const rollupBundle = await rollup(inputOptions);
const { code, map } = await rollupBundle.generate(outputOptions);

// Monkey patch magic-string internals
// to support characters outside of the Latin1 range, e.g. Cyrillic.
const toString = map.toString.bind(map);
map.toString = () => unescape(encodeURIComponent(toString()));

// Inspired by https://github.com/rollup/rollup/issues/121
const codeWithSourceMap = code + '\n//# sourceMappingURL=' + map.toUrl();

Related reading:

I'd be happy to submit a PR incorporating this into magic-string, if there's interest.

Problem with undefined chunks

I have a problem trying to get bublé in Debian : for some reason, it manages to trigger a code path where chunk ends up undefined, so I get an error. With the attached patch, I get my tentative bublé to accept the sample code given on the bublé homepage (which still points to gitlab although the last version is only on github...)

fix_undefined_chunk.txt

proposal: prependLeft(), prependRight(), appendLeft(), appendRight()

Motivation

There is a gap in the magic-string API that insertLeft() and insertRight() cannot accommodate.

Proposal

New methods:

  • s.prependLeft( index, content )

  • s.appendRight( index, content )

Synonyms for existing methods for naming consistency:

  • s.appendLeft( index, content )

    • appendLeft is a synonym for insertLeft
  • s.prependRight( index, content )

    • prependRight is a synonym for insertRight

Illustrative example:

var s = new MagicString('0123456789');

s.insertLeft(5, 'A');
s.insertRight(5, 'a');
s.insertRight(5, 'b');
s.insertLeft(5, 'B');
s.insertLeft(5, 'C');
s.insertRight(5, 'c');

// s === '01234ABCcba56789'
console.log(s.toString());

console.log('left  of 5:', s.slice(0, 5));
// left  of 5: 01234ABC

console.log('right of 5:', s.slice(5));
// right of 5: cba56789

//
// Proposed methods
//

s.prependLeft(5, '<');
s.prependLeft(5, '{');
// s === '01234{<ABCcba56789'

s.appendRight(5, '>');
s.appendRight(5, '}');
// s === '01234{<ABCcba>}56789'

s.appendLeft(5, '(');   // appendLeft is a synonym for insertLeft
s.appendLeft(5, '[');   // appendLeft is a synonym for insertLeft
// s === '01234{<ABC([cba>}56789'

s.prependRight(5, ')'); // prependRight is a synonym for insertRight
s.prependRight(5, ']'); // prependRight is a synonym for insertRight
// s === '01234{<ABC([])cba>}56789'

console.log('left  of 5:', s.slice(0, 5));
// left  of 5: '01234{<ABC(['

console.log('right of 5:', s.slice(5));
// right of 5: '])cba>}56789'

Flow type annotations?

Any interest in distributing flow type declarations with the package? I'm trying to get everything under decaffeinate type checked, and this would help. I could also just add it to flow-typed. It would require updating the file when the external API changes.

Input source maps?

I thought this might be a good tool to insert CSS into a JS bundle. It works great, except that I loose the source map to my CSS. Is it within the scope of this project to allow input source maps so that when in use overwrite to insert CSS, it maps to both the JS and the CSS source?

[Question] How to chain sourcemaps?

I'm sorry to post this as an issue, there is no discussions page here, but you may view this as a "feature request".

Assuming you have two preprocessors A and B, each of them takes the source code and outputs a pair of an output code and a sourcemap.

You now take some sourcecode, and preprocess it via A. You then take the resulted output, and preprocess it via B.

How can you chain the sourcemap generated by A and B, in the logical sense, to the final output? Is this something that is done somehow by this library? I am sure Svelte do this for its preprocessors, don't know how however.

Sources with no indented lines should be disregarded in bundle auto-indent

Currently, a source that doesn't have any indented lines will be given the default indentStr of \t. When bundling several sources, tabs will therefore be over-represented - so in this situation...

// foo.js
export default 'this is foo';

// bar.js
export default 'this is bar';

// main.js
import foo from 'foo';
import bar from 'bar';

function logFoo () {
  console.log( foo );
}

function logBar () {
  console.log( bar );
}

...the bundle will incorrectly assume that tabs should be used, instead of two spaces

[bug]

example code:

const MagicString = require('../dist/magic-string.umd.js')
const SourceMapConsumer = require( 'source-map' ).SourceMapConsumer;

const s = '0123456789';
const ms = new MagicString(s);

//remove '67' from s
const ns = ms.remove(6,8).toString();

var map = ms.generateMap({hires: true});
var smc = new SourceMapConsumer(map);

console.log(`${ns} = ${s}`);
for (var i = 0;i < ns.length; i++) {
    loc = smc.originalPositionFor({ line: 1, column: i });
    console.log(`${ns[i]} = ${s[loc.column]}`);
}

console:

01234589 = 0123456789
0 = 0
1 = 1
2 = 2
3 = 3
4 = 4
5 = 5
8 = 6 //is this right???
9 = 9

/src/utils/Mappings.js
row:50
this.pending = [this.generatedCodeColumn, sourceIndex, loc.line, loc.column];
row:19
this.rawSegments.push(this.pending);

RFC: Add a `move` method, or tweak `insert`

Now that there exists a Rollup issue for SystemJS, we'll need to be able to hoist function declarations, while retaining SourceMaps.

I haven't been able to find any move method, and the method combination that could have replicated the behavior (snip, remove and insert) requires insert to work on MagicStrings in addition to strings.

I suggest extending MagicStrings API with either a move method, or an insert that supports MagicStrings.

Memory overhead of `sourcemapLocations` is high

We were profiling the memory use of our rollup builds, and a surprising amount is due to sourcemapLocations. As far as we can tell, each AST node ends up adding two locations (https://github.com/rollup/rollup/blob/121a7f41fb88a294d9680c3fa11f4f72ebf9e9ff/src/ast/nodes/shared/Node.ts#L113-L114). Here's a screenshot from heap dump from the rollup process:

image

sourcemapLocations is an object being used as a set, if it's changed to a real Set it becomes ~4x smaller (guessing this is because it avoids the implicit stringification of object keys):

image

@Rich-Harris is this something you're open to? If so, happy to send a PR.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.