├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── lib └── index.ts ├── package.json ├── test ├── _utils.js ├── index.js └── mocha.opts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib 2 | tsconfig.json 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "14.4.0" 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Babel Transform: Node CommonJS to ES modules 2 | 3 | [![Build Status](https://travis-ci.org/tbranyen/babel-plugin-transform-commonjs.svg?branch=master)](https://travis-ci.org/tbranyen/babel-plugin-transform-commonjs) 4 | 5 | A Babel 7 compatible transform to convert Node-style CommonJS modules into the 6 | ES module specification. This was created specifically for an experimental 7 | module bundler, but has many uses outside of that initial use case. All major 8 | browsers have shipped support for ESM and Node currently has experimental 9 | support behind a flag. Babel offers a bridge to bring the old to the new, which 10 | is humorous given the origins of Babel which brought the new to the old. This 11 | module can reconcile differences as best as possible without resorting to 12 | hacks. 13 | 14 | The goal of this transform is to produce spec-compliant code. Any behavior that 15 | diverges will throw by default. There are escape hatches however, if 16 | you know what they do. 17 | 18 | This module will ignore existing ESM modules by default, so long as they do not 19 | reference the following globals: `require`, `module.exports`, or `exports`. 20 | 21 | ### Notes 22 | 23 | What to expect: 24 | 25 | - A transform that can transform a majority of Node CommonJS modules to ES modules 26 | - The integrity of `module.exports` intact, no tricks to separate this object 27 | - Early returns are wrapped in an arrow function IIFE 28 | 29 | What not to expect: 30 | 31 | - `require.extensions` support, as this is a runtime concern 32 | - Hoisting tricks, excessive code rewriting 33 | - Browser support for core Node modules 34 | 35 | Notable features not supported: 36 | 37 | - Non-static requires are invalid and will raise an exception 38 | - Nested requires will always be hoisted, unless they are non-static, see above 39 | - Invalid named exports (`exports["I'mateapot"]`) will only be available on the default export 40 | 41 | Notable features supported: 42 | 43 | - Early return 44 | - Setting export values on `this` 45 | 46 | ### Usage 47 | 48 | ```sh 49 | npm install --save-dev babel-plugin-transform-commonjs 50 | ``` 51 | 52 | Update your babel configuration: 53 | 54 | ```json 55 | { 56 | "plugins": ["transform-commonjs"] 57 | } 58 | ``` 59 | 60 | Now code like this: 61 | 62 | ```javascript 63 | var { readFileSync } = require('path'); 64 | exports.readFileSync = readFileSync; 65 | ``` 66 | 67 | Will turn into this: 68 | 69 | ``` javascript 70 | import { readFileSync as _readFileSync } from "path"; 71 | var module = { 72 | exports: {} 73 | }; 74 | exports.readFileSync = _readFileSync; 75 | export const readFileSync = _readFileSync; 76 | export default module.exports; 77 | ``` 78 | 79 | ### Options 80 | 81 | - `synchronousImport` - Convert non-static require to a compatible dynamic 82 | import. If the bundler can inline and link synchronously, this should be 83 | okay, although this will produce invalid code for any other case. Use with 84 | caution! 85 | 86 | ```json 87 | { 88 | "plugins": [ 89 | ["transform-commonjs", { "synchronousImport": true }] 90 | ] 91 | } 92 | ``` 93 | 94 | - `exportsOnly` - Keep `require` calls and process exports only. 95 | 96 | ```json 97 | { 98 | "plugins": [ 99 | ["transform-commonjs", { "exportsOnly": true }] 100 | ] 101 | } 102 | ``` 103 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import { declare } from '@babel/helper-plugin-utils'; 2 | import { types as t } from '@babel/core'; 3 | import { check } from 'reserved-words'; 4 | 5 | export default declare((api, options) => { 6 | api.assertVersion(7); 7 | 8 | const state = { 9 | globals: new Set(), 10 | renamed: new Map(), 11 | identifiers: new Set(), 12 | isCJS: false, 13 | }; 14 | 15 | const enter = path => { 16 | let cursor = path; 17 | 18 | // Find the closest function scope or parent. 19 | do { 20 | // Ignore block statements. 21 | if (t.isBlockStatement(cursor.scope.path)) { 22 | continue; 23 | } 24 | 25 | if (t.isFunction(cursor.scope.path) || t.isProgram(cursor.scope.path)) { 26 | break; 27 | } 28 | } while (cursor = cursor.scope.path.parentPath); 29 | 30 | if (t.isProgram(cursor.scope.path)) { 31 | const nodes = []; 32 | const inner = []; 33 | 34 | // Break up the program, separate Nodes added by us from the nodes 35 | // created by the user. 36 | cursor.scope.path.node.body.filter(node => { 37 | // Keep replaced nodes together, these will not be wrapped. 38 | if (node.__replaced) { 39 | nodes.push(node); 40 | } 41 | else { 42 | inner.push(node); 43 | } 44 | }); 45 | 46 | const program = t.program([ 47 | ...nodes, 48 | t.expressionStatement( 49 | t.callExpression( 50 | t.memberExpression( 51 | t.functionExpression( 52 | null, 53 | [], 54 | t.blockStatement(inner), 55 | ), 56 | t.identifier('call'), 57 | ), 58 | [t.identifier('module.exports')], 59 | ) 60 | ), 61 | ]); 62 | 63 | cursor.scope.path.replaceWith(program); 64 | state.isCJS = true; 65 | } 66 | }; 67 | 68 | return { 69 | post() { 70 | state.globals.clear(); 71 | state.renamed.clear(); 72 | state.identifiers.clear(); 73 | state.isCJS = false; 74 | }, 75 | 76 | visitor: { 77 | Program: { 78 | exit(path) { 79 | 80 | path.traverse({ 81 | CallExpression: { 82 | exit(path) { 83 | const { node } = path; 84 | 85 | // Look for `require()` any renaming is assumed to be intentionally 86 | // done to break state kind of check, so we won't look for aliases. 87 | if (!options.exportsOnly && t.isIdentifier(node.callee) && node.callee.name === 'require') { 88 | // Require must be global for us to consider this a CommonJS 89 | // module. 90 | state.isCJS = true; 91 | 92 | // Check for nested string and template literals. 93 | const isString = t.isStringLiteral(node.arguments[0]); 94 | const isLiteral = t.isTemplateLiteral(node.arguments[0]); 95 | 96 | // Normalize the string value, default to the standard string 97 | // literal format of `{ value: "" }`. 98 | let str = null; 99 | 100 | if (isString) { 101 | str = node.arguments[0]; 102 | } 103 | else if (isLiteral) { 104 | str = { 105 | value: (node.arguments[0]).quasis[0].value.raw, 106 | }; 107 | } 108 | else if (options.synchronousImport) { 109 | const str = node.arguments[0]; 110 | const newNode = t.expressionStatement( 111 | t.callExpression(t.import(), [str]) 112 | ); 113 | 114 | // @ts-ignore 115 | newNode.__replaced = true; 116 | 117 | path.replaceWith(newNode); 118 | 119 | return; 120 | } 121 | else { 122 | throw new Error(`Invalid require signature: ${path.toString()}`); 123 | } 124 | 125 | const specifiers = []; 126 | 127 | // Convert to named import. 128 | if (t.isObjectPattern(path.parentPath.node.id)) { 129 | path.parentPath.node.id.properties.forEach(prop => { 130 | specifiers.push(t.importSpecifier( 131 | prop.value, 132 | prop.key, 133 | )); 134 | 135 | state.globals.add(prop.value.name); 136 | }); 137 | 138 | const decl = t.importDeclaration( 139 | specifiers, 140 | t.stringLiteral(str.value), 141 | ); 142 | 143 | // @ts-ignore 144 | decl.__replaced = true; 145 | 146 | path.scope.getProgramParent().path.unshiftContainer('body', decl); 147 | path.parentPath.remove(); 148 | } 149 | // Convert to default import. 150 | else if (str) { 151 | const { parentPath } = path; 152 | const { left } = parentPath.node; 153 | // @ts-ignore 154 | const oldId = !t.isMemberExpression(left) ? left : left.id; 155 | 156 | // Default to the closest likely identifier. 157 | let id = oldId; 158 | 159 | // If we can't find an id, generate one from the import path. 160 | if (!oldId || !t.isProgram(parentPath.scope.path.type)) { 161 | id = path.scope.generateUidIdentifier(str.value); 162 | } 163 | 164 | // Add state global name to the list. 165 | state.globals.add(id.name); 166 | 167 | // Create an import declaration. 168 | const decl = t.importDeclaration( 169 | [t.importDefaultSpecifier(id)], 170 | t.stringLiteral(str.value), 171 | ); 172 | 173 | // @ts-ignore 174 | decl.__replaced = true; 175 | 176 | // Push the declaration in the root scope. 177 | path.scope.getProgramParent().path.unshiftContainer('body', decl); 178 | 179 | // If we needed to generate or the change the id, then make an 180 | // assignment so the values stay in sync. 181 | if (oldId && !t.isNodesEquivalent(oldId, id)) { 182 | const newNode = t.expressionStatement( 183 | t.assignmentExpression( 184 | '=', 185 | oldId, 186 | id, 187 | ) 188 | ); 189 | 190 | // @ts-ignore 191 | newNode.__replaced = true; 192 | 193 | path.parentPath.parentPath.replaceWith(newNode); 194 | } 195 | // If we generated a new identifier for state, replace the inline 196 | // call with the variable. 197 | else if (!oldId) { 198 | path.replaceWith(id); 199 | } 200 | // Otherwise completely remove. 201 | else { 202 | path.parentPath.remove(); 203 | } 204 | } 205 | } 206 | } 207 | }, 208 | }); 209 | 210 | const programPath = path.scope.getProgramParent().path; 211 | 212 | // Even though we are pretty sure this isn't a CommonJS file, lets 213 | // do one last sanity check for an `import` or `export` in the 214 | // program path. 215 | if (!state.isCJS) { 216 | const lastImport = programPath 217 | .get('body') 218 | .filter(p => p.isImportDeclaration()) 219 | .pop(); 220 | 221 | const lastExport = programPath 222 | .get('body') 223 | .filter(p => p.isExportDeclaration()) 224 | .pop(); 225 | 226 | // Maybe it is a CJS file after-all. 227 | if (!lastImport && !lastExport) { 228 | state.isCJS = true; 229 | } 230 | } 231 | 232 | if (path.node.__replaced || !state.isCJS) { return; } 233 | 234 | const exportsAlias = t.variableDeclaration('var', [ 235 | t.variableDeclarator( 236 | t.identifier('exports'), 237 | t.memberExpression( 238 | t.identifier('module'), 239 | t.identifier('exports'), 240 | ) 241 | ) 242 | ]); 243 | 244 | const moduleExportsAlias = t.variableDeclaration('var', [ 245 | t.variableDeclarator( 246 | t.identifier('module'), 247 | t.objectExpression([ 248 | t.objectProperty( 249 | t.identifier('exports'), 250 | t.objectExpression([]), 251 | ) 252 | ]), 253 | ) 254 | ]); 255 | 256 | // @ts-ignore 257 | exportsAlias.__replaced = true; 258 | // @ts-ignore 259 | moduleExportsAlias.__replaced = true; 260 | 261 | // Add the `module` and `exports` globals into the program body, 262 | // after the last `import` declaration. 263 | const lastImport = programPath 264 | .get('body') 265 | .filter(p => p.isImportDeclaration()) 266 | .pop(); 267 | 268 | if (lastImport) { 269 | lastImport.insertAfter(exportsAlias); 270 | lastImport.insertAfter(moduleExportsAlias); 271 | } 272 | else { 273 | programPath.unshiftContainer('body', exportsAlias); 274 | programPath.unshiftContainer('body', moduleExportsAlias); 275 | } 276 | 277 | const defaultExport = t.exportDefaultDeclaration( 278 | t.memberExpression( 279 | t.identifier('module'), 280 | t.identifier('exports'), 281 | ) 282 | ); 283 | 284 | path.node.__replaced = true; 285 | // @ts-ignore 286 | defaultExport.__replaced = true; 287 | 288 | programPath.pushContainer('body', defaultExport); 289 | } 290 | }, 291 | 292 | ThisExpression: { enter }, 293 | ReturnStatement: { enter }, 294 | 295 | ImportSpecifier: { 296 | enter(path) { 297 | const { name } = path.node.local; 298 | 299 | // If state import was renamed, ensure the source reflects it. 300 | if (state.renamed.has(name)) { 301 | const oldName = t.identifier(name); 302 | const newName = t.identifier(state.renamed.get(name)); 303 | 304 | path.replaceWith(t.importSpecifier(newName, oldName)); 305 | } 306 | } 307 | }, 308 | 309 | AssignmentExpression: { 310 | enter(path) { 311 | if (path.node.__ignore) { 312 | return; 313 | } 314 | 315 | path.node.__ignore = true; 316 | 317 | // Check for module.exports. 318 | if (t.isMemberExpression(path.node.left)) { 319 | const moduleBinding = path.scope.getBinding('module'); 320 | const exportsBinding = path.scope.getBinding('exports'); 321 | 322 | // Something like `module.exports.namedExport = true;`. 323 | if (t.isMemberExpression(path.node.left.object) && ( 324 | path.node.left.object.object.name === 'module' 325 | )) { 326 | if (!moduleBinding) { 327 | state.isCJS = true; 328 | return; 329 | } 330 | } 331 | else if ( 332 | t.isIdentifier(path.node.left.object) && ( 333 | path.node.left.object.name === 'module' 334 | ) 335 | ) { 336 | if (!moduleBinding) { 337 | state.isCJS = true; 338 | 339 | // Looking at a re-exports, handled above. 340 | if (t.isCallExpression(path.node.right)) { 341 | return; 342 | } 343 | } 344 | } 345 | // Check for regular exports 346 | else if (path.node.left.object.name === 'exports') { 347 | const { name } = path.node.left.property; 348 | if ( 349 | exportsBinding 350 | // If export is named "default" leave as is. 351 | // It is not possible to export "default" as a named export. 352 | // e.g. `export.default = 'a'` 353 | || name === 'default' 354 | ) { 355 | return; 356 | } 357 | 358 | state.isCJS = true; 359 | 360 | let prop = path.node.right; 361 | 362 | if ( 363 | ( 364 | path.scope.getProgramParent().hasBinding(prop.name) || 365 | state.globals.has(prop.name) 366 | // Don't rename `undefined`. 367 | ) && prop.name !== 'undefined' 368 | ) { 369 | 370 | prop = path.scope.generateUidIdentifier(prop.name); 371 | 372 | const oldName = path.node.right.name; 373 | state.renamed.set(oldName, prop.name); 374 | 375 | // Add this new identifier into the globals and replace the 376 | // right hand side with this replacement. 377 | state.globals.add(prop.name); 378 | path.get('right').replaceWith(prop); 379 | path.scope.rename(oldName, prop.name); 380 | } 381 | 382 | // If we set an invalid name, then abort out. 383 | try { 384 | // Ensure that the scope is clean before we inject new, 385 | // potentially conflicting, variables. 386 | const newName = path.scope.generateUidIdentifier(name).name; 387 | 388 | path.scope.rename(name, newName); 389 | 390 | // Check if this name is reserved, if so, then bail out. 391 | if (check(name)) { 392 | return; 393 | } 394 | 395 | const decl = t.exportNamedDeclaration( 396 | t.variableDeclaration('let', [ 397 | t.variableDeclarator( 398 | path.node.left.property, 399 | t.memberExpression( 400 | t.identifier('exports'), 401 | path.node.left.property 402 | ) 403 | ) 404 | ]), 405 | [], 406 | ); 407 | 408 | if (!state.identifiers.has(name)) { 409 | path.scope.getProgramParent().path.pushContainer('body', decl); 410 | state.identifiers.add(name); 411 | } 412 | } 413 | catch {} 414 | } 415 | } 416 | } 417 | }, 418 | }, 419 | }; 420 | }); 421 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-transform-commonjs", 3 | "version": "1.1.6", 4 | "description": "A Babel plugin transform to convert CommonJS to ES modules", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "prepack": "tsc", 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "mocha" 11 | }, 12 | "keywords": [ 13 | "babel", 14 | "transform", 15 | "plugin", 16 | "commonjs", 17 | "modules", 18 | "esm" 19 | ], 20 | "author": "Tim Branyen (@tbranyen)", 21 | "license": "MIT", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/tbranyen/babel-plugin-transform-commonjs.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/tbranyen/babel-plugin-transform-commonjs/issues" 28 | }, 29 | "homepage": "https://github.com/tbranyen/babel-plugin-transform-commonjs#readme", 30 | "dependencies": { 31 | "@babel/helper-plugin-utils": "^7.0.0", 32 | "reserved-words": "^0.1.2" 33 | }, 34 | "peerDependencies": { 35 | "@babel/core": ">=7" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.1.2", 39 | "@types/babel__core": "^7.1.3", 40 | "@types/reserved-words": "^0.1.0", 41 | "mocha": "^5.2.0", 42 | "ts-node": "7.0.1", 43 | "typescript": "^3.1.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/_utils.js: -------------------------------------------------------------------------------- 1 | exports.format = strings => { 2 | const source = strings.join('').trim(); 3 | const lines = source.split('\n'); 4 | 5 | if (lines.length === 1) { 6 | return source; 7 | } 8 | 9 | const space = lines[lines.length - 1].match(/\s+/)[0]; 10 | const exp = new RegExp(`${space}`, 'g'); 11 | 12 | return source.replace(exp, ''); 13 | }; 14 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const { equal, deepEqual, match } = require('assert'); 2 | const { transformAsync } = require('@babel/core'); 3 | const { default: traverseAst } = require('@babel/traverse'); 4 | const { format } = require('./_utils'); 5 | const plugin = require('../lib/index.ts'); 6 | 7 | describe('Transform CommonJS', function() { 8 | const defaults = { 9 | plugins: [plugin], 10 | sourceType: 'module', 11 | }; 12 | 13 | describe('General behavior', () => { 14 | it('can ignore esm modules', async () => { 15 | const input = ` 16 | export const undef = undefined; 17 | `; 18 | 19 | const { code } = await transformAsync(input, { 20 | ...defaults, 21 | sourceType: 'module', 22 | }); 23 | 24 | equal(code, format` 25 | export const undef = undefined; 26 | `); 27 | }); 28 | 29 | it('can ignore esm modules with module argument', async () => { 30 | const input = ` 31 | function fakeModule(module) { 32 | module.exports = {}; 33 | module.exports.fake = 'not real'; 34 | } 35 | 36 | export const undef = undefined; 37 | `; 38 | 39 | const { code } = await transformAsync(input, { 40 | ...defaults, 41 | sourceType: 'module', 42 | }); 43 | 44 | equal(code, format` 45 | function fakeModule(module) { 46 | module.exports = {}; 47 | module.exports.fake = 'not real'; 48 | } 49 | 50 | export const undef = undefined; 51 | `); 52 | }); 53 | 54 | it('can ignore esm modules with exports argument', async () => { 55 | const input = ` 56 | function fakeExports(exports) { 57 | exports = {}; 58 | exports.fake = 'not real'; 59 | } 60 | 61 | export const undef = undefined; 62 | `; 63 | 64 | const { code } = await transformAsync(input, { 65 | ...defaults, 66 | sourceType: 'module', 67 | }); 68 | 69 | equal(code, format` 70 | function fakeExports(exports) { 71 | exports = {}; 72 | exports.fake = 'not real'; 73 | } 74 | 75 | export const undef = undefined; 76 | `); 77 | }); 78 | 79 | it('can ignore esm modules with this set in function', async () => { 80 | const input = ` 81 | function fakeExports() { 82 | this.fake = 'not real'; 83 | } 84 | 85 | export const undef = undefined; 86 | `; 87 | 88 | const { code } = await transformAsync(input, { 89 | ...defaults, 90 | sourceType: 'module', 91 | }); 92 | 93 | equal(code, format` 94 | function fakeExports() { 95 | this.fake = 'not real'; 96 | } 97 | 98 | export const undef = undefined; 99 | `); 100 | }); 101 | 102 | it('can support a cjs module that has a binding to module', async () => { 103 | const input = ` 104 | const { module } = global; 105 | 106 | module.exports = true; 107 | `; 108 | 109 | const { code } = await transformAsync(input, { 110 | ...defaults, 111 | sourceType: 'module', 112 | }); 113 | 114 | equal(code, format` 115 | var module = { 116 | exports: {} 117 | }; 118 | var exports = module.exports; 119 | const { 120 | module 121 | } = global; 122 | module.exports = true; 123 | export default module.exports; 124 | `); 125 | }); 126 | 127 | it('can support exporting all literal types', async () => { 128 | const input = ` 129 | exports.Undefined = undefined; 130 | exports.Null = null; 131 | exports.Symbol = Symbol('test'); 132 | exports.Number = 5; 133 | exports.Boolean = false; 134 | exports.String = 'hello world'; 135 | exports.Function = function() {}; 136 | `; 137 | 138 | const { code } = await transformAsync(input, { ...defaults }); 139 | 140 | equal(code, format` 141 | var module = { 142 | exports: {} 143 | }; 144 | var exports = module.exports; 145 | exports.Undefined = undefined; 146 | exports.Null = null; 147 | exports.Symbol = Symbol('test'); 148 | exports.Number = 5; 149 | exports.Boolean = false; 150 | exports.String = 'hello world'; 151 | 152 | exports.Function = function () {}; 153 | 154 | export let Undefined = exports.Undefined; 155 | export let Null = exports.Null; 156 | export let Symbol = exports.Symbol; 157 | export let Number = exports.Number; 158 | export let Boolean = exports.Boolean; 159 | export let String = exports.String; 160 | export let Function = exports.Function; 161 | export default module.exports; 162 | `); 163 | }); 164 | 165 | it('can support a simple default export', async () => { 166 | const input = ` 167 | module.exports = "hello world"; 168 | `; 169 | 170 | const { code } = await transformAsync(input, { ...defaults }); 171 | 172 | equal(code, format` 173 | var module = { 174 | exports: {} 175 | }; 176 | var exports = module.exports; 177 | module.exports = "hello world"; 178 | export default module.exports; 179 | `); 180 | }); 181 | 182 | it('can support a simple named export through module.exports', async () => { 183 | const input = ` 184 | module.exports.test = "hello world"; 185 | `; 186 | 187 | const { code } = await transformAsync(input, { ...defaults }); 188 | 189 | equal(code, format` 190 | var module = { 191 | exports: {} 192 | }; 193 | var exports = module.exports; 194 | module.exports.test = "hello world"; 195 | export default module.exports; 196 | `); 197 | }); 198 | 199 | it('can support tracking nested identifiers properly', async () => { 200 | const input = ` 201 | function test() { 202 | var l = true; 203 | reference[l]; 204 | } 205 | `; 206 | 207 | const { code, ast } = await transformAsync(input, { 208 | ...defaults, 209 | ast: true, 210 | }); 211 | 212 | let bindings = null; 213 | 214 | traverseAst(ast, { 215 | Program(path) { 216 | bindings = path.scope.getAllBindings(); 217 | } 218 | }); 219 | 220 | equal(bindings.test.referenced, false, 'test is in the global scope'); 221 | equal(bindings.l, undefined, 'l is not in the global scope'); 222 | }); 223 | 224 | it('can support early return', async () => { 225 | const input = ` 226 | const { isMaster } = require('cluster'); 227 | 228 | if (isMaster) { 229 | return; 230 | } 231 | 232 | console.log('Is Worker'); 233 | `; 234 | 235 | const { code } = await transformAsync(input, { 236 | ...defaults, 237 | parserOpts: { 238 | allowReturnOutsideFunction: true, 239 | }, 240 | }); 241 | 242 | equal(code, format` 243 | import { isMaster } from "cluster"; 244 | var module = { 245 | exports: {} 246 | }; 247 | var exports = module.exports; 248 | (function () { 249 | if (isMaster) { 250 | return; 251 | } 252 | 253 | console.log('Is Worker'); 254 | }).call(module.exports); 255 | export default module.exports; 256 | `); 257 | }); 258 | 259 | it('can ignore invalid named exports, keeping them on default', async () => { 260 | const input = ` 261 | exports["I'mateapot"] = { 262 | a: true, 263 | }; 264 | `; 265 | 266 | const { code } = await transformAsync(input, { 267 | ...defaults, 268 | }); 269 | 270 | equal(code, format` 271 | var module = { 272 | exports: {} 273 | }; 274 | var exports = module.exports; 275 | exports["I'mateapot"] = { 276 | a: true 277 | }; 278 | export default module.exports; 279 | `); 280 | }); 281 | 282 | it('can support exporting via `this`', async () => { 283 | const input = ` 284 | this.export = 'true'; 285 | `; 286 | 287 | const { code } = await transformAsync(input, { 288 | ...defaults, 289 | parserOpts: { 290 | allowReturnOutsideFunction: true, 291 | }, 292 | }); 293 | 294 | equal(code, format` 295 | var module = { 296 | exports: {} 297 | }; 298 | var exports = module.exports; 299 | (function () { 300 | this.export = 'true'; 301 | }).call(module.exports); 302 | export default module.exports; 303 | `); 304 | }); 305 | }); 306 | 307 | describe('Require', () => { 308 | it('can support a single require call', async () => { 309 | const input = ` 310 | var a = require('path'); 311 | `; 312 | 313 | const { code } = await transformAsync(input, { ...defaults }); 314 | 315 | equal(code, format` 316 | import _path from "path"; 317 | var module = { 318 | exports: {} 319 | }; 320 | var exports = module.exports; 321 | var a = _path; 322 | export default module.exports; 323 | `); 324 | }); 325 | 326 | it('can support a single require call using template literal', async () => { 327 | const input = ` 328 | var a = require(\`path\`); 329 | `; 330 | 331 | const { code } = await transformAsync(input, { ...defaults }); 332 | 333 | equal(code, format` 334 | import _path from "path"; 335 | var module = { 336 | exports: {} 337 | }; 338 | var exports = module.exports; 339 | var a = _path; 340 | export default module.exports; 341 | `); 342 | }); 343 | 344 | it('can support a wrapped require call', async () => { 345 | const input = ` 346 | var a = wrapped(require('path')); 347 | `; 348 | 349 | const { code } = await transformAsync(input, { ...defaults }); 350 | 351 | equal(code, format` 352 | import _path from "path"; 353 | var module = { 354 | exports: {} 355 | }; 356 | var exports = module.exports; 357 | var a = wrapped(_path); 358 | export default module.exports; 359 | `); 360 | }); 361 | 362 | it('can produce a unique name for an anonymous require', async () => { 363 | const input = ` 364 | ((a) => {console.log(a)})(deeply(nested(require('./some/complex/path')))); 365 | `; 366 | 367 | const { code } = await transformAsync(input, { ...defaults }); 368 | 369 | equal(code, format` 370 | import _someComplexPath from "./some/complex/path"; 371 | var module = { 372 | exports: {} 373 | }; 374 | var exports = module.exports; 375 | 376 | (a => { 377 | console.log(a); 378 | })(deeply(nested(_someComplexPath))); 379 | 380 | export default module.exports; 381 | `); 382 | }); 383 | 384 | it('can support a memberexpression import assignment', async () => { 385 | const input = ` 386 | var ArrayObservable_1 = require('./ArrayObservable'); 387 | exports.of = ArrayObservable_1.ArrayObservable.of; 388 | `; 389 | 390 | const { code } = await transformAsync(input, { ...defaults }); 391 | 392 | equal(code, format` 393 | import _ArrayObservable from "./ArrayObservable"; 394 | var module = { 395 | exports: {} 396 | }; 397 | var exports = module.exports; 398 | var ArrayObservable_1 = _ArrayObservable; 399 | exports.of = ArrayObservable_1.ArrayObservable.of; 400 | export let of = exports.of; 401 | export default module.exports; 402 | `); 403 | }); 404 | }); 405 | 406 | describe('Imports', () => { 407 | it('can ignore imports if exportsOnly is set', async () => { 408 | const input = ` 409 | var a = require('path'); 410 | exports.test = true; 411 | `; 412 | 413 | const { code } = await transformAsync(input, { 414 | ...defaults, 415 | plugins: [[plugin, { 416 | exportsOnly: true, 417 | }]], 418 | }); 419 | 420 | equal(code, format` 421 | var module = { 422 | exports: {} 423 | }; 424 | var exports = module.exports; 425 | 426 | var a = require('path'); 427 | 428 | exports.test = true; 429 | export let test = exports.test; 430 | export default module.exports; 431 | `); 432 | }); 433 | 434 | it('can support top-level default', async () => { 435 | const input = ` 436 | var a = require('path'); 437 | `; 438 | 439 | const { code } = await transformAsync(input, { ...defaults }); 440 | 441 | equal(code, format` 442 | import _path from "path"; 443 | var module = { 444 | exports: {} 445 | }; 446 | var exports = module.exports; 447 | var a = _path; 448 | export default module.exports; 449 | `); 450 | }); 451 | 452 | it('can support nested default', async () => { 453 | const input = ` 454 | if (true) { 455 | var a = require('path'); 456 | } 457 | `; 458 | 459 | const { code } = await transformAsync(input, { ...defaults }); 460 | 461 | equal(code, format` 462 | import _path from "path"; 463 | var module = { 464 | exports: {} 465 | }; 466 | var exports = module.exports; 467 | 468 | if (true) { 469 | var a = _path; 470 | } 471 | 472 | export default module.exports; 473 | `); 474 | }); 475 | 476 | it('can not support non-static import inside a try/catch by default', async () => { 477 | const input = ` 478 | function test() { 479 | try { 480 | return require(name); 481 | } finally { 482 | LOADING_MODULES.delete(name); 483 | } 484 | } 485 | `; 486 | 487 | await transformAsync(input, { ...defaults }).catch(ex => { 488 | match(ex.toString(), /Invalid require signature: require\(name\)/); 489 | }); 490 | }); 491 | 492 | it('can support non-static import inside a try/catch, with option', async () => { 493 | const input = ` 494 | function test() { 495 | try { 496 | return require(name); 497 | } finally { 498 | LOADING_MODULES.delete(name); 499 | } 500 | } 501 | `; 502 | 503 | const { code } = await transformAsync(input, { 504 | ...defaults, 505 | plugins: [[plugin, { 506 | synchronousImport: true, 507 | }]], 508 | }); 509 | 510 | equal(code, format` 511 | var module = { 512 | exports: {} 513 | }; 514 | var exports = module.exports; 515 | 516 | function test() { 517 | try { 518 | return import(name); 519 | } finally { 520 | LOADING_MODULES.delete(name); 521 | } 522 | } 523 | 524 | export default module.exports; 525 | `); 526 | }); 527 | 528 | it('can not support interpolated require call', async () => { 529 | const input = ` 530 | var a = require('pat' + 'h'); 531 | `; 532 | 533 | await transformAsync(input, { ...defaults }).catch(ex => { 534 | match(ex.toString(), /Invalid require signature: require\('pat' \+ 'h'\)/); 535 | }); 536 | }); 537 | 538 | it('can support interpolated require call with option', async () => { 539 | const input = ` 540 | var a = require('pat' + 'h'); 541 | `; 542 | 543 | const { code } = await transformAsync(input, { 544 | ...defaults, 545 | plugins: [[plugin, { 546 | synchronousImport: true, 547 | }]] 548 | }); 549 | 550 | equal(code, format` 551 | var module = { 552 | exports: {} 553 | }; 554 | var exports = module.exports; 555 | var a = import('pat' + 'h'); 556 | export default module.exports; 557 | `); 558 | }); 559 | 560 | it('can support top-level nested', async () => { 561 | const input = ` 562 | var { a } = require('path'); 563 | `; 564 | 565 | const { code } = await transformAsync(input, { ...defaults }); 566 | 567 | equal(code, format` 568 | import { a } from "path"; 569 | var module = { 570 | exports: {} 571 | }; 572 | var exports = module.exports; 573 | export default module.exports; 574 | `); 575 | }); 576 | 577 | it('can support top-level nested renaming', async () => { 578 | const input = ` 579 | var { a: b } = require('path'); 580 | `; 581 | 582 | const { code } = await transformAsync(input, { ...defaults }); 583 | 584 | equal(code, format` 585 | import { a as b } from "path"; 586 | var module = { 587 | exports: {} 588 | }; 589 | var exports = module.exports; 590 | export default module.exports; 591 | `); 592 | }); 593 | 594 | it('can support require inside of a call expression', async () => { 595 | const input = ` 596 | const data = _interopRequireDefault(require('a')); 597 | `; 598 | 599 | const { code } = await transformAsync(input, { ...defaults }); 600 | 601 | equal(code, format` 602 | import _a from "a"; 603 | var module = { 604 | exports: {} 605 | }; 606 | var exports = module.exports; 607 | 608 | const data = _interopRequireDefault(_a); 609 | 610 | export default module.exports; 611 | `); 612 | }); 613 | }); 614 | 615 | describe('Exports', () => { 616 | it('can support top-level default', async () => { 617 | const input = ` 618 | module.exports = 'a'; 619 | `; 620 | 621 | const { code } = await transformAsync(input, { ...defaults }); 622 | 623 | equal(code, format` 624 | var module = { 625 | exports: {} 626 | }; 627 | var exports = module.exports; 628 | module.exports = 'a'; 629 | export default module.exports; 630 | `); 631 | }); 632 | 633 | it('can support a reserved word identifier', async () => { 634 | const input = ` 635 | exports.super = () => {}; 636 | `; 637 | 638 | const { code } = await transformAsync(input, { ...defaults }); 639 | 640 | equal(code, format` 641 | var module = { 642 | exports: {} 643 | }; 644 | var exports = module.exports; 645 | 646 | exports.super = () => {}; 647 | 648 | export default module.exports; 649 | `); 650 | 651 | }); 652 | 653 | it('can support nested default', async () => { 654 | const input = ` 655 | if (true) { 656 | module.exports = 'a'; 657 | } 658 | `; 659 | 660 | const { code } = await transformAsync(input, { ...defaults }); 661 | 662 | equal(code, format` 663 | var module = { 664 | exports: {} 665 | }; 666 | var exports = module.exports; 667 | 668 | if (true) { 669 | module.exports = 'a'; 670 | } 671 | 672 | export default module.exports; 673 | `); 674 | }); 675 | 676 | it('can support top-level named', async () => { 677 | const input = ` 678 | const { a } = require('path'); 679 | 680 | exports.a = a; 681 | `; 682 | 683 | const { code } = await transformAsync(input, { ...defaults }); 684 | 685 | equal(code, format` 686 | import { a as _a } from "path"; 687 | var module = { 688 | exports: {} 689 | }; 690 | var exports = module.exports; 691 | exports.a = _a; 692 | export let a = exports.a; 693 | export default module.exports; 694 | `); 695 | }); 696 | 697 | it('can support named default', async () => { 698 | const input = ` 699 | const { a } = require('path'); 700 | 701 | exports.default = a; 702 | `; 703 | 704 | const { code } = await transformAsync(input, { ...defaults }); 705 | 706 | equal(code, format` 707 | import { a } from "path"; 708 | var module = { 709 | exports: {} 710 | }; 711 | var exports = module.exports; 712 | exports.default = a; 713 | export default module.exports; 714 | `); 715 | }); 716 | 717 | it('can support named default with default', async () => { 718 | // export.default should be overridden 719 | const input = ` 720 | const { a } = require('path'); 721 | const thing = 'thing'; 722 | 723 | exports.default = a; 724 | module.exports = thing; 725 | `; 726 | 727 | const { code } = await transformAsync(input, { ...defaults }); 728 | 729 | equal(code, format` 730 | import { a } from "path"; 731 | var module = { 732 | exports: {} 733 | }; 734 | var exports = module.exports; 735 | const thing = 'thing'; 736 | exports.default = a; 737 | module.exports = thing; 738 | export default module.exports; 739 | `); 740 | }); 741 | 742 | it('can support duplicate named with initialization', async () => { 743 | const input = ` 744 | exports.a = undefined; 745 | 746 | var a = exports.a = () => {}; 747 | `; 748 | 749 | const { code } = await transformAsync(input, { ...defaults }); 750 | 751 | equal(code, format` 752 | var module = { 753 | exports: {} 754 | }; 755 | var exports = module.exports; 756 | exports.a = undefined; 757 | 758 | var _a = exports.a = () => {}; 759 | 760 | export let a = exports.a; 761 | export default module.exports; 762 | `); 763 | }); 764 | 765 | it('can support nested named', async () => { 766 | const input = ` 767 | { 768 | exports.a = true; 769 | } 770 | `; 771 | 772 | const { code } = await transformAsync(input, { ...defaults }); 773 | 774 | equal(code, format` 775 | var module = { 776 | exports: {} 777 | }; 778 | var exports = module.exports; 779 | { 780 | exports.a = true; 781 | } 782 | export let a = exports.a; 783 | export default module.exports; 784 | `); 785 | }); 786 | 787 | it('can support reading named exports from exports object', async () => { 788 | const input = ` 789 | var { readFileSync } = require('path'); 790 | exports.readFileSync = readFileSync; 791 | console.log(module.exports.readFileSync); 792 | `; 793 | 794 | const { code } = await transformAsync(input, { ...defaults }); 795 | 796 | equal(code, format` 797 | import { readFileSync as _readFileSync } from "path"; 798 | var module = { 799 | exports: {} 800 | }; 801 | var exports = module.exports; 802 | exports.readFileSync = _readFileSync; 803 | console.log(module.exports.readFileSync); 804 | export let readFileSync = exports.readFileSync; 805 | export default module.exports; 806 | `); 807 | }); 808 | 809 | it('can support conditional mutable bindings', async () => { 810 | const input = ` 811 | if (hasNativePerformanceNow) { 812 | var Performance = performance; 813 | exports.unstable_now = function () { 814 | return Performance.now(); 815 | }; 816 | } else { 817 | exports.unstable_now = function () { 818 | return localDate.now(); 819 | }; 820 | } 821 | `; 822 | 823 | const { code } = await transformAsync(input, { ...defaults }); 824 | 825 | equal(code, format` 826 | var module = { 827 | exports: {} 828 | }; 829 | var exports = module.exports; 830 | 831 | if (hasNativePerformanceNow) { 832 | var Performance = performance; 833 | 834 | exports.unstable_now = function () { 835 | return Performance.now(); 836 | }; 837 | } else { 838 | exports.unstable_now = function () { 839 | return localDate.now(); 840 | }; 841 | } 842 | 843 | export let unstable_now = exports.unstable_now; 844 | export default module.exports; 845 | `); 846 | }); 847 | 848 | it.skip('can support defineProperty', async () => { 849 | /* Something needs to set state.isCJS for Object.defineProperty 850 | * and Object.defineProperties for this test to pass. */ 851 | const input = ` 852 | Object.defineProperty(exports, "__esModule", { value: true }); 853 | `; 854 | 855 | const { code } = await transformAsync(input, { ...defaults }); 856 | 857 | equal(code, format` 858 | var module = { 859 | exports: {} 860 | }; 861 | var exports = module.exports; 862 | Object.defineProperty(exports, "__esModule", { 863 | value: true 864 | }); 865 | export let __esModule = exports.__esModule; 866 | export default module.exports; 867 | `); 868 | }); 869 | 870 | it.skip('can support defineProperties', async () => { 871 | /* Something needs to set state.isCJS for Object.defineProperty 872 | * and Object.defineProperties for this test to pass. */ 873 | const input = ` 874 | Object.defineProperties(exports, { 875 | __esModule: { value: true }, 876 | }); 877 | `; 878 | 879 | const { code } = await transformAsync(input, { ...defaults }); 880 | 881 | equal(code, format` 882 | var module = { 883 | exports: {} 884 | }; 885 | var exports = module.exports; 886 | Object.defineProperties(exports, { 887 | __esModule: { 888 | value: true 889 | } 890 | }); 891 | export let __esModule = exports.__esModule; 892 | export default module.exports; 893 | `); 894 | }); 895 | }); 896 | 897 | describe('Re-exports', () => { 898 | it('can support top-level named', async () => { 899 | const input = ` 900 | exports.a = require('path'); 901 | `; 902 | 903 | const { code } = await transformAsync(input, { ...defaults }); 904 | 905 | equal(code, format` 906 | import _path from "path"; 907 | var module = { 908 | exports: {} 909 | }; 910 | var exports = module.exports; 911 | exports.a = _path; 912 | export let a = exports.a; 913 | export default module.exports; 914 | `); 915 | }); 916 | 917 | it('can support top-level default', async () => { 918 | const input = ` 919 | module.exports = require('path'); 920 | `; 921 | 922 | const { code } = await transformAsync(input, { ...defaults }); 923 | 924 | equal(code, format` 925 | import _path from "path"; 926 | var module = { 927 | exports: {} 928 | }; 929 | var exports = module.exports; 930 | module.exports = _path; 931 | export default module.exports; 932 | `); 933 | }); 934 | 935 | it('can ensure export names will not collide', async () => { 936 | const input = ` 937 | var a = require('path'); 938 | exports.a = a; 939 | `; 940 | 941 | const { code } = await transformAsync(input, { ...defaults }); 942 | 943 | equal(code, format` 944 | import _path from "path"; 945 | var module = { 946 | exports: {} 947 | }; 948 | var exports = module.exports; 949 | var _a = _path; 950 | exports.a = _a; 951 | export let a = exports.a; 952 | export default module.exports; 953 | `); 954 | }); 955 | 956 | it('can ensure export names from named imports will not collide', async () => { 957 | const input = ` 958 | var { readFileSync } = require('path'); 959 | exports.readFileSync = readFileSync; 960 | `; 961 | 962 | const { code } = await transformAsync(input, { ...defaults }); 963 | 964 | equal(code, format` 965 | import { readFileSync as _readFileSync } from "path"; 966 | var module = { 967 | exports: {} 968 | }; 969 | var exports = module.exports; 970 | exports.readFileSync = _readFileSync; 971 | export let readFileSync = exports.readFileSync; 972 | export default module.exports; 973 | `); 974 | }); 975 | 976 | it('can support nested default', async () => { 977 | const input = ` 978 | { 979 | module.exports = require('path'); 980 | } 981 | `; 982 | 983 | const { code } = await transformAsync(input, { ...defaults }); 984 | 985 | equal(code, format` 986 | import _path from "path"; 987 | var module = { 988 | exports: {} 989 | }; 990 | var exports = module.exports; 991 | { 992 | module.exports = _path; 993 | } 994 | export default module.exports; 995 | `); 996 | }); 997 | 998 | it('can support nested named', async () => { 999 | const input = ` 1000 | { 1001 | exports.a = require('path'); 1002 | } 1003 | `; 1004 | 1005 | const { code } = await transformAsync(input, { ...defaults }); 1006 | 1007 | equal(code, format` 1008 | import _path from "path"; 1009 | var module = { 1010 | exports: {} 1011 | }; 1012 | var exports = module.exports; 1013 | { 1014 | exports.a = _path; 1015 | } 1016 | export let a = exports.a; 1017 | export default module.exports; 1018 | `); 1019 | }); 1020 | 1021 | it('supports multiple export assignment', async () => { 1022 | const input = ` 1023 | exports.a = exports.b = undefined; 1024 | `; 1025 | 1026 | const { code } = await transformAsync(input, { ...defaults }); 1027 | 1028 | equal(code, format` 1029 | var module = { 1030 | exports: {} 1031 | }; 1032 | var exports = module.exports; 1033 | exports.a = exports.b = undefined; 1034 | export let a = exports.a; 1035 | export let b = exports.b; 1036 | export default module.exports; 1037 | `); 1038 | }); 1039 | }); 1040 | }); 1041 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --watch-extensions ts 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "strict": false, 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "inlineSourceMap": true, 9 | "outDir": "dist", 10 | "lib": ["es2017", "dom"] 11 | }, 12 | "include": [ 13 | "./lib/index.ts" 14 | ] 15 | } 16 | 17 | --------------------------------------------------------------------------------