From 410ccf5e0a0fda1ce8d0d0bc16d23d368ac8144e Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Thu, 30 Mar 2023 00:19:28 +1300 Subject: [PATCH] feat: a new unassertCode function that returns the modified code and sourcemap --- README.md | 59 +++++++- package-lock.json | 32 +++- package.json | 3 +- src/index.d.mts | 46 ++++++ src/index.mjs | 367 ++++++++++++++++++++++++++++++++++++++++++---- test/test.mjs | 55 +++++-- 6 files changed, 522 insertions(+), 40 deletions(-) create mode 100644 src/index.d.mts diff --git a/README.md b/README.md index f5b278b..9621a01 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,17 @@ function add(a, b) { API --------------------------------------- -unassert package exports three functions. [`unassertAst`](https://github.com/unassert-js/unassert#const-modifiedast--unassertastast-options) is the main function. [`createVisitor`](https://github.com/unassert-js/unassert#const-visitor--createvisitoroptions) and [`defaultOptions`](https://github.com/unassert-js/unassert#const-options--defaultoptions) are for customization. +unassert package exports four functions. +Main functions: + +* [`unassertAst`](https://github.com/unassert-js/unassert#const-modifiedast--unassertastast-options) +* [`unassertCode`](https://github.com/unassert-js/unassert#const-unasserted--unassertcodecodeast-options) + +For customization: + +* [`createVisitor`](https://github.com/unassert-js/unassert#const-visitor--createvisitoroptions) +* [`defaultOptions`](https://github.com/unassert-js/unassert#const-options--defaultoptions) ### const modifiedAst = unassertAst(ast, options) @@ -128,6 +137,7 @@ For example, the default target modules are as follows. 'node:assert', 'node:assert/strict' ] +} ``` In this case, unassert will remove assert variable declarations such as, @@ -201,6 +211,41 @@ unassert removes all `strictAssert`, `ok`, `eq` calls. Please see [customization example](https://github.com/unassert-js/unassert#example-1) for more details. +### const unasserted = unassertCode(code, ast, options) + +```javascript +const { unassertCode } = require('unassert') +``` + +```javascript +import { unassertCode } from 'unassert' +``` + +| return type | +|:--------------------------------------------------------------| +| `{ code: string; map: SourceMap | null; }` | + +Remove assertion calls from the code. Default behaviour can be customized by `options`. +Note that the `ast` is manipulated directly. + +#### MagicString + +If a [MagicString](https://www.npmjs.com/package/magic-string) is passed instead of a normal string, +this function will simply modify that string and return it. + +#### options + +Object for configuration options. passed `options` is `Object.assign`ed with default options. If not passed, default options will be used. + +##### options.modules + +The same as for [`unassertAst`](#optionsmodules). + +##### options.sourceMap + +If `true`, a sourcemap of the changes will be generated in addition to the code. + +You can alternatively specify [sourcemap options](https://github.com/rich-harris/magic-string#sgeneratemap-options-) if you which to customize how the sourcemap is generated. ### const visitor = createVisitor(options) @@ -218,6 +263,18 @@ import { createVisitor } from 'unassert' Create visitor object to be used with `estraverse.replace`. Visitor can be customized by `options`. +#### options + +Object for configuration options. passed `options` is `Object.assign`ed with default options. If not passed, default options will be used. + +##### options.modules + +The same as for [`unassertAst`](#optionsmodules). + +##### options.code + +A [MagicString](https://www.npmjs.com/package/magic-string) of the code the ast represents. +If given, this code will be updated with the changes made to the ast. ### const options = defaultOptions() diff --git a/package-lock.json b/package-lock.json index 46cb844..c2f694c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "2.0.0", "license": "MIT", "dependencies": { - "estraverse": "^5.0.0" + "estraverse": "^5.0.0", + "magic-string": "^0.30.0" }, "devDependencies": { "@twada/benchmark-commits": "^0.1.0", @@ -231,6 +232,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, "node_modules/@twada/benchmark-commits": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@twada/benchmark-commits/-/benchmark-commits-0.1.0.tgz", @@ -2353,6 +2359,17 @@ "node": ">=10" } }, + "node_modules/magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/minimatch": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", @@ -3821,6 +3838,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, "@twada/benchmark-commits": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@twada/benchmark-commits/-/benchmark-commits-0.1.0.tgz", @@ -5391,6 +5413,14 @@ "yallist": "^4.0.0" } }, + "magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, "minimatch": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", diff --git a/package.json b/package.json index d010bf6..3be09fa 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ } ], "dependencies": { - "estraverse": "^5.0.0" + "estraverse": "^5.0.0", + "magic-string": "^0.30.0" }, "devDependencies": { "@twada/benchmark-commits": "^0.1.0", diff --git a/src/index.d.mts b/src/index.d.mts new file mode 100644 index 0000000..b284148 --- /dev/null +++ b/src/index.d.mts @@ -0,0 +1,46 @@ +import type MagicString from "magic-string"; +import type { SourceMap, SourceMapOptions } from "magic-string"; +import type { Node } from "acorn"; +import type { Visitor } from "estraverse"; + +export type UnassertAstOptions = Partial<{ + modules: string[]; + variables: string[]; +}>; + +export type UnassertCodeOptions = UnassertAstOptions & + Partial<{ + sourceMap: boolean | SourceMapOptions; + }>; + +export type CreateVisitorOptions = UnassertAstOptions & + Partial<{ + code: MagicString; + }>; + +export type UnassertCodeResult = { + code: string; + map: SourceMap | null; +}; + +export function unassertAst(ast: Node, options?: UnassertAstOptions): Node; + +export function unassertCode( + code: string, + ast: Node, + options?: UnassertCodeOptions +): UnassertCodeResult; +export function unassertCode( + code: MagicString, + ast: Node, + options?: UnassertCodeOptions +): MagicString; +export function unassertCode( + code: string | MagicString, + ast: Node, + options?: UnassertCodeOptions +): UnassertCodeResult | MagicString; + +export function defaultOptions(): UnassertAstOptions; + +export function createVisitor(options?: CreateVisitorOptions): Visitor; diff --git a/src/index.mjs b/src/index.mjs index f0159fa..15e195d 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -8,33 +8,102 @@ * Licensed under the MIT license. * https://github.com/unassert-js/unassert/blob/master/LICENSE */ -import { replace } from 'estraverse'; +import { replace, traverse } from 'estraverse'; +import MagicString from 'magic-string'; +/** + * @param {import('estree').Node | undefined | null} node + * @returns {node is import('acorn').Node} + */ +function isAcornNode (node) { + return typeof node === 'object' && node !== null && typeof node.start === 'number' && typeof node.end === 'number'; +} + +/** + * @param {import('estree').Node | undefined | null} node + * @returns {node is import('estree').Literal} + */ function isLiteral (node) { - return node && node.type === 'Literal'; + return node?.type === 'Literal'; } + +/** + * @param {import('estree').Node | undefined | null} node + * @returns {node is import('estree').Identifier} + */ function isIdentifier (node) { - return node && node.type === 'Identifier'; + return node?.type === 'Identifier'; } + +/** + * @param {import('estree').Node | undefined | null} node + * @returns {node is import('estree').ObjectPattern} + */ function isObjectPattern (node) { - return node && node.type === 'ObjectPattern'; + return node?.type === 'ObjectPattern'; } + +/** + * @param {import('estree').Node | undefined | null} node + * @returns {node is import('estree').MemberExpression} + */ function isMemberExpression (node) { - return node && node.type === 'MemberExpression'; + return node?.type === 'MemberExpression'; } + +/** + * @param {import('estree').Node | undefined | null} node + * @returns {node is import('estree').CallExpression} + */ function isCallExpression (node) { - return node && node.type === 'CallExpression'; + return node?.type === 'CallExpression'; } + +/** + * @param {import('estree').Node | undefined | null} node + * @returns {node is import('estree').ExpressionStatement} + */ function isExpressionStatement (node) { - return node && node.type === 'ExpressionStatement'; + return node?.type === 'ExpressionStatement'; } + +/** + * @param {import('estree').Node | undefined | null} node + * @returns {node is import('estree').IfStatement} + */ function isIfStatement (node) { - return node && node.type === 'IfStatement'; + return node?.type === 'IfStatement'; } + +/** + * @param {import('estree').Node | undefined | null} node + * @returns {node is import('estree').ImportDeclaration} + */ function isImportDeclaration (node) { - return node && node.type === 'ImportDeclaration'; + return node?.type === 'ImportDeclaration'; } +/** + * @param {import('estree').Node | undefined | null} node + * @returns {node is import('estree').Property} + */ +function isProperty (node) { + return node?.type === 'Property'; +} + +/** + * @param {import('estree').Node | undefined | null} node + * @returns {node is import('estree').VariableDeclarator} + */ +function isVariableDeclarator (node) { + return node?.type === 'VariableDeclarator'; +} + +/** + * @param {import('estree').Node | undefined | null} node + * @param {string | number} key + * @returns {boolean} + */ function isBodyOfNodeHavingNonBlockStatementAsBody (node, key) { if (!node) { return false; @@ -55,30 +124,67 @@ function isBodyOfNodeHavingNonBlockStatementAsBody (node, key) { return false; } +/** + * @param {import('estree').Node | undefined | null} node + * @param {string | number} key + * @returns {boolean} + */ function isBodyOfIfStatement (node, key) { return isIfStatement(node) && (key === 'consequent' || key === 'alternate'); } - +/** + * @param {import('estree').Node} currentNode + * @param {import('estree').Node} parentNode + * @param {string | number} key + * @returns {boolean} + */ function isNonBlockChildOfParentNode (currentNode, parentNode, key) { return isExpressionStatement(currentNode) && isCallExpression(currentNode.expression) && (isBodyOfIfStatement(parentNode, key) || isBodyOfNodeHavingNonBlockStatementAsBody(parentNode, key)); } +/** + * @param {import('./index.mjs').CreateVisitorOptions} [options] + * @returns {import('estraverse').Visitor} + */ function createVisitor (options) { const config = Object.assign(defaultOptions(), options); const targetModules = new Set(config.modules); const targetVariables = new Set(config.variables); - + const { code } = config; + + /** + * @type {WeakMap< + * import('estree').Node, + * | { + * code: string; + * node: import('estree').Node + * } + * | null + * >} + */ const nodeUpdates = new WeakMap(); + /** + * @param {import('estree').Node} lit + * @returns {boolean} + */ function isAssertionModuleName (lit) { - return isLiteral(lit) && targetModules.has(lit.value); + return isLiteral(lit) && targetModules.has(/** @type {string} */ (lit.value)); } + /** + * @param {import('estree').Node} id + * @returns {boolean} + */ function isAssertionVariableName (id) { return isIdentifier(id) && targetVariables.has(id.name); } + /** + * @param {import('estree').Expression | import('estree').Super} callee + * @returns {boolean} + */ function isAssertionMethod (callee) { if (!isMemberExpression(callee)) { return false; @@ -91,10 +197,18 @@ function createVisitor (options) { } } + /** + * @param {import('estree').Expression | import('estree').Super} callee + * @returns {boolean} + */ function isAssertionFunction (callee) { return isAssertionVariableName(callee); } + /** + * @param {import('estree').Expression | import('estree').Super} callee + * @returns {boolean} + */ function isConsoleAssert (callee) { if (!isMemberExpression(callee)) { return false; @@ -104,24 +218,44 @@ function createVisitor (options) { isIdentifier(prop) && prop.name === 'assert'; } + /** + * @param {import('estree').Node} id + * @returns {void} + */ function registerIdentifierAsAssertionVariable (id) { if (isIdentifier(id)) { targetVariables.add(id.name); } } + /** + * @param {import('estree').ObjectPattern} objectPattern + * @returns {void} + */ function handleDestructuredAssertionAssignment (objectPattern) { - for (const { value } of objectPattern.properties) { - registerIdentifierAsAssertionVariable(value); + for (const property of objectPattern.properties) { + if (isProperty(property)) { + registerIdentifierAsAssertionVariable(property.value); + } else { + // TODO: handle rest element. + } } } + /** + * @param {import('estree').ImportDeclaration} importDeclaration + * @returns {void} + */ function handleImportSpecifiers (importDeclaration) { for (const { local } of importDeclaration.specifiers) { registerIdentifierAsAssertionVariable(local); } } + /** + * @param {import('estree').Node} node + * @returns {void} + */ function registerAssertionVariables (node) { if (isIdentifier(node)) { registerIdentifierAsAssertionVariable(node); @@ -132,6 +266,11 @@ function createVisitor (options) { } } + /** + * @param {import('estree').Pattern} id + * @param {import('estree').Expression | import('estree').Super | undefined | null} init + * @returns {boolean} + */ function isRequireAssert (id, init) { if (!isCallExpression(init)) { return false; @@ -147,6 +286,11 @@ function createVisitor (options) { return isIdentifier(id) || isObjectPattern(id); } + /** + * @param {import('estree').Pattern} id + * @param {import('estree').Expression | import('estree').Super | undefined | null} init + * @returns {boolean} + */ function isRequireAssertDotStrict (id, init) { if (!isMemberExpression(init)) { return false; @@ -161,43 +305,105 @@ function createVisitor (options) { return prop.name === 'strict'; } + /** + * @param {import('estree').Pattern} id + * @param {import('estree').Expression | import('estree').Super | undefined | null} init + * @returns {boolean} + */ function isRemovalTargetRequire (id, init) { return isRequireAssert(id, init) || isRequireAssertDotStrict(id, init); } + /** + * @param {import('estree').Expression | import('estree').Super} callee + * @returns {boolean} + */ function isRemovalTargetAssertion (callee) { return isAssertionFunction(callee) || isAssertionMethod(callee) || isConsoleAssert(callee); } + /** + * @param {import('estree').Node} node + * @returns {void} + */ function removeNode (node) { nodeUpdates.set(node, null); } + /** + * @param {import('estree').Node} node + * @returns {void} + */ function replaceNode (node, replacement) { nodeUpdates.set(node, replacement); } + /** + * @param {import('acorn').Node} node + * @param {string} code + * @returns {{start: number; end: number;}} + */ + function getStartAndEnd (node, code) { + let { start, end } = node; + while (/\s/.test(code[start - 1])) { + start -= 1; + } + if (isVariableDeclarator(node)) { + while (/\s/.test(code[end])) { + end += 1; + } + if (/,/.test(code[end])) { + end += 1; + } else { + // Critical Error -- should be impossible with a valid ast. + } + } + return { start, end }; + } + + /** + * @returns {{ + * code: string; + * node: import('estree').Expression + * }} + */ function createNoopExpression () { return { - type: 'UnaryExpression', - operator: 'void', - prefix: true, - argument: { - type: 'Literal', - value: 0, - raw: '0' + code: '(void 0)', + node: { + type: 'UnaryExpression', + operator: 'void', + prefix: true, + argument: { + type: 'Literal', + value: 0, + raw: '0' + } } }; } + /** + * @returns {{ + * code: string; + * node: import('estree').BlockStatement + * }} + */ function createNoopStatement () { return { - type: 'BlockStatement', - body: [] + code: '{}', + node: { + type: 'BlockStatement', + body: [] + } }; } - function unassertImportDeclaration (currentNode, parentNode) { + /** + * @this {import('estraverse').Controller} + * @param {import('estree').ImportDeclaration} currentNode + */ + function unassertImportDeclaration (currentNode) { const source = currentNode.source; if (!(isAssertionModuleName(source))) { return; @@ -209,6 +415,11 @@ function createVisitor (options) { registerAssertionVariables(currentNode); } + /** + * @this {import('estraverse').Controller} + * @param {import('estree').VariableDeclarator} currentNode + * @param {import('estree').VariableDeclaration} parentNode + */ function unassertVariableDeclarator (currentNode, parentNode) { if (isRemovalTargetRequire(currentNode.id, currentNode.init)) { if (parentNode.declarations.length === 1) { @@ -225,6 +436,11 @@ function createVisitor (options) { } } + /** + * @this {import('estraverse').Controller} + * @param {import('estree').AssignmentExpression} currentNode + * @param {import('estree').Node} parentNode + */ function unassertAssignmentExpression (currentNode, parentNode) { if (currentNode.operator !== '=') { return; @@ -241,6 +457,11 @@ function createVisitor (options) { } } + /** + * @this {import('estraverse').Controller} + * @param {import('estree').CallExpression} currentNode + * @param {import('estree').Node} parentNode + */ function unassertCallExpression (currentNode, parentNode) { const callee = currentNode.callee; if (!isRemovalTargetAssertion(callee)) { @@ -262,6 +483,11 @@ function createVisitor (options) { } } + /** + * @this {import('estraverse').Controller} + * @param {import('estree').AwaitExpression} currentNode + * @param {import('estree').Node} parentNode + */ function unassertAwaitExpression (currentNode, parentNode) { const childNode = currentNode.argument; if (isExpressionStatement(parentNode) && isCallExpression(childNode)) { @@ -276,6 +502,11 @@ function createVisitor (options) { return { enter: function (currentNode, parentNode) { + if (code && isAcornNode(currentNode)) { + code.addSourcemapLocation(currentNode.start); + code.addSourcemapLocation(currentNode.end); + } + switch (currentNode.type) { case 'ImportDeclaration': { unassertImportDeclaration.bind(this)(currentNode, parentNode); @@ -301,29 +532,108 @@ function createVisitor (options) { }, leave: function (currentNode, parentNode) { const update = nodeUpdates.get(currentNode); + if (update === undefined) { return undefined; } + if (update === null) { if (isExpressionStatement(currentNode)) { const path = this.path(); - const key = path[path.length - 1]; - if (isNonBlockChildOfParentNode(currentNode, parentNode, key)) { - return createNoopStatement(); + if (path) { + const key = path[path.length - 1]; + if (parentNode && isNonBlockChildOfParentNode(currentNode, parentNode, key)) { + const replacement = createNoopStatement(); + if (code && isAcornNode(currentNode)) { + const { start, end } = getStartAndEnd(currentNode, code.toString()); + code.overwrite(start, end, replacement.code); + } + return replacement.node; + } } } + + if (code && isAcornNode(currentNode)) { + const { start, end } = getStartAndEnd(currentNode, code.toString()); + code.remove(start, end); + } + this.remove(); return undefined; } - return update; + + if (code && isAcornNode(currentNode)) { + const { start, end } = getStartAndEnd(currentNode, code.toString()); + code.overwrite(start, end, update.code); + } + + return update.node; } }; } +/** + * @param {import('estree').Node} ast + * @param {import('./index.mjs').UnassertAstOptions} [options] + * @returns {import('estree').Node} + */ function unassertAst (ast, options) { return replace(ast, createVisitor(options)); } +/** + * @overload + * @param {string} code + * @param {import('estree').Node} ast + * @param {import('./index.mjs').UnassertCodeOptions} [options] + * @returns {import('./index.mjs').UnassertCodeResult} + */ + +/** + * @overload + * @param {MagicString} code + * @param {import('estree').Node} ast + * @param {import('./index.mjs').UnassertCodeOptions} [options] + * @returns {MagicString} + */ + +/** + * @param {string|MagicString} code + * @param {import('estree').Node} ast + * @param {import('./index.mjs').UnassertCodeOptions} [options] + * @returns {import('./index.mjs').UnassertCodeResult | MagicString} + */ +function unassertCode (code, ast, options) { + const { + sourceMap, + ...traverseOptions + } = options ?? {}; + const usingMagic = code instanceof MagicString; + const magicCode = usingMagic ? code : new MagicString(code); + + traverse(ast, createVisitor({ + ...traverseOptions, + code: magicCode + })); + + if (usingMagic) { + return magicCode; + } + + const unassertedCode = magicCode.toString(); + const map = sourceMap + ? magicCode.generateMap(sourceMap === true ? undefined : sourceMap) + : null; + + return { + code: unassertedCode, + map + }; +} + +/** + * @returns {import('./index.mjs').UnassertAstOptions} + */ function defaultOptions () { return { modules: [ @@ -337,6 +647,7 @@ function defaultOptions () { export { unassertAst, + unassertCode, defaultOptions, createVisitor }; diff --git a/test/test.mjs b/test/test.mjs index f82b758..160e8ac 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -1,10 +1,11 @@ -import { unassertAst, createVisitor } from '../src/index.mjs'; +import { unassertAst, unassertCode, createVisitor } from '../src/index.mjs'; import { strict as assert } from 'assert'; import { resolve, dirname } from 'path'; import { readFileSync, existsSync } from 'fs'; import { parse } from 'acorn'; import { generate } from 'escodegen'; -import { replace } from 'estraverse'; +import { replace, traverse } from 'estraverse'; +import MagicString from 'magic-string'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -12,8 +13,12 @@ function parseFixture (filepath) { return parse(readFileSync(filepath), { sourceType: 'module', ecmaVersion: '2022' }); } -function createFixture ({ code, postlude, prelude }) { - return parse(prelude + '\n' + code + '\n' + postlude, { sourceType: 'module', ecmaVersion: '2022' }); +function createFixtureCode ({ code, postlude, prelude }) { + return prelude + '\n' + code + '\n' + postlude; +} + +function parseFixtureCode (code) { + return parse(code, { sourceType: 'module', ecmaVersion: '2022' }); } function testWithGeneratedFixture (ext, code) { @@ -22,18 +27,26 @@ function testWithGeneratedFixture (ext, code) { const postludeFilepath = resolve(__dirname, 'fixtures', ext, `postlude.${ext}`); const postlude = readFileSync(postludeFilepath).toString(); const expectedFilepath = resolve(__dirname, 'fixtures', ext, `expected.${ext}`); - const expected = readFileSync(expectedFilepath).toString(); + const expected = generate(parseFixtureCode(readFileSync(expectedFilepath).toString())); function deftest (name, fun) { it(`${code} : ${name}`, () => { - const ast = createFixture({ code, postlude, prelude }); + const fixCode = createFixtureCode({ code, postlude, prelude }); + const ast = parseFixtureCode(fixCode); const modifiedAst = fun(ast); const actual = generate(modifiedAst); - assert.equal(actual + '\n', expected); + assert.equal(actual, expected); }); } deftest('unassertAst', (ast) => unassertAst(ast)); deftest('createVisitor', (ast) => replace(ast, createVisitor())); + + it(`${code} : unassertCode`, () => { + const fixtureCode = createFixtureCode({ code, postlude, prelude }); + const ast = parseFixtureCode(fixtureCode); + const actual = generate(parseFixtureCode(unassertCode(fixtureCode, ast).code)); + assert.equal(actual, expected); + }); } function testESM (code) { @@ -47,18 +60,42 @@ function testCJS (code) { function testWithFixture (fixtureName, options) { const fixtureFilepath = resolve(__dirname, 'fixtures', fixtureName, 'fixture.js'); const expectedFilepath = resolve(__dirname, 'fixtures', fixtureName, 'expected.js'); - const expected = readFileSync(expectedFilepath).toString(); + const code = readFileSync(fixtureFilepath).toString(); + const expected = generate(parseFixtureCode(readFileSync(expectedFilepath).toString())); function deftest (name, fun) { it(`${fixtureName} : ${name}`, () => { const ast = parseFixture(fixtureFilepath); const modifiedAst = fun(ast); const actual = generate(modifiedAst); - assert.equal(actual + '\n', expected); + assert.equal(actual, expected); }); } deftest('unassertAst', (ast) => unassertAst(ast, options)); deftest('createVisitor', (ast) => replace(ast, createVisitor(options))); + + it(`${fixtureName} : unassertCode`, () => { + const ast = parseFixture(fixtureFilepath); + const actual = generate(parseFixtureCode(unassertCode(code, ast, options).code)); + assert.equal(actual, expected); + }); + + it(`${fixtureName} : unassertCode MagicString`, () => { + const ast = parseFixture(fixtureFilepath); + const magicCode = new MagicString(code); + const result = unassertCode(magicCode, ast, options); + const actual = generate(parseFixtureCode(magicCode.toString())); + assert.equal(actual, expected); + assert.equal(result, magicCode); + }); + + it(`${fixtureName} : createVisitor to update code`, () => { + const ast = parseFixture(fixtureFilepath); + const magicCode = new MagicString(code); + traverse(ast, createVisitor({ ...options, code: magicCode })); + const actual = generate(parseFixtureCode(magicCode.toString())); + assert.equal(actual, expected); + }); } describe('with default options', () => {