diff --git a/packages/js-multiline-to-singleline/src/index.spec.ts b/packages/js-multiline-to-singleline/src/index.spec.ts index 263efe2cad..6c10448728 100644 --- a/packages/js-multiline-to-singleline/src/index.spec.ts +++ b/packages/js-multiline-to-singleline/src/index.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-eval */ import { expect } from 'chai'; import { makeMultilineJSIntoSingleLine as toSingleLine } from './'; @@ -12,7 +13,7 @@ describe('makeMultilineJSIntoSingleLine', () => { expect(toSingleLine('() => { return\n42\n }')).to.equal('() => {return; 42; }'); }); - it('treats comments propertly', () => { + it('treats comments properly', () => { expect(toSingleLine('a // comment\n b')).to.equal('a; /* comment*/ b'); expect(toSingleLine('a /* comment*/\n b')).to.equal('a; /* comment*/ b'); }); @@ -20,4 +21,26 @@ describe('makeMultilineJSIntoSingleLine', () => { it('keeps invalid code as-is', () => { expect(toSingleLine('---\n---')).to.equal('--- ---'); }); + + it('treats multiline template strings properly', () => { + for (const original of [ + '(`1\\t2`);', + '(`1\\t\n2`);', + '(`1\\t\r\n2`);', + '(`1\\t\r2`);', + '(`1\\t\\nn2`);', + '(`1${\nnull\n}\n2`);', + '(String.raw `1\\t2`);', + '(String.raw `1\\t\n2`);', + '(String.raw `1\\t\\n2`);', + '(String.raw `1\\t\\r\\n2`);', + '(String.raw `1\\t\\r2`);', + '(String.raw `1${\nnull\n}\n2`);', + '(String.raw `1${\nnull\n}\\n2`);', + ]) { + const singleLine = toSingleLine(original); + expect(singleLine).to.not.match(/[\r\n]/); // singleline + expect(eval(singleLine)).to.equal(eval(original)); // same behavior + } + }); }); diff --git a/packages/js-multiline-to-singleline/src/index.ts b/packages/js-multiline-to-singleline/src/index.ts index c03993491e..aa121861e3 100644 --- a/packages/js-multiline-to-singleline/src/index.ts +++ b/packages/js-multiline-to-singleline/src/index.ts @@ -1,4 +1,5 @@ import * as babel from '@babel/core'; +import * as BabelTypes from '@babel/types'; // Babel plugin that turns all single-line // comments into /* ... */ block comments function lineCommentToBlockComment(): babel.PluginObj { @@ -40,6 +41,35 @@ function lineCommentToBlockComment(): babel.PluginObj { }; } +// Babel plugin that turns all multi-line `...` template strings into single-line template strings +function multilineTemplateStringToSingleLine({ types: t }: { types: typeof BabelTypes }): babel.PluginObj { + return { + visitor: { + TemplateLiteral(path) { + if (!path.node.quasis.some(({ value }) => value.raw.match(/[\r\n]/))) { + return; // is already single line, nothing to do + } + if (path.parentPath.isTaggedTemplateExpression()) { + // Just wrap it in `eval()`. There isn't much we can do about e.g. String.raw `ab` + // that would remove the newline but reserve the template tag behavior. + path.parentPath.replaceWith(t.callExpression( + t.identifier('eval'), + [t.stringLiteral(this.file.code.slice(path.parent.start ?? undefined, path.parent.end ?? undefined))] + )); + return; + } + // Escape newlines directly (note that \r and \r\n are being turned into \n here!) + path.replaceWith(t.templateLiteral( + path.node.quasis.map(el => t.templateElement({ + raw: el.value.raw.replace(/\n|\r\n?/g, '\\n') + }, el.tail)), + path.node.expressions + )); + } + } + }; +} + export function makeMultilineJSIntoSingleLine(src: string): string { // We use babel without any actual transformation steps, and only for ASI // effects here, e.g. turning `return\n42` into `return;\n42;` @@ -60,7 +90,7 @@ export function makeMultilineJSIntoSingleLine(src: string): string { configFile: false, babelrc: false, browserslistConfigFile: false, - plugins: [lineCommentToBlockComment], + plugins: [lineCommentToBlockComment, multilineTemplateStringToSingleLine], sourceType: 'script' })?.code ?? src; } catch {