├── .README ├── stack-trace-before-and-after.png ├── stack-trace-with-name.png └── stack-trace-without-name.png ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── deriveName.js ├── index.js ├── normalizeName.js └── resolveConflictingName.js └── test ├── .eslintrc └── index.js /.README/stack-trace-before-and-after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/babel-plugin-transform-export-default-name/be066e1edf0db82e1b4be2639e78bb62220866ca/.README/stack-trace-before-and-after.png -------------------------------------------------------------------------------- /.README/stack-trace-with-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/babel-plugin-transform-export-default-name/be066e1edf0db82e1b4be2639e78bb62220866ca/.README/stack-trace-with-name.png -------------------------------------------------------------------------------- /.README/stack-trace-without-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/babel-plugin-transform-export-default-name/be066e1edf0db82e1b4be2639e78bb62220866ca/.README/stack-trace-without-name.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-es2015-modules-commonjs", 4 | "transform-es2015-parameters", 5 | "transform-es2015-block-scoping", 6 | "transform-es2015-destructuring", 7 | "transform-flow-strip-types" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "canonical", 3 | "root": true 4 | } 5 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules/config-chain/test/broken.json 3 | /node_modules/conventional-changelog-core/test/fixtures/_malformation.json 4 | /node_modules/npmconf/test/fixtures/package.json 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | *.log 5 | .* 6 | !.babelrc 7 | !.editorconfig 8 | !.eslintignore 9 | !.eslintrc 10 | !.flowconfig 11 | !.gitignore 12 | !.npmignore 13 | !.travis.yml 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | fixtures 3 | src 4 | test 5 | .* 6 | *.log 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - 7 5 | - 6 6 | - 5 7 | notifications: 8 | email: false 9 | sudo: false 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Gajus Kuizinas (http://gajus.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-transform-export-default-name 2 | 3 | [![NPM version](http://img.shields.io/npm/v/babel-plugin-transform-export-default-name.svg?style=flat-square)](https://www.npmjs.org/package/babel-plugin-transform-export-default-name) 4 | [![Travis build status](http://img.shields.io/travis/gajus/babel-plugin-transform-export-default-name/master.svg?style=flat-square)](https://travis-ci.org/gajus/babel-plugin-transform-export-default-name) 5 | [![js-canonical-style](https://img.shields.io/badge/code%20style-canonical-blue.svg?style=flat-square)](https://github.com/gajus/canonical) 6 | 7 | Babel plugin that transforms `export default` of anonymous functions to named function export. 8 | 9 | Plugin uses the name of the target file to create a temporary variable. Target resource (arrow function or an anonymous function) is assigned to the latter temporary variable. Temporary value is used in place of function in the export declaration. 10 | 11 | ## Implementation 12 | 13 | Values that are affected: 14 | 15 | * anonymous function 16 | * arrow function 17 | * anonymous class 18 | 19 | Named function, named class and other object as well as literal values are not transformed. 20 | 21 | ### Export Name 22 | 23 | The name used for a temporary variable is derived from the name of the file (excluding `.js` extension). [`_.camelCase`](https://lodash.com/docs#camelCase) is used to sanitize file name (i.e. `foo-bar.js` becomes `fooBar`). 24 | 25 | ## Problem 26 | 27 | Executing a function without a name (arrow function or an anonymous function) appears as an `(anonymous function)` in the stack trace, e.g. 28 | 29 | ```js 30 | (() => { 31 | throw new Error('Hello, World!'); 32 | })(); 33 | ``` 34 | 35 | ![Stack trace without function name](./.README/stack-trace-without-name.png) 36 | 37 | However, if an arrow function is defined on the right-hand-side of an assignment expression, the engine will take the name on the left-hand-side and use it to set the arrow function's `.name`, e.g. 38 | 39 | ```js 40 | let test; 41 | 42 | test = () => { 43 | throw new Error('Hello, World!'); 44 | }; 45 | 46 | test(); 47 | ``` 48 | 49 | ![Stack trace without function name](./.README/stack-trace-with-name.png) 50 | 51 | When you export an anonymous function using `export default`, this function will appear as an `(anonymous function)` the stack trace. `babel-plugin-transform-export-default-name` plugin transforms the code to assign function a name before it is exported. 52 | 53 | `./index.js` 54 | 55 | ```js 56 | import foo from './foo'; 57 | 58 | foo(); 59 | ``` 60 | 61 | `./foo.js` 62 | 63 | ```js 64 | import bar from './bar'; 65 | 66 | export default () => { 67 | bar(); 68 | }; 69 | ``` 70 | 71 | `./bar.js` 72 | 73 | ```js 74 | import baz from './baz'; 75 | 76 | export default () => { 77 | baz(); 78 | }; 79 | ``` 80 | 81 | `./baz.js` 82 | 83 | ```js 84 | export default () => { 85 | throw new Error('test'); 86 | }; 87 | ``` 88 | 89 | ![Stack trace before and after export is given a name](./.README/stack-trace-before-and-after.png) 90 | 91 | ## Example 92 | 93 | Input file is `./foo.js`. 94 | 95 | Input code: 96 | 97 | ```js 98 | export default () => {}; 99 | ``` 100 | 101 | Output code: 102 | 103 | ```js 104 | let foo = () => {}; 105 | 106 | export default foo; 107 | ``` 108 | 109 | ## Usage 110 | 111 | Add to `.babelrc`: 112 | 113 | ```js 114 | { 115 | "plugins": [ 116 | "transform-export-default-name" 117 | ] 118 | } 119 | ``` 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "gajus@gajus.com", 4 | "name": "Gajus Kuizinas" 5 | }, 6 | "ava": { 7 | "babel": "inherit", 8 | "require": [ 9 | "babel-register" 10 | ] 11 | }, 12 | "dependencies": { 13 | "camelcase": "^4.0.0" 14 | }, 15 | "description": "Babel plugin that transforms default exports to named exports.", 16 | "devDependencies": { 17 | "ava": "^0.17.0", 18 | "babel-cli": "^6.18.0", 19 | "babel-core": "^6.18.2", 20 | "babel-plugin-transform-es2015-block-scoping": "^6.18.0", 21 | "babel-plugin-transform-es2015-destructuring": "^6.19.0", 22 | "babel-plugin-transform-es2015-modules-commonjs": "^6.18.0", 23 | "babel-plugin-transform-es2015-parameters": "^6.18.0", 24 | "babel-plugin-transform-flow-strip-types": "^6.18.0", 25 | "eslint": "^3.11.1", 26 | "eslint-config-canonical": "^5.8.0", 27 | "flow-bin": "^0.36.0", 28 | "husky": "^0.11.9" 29 | }, 30 | "keywords": [ 31 | "babel-plugin" 32 | ], 33 | "license": "BSD-3-Clause", 34 | "main": "./dist/index.js", 35 | "name": "babel-plugin-transform-export-default-name", 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/gajus/babel-plugin-transform-export-default-name.git" 39 | }, 40 | "scripts": { 41 | "build": "NODE_ENV=production babel ./src --out-dir ./dist --copy-files", 42 | "lint": "eslint ./src ./test", 43 | "precommit": "npm run lint && npm run test", 44 | "test": "npm run build && npm run lint && flow && NODE_ENV=development ava --test" 45 | }, 46 | "version": "2.0.2" 47 | } 48 | -------------------------------------------------------------------------------- /src/deriveName.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import path from 'path'; 4 | import normalizeName from './normalizeName'; 5 | import resolveConflictingName from './resolveConflictingName'; 6 | 7 | export default (state: Object, scope: Object): string => { 8 | const filename = state.file.opts.filename; 9 | 10 | let name = filename; 11 | 12 | name = path.parse(name).name; 13 | 14 | if (name === 'index') { 15 | name = path.basename(path.dirname(filename)); 16 | } 17 | 18 | name = normalizeName(name); 19 | 20 | return resolveConflictingName(name, scope); 21 | }; 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import deriveName from './deriveName'; 4 | 5 | export default ({ 6 | types: t 7 | }: { 8 | types: Object 9 | }) => { 10 | const replace = (path, name: string, replacement) => { 11 | const id = t.identifier(name); 12 | 13 | const [varDeclPath] = path.replaceWithMultiple([ 14 | t.variableDeclaration('const', [ 15 | t.variableDeclarator(id, replacement) 16 | ]), 17 | t.exportDefaultDeclaration(id) 18 | ]); 19 | 20 | path.scope.registerDeclaration(varDeclPath); 21 | }; 22 | 23 | return { 24 | visitor: { 25 | ExportDefaultDeclaration (path: Object, state: Object) { 26 | const declaration = path.node.declaration; 27 | 28 | if (declaration.id && declaration.id.name) { 29 | return; 30 | } 31 | 32 | const name = deriveName(state, path.scope); 33 | 34 | if (t.isArrowFunctionExpression(declaration)) { 35 | const declarationReplacement = t.arrowFunctionExpression(declaration.params, declaration.body, declaration.generator); 36 | 37 | declarationReplacement.async = declaration.async; 38 | 39 | replace(path, name, declarationReplacement); 40 | 41 | return; 42 | } 43 | 44 | if (t.isFunctionDeclaration(declaration)) { 45 | const declarationReplacement = t.functionExpression(null, declaration.params, declaration.body, declaration.generator); 46 | 47 | declarationReplacement.async = declaration.async; 48 | 49 | replace(path, name, declarationReplacement); 50 | 51 | return; 52 | } 53 | 54 | if (t.isFunctionExpression(declaration)) { 55 | const declarationReplacement = t.functionExpression(null, declaration.params, declaration.body, declaration.generator); 56 | 57 | declarationReplacement.async = declaration.async; 58 | 59 | replace(path, name, declarationReplacement); 60 | 61 | return; 62 | } 63 | 64 | if (t.isClassDeclaration(declaration)) { 65 | const declarationReplacement = t.classExpression(null, declaration.superClass, declaration.body, declaration.decorators || []); 66 | 67 | replace(path, name, declarationReplacement); 68 | 69 | // eslint-disable-next-line no-useless-return 70 | return; 71 | } 72 | } 73 | } 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /src/normalizeName.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import camelcase from 'camelcase'; 4 | 5 | const ValidNameRegex = /^[a-zA-Z_$][0-9a-zA-Z_$]+$/; 6 | const AlphabeticalCharacterRegex = /[a-zA-Z_$]/i; 7 | 8 | export default (name: string): string => { 9 | if (ValidNameRegex.test(name)) { 10 | return name; 11 | } 12 | 13 | const firstCharacter = name.slice(0, 1); 14 | 15 | if (!AlphabeticalCharacterRegex.test(firstCharacter)) { 16 | throw new Error('Invalid name.'); 17 | } 18 | 19 | const firstCharacterUpperCase = firstCharacter.toUpperCase() === firstCharacter; 20 | 21 | const camelCaseName = camelcase(name); 22 | 23 | if (firstCharacterUpperCase) { 24 | return camelCaseName.slice(0, 1).toUpperCase() + camelCaseName.slice(1); 25 | } 26 | 27 | return camelCaseName; 28 | }; 29 | -------------------------------------------------------------------------------- /src/resolveConflictingName.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default (name: string, scope: Object) => { 4 | let index = 0; 5 | let resolvedName = name; 6 | 7 | while (scope.hasBinding(resolvedName)) { 8 | resolvedName = name + index++; 9 | 10 | if (index > 100) { 11 | throw Error('Couldn\'t resolve clashing name "' + name + '".'); 12 | } 13 | } 14 | 15 | return resolvedName; 16 | }; 17 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "canonical/ava" 3 | } 4 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { 3 | transform as nativeTransform 4 | } from 'babel-core'; 5 | import plugin from '../src'; 6 | 7 | const transform = (code, filename = 'unset.js') => { 8 | return nativeTransform(code, { 9 | babelrc: false, 10 | filename, 11 | plugins: [ 12 | plugin 13 | ] 14 | }).code.replace(/\n/g, ' ').replace(/\s+/g, ' '); 15 | }; 16 | 17 | test('exporting an arrow function with a safe file name: uses the file name to create an export variable', (t) => { 18 | const actual = transform('export default () => {};', 'foo.js'); 19 | const expected = 'const foo = () => {}; export default foo;'; 20 | 21 | t.true(actual === expected); 22 | }); 23 | 24 | test('exporting an arrow function with an unsafe file name: sanitizes the file name using camelCase', (t) => { 25 | const actual = transform('export default () => {};', 'foo bar.js'); 26 | const expected = 'const fooBar = () => {}; export default fooBar;'; 27 | 28 | t.true(actual === expected); 29 | }); 30 | 31 | test('exporting an arrow function with a file name that matches an existing variable: derives a new name using an incremental index', (t) => { 32 | const actual = transform('const foo = true; export default () => {};', 'foo.js'); 33 | const expected = 'const foo = true; const foo0 = () => {}; export default foo0;'; 34 | 35 | t.true(actual === expected); 36 | }); 37 | 38 | test('exporting an arrow function with a file name that matches an existing variable: derives a new name using an incremental index (multiple iterations)', (t) => { 39 | const actual = transform('const foo = true;\nconst foo0 = true; export default () => {};', 'foo.js'); 40 | const expected = 'const foo = true; const foo0 = true; const foo1 = () => {}; export default foo1;'; 41 | 42 | t.true(actual === expected); 43 | }); 44 | 45 | test('exporting an arrow function with "index" file name: uses the directory name', (t) => { 46 | const actual = transform('export default () => {};', 'foo/index.js'); 47 | const expected = 'const foo = () => {}; export default foo;'; 48 | 49 | t.true(actual === expected); 50 | }); 51 | 52 | test('exporting an async arrow function', (t) => { 53 | const actual = transform('export default async () => {};', 'foo/index.js'); 54 | const expected = 'const foo = async () => {}; export default foo;'; 55 | 56 | t.true(actual === expected); 57 | }); 58 | 59 | test('exporting an anonymous async function', (t) => { 60 | const actual = transform('export default async function () {};', 'foo.js'); 61 | const expected = 'const foo = async function () {}; export default foo;'; 62 | 63 | t.true(actual === expected); 64 | }); 65 | 66 | test('exporting an anonymous class: uses the file name to create an export variable', (t) => { 67 | const actual = transform('export default class {}', 'Foo.js'); 68 | const expected = 'const Foo = class {}; export default Foo;'; 69 | 70 | t.true(actual === expected); 71 | }); 72 | 73 | const fixtures = [ 74 | '[]', 75 | '\'a string\'', 76 | '1', 77 | 'true', 78 | 'new String("foo")', 79 | 'null', 80 | 'class Foo {}', 81 | 'function foo() {}' 82 | ]; 83 | 84 | for (const fixture of fixtures) { 85 | test('exporting ' + fixture + ' does not transform code', (t) => { 86 | t.true(transform('export default ' + fixture + ';') === 'export default ' + fixture + ';'); 87 | }); 88 | } 89 | --------------------------------------------------------------------------------