diff --git a/.eslintrc b/.eslintrc index d2147b4..e6dd07c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -31,7 +31,7 @@ }, "extends": "eslint:recommended", "parserOptions": { - "ecmaVersion": 6, + "ecmaVersion": 2020, "sourceType": "module" } } diff --git a/benchmark/data-min.js b/benchmark/data-min.js new file mode 100644 index 0000000..6ecb61f --- /dev/null +++ b/benchmark/data-min.js @@ -0,0 +1,12 @@ +/* eslint-disable */ +import{encode as k}from"sourcemap-codec";class p{constructor(t){this.bits=t instanceof p?t.bits.slice():[]}add(t){this.bits[t>>5]|=1<<(t&31)}has(t){return!!(this.bits[t>>5]&1<<(t&31))}}class g{constructor(t,e,n){this.start=t,this.end=e,this.original=n,this.intro="",this.outro="",this.content=n,this.storeName=!1,this.edited=!1,Object.defineProperties(this,{previous:{writable:!0,value:null},next:{writable:!0,value:null}})}appendLeft(t){this.outro+=t}appendRight(t){this.intro=this.intro+t}clone(){const t=new g(this.start,this.end,this.original);return t.intro=this.intro,t.outro=this.outro,t.content=this.content,t.storeName=this.storeName,t.edited=this.edited,t}contains(t){return this.start{throw new Error("Unsupported environment: `window.btoa` or `Buffer` should be supported.")};typeof window<"u"&&typeof window.btoa=="function"?w=l=>window.btoa(unescape(encodeURIComponent(l))):typeof Buffer=="function"&&(w=l=>Buffer.from(l,"utf-8").toString("base64"));class b{constructor(t){this.version=3,this.file=t.file,this.sources=t.sources,this.sourcesContent=t.sourcesContent,this.names=t.names,this.mappings=k(t.mappings)}toString(){return JSON.stringify(this)}toUrl(){return"data:application/json;charset=utf-8;base64,"+w(this.toString())}}function L(l){const t=l.split(` +`),e=t.filter(r=>/^\t+/.test(r)),n=t.filter(r=>/^ {2,}/.test(r));if(e.length===0&&n.length===0)return null;if(e.length>=n.length)return" ";const i=n.reduce((r,s)=>{const h=/^ +/.exec(s)[0].length;return Math.min(h,r)},1/0);return new Array(i+1).join(" ")}function E(l,t){const e=l.split(/[/\\]/),n=t.split(/[/\\]/);for(e.pop();e[0]===n[0];)e.shift(),n.shift();if(e.length){let i=e.length;for(;i--;)e[i]=".."}return e.concat(n).join("/")}const R=Object.prototype.toString;function x(l){return R.call(l)==="[object Object]"}function C(l){const t=l.split(` +`),e=[];for(let n=0,i=0;n>1;i=0&&r.push(i),this.rawSegments.push(r)}else this.pending&&this.rawSegments.push(this.pending);this.advance(e),this.pending=null}addUneditedChunk(t,e,n,i,r){let s=e.start,h=!0;for(;s1){for(let n=0;n{const h=r(s.start);s.intro.length&&i.advance(s.intro),s.edited?i.addEdit(e,s.content,h,s.storeName?n.indexOf(s.original):-1):i.addUneditedChunk(e,s,this.original,h,this.sourcemapLocations),s.outro.length&&i.advance(s.outro)}),{file:t.file?t.file.split(/[/\\]/).pop():null,sources:[t.source?E(t.file||"",t.source):null],sourcesContent:t.includeContent?[this.original]:[null],names:n,mappings:i.raw}}generateMap(t){return new b(this.generateDecodedMap(t))}getIndentString(){return this.indentStr===null?" ":this.indentStr}indent(t,e){const n=/^[^\r\n]/gm;if(x(t)&&(e=t,t=void 0),t=t!==void 0?t:this.indentStr||" ",t==="")return this;e=e||{};const i={};e.exclude&&(typeof e.exclude[0]=="number"?[e.exclude]:e.exclude).forEach(u=>{for(let m=u[0];mr?`${t}${a}`:(r=!0,a);this.intro=this.intro.replace(n,s);let h=0,o=this.firstChunk;for(;o;){const a=o.end;if(o.edited)i[h]||(o.content=o.content.replace(n,s),o.content.length&&(r=o.content[o.content.length-1]===` +`));else for(h=o.start;h=t&&n<=e)throw new Error("Cannot move a selection inside itself");this._split(t),this._split(e),this._split(n);const i=this.byStart[t],r=this.byEnd[e],s=i.previous,h=r.next,o=this.byStart[n];if(!o&&r===this.lastChunk)return this;const a=o?o.previous:this.lastChunk;return s&&(s.next=h),h&&(h.previous=s),a&&(a.next=i),o&&(o.previous=r),i.previous||(this.firstChunk=r.next),r.next||(this.lastChunk=i.previous,this.lastChunk.next=null),i.previous=a,r.next=o||null,a||(this.firstChunk=i),o||(this.lastChunk=r),this}overwrite(t,e,n,i){if(typeof n!="string")throw new TypeError("replacement content must be a string");for(;t<0;)t+=this.original.length;for(;e<0;)e+=this.original.length;if(e>this.original.length)throw new Error("end is out of bounds");if(t===e)throw new Error("Cannot overwrite a zero-length range \u2013 use appendLeft or prependRight instead");this._split(t),this._split(e),i===!0&&(c.storeName||(console.warn("The final argument to magicString.overwrite(...) should be an options object. See https://github.com/rich-harris/magic-string"),c.storeName=!0),i={storeName:!0});const r=i!==void 0?i.storeName:!1,s=i!==void 0?i.contentOnly:!1;if(r){const a=this.original.slice(t,e);Object.defineProperty(this.storedNames,a,{writable:!0,value:!0,enumerable:!0})}const h=this.byStart[t],o=this.byEnd[e];if(h){let a=h;for(;a!==o;){if(a.next!==this.byStart[a.end])throw new Error("Cannot overwrite across a split point");a=a.next,a.edit("",!1)}h.edit(n,r,s)}else{const a=new g(t,e,"").edit(n,r);o.next=a,a.previous=o}return this}prepend(t){if(typeof t!="string")throw new TypeError("outro content must be a string");return this.intro=t+this.intro,this}prependLeft(t,e){if(typeof e!="string")throw new TypeError("inserted content must be a string");this._split(t);const n=this.byEnd[t];return n?n.prependLeft(e):this.intro=e+this.intro,this}prependRight(t,e){if(typeof e!="string")throw new TypeError("inserted content must be a string");this._split(t);const n=this.byStart[t];return n?n.prependRight(e):this.outro=e+this.outro,this}remove(t,e){for(;t<0;)t+=this.original.length;for(;e<0;)e+=this.original.length;if(t===e)return this;if(t<0||e>this.original.length)throw new Error("Character is out of bounds");if(t>e)throw new Error("end must be greater than start");this._split(t),this._split(e);let n=this.byStart[t];for(;n;)n.intro="",n.outro="",n.edit(""),n=e>n.end?this.byStart[n.end]:null;return this}lastChar(){if(this.outro.length)return this.outro[this.outro.length-1];let t=this.lastChunk;do{if(t.outro.length)return t.outro[t.outro.length-1];if(t.content.length)return t.content[t.content.length-1];if(t.intro.length)return t.intro[t.intro.length-1]}while(t=t.previous);return this.intro.length?this.intro[this.intro.length-1]:""}lastLine(){let t=this.outro.lastIndexOf(d);if(t!==-1)return this.outro.substr(t+1);let e=this.outro,n=this.lastChunk;do{if(n.outro.length>0){if(t=n.outro.lastIndexOf(d),t!==-1)return n.outro.substr(t+1)+e;e=n.outro+e}if(n.content.length>0){if(t=n.content.lastIndexOf(d),t!==-1)return n.content.substr(t+1)+e;e=n.content+e}if(n.intro.length>0){if(t=n.intro.lastIndexOf(d),t!==-1)return n.intro.substr(t+1)+e;e=n.intro+e}}while(n=n.previous);return t=this.intro.lastIndexOf(d),t!==-1?this.intro.substr(t+1)+e:this.intro+e}slice(t=0,e=this.original.length){for(;t<0;)t+=this.original.length;for(;e<0;)e+=this.original.length;let n="",i=this.firstChunk;for(;i&&(i.start>t||i.end<=t);){if(i.start=e)return n;i=i.next}if(i&&i.edited&&i.start!==t)throw new Error(`Cannot use replaced character ${t} as slice start anchor.`);const r=i;for(;i;){i.intro&&(r!==i||i.start===t)&&(n+=i.intro);const s=i.start=e;if(s&&i.edited&&i.end!==e)throw new Error(`Cannot use replaced character ${e} as slice end anchor.`);const h=r===i?t-i.start:0,o=s?i.content.length+e-i.end:i.content.length;if(n+=i.content.slice(h,o),i.outro&&(!s||i.end===e)&&(n+=i.outro),s)break;i=i.next}return n}snip(t,e){const n=this.clone();return n.remove(0,t),n.remove(e,n.original.length),n}_split(t){if(this.byStart[t]||this.byEnd[t])return;let e=this.lastSearchedChunk;const n=t>e.end;for(;e;){if(e.contains(t))return this._splitChunk(e,t);e=n?this.byStart[e.end]:this.byEnd[e.start]}}_splitChunk(t,e){if(t.edited&&t.content.length){const i=C(this.original)(e);throw new Error(`Cannot split a chunk that has already been edited (${i.line}:${i.column} \u2013 "${t.original}")`)}const n=t.split(e);return this.byEnd[e]=t,this.byStart[e]=n,this.byEnd[n.end]=n,t===this.lastChunk&&(this.lastChunk=n),this.lastSearchedChunk=t,!0}toString(){let t=this.intro,e=this.firstChunk;for(;e;)t+=e.toString(),e=e.next;return t+this.outro}isEmpty(){let t=this.firstChunk;do if(t.intro.length&&t.intro.trim()||t.content.length&&t.content.trim()||t.outro.length&&t.outro.trim())return!1;while(t=t.next);return!0}length(){let t=this.firstChunk,e=0;do e+=t.intro.length+t.content.length+t.outro.length;while(t=t.next);return e}trimLines(){return this.trim("[\\r\\n]")}trim(t){return this.trimStart(t).trimEnd(t)}trimEndAborted(t){const e=new RegExp((t||"\\s")+"+$");if(this.outro=this.outro.replace(e,""),this.outro.length)return!0;let n=this.lastChunk;do{const i=n.end,r=n.trimEnd(e);if(n.end!==i&&(this.lastChunk===n&&(this.lastChunk=n.next),this.byEnd[n.end]=n,this.byStart[n.next.start]=n.next,this.byEnd[n.next.end]=n.next),r)return!0;n=n.previous}while(n);return!1}trimEnd(t){return this.trimEndAborted(t),this}trimStartAborted(t){const e=new RegExp("^"+(t||"\\s")+"+");if(this.intro=this.intro.replace(e,""),this.intro.length)return!0;let n=this.firstChunk;do{const i=n.end,r=n.trimStart(e);if(n.end!==i&&(n===this.lastChunk&&(this.lastChunk=n.next),this.byEnd[n.end]=n,this.byStart[n.next.start]=n.next,this.byEnd[n.next.end]=n.next),r)return!0;n=n.next}while(n);return!1}trimStart(t){return this.trimStartAborted(t),this}hasChanged(){return this.original!==this.toString()}replace(t,e){function n(r,s){return typeof e=="string"?e.replace(/\$(\$|&|\d+)/g,(h,o)=>o==="$"?"$":o==="&"?r[0]:+o{s.index!=null&&this.overwrite(s.index,s.index+s[0].length,n(s,this.original))});else{const r=this.original.match(t);r&&r.index!=null&&this.overwrite(r.index,r.index+r[0].length,n(r,this.original))}return this}}const v=Object.prototype.hasOwnProperty;class S{constructor(t={}){this.intro=t.intro||"",this.separator=t.separator!==void 0?t.separator:` +`,this.sources=[],this.uniqueSources=[],this.uniqueSourceIndexByFilename={}}addSource(t){if(t instanceof f)return this.addSource({content:t,filename:t.filename,separator:this.separator});if(!x(t)||!t.content)throw new Error("bundle.addSource() takes an object with a `content` property, which should be an instance of MagicString, and an optional `filename`");if(["filename","indentExclusionRanges","separator"].forEach(e=>{v.call(t,e)||(t[e]=t.content[e])}),t.separator===void 0&&(t.separator=this.separator),t.filename)if(!v.call(this.uniqueSourceIndexByFilename,t.filename))this.uniqueSourceIndexByFilename[t.filename]=this.uniqueSources.length,this.uniqueSources.push({filename:t.filename,content:t.content.original});else{const e=this.uniqueSources[this.uniqueSourceIndexByFilename[t.filename]];if(t.content.original!==e.content)throw new Error(`Illegal source: same filename (${t.filename}), different contents`)}return this.sources.push(t),this}append(t,e){return this.addSource({content:new f(t),separator:e&&e.separator||""}),this}clone(){const t=new S({intro:this.intro,separator:this.separator});return this.sources.forEach(e=>{t.addSource({filename:e.filename,content:e.content.clone(),separator:e.separator})}),t}generateDecodedMap(t={}){const e=[];this.sources.forEach(i=>{Object.keys(i.content.storedNames).forEach(r=>{~e.indexOf(r)||e.push(r)})});const n=new y(t.hires);return this.intro&&n.advance(this.intro),this.sources.forEach((i,r)=>{r>0&&n.advance(this.separator);const s=i.filename?this.uniqueSourceIndexByFilename[i.filename]:-1,h=i.content,o=C(h.original);h.intro&&n.advance(h.intro),h.firstChunk.eachNext(a=>{const u=o(a.start);a.intro.length&&n.advance(a.intro),i.filename?a.edited?n.addEdit(s,a.content,u,a.storeName?e.indexOf(a.original):-1):n.addUneditedChunk(s,a,h.original,u,h.sourcemapLocations):n.advance(a.content),a.outro.length&&n.advance(a.outro)}),h.outro&&n.advance(h.outro)}),{file:t.file?t.file.split(/[/\\]/).pop():null,sources:this.uniqueSources.map(i=>t.file?E(t.file,i.filename):i.filename),sourcesContent:this.uniqueSources.map(i=>t.includeContent?i.content:null),names:e,mappings:n.raw}}generateMap(t){return new b(this.generateDecodedMap(t))}getIndentString(){const t={};return this.sources.forEach(e=>{const n=e.content.indentStr;n!==null&&(t[n]||(t[n]=0),t[n]+=1)}),Object.keys(t).sort((e,n)=>t[e]-t[n])[0]||" "}indent(t){if(arguments.length||(t=this.getIndentString()),t==="")return this;let e=!this.intro||this.intro.slice(-1)===` +`;return this.sources.forEach((n,i)=>{const r=n.separator!==void 0?n.separator:this.separator,s=e||i>0&&/\r?\n$/.test(r);n.content.indent(t,{exclude:n.indentExclusionRanges,indentStart:s}),e=n.content.lastChar()===` +`}),this.intro&&(this.intro=t+this.intro.replace(/^[^\n]/gm,(n,i)=>i>0?t+n:n)),this}prepend(t){return this.intro=t+this.intro,this}toString(){const t=this.sources.map((e,n)=>{const i=e.separator!==void 0?e.separator:this.separator;return(n>0?i:"")+e.content.toString()}).join("");return this.intro+t}isEmpty(){return!(this.intro.length&&this.intro.trim()||this.sources.some(t=>!t.content.isEmpty()))}length(){return this.sources.reduce((t,e)=>t+e.content.length(),this.intro.length)}trimLines(){return this.trim("[\\r\\n]")}trim(t){return this.trimStart(t).trimEnd(t)}trimStart(t){const e=new RegExp("^"+(t||"\\s")+"+");if(this.intro=this.intro.replace(e,""),!this.intro){let n,i=0;do if(n=this.sources[i++],!n)break;while(!n.content.trimStartAborted(t))}return this}trimEnd(t){const e=new RegExp((t||"\\s")+"+$");let n,i=this.sources.length-1;do if(n=this.sources[i--],!n){this.intro=this.intro.replace(e,"");break}while(!n.content.trimEndAborted(t));return this}}export{S as Bundle,b as SourceMap,f as default}; diff --git a/benchmark/data.js b/benchmark/data.js new file mode 100644 index 0000000..202d9c8 --- /dev/null +++ b/benchmark/data.js @@ -0,0 +1,1075 @@ +/* eslint-disable */ +import { encode } from "sourcemap-codec"; +class BitSet { + constructor(arg) { + this.bits = arg instanceof BitSet ? arg.bits.slice() : []; + } + add(n2) { + this.bits[n2 >> 5] |= 1 << (n2 & 31); + } + has(n2) { + return !!(this.bits[n2 >> 5] & 1 << (n2 & 31)); + } +} +class Chunk { + constructor(start, end, content) { + this.start = start; + this.end = end; + this.original = content; + this.intro = ""; + this.outro = ""; + this.content = content; + this.storeName = false; + this.edited = false; + Object.defineProperties(this, { + previous: { writable: true, value: null }, + next: { writable: true, value: null } + }); + } + appendLeft(content) { + this.outro += content; + } + appendRight(content) { + this.intro = this.intro + content; + } + clone() { + const chunk = new Chunk(this.start, this.end, this.original); + chunk.intro = this.intro; + chunk.outro = this.outro; + chunk.content = this.content; + chunk.storeName = this.storeName; + chunk.edited = this.edited; + return chunk; + } + contains(index) { + return this.start < index && index < this.end; + } + eachNext(fn) { + let chunk = this; + while (chunk) { + fn(chunk); + chunk = chunk.next; + } + } + eachPrevious(fn) { + let chunk = this; + while (chunk) { + fn(chunk); + chunk = chunk.previous; + } + } + edit(content, storeName, contentOnly) { + this.content = content; + if (!contentOnly) { + this.intro = ""; + this.outro = ""; + } + this.storeName = storeName; + this.edited = true; + return this; + } + prependLeft(content) { + this.outro = content + this.outro; + } + prependRight(content) { + this.intro = content + this.intro; + } + split(index) { + const sliceIndex = index - this.start; + const originalBefore = this.original.slice(0, sliceIndex); + const originalAfter = this.original.slice(sliceIndex); + this.original = originalBefore; + const newChunk = new Chunk(index, this.end, originalAfter); + newChunk.outro = this.outro; + this.outro = ""; + this.end = index; + if (this.edited) { + newChunk.edit("", false); + this.content = ""; + } else { + this.content = originalBefore; + } + newChunk.next = this.next; + if (newChunk.next) + newChunk.next.previous = newChunk; + newChunk.previous = this; + this.next = newChunk; + return newChunk; + } + toString() { + return this.intro + this.content + this.outro; + } + trimEnd(rx) { + this.outro = this.outro.replace(rx, ""); + if (this.outro.length) + return true; + const trimmed = this.content.replace(rx, ""); + if (trimmed.length) { + if (trimmed !== this.content) { + this.split(this.start + trimmed.length).edit("", void 0, true); + } + return true; + } else { + this.edit("", void 0, true); + this.intro = this.intro.replace(rx, ""); + if (this.intro.length) + return true; + } + } + trimStart(rx) { + this.intro = this.intro.replace(rx, ""); + if (this.intro.length) + return true; + const trimmed = this.content.replace(rx, ""); + if (trimmed.length) { + if (trimmed !== this.content) { + this.split(this.end - trimmed.length); + this.edit("", void 0, true); + } + return true; + } else { + this.edit("", void 0, true); + this.outro = this.outro.replace(rx, ""); + if (this.outro.length) + return true; + } + } +} +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"); +} +class SourceMap { + constructor(properties) { + this.version = 3; + this.file = properties.file; + this.sources = properties.sources; + this.sourcesContent = properties.sourcesContent; + this.names = properties.names; + this.mappings = encode(properties.mappings); + } + toString() { + return JSON.stringify(this); + } + toUrl() { + return "data:application/json;charset=utf-8;base64," + btoa(this.toString()); + } +} +function guessIndent(code) { + const lines = code.split("\n"); + const tabbed = lines.filter((line) => /^\t+/.test(line)); + const spaced = lines.filter((line) => /^ {2,}/.test(line)); + if (tabbed.length === 0 && spaced.length === 0) { + return null; + } + if (tabbed.length >= spaced.length) { + return " "; + } + const min = spaced.reduce((previous, current) => { + const numSpaces = /^ +/.exec(current)[0].length; + return Math.min(numSpaces, previous); + }, Infinity); + return new Array(min + 1).join(" "); +} +function getRelativePath(from, to) { + const fromParts = from.split(/[/\\]/); + const toParts = to.split(/[/\\]/); + fromParts.pop(); + while (fromParts[0] === toParts[0]) { + fromParts.shift(); + toParts.shift(); + } + if (fromParts.length) { + let i = fromParts.length; + while (i--) + fromParts[i] = ".."; + } + return fromParts.concat(toParts).join("/"); +} +const toString = Object.prototype.toString; +function isObject(thing) { + return toString.call(thing) === "[object Object]"; +} +function getLocator(source) { + const originalLines = source.split("\n"); + const lineOffsets = []; + for (let i = 0, pos = 0; i < originalLines.length; i++) { + lineOffsets.push(pos); + pos += originalLines[i].length + 1; + } + return function locate(index) { + let i = 0; + let j = lineOffsets.length; + while (i < j) { + const m = i + j >> 1; + if (index < lineOffsets[m]) { + j = m; + } else { + i = m + 1; + } + } + const line = i - 1; + const column = index - lineOffsets[line]; + return { line, column }; + }; +} +class Mappings { + constructor(hires) { + this.hires = hires; + this.generatedCodeLine = 0; + this.generatedCodeColumn = 0; + this.raw = []; + this.rawSegments = this.raw[this.generatedCodeLine] = []; + this.pending = null; + } + addEdit(sourceIndex, content, loc, nameIndex) { + if (content.length) { + const segment = [this.generatedCodeColumn, sourceIndex, loc.line, loc.column]; + if (nameIndex >= 0) { + segment.push(nameIndex); + } + this.rawSegments.push(segment); + } else if (this.pending) { + this.rawSegments.push(this.pending); + } + this.advance(content); + this.pending = null; + } + addUneditedChunk(sourceIndex, chunk, original, loc, sourcemapLocations) { + let originalCharIndex = chunk.start; + let first = true; + while (originalCharIndex < chunk.end) { + if (this.hires || first || sourcemapLocations.has(originalCharIndex)) { + this.rawSegments.push([this.generatedCodeColumn, sourceIndex, loc.line, loc.column]); + } + if (original[originalCharIndex] === "\n") { + loc.line += 1; + loc.column = 0; + this.generatedCodeLine += 1; + this.raw[this.generatedCodeLine] = this.rawSegments = []; + this.generatedCodeColumn = 0; + first = true; + } else { + loc.column += 1; + this.generatedCodeColumn += 1; + first = false; + } + originalCharIndex += 1; + } + this.pending = null; + } + advance(str) { + if (!str) + return; + const lines = str.split("\n"); + if (lines.length > 1) { + for (let i = 0; i < lines.length - 1; i++) { + this.generatedCodeLine++; + this.raw[this.generatedCodeLine] = this.rawSegments = []; + } + this.generatedCodeColumn = 0; + } + this.generatedCodeColumn += lines[lines.length - 1].length; + } +} +const n = "\n"; +const warned = { + insertLeft: false, + insertRight: false, + storeName: false +}; +class MagicString { + constructor(string, options = {}) { + const chunk = new Chunk(0, string.length, string); + Object.defineProperties(this, { + original: { writable: true, value: string }, + outro: { writable: true, value: "" }, + intro: { writable: true, value: "" }, + firstChunk: { writable: true, value: chunk }, + lastChunk: { writable: true, value: chunk }, + lastSearchedChunk: { writable: true, value: chunk }, + byStart: { writable: true, value: {} }, + byEnd: { writable: true, value: {} }, + filename: { writable: true, value: options.filename }, + indentExclusionRanges: { writable: true, value: options.indentExclusionRanges }, + sourcemapLocations: { writable: true, value: new BitSet() }, + storedNames: { writable: true, value: {} }, + indentStr: { writable: true, value: guessIndent(string) } + }); + this.byStart[0] = chunk; + this.byEnd[string.length] = chunk; + } + addSourcemapLocation(char) { + this.sourcemapLocations.add(char); + } + append(content) { + if (typeof content !== "string") + throw new TypeError("outro content must be a string"); + this.outro += content; + return this; + } + appendLeft(index, content) { + if (typeof content !== "string") + throw new TypeError("inserted content must be a string"); + this._split(index); + const chunk = this.byEnd[index]; + if (chunk) { + chunk.appendLeft(content); + } else { + this.intro += content; + } + return this; + } + appendRight(index, content) { + if (typeof content !== "string") + throw new TypeError("inserted content must be a string"); + this._split(index); + const chunk = this.byStart[index]; + if (chunk) { + chunk.appendRight(content); + } else { + this.outro += content; + } + return this; + } + clone() { + const cloned = new MagicString(this.original, { filename: this.filename }); + let originalChunk = this.firstChunk; + let clonedChunk = cloned.firstChunk = cloned.lastSearchedChunk = originalChunk.clone(); + while (originalChunk) { + cloned.byStart[clonedChunk.start] = clonedChunk; + cloned.byEnd[clonedChunk.end] = clonedChunk; + const nextOriginalChunk = originalChunk.next; + const nextClonedChunk = nextOriginalChunk && nextOriginalChunk.clone(); + if (nextClonedChunk) { + clonedChunk.next = nextClonedChunk; + nextClonedChunk.previous = clonedChunk; + clonedChunk = nextClonedChunk; + } + originalChunk = nextOriginalChunk; + } + cloned.lastChunk = clonedChunk; + if (this.indentExclusionRanges) { + cloned.indentExclusionRanges = this.indentExclusionRanges.slice(); + } + cloned.sourcemapLocations = new BitSet(this.sourcemapLocations); + cloned.intro = this.intro; + cloned.outro = this.outro; + return cloned; + } + generateDecodedMap(options) { + options = options || {}; + const sourceIndex = 0; + const names = Object.keys(this.storedNames); + const mappings = new Mappings(options.hires); + const locate = getLocator(this.original); + if (this.intro) { + mappings.advance(this.intro); + } + this.firstChunk.eachNext((chunk) => { + const loc = locate(chunk.start); + if (chunk.intro.length) + mappings.advance(chunk.intro); + if (chunk.edited) { + mappings.addEdit(sourceIndex, chunk.content, loc, chunk.storeName ? names.indexOf(chunk.original) : -1); + } else { + mappings.addUneditedChunk(sourceIndex, chunk, this.original, loc, this.sourcemapLocations); + } + if (chunk.outro.length) + mappings.advance(chunk.outro); + }); + return { + file: options.file ? options.file.split(/[/\\]/).pop() : null, + sources: [options.source ? getRelativePath(options.file || "", options.source) : null], + sourcesContent: options.includeContent ? [this.original] : [null], + names, + mappings: mappings.raw + }; + } + generateMap(options) { + return new SourceMap(this.generateDecodedMap(options)); + } + getIndentString() { + return this.indentStr === null ? " " : this.indentStr; + } + indent(indentStr, options) { + const pattern = /^[^\r\n]/gm; + if (isObject(indentStr)) { + options = indentStr; + indentStr = void 0; + } + indentStr = indentStr !== void 0 ? indentStr : this.indentStr || " "; + if (indentStr === "") + return this; + options = options || {}; + const isExcluded = {}; + if (options.exclude) { + const exclusions = typeof options.exclude[0] === "number" ? [options.exclude] : options.exclude; + exclusions.forEach((exclusion) => { + for (let i = exclusion[0]; i < exclusion[1]; i += 1) { + isExcluded[i] = true; + } + }); + } + let shouldIndentNextCharacter = options.indentStart !== false; + const replacer = (match) => { + if (shouldIndentNextCharacter) + return `${indentStr}${match}`; + shouldIndentNextCharacter = true; + return match; + }; + this.intro = this.intro.replace(pattern, replacer); + let charIndex = 0; + let chunk = this.firstChunk; + while (chunk) { + const end = chunk.end; + if (chunk.edited) { + if (!isExcluded[charIndex]) { + chunk.content = chunk.content.replace(pattern, replacer); + if (chunk.content.length) { + shouldIndentNextCharacter = chunk.content[chunk.content.length - 1] === "\n"; + } + } + } else { + charIndex = chunk.start; + while (charIndex < end) { + if (!isExcluded[charIndex]) { + const char = this.original[charIndex]; + if (char === "\n") { + shouldIndentNextCharacter = true; + } else if (char !== "\r" && shouldIndentNextCharacter) { + shouldIndentNextCharacter = false; + if (charIndex === chunk.start) { + chunk.prependRight(indentStr); + } else { + this._splitChunk(chunk, charIndex); + chunk = chunk.next; + chunk.prependRight(indentStr); + } + } + } + charIndex += 1; + } + } + charIndex = chunk.end; + chunk = chunk.next; + } + this.outro = this.outro.replace(pattern, replacer); + return this; + } + insert() { + throw new Error("magicString.insert(...) is deprecated. Use prependRight(...) or appendLeft(...)"); + } + insertLeft(index, content) { + if (!warned.insertLeft) { + console.warn("magicString.insertLeft(...) is deprecated. Use magicString.appendLeft(...) instead"); + warned.insertLeft = true; + } + return this.appendLeft(index, content); + } + insertRight(index, content) { + if (!warned.insertRight) { + console.warn("magicString.insertRight(...) is deprecated. Use magicString.prependRight(...) instead"); + warned.insertRight = true; + } + return this.prependRight(index, content); + } + move(start, end, index) { + if (index >= start && index <= end) + throw new Error("Cannot move a selection inside itself"); + this._split(start); + this._split(end); + this._split(index); + const first = this.byStart[start]; + const last = this.byEnd[end]; + const oldLeft = first.previous; + const oldRight = last.next; + const newRight = this.byStart[index]; + if (!newRight && last === this.lastChunk) + return this; + const newLeft = newRight ? newRight.previous : this.lastChunk; + if (oldLeft) + oldLeft.next = oldRight; + if (oldRight) + oldRight.previous = oldLeft; + if (newLeft) + newLeft.next = first; + if (newRight) + newRight.previous = last; + if (!first.previous) + this.firstChunk = last.next; + if (!last.next) { + this.lastChunk = first.previous; + this.lastChunk.next = null; + } + first.previous = newLeft; + last.next = newRight || null; + if (!newLeft) + this.firstChunk = first; + if (!newRight) + this.lastChunk = last; + return this; + } + overwrite(start, end, content, options) { + if (typeof content !== "string") + throw new TypeError("replacement content must be a string"); + while (start < 0) + start += this.original.length; + while (end < 0) + end += this.original.length; + if (end > this.original.length) + throw new Error("end is out of bounds"); + if (start === end) + throw new Error("Cannot overwrite a zero-length range \u2013 use appendLeft or prependRight instead"); + this._split(start); + this._split(end); + if (options === true) { + if (!warned.storeName) { + console.warn("The final argument to magicString.overwrite(...) should be an options object. See https://github.com/rich-harris/magic-string"); + warned.storeName = true; + } + options = { storeName: true }; + } + const storeName = options !== void 0 ? options.storeName : false; + const contentOnly = options !== void 0 ? options.contentOnly : false; + if (storeName) { + const original = this.original.slice(start, end); + Object.defineProperty(this.storedNames, original, { + writable: true, + value: true, + enumerable: true + }); + } + const first = this.byStart[start]; + const last = this.byEnd[end]; + if (first) { + let chunk = first; + while (chunk !== last) { + if (chunk.next !== this.byStart[chunk.end]) { + throw new Error("Cannot overwrite across a split point"); + } + chunk = chunk.next; + chunk.edit("", false); + } + first.edit(content, storeName, contentOnly); + } else { + const newChunk = new Chunk(start, end, "").edit(content, storeName); + last.next = newChunk; + newChunk.previous = last; + } + return this; + } + prepend(content) { + if (typeof content !== "string") + throw new TypeError("outro content must be a string"); + this.intro = content + this.intro; + return this; + } + prependLeft(index, content) { + if (typeof content !== "string") + throw new TypeError("inserted content must be a string"); + this._split(index); + const chunk = this.byEnd[index]; + if (chunk) { + chunk.prependLeft(content); + } else { + this.intro = content + this.intro; + } + return this; + } + prependRight(index, content) { + if (typeof content !== "string") + throw new TypeError("inserted content must be a string"); + this._split(index); + const chunk = this.byStart[index]; + if (chunk) { + chunk.prependRight(content); + } else { + this.outro = content + this.outro; + } + return this; + } + remove(start, end) { + while (start < 0) + start += this.original.length; + while (end < 0) + end += this.original.length; + if (start === end) + return this; + if (start < 0 || end > this.original.length) + throw new Error("Character is out of bounds"); + if (start > end) + throw new Error("end must be greater than start"); + this._split(start); + this._split(end); + let chunk = this.byStart[start]; + while (chunk) { + chunk.intro = ""; + chunk.outro = ""; + chunk.edit(""); + chunk = end > chunk.end ? this.byStart[chunk.end] : null; + } + return this; + } + lastChar() { + if (this.outro.length) + return this.outro[this.outro.length - 1]; + let chunk = this.lastChunk; + do { + if (chunk.outro.length) + return chunk.outro[chunk.outro.length - 1]; + if (chunk.content.length) + return chunk.content[chunk.content.length - 1]; + if (chunk.intro.length) + return chunk.intro[chunk.intro.length - 1]; + } while (chunk = chunk.previous); + if (this.intro.length) + return this.intro[this.intro.length - 1]; + return ""; + } + lastLine() { + let lineIndex = this.outro.lastIndexOf(n); + if (lineIndex !== -1) + return this.outro.substr(lineIndex + 1); + let lineStr = this.outro; + let chunk = this.lastChunk; + do { + if (chunk.outro.length > 0) { + lineIndex = chunk.outro.lastIndexOf(n); + if (lineIndex !== -1) + return chunk.outro.substr(lineIndex + 1) + lineStr; + lineStr = chunk.outro + lineStr; + } + if (chunk.content.length > 0) { + lineIndex = chunk.content.lastIndexOf(n); + if (lineIndex !== -1) + return chunk.content.substr(lineIndex + 1) + lineStr; + lineStr = chunk.content + lineStr; + } + if (chunk.intro.length > 0) { + lineIndex = chunk.intro.lastIndexOf(n); + if (lineIndex !== -1) + return chunk.intro.substr(lineIndex + 1) + lineStr; + lineStr = chunk.intro + lineStr; + } + } while (chunk = chunk.previous); + lineIndex = this.intro.lastIndexOf(n); + if (lineIndex !== -1) + return this.intro.substr(lineIndex + 1) + lineStr; + return this.intro + lineStr; + } + slice(start = 0, end = this.original.length) { + while (start < 0) + start += this.original.length; + while (end < 0) + end += this.original.length; + let result = ""; + let chunk = this.firstChunk; + while (chunk && (chunk.start > start || chunk.end <= start)) { + if (chunk.start < end && chunk.end >= end) { + return result; + } + chunk = chunk.next; + } + if (chunk && chunk.edited && chunk.start !== start) + throw new Error(`Cannot use replaced character ${start} as slice start anchor.`); + const startChunk = chunk; + while (chunk) { + if (chunk.intro && (startChunk !== chunk || chunk.start === start)) { + result += chunk.intro; + } + const containsEnd = chunk.start < end && chunk.end >= end; + if (containsEnd && chunk.edited && chunk.end !== end) + throw new Error(`Cannot use replaced character ${end} as slice end anchor.`); + const sliceStart = startChunk === chunk ? start - chunk.start : 0; + const sliceEnd = containsEnd ? chunk.content.length + end - chunk.end : chunk.content.length; + result += chunk.content.slice(sliceStart, sliceEnd); + if (chunk.outro && (!containsEnd || chunk.end === end)) { + result += chunk.outro; + } + if (containsEnd) { + break; + } + chunk = chunk.next; + } + return result; + } + snip(start, end) { + const clone = this.clone(); + clone.remove(0, start); + clone.remove(end, clone.original.length); + return clone; + } + _split(index) { + if (this.byStart[index] || this.byEnd[index]) + return; + let chunk = this.lastSearchedChunk; + const searchForward = index > chunk.end; + while (chunk) { + if (chunk.contains(index)) + return this._splitChunk(chunk, index); + chunk = searchForward ? this.byStart[chunk.end] : this.byEnd[chunk.start]; + } + } + _splitChunk(chunk, index) { + if (chunk.edited && chunk.content.length) { + const loc = getLocator(this.original)(index); + throw new Error(`Cannot split a chunk that has already been edited (${loc.line}:${loc.column} \u2013 "${chunk.original}")`); + } + const newChunk = chunk.split(index); + this.byEnd[index] = chunk; + this.byStart[index] = newChunk; + this.byEnd[newChunk.end] = newChunk; + if (chunk === this.lastChunk) + this.lastChunk = newChunk; + this.lastSearchedChunk = chunk; + return true; + } + toString() { + let str = this.intro; + let chunk = this.firstChunk; + while (chunk) { + str += chunk.toString(); + chunk = chunk.next; + } + return str + this.outro; + } + isEmpty() { + let chunk = this.firstChunk; + do { + if (chunk.intro.length && chunk.intro.trim() || chunk.content.length && chunk.content.trim() || chunk.outro.length && chunk.outro.trim()) + return false; + } while (chunk = chunk.next); + return true; + } + length() { + let chunk = this.firstChunk; + let length = 0; + do { + length += chunk.intro.length + chunk.content.length + chunk.outro.length; + } while (chunk = chunk.next); + return length; + } + trimLines() { + return this.trim("[\\r\\n]"); + } + trim(charType) { + return this.trimStart(charType).trimEnd(charType); + } + trimEndAborted(charType) { + const rx = new RegExp((charType || "\\s") + "+$"); + this.outro = this.outro.replace(rx, ""); + if (this.outro.length) + return true; + let chunk = this.lastChunk; + do { + const end = chunk.end; + const aborted = chunk.trimEnd(rx); + if (chunk.end !== end) { + if (this.lastChunk === chunk) { + this.lastChunk = chunk.next; + } + this.byEnd[chunk.end] = chunk; + this.byStart[chunk.next.start] = chunk.next; + this.byEnd[chunk.next.end] = chunk.next; + } + if (aborted) + return true; + chunk = chunk.previous; + } while (chunk); + return false; + } + trimEnd(charType) { + this.trimEndAborted(charType); + return this; + } + trimStartAborted(charType) { + const rx = new RegExp("^" + (charType || "\\s") + "+"); + this.intro = this.intro.replace(rx, ""); + if (this.intro.length) + return true; + let chunk = this.firstChunk; + do { + const end = chunk.end; + const aborted = chunk.trimStart(rx); + if (chunk.end !== end) { + if (chunk === this.lastChunk) + this.lastChunk = chunk.next; + this.byEnd[chunk.end] = chunk; + this.byStart[chunk.next.start] = chunk.next; + this.byEnd[chunk.next.end] = chunk.next; + } + if (aborted) + return true; + chunk = chunk.next; + } while (chunk); + return false; + } + trimStart(charType) { + this.trimStartAborted(charType); + return this; + } + hasChanged() { + return this.original !== this.toString(); + } + replace(searchValue, replacement) { + function getReplacement(match, str) { + if (typeof replacement === "string") { + return replacement.replace(/\$(\$|&|\d+)/g, (_, i) => { + 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 (typeof searchValue !== "string" && 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; + } +} +const hasOwnProp = Object.prototype.hasOwnProperty; +class Bundle { + constructor(options = {}) { + this.intro = options.intro || ""; + this.separator = options.separator !== void 0 ? options.separator : "\n"; + this.sources = []; + this.uniqueSources = []; + this.uniqueSourceIndexByFilename = {}; + } + addSource(source) { + if (source instanceof MagicString) { + return this.addSource({ + content: source, + filename: source.filename, + separator: this.separator + }); + } + if (!isObject(source) || !source.content) { + throw new Error("bundle.addSource() takes an object with a `content` property, which should be an instance of MagicString, and an optional `filename`"); + } + ["filename", "indentExclusionRanges", "separator"].forEach((option) => { + if (!hasOwnProp.call(source, option)) + source[option] = source.content[option]; + }); + if (source.separator === void 0) { + source.separator = this.separator; + } + if (source.filename) { + if (!hasOwnProp.call(this.uniqueSourceIndexByFilename, source.filename)) { + this.uniqueSourceIndexByFilename[source.filename] = this.uniqueSources.length; + this.uniqueSources.push({ filename: source.filename, content: source.content.original }); + } else { + const uniqueSource = this.uniqueSources[this.uniqueSourceIndexByFilename[source.filename]]; + if (source.content.original !== uniqueSource.content) { + throw new Error(`Illegal source: same filename (${source.filename}), different contents`); + } + } + } + this.sources.push(source); + return this; + } + append(str, options) { + this.addSource({ + content: new MagicString(str), + separator: options && options.separator || "" + }); + return this; + } + clone() { + const bundle = new Bundle({ + intro: this.intro, + separator: this.separator + }); + this.sources.forEach((source) => { + bundle.addSource({ + filename: source.filename, + content: source.content.clone(), + separator: source.separator + }); + }); + return bundle; + } + generateDecodedMap(options = {}) { + const names = []; + this.sources.forEach((source) => { + Object.keys(source.content.storedNames).forEach((name) => { + if (!~names.indexOf(name)) + names.push(name); + }); + }); + const mappings = new Mappings(options.hires); + if (this.intro) { + mappings.advance(this.intro); + } + this.sources.forEach((source, i) => { + if (i > 0) { + mappings.advance(this.separator); + } + const sourceIndex = source.filename ? this.uniqueSourceIndexByFilename[source.filename] : -1; + const magicString = source.content; + const locate = getLocator(magicString.original); + if (magicString.intro) { + mappings.advance(magicString.intro); + } + magicString.firstChunk.eachNext((chunk) => { + const loc = locate(chunk.start); + if (chunk.intro.length) + mappings.advance(chunk.intro); + if (source.filename) { + if (chunk.edited) { + mappings.addEdit(sourceIndex, chunk.content, loc, chunk.storeName ? names.indexOf(chunk.original) : -1); + } else { + mappings.addUneditedChunk(sourceIndex, chunk, magicString.original, loc, magicString.sourcemapLocations); + } + } else { + mappings.advance(chunk.content); + } + if (chunk.outro.length) + mappings.advance(chunk.outro); + }); + if (magicString.outro) { + mappings.advance(magicString.outro); + } + }); + return { + file: options.file ? options.file.split(/[/\\]/).pop() : null, + sources: this.uniqueSources.map((source) => { + return options.file ? getRelativePath(options.file, source.filename) : source.filename; + }), + sourcesContent: this.uniqueSources.map((source) => { + return options.includeContent ? source.content : null; + }), + names, + mappings: mappings.raw + }; + } + generateMap(options) { + return new SourceMap(this.generateDecodedMap(options)); + } + getIndentString() { + const indentStringCounts = {}; + this.sources.forEach((source) => { + const indentStr = source.content.indentStr; + if (indentStr === null) + return; + if (!indentStringCounts[indentStr]) + indentStringCounts[indentStr] = 0; + indentStringCounts[indentStr] += 1; + }); + return Object.keys(indentStringCounts).sort((a, b) => { + return indentStringCounts[a] - indentStringCounts[b]; + })[0] || " "; + } + indent(indentStr) { + if (!arguments.length) { + indentStr = this.getIndentString(); + } + if (indentStr === "") + return this; + let trailingNewline = !this.intro || this.intro.slice(-1) === "\n"; + this.sources.forEach((source, i) => { + const separator = source.separator !== void 0 ? source.separator : this.separator; + const indentStart = trailingNewline || i > 0 && /\r?\n$/.test(separator); + source.content.indent(indentStr, { + exclude: source.indentExclusionRanges, + indentStart + }); + trailingNewline = source.content.lastChar() === "\n"; + }); + if (this.intro) { + this.intro = indentStr + this.intro.replace(/^[^\n]/gm, (match, index) => { + return index > 0 ? indentStr + match : match; + }); + } + return this; + } + prepend(str) { + this.intro = str + this.intro; + return this; + } + toString() { + const body = this.sources.map((source, i) => { + const separator = source.separator !== void 0 ? source.separator : this.separator; + const str = (i > 0 ? separator : "") + source.content.toString(); + return str; + }).join(""); + return this.intro + body; + } + isEmpty() { + if (this.intro.length && this.intro.trim()) + return false; + if (this.sources.some((source) => !source.content.isEmpty())) + return false; + return true; + } + length() { + return this.sources.reduce((length, source) => length + source.content.length(), this.intro.length); + } + trimLines() { + return this.trim("[\\r\\n]"); + } + trim(charType) { + return this.trimStart(charType).trimEnd(charType); + } + trimStart(charType) { + const rx = new RegExp("^" + (charType || "\\s") + "+"); + this.intro = this.intro.replace(rx, ""); + if (!this.intro) { + let source; + let i = 0; + do { + source = this.sources[i++]; + if (!source) { + break; + } + } while (!source.content.trimStartAborted(charType)); + } + return this; + } + trimEnd(charType) { + const rx = new RegExp((charType || "\\s") + "+$"); + let source; + let i = this.sources.length - 1; + do { + source = this.sources[i--]; + if (!source) { + this.intro = this.intro.replace(rx, ""); + break; + } + } while (!source.content.trimEndAborted(charType)); + return this; + } +} +export { + Bundle, + SourceMap, + MagicString as default +}; diff --git a/benchmark/index.mjs b/benchmark/index.mjs new file mode 100644 index 0000000..1898a7a --- /dev/null +++ b/benchmark/index.mjs @@ -0,0 +1,80 @@ +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 = []; + return 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'); + }); +} + +bench(); diff --git a/package-lock.json b/package-lock.json index 8106bea..c49cd48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "magic-string", - "version": "0.26.1", + "version": "0.26.2", "license": "MIT", "dependencies": { "sourcemap-codec": "^1.4.8" @@ -14,6 +14,7 @@ "devDependencies": { "@rollup/plugin-node-resolve": "^13.1.3", "@rollup/plugin-replace": "^4.0.0", + "benchmark": "^2.1.4", "bumpp": "^7.1.1", "conventional-changelog-cli": "^2.2.2", "eslint": "^8.10.0", @@ -397,6 +398,16 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "node_modules/benchmark": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", + "integrity": "sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ==", + "dev": true, + "dependencies": { + "lodash": "^4.17.4", + "platform": "^1.3.3" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2914,6 +2925,12 @@ "node": ">=0.10.0" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "dev": true + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4169,6 +4186,16 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "benchmark": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", + "integrity": "sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ==", + "dev": true, + "requires": { + "lodash": "^4.17.4", + "platform": "^1.3.3" + } + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -6069,6 +6096,12 @@ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, + "platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "dev": true + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index e231a97..703eed9 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "release": "bumpp -x \"npm run changelog\" --all --commit --tag --push && npm publish", "pretest": "npm run lint && npm run build", "test": "mocha", + "bench": "npm run build && node benchmark/index.mjs", "watch": "rollup -cw" }, "dependencies": { @@ -47,6 +48,7 @@ "devDependencies": { "@rollup/plugin-node-resolve": "^13.1.3", "@rollup/plugin-replace": "^4.0.0", + "benchmark": "^2.1.4", "bumpp": "^7.1.1", "conventional-changelog-cli": "^2.2.2", "eslint": "^8.10.0", diff --git a/src/Bundle.js b/src/Bundle.js index 33b3768..e682fb5 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -164,7 +164,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; diff --git a/src/MagicString.js b/src/MagicString.js index d5e35bf..e45ad2e 100644 --- a/src/MagicString.js +++ b/src/MagicString.js @@ -33,7 +33,7 @@ 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 }, }); if (DEBUG) { @@ -175,7 +175,19 @@ export default class MagicString { 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 +199,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