├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── docs ├── app.js ├── index.html ├── main.css └── main.js ├── package.json ├── scripts ├── backup.js ├── core.js ├── gulp.js ├── restore.js └── utils.js ├── src ├── core.js └── gulp.js └── test ├── bad.js ├── fixtures ├── bad.md └── supported.md ├── supported.js └── test.js /.gitattributes: -------------------------------------------------------------------------------- 1 | docs/app.js binary 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: {} 4 | push: 5 | branches: ["main"] 6 | 7 | jobs: 8 | test: 9 | name: "Test" 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | - run: npm i 15 | - run: npm test 16 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: npm publish 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | publish-npm: 8 | name: Build and Publish to npm 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | with: 14 | registry-url: "https://registry.npmjs.org" 15 | - run: npm i 16 | - run: npm run publish:core 17 | env: 18 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 19 | - run: npm run publish:gulp 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | package.backup.json 4 | index.js 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | scripts 3 | src 4 | package.backup.json 5 | .gitignore 6 | docs 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (MIT License) 2 | 3 | Copyright 2018 Sid Vishnoi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## esm-to-cjs 2 | 3 | > Transform ESM to Common JS for present NodeJS, without any junk wrappers or useless renaming. 4 | 5 | ## Motivation 6 | 7 | I was working on a TypeScript project for NodeJS and using ES modules. The transformations to CommonJS by TypeScript (or by Babel + plugins) causes variable renaming (prefixing) and adds some wrapper functions to the transformed code. 8 | 9 | I was not happy with this transformation. So I created this tool to convert the ESM import/exports to the kinds that a NodeJS developer would write today. 10 | 11 | ``` js 12 | // input 13 | import { resolve as resolvePath } from "path"; 14 | resolvePath("./hello") 15 | 16 | // what i wanted 17 | const { resolve: resolvePath } = require("path"); 18 | resolvePath("./hello") 19 | 20 | // typescript gave me: 21 | Object.defineProperty(exports, "__esModule", { value: true }); 22 | const path_1 = require("path"); 23 | path_1.resolve("./hello"); 24 | ``` 25 | 26 | ``` js 27 | // input 28 | async () => { 29 | const path = await import("path"); 30 | } 31 | 32 | // what i wanted 33 | async () => { 34 | const path = require("path"); 35 | } 36 | 37 | // typescript gave me: 38 | var __importStar = (this && this.__importStar) || function (mod) { 39 | if (mod && mod.__esModule) return mod; 40 | var result = {}; 41 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 42 | result["default"] = mod; 43 | return result; 44 | }; 45 | async () => { 46 | const path = await Promise.resolve().then(() => __importStar(require("path"))); 47 | }; 48 | ``` 49 | 50 | ``` js 51 | // input 52 | export const foo = 5; 53 | console.log(foo); 54 | 55 | // what i wanted 56 | const foo = 5; 57 | module.exports = { 58 | foo 59 | } 60 | 61 | // typescript gave me: 62 | Object.defineProperty(exports, "__esModule", { value: true }); 63 | exports.foo = 5; 64 | console.log(exports.foo); 65 | ``` 66 | 67 | 68 | ``` js 69 | // input 70 | export { foo as wow, bar } from "baz"; 71 | export { baz } from "lorem"; 72 | 73 | // what i wanted 74 | const { foo: __foo__, bar: __bar__ } = require("baz"); 75 | module.exports = { 76 | wow: __foo__, 77 | bar: __bar__, 78 | baz: require("lorem").baz 79 | } 80 | 81 | // typescript gave me 82 | Object.defineProperty(exports, "__esModule", { value: true }); 83 | var baz_1 = require("baz"); 84 | var lorem_1 = require("lorem"); 85 | exports.wow = baz_1.foo; 86 | exports.bar = baz_1.bar; 87 | exports.baz = lorem1.baz; 88 | ``` 89 | 90 | 91 | So, I created this tool using some simple string manipulations. A lot of sample input/output are [available here](https://github.com/sidvishnoi/esm-to-cjs/blob/main/test/fixtures/supported.md). 92 | 93 | 94 | 95 | ## Limitations 96 | 97 | - `import * as foo from "bar";` is converted to `const foo = require("bar");`. Not sure if this is what everyone wants. I did it as per my project requirements. 98 | - Also, `import foo from "bar";` is converted to `const foo = require("bar").default;"`. 99 | - No support for `export *` syntax. 100 | - No mixing of default import, named imports and `import *` in same statement. 101 | - See Bug: ["The simpler transform is semantically wrong"](https://github.com/sidvishnoi/esm-to-cjs/issues/4) 102 | 103 | ## Packages 104 | 105 | This tool is available in form of two packages: 106 | 107 | - `esm-to-cjs`: the core module. 108 | - `gulp-esm-to-cjs`: as a gulp plugin. 109 | 110 | ### `esm-to-cjs` 111 | 112 | This is the core module. It includes a tokenizer and a transformer. I didn't use some specific JS parser due to overheads and created my own using string manipulation. 113 | 114 | **Install:** 115 | 116 | ``` 117 | npm i --save-dev esm-to-cjs 118 | ``` 119 | 120 | **Usage:** 121 | 122 | ``` js 123 | const { runTransform } = require("esm-to-cjs"); 124 | 125 | const input = `import { resolve as resolvePath } from "path";` 126 | const options = { quote: "double" }; // see details below 127 | const output = runTransform(input, options); 128 | console.log(output); 129 | // const { resolve: resolvePath } = require("path"); 130 | ``` 131 | 132 | **Options:** 133 | 134 | ``` yaml 135 | quote: 136 | type: string 137 | default: "double" 138 | available: "double" | "single" 139 | description: Tells parser what kind of quotes you use in module names, i.e. like `from "moduleName"` 140 | 141 | lenDestructure: 142 | type: number (of characters) 143 | default: 60 144 | description: Used by parser to improve performance. Set to a higher value if your object destruturing statements are longer. 145 | 146 | lenModuleName: 147 | type: number (of characters) 148 | default: 20 149 | description: Used by parser to improve performance. Set to a higher value if your module names are longer. 150 | 151 | lenIdentifier: 152 | type: number (of characters) 153 | default: 60 154 | description: Used by parser to improve performance. Set to a higher value if your identifier names are longer. 155 | 156 | indent: 157 | type: number 158 | default: 2 159 | description: Indentation (spaces) in output code. 160 | ``` 161 | 162 | 163 | ### `gulp-esm-to-cjs` 164 | 165 | Gulp plugin for esm-to-cjs. 166 | 167 | **Install**: 168 | 169 | ``` 170 | npm i --save-dev gulp-esm-to-cjs 171 | ``` 172 | 173 | **Usage**: 174 | 175 | ``` js 176 | // gulpfile.js 177 | const esmToCjs = require("gulp-esm-to-cjs"); 178 | 179 | function convert() { 180 | return gulp 181 | .src(src) 182 | .pipe(esmToCjs(options)) 183 | .pipe(gulp.dest(dest)); 184 | } 185 | module.exports.convert = convert; 186 | 187 | // use as: 188 | // $ gulp convert 189 | ``` 190 | 191 | 192 | ## Contributing 193 | 194 | - If you've issues regarding the project - documentation, supported features and transformations etc., please file them on [GitHub](https://github.com/sidvishnoi/esm-to-cjs/issues) where we can discuss. 195 | - Pull requests are welcome! 196 | - Please bear in mind that I created this project in a hurry, so the code isn't very impressive. Also, I didn't add all the transformations. See limitations above. Would be nice if we can overcome them! 197 | -------------------------------------------------------------------------------- /docs/app.js: -------------------------------------------------------------------------------- 1 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i"// "+s).join("\n");output.getSession().setValue(errorMessage)}}let timeout=false;input.on("input",function(){if(timeout){clearTimeout(timeout)}timeout=setTimeout(function(){transform();timeout=false},500)});const settingsButton=document.getElementById("settings");const optionsForm=document.getElementById("options");settingsButton.addEventListener("click",()=>{optionsForm.classList.toggle("open");const isOpen=optionsForm.classList.contains("open");settingsButton.textContent=isOpen?"❌ Close":"⚙ Settings";if(!isOpen){const data=new FormData(optionsForm);for(const[key,value]of data.entries()){const val=parseInt(value,10);options[key]=val.toString()!=="NaN"?val:value}transform()}});transform()},{"../src/core":2}],2:[function(require,module,exports){module.exports.runTransform=runTransform;let LOOKING_FOR;const DISTANCE=6;const defaultOptions={quote:"double",lenDestructure:60,lenModuleName:20,lenIdentifier:20,indent:2};function runTransform(str,options={}){options={...defaultOptions,...options};options.quote=options.quote==="single"?"'":'"';const buffer=[];const exportBuffer={items:[],requires:[]};let pos=0;for(const token of tokenize(str,options)){buffer.push(str.slice(pos,token.start));buffer.push(transform(token,str,exportBuffer,options));pos=token.end+1}pos=skipNewLines(str,pos);buffer.push(str.slice(pos,str.length));if(exportBuffer.items.length){const indent=" ".repeat(options.indent);for(const item of exportBuffer.requires){buffer.push(item)}buffer.push("\nmodule.exports = {\n");const exportNames=exportBuffer.items.map(item=>`${indent}${item[0]}${item[1]?`: ${item[1]}`:""}`);buffer.push(exportNames.join(",\n"));buffer.push("\n}")}buffer.push("\n");return buffer.join("")}function transform(token,str,exportBuffer,{indent:indent}){indent=" ".repeat(indent);const{type:type}=token;switch(type){case"import":{const identifiers=token.modules.map(s=>s.join(": ")).join(", ");return`const { ${identifiers} } = require(${token.moduleName})`}case"import*":case"importDefault":{const{identifier:identifier,moduleName:moduleName}=token;return`const ${identifier} = require(${moduleName})${token.isDefaultImport?".default":""}`}case"awaitImport":{return`require(${token.moduleName})`}case"export":{exportBuffer.items.push(token.modules);return""}case"reExport":{const{moduleName:moduleName}=token;if(token.modules.length===1){const[original,alias]=token.modules[0];exportBuffer.items.push([alias?alias:original,`require(${moduleName}).${original}`]);return}exportBuffer.requires.push("const {\n");const names=token.modules.map(([original])=>`${indent}${original}: __${original}__`).join(",\n");exportBuffer.requires.push(names);exportBuffer.requires.push(`\n} = require(${moduleName});`);for(const[original,alias]of token.modules){exportBuffer.items.push([alias?alias:original,`__${original}__`])}return""}case"reExportImported":{exportBuffer.items.push(...token.modules);return""}default:throw new Error("should not reach here")}}String.prototype.indexWithin=indexWithin;function*tokenize(str,options){const{quote:quote,lenDestructure:lenDestructure,lenModuleName:lenModuleName,lenIdentifier:lenIdentifier}=options;let start=0;let pos;const types=new Map([["import","import "],["export","export "],["awaitImport","await import("]]);while(types.size!==0){pos=Number.POSITIVE_INFINITY;let type;for(const t of types.keys()){const idx=str.indexOf(types.get(t),start);if(idx===-1){types.delete(t)}else if(idxi.trim()).filter(i=>i).map(i=>i.split(/\s*as\s*/))}}function indexWithin(needle,from,within=99,throws=true){for(let i=from,L=from+within,j=0;i 2 | 3 | 4 | 5 | 6 | esm-to-cjs playground 7 | 8 | 9 | 10 | 11 | 12 |
13 |

esm-to-cjs playground

14 | 15 |
16 |
17 |
// add your ESM code here 18 | import { resolve as resolvePath, join } from "path"; 19 | import { readFileSync } from "fs"; 20 | 21 | export function read(src) { 22 | return readFileSync(join(__dirname, src), "utf8"); 23 | }
24 | 25 |
aha
26 |
27 | 28 |
29 |
30 |

Options

31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 | 39 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | overflow: hidden; 9 | } 10 | 11 | header { 12 | height: 4em; 13 | padding: 1em; 14 | width: 100vw; 15 | background: #115; 16 | color: #fff; 17 | font-family: "Roboto Mono", "Courier New", Courier, monospace; 18 | display: flex; 19 | justify-content: space-between; 20 | align-items: center; 21 | border-bottom: 0.2em solid #0606af; 22 | } 23 | 24 | header a { 25 | color: #ffeb3b 26 | } 27 | 28 | header button { 29 | border: none; 30 | background: #b71c1c; 31 | color: #fff; 32 | font-size: 1.2em; 33 | padding: 0.5em; 34 | } 35 | 36 | .editors { 37 | display: flex; 38 | width: 100vw; 39 | height: 100vh; 40 | position: absolute; 41 | top: 4em; 42 | overflow: auto; 43 | } 44 | 45 | .editor { 46 | height: 100%; 47 | width: 50%; 48 | position: absolute; 49 | top: 0; 50 | right: 0; 51 | bottom: 0; 52 | left: 0; 53 | } 54 | 55 | #output { 56 | left: 50%; 57 | } 58 | 59 | #options { 60 | height: 100vh; 61 | width: 100%; 62 | background: #fff; 63 | height: calc(100vh - 4em); 64 | position: fixed; 65 | top: -200vh; 66 | z-index: 999; 67 | display: flex; 68 | justify-content: center; 69 | font-family: 'Courier New', Courier, monospace; 70 | } 71 | 72 | #options.open { 73 | top: 4em; 74 | } 75 | 76 | #options div { 77 | padding: 3em 0; 78 | width: 300px; 79 | } 80 | 81 | #options label { 82 | display: flex; 83 | justify-content: space-between; 84 | margin: 1em 0; 85 | } 86 | 87 | #options input { 88 | width: 4em; 89 | } 90 | -------------------------------------------------------------------------------- /docs/main.js: -------------------------------------------------------------------------------- 1 | const { runTransform } = require("../src/core"); 2 | 3 | const input = ace.edit("input", { 4 | mode: "ace/mode/javascript" 5 | }); 6 | input.setTheme("ace/theme/xcode"); 7 | input.getSession().setUseWrapMode(true); 8 | 9 | const output = ace.edit("output", { 10 | mode: "ace/mode/javascript", 11 | readOnly: true 12 | }); 13 | output.setTheme("ace/theme/xcode"); 14 | output.getSession().setUseWrapMode(true); 15 | 16 | let options = {}; 17 | 18 | function transform() { 19 | const inputValue = input.getSession().getValue(); 20 | try { 21 | const outputValue = runTransform(inputValue, options); 22 | output.getSession().setValue(outputValue); 23 | } catch (err) { 24 | const errorMessage = err.message 25 | .split("\n") 26 | .map(s => "// " + s) 27 | .join("\n"); 28 | output.getSession().setValue(errorMessage); 29 | } 30 | } 31 | 32 | let timeout = false; 33 | input.on("input", function() { 34 | if (timeout) { 35 | clearTimeout(timeout); 36 | } 37 | 38 | timeout = setTimeout(function() { 39 | transform(); 40 | timeout = false; 41 | }, 500); 42 | }); 43 | 44 | const settingsButton = document.getElementById("settings"); 45 | const optionsForm = document.getElementById("options"); 46 | settingsButton.addEventListener("click", () => { 47 | optionsForm.classList.toggle("open"); 48 | const isOpen = optionsForm.classList.contains("open"); 49 | settingsButton.textContent = isOpen ? "❌ Close" : "⚙ Settings"; 50 | if (!isOpen) { 51 | const data = new FormData(optionsForm); 52 | for (const [key, value] of data.entries()) { 53 | const val = parseInt(value, 10); 54 | options[key] = val.toString() !== "NaN" ? val : value; 55 | } 56 | transform(); 57 | } 58 | }); 59 | 60 | transform(); 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm-to-cjs", 3 | "version": "1.2.1", 4 | "description": "Transform ESM to Common JS for present NodeJS, without any junk wrappers or useless renaming", 5 | "keywords": [ 6 | "commonjs", 7 | "cjs", 8 | "es-module", 9 | "transpile", 10 | "compile", 11 | "esm", 12 | "module" 13 | ], 14 | "homepage": "https://github.com/sidvishnoi/esm-to-cjs#readme", 15 | "bugs": { 16 | "url": "https://github.com/sidvishnoi/esm-to-cjs/issues" 17 | }, 18 | "repository": "sidvishnoi/esm-to-cjs", 19 | "license": "MIT", 20 | "author": { 21 | "name": "Sid Vishnoi", 22 | "email": "sidvishnoi8@gmail.com", 23 | "url": "https://sidvishnoi.github.io" 24 | }, 25 | "files": [ 26 | "index.js" 27 | ], 28 | "main": "index.js", 29 | "scripts": { 30 | "build:core": "node scripts/core.js", 31 | "build:gulp": "node scripts/gulp.js", 32 | "build:site": "browserify src/core.js docs/main.js -o docs/app.js && uglifyjs.cmd docs/app.js -o docs/app.js", 33 | "publish:core": "node scripts/backup.js && node scripts/core.js && npm publish && node scripts/restore.js", 34 | "publish:gulp": "node scripts/backup.js && node scripts/gulp.js && npm publish && node scripts/restore.js", 35 | "test": "node test/test.js", 36 | "util:backup": "node scripts/backup.js", 37 | "util:restore": "node scripts/restore.js" 38 | }, 39 | "dependencies": { 40 | "through2": "^2.0.3" 41 | }, 42 | "devDependencies": { 43 | "easy-table": "^1.1.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /scripts/backup.js: -------------------------------------------------------------------------------- 1 | const { backupPackageJson } = require("./utils"); 2 | backupPackageJson(); 3 | -------------------------------------------------------------------------------- /scripts/core.js: -------------------------------------------------------------------------------- 1 | const { restorePackageJson, editPackageJson, copyFile } = require("./utils"); 2 | 3 | try { 4 | editPackageJson(json => { 5 | json.scripts = undefined; 6 | json.tests = undefined; 7 | json.dependencies = undefined; 8 | json.devDependencies = undefined; 9 | }); 10 | copyFile("./src/core.js", "./index.js"); 11 | } catch (err) { 12 | console.error(err); 13 | restorePackageJson(); 14 | } 15 | -------------------------------------------------------------------------------- /scripts/gulp.js: -------------------------------------------------------------------------------- 1 | const { restorePackageJson, editPackageJson, copyFile } = require("./utils"); 2 | 3 | try { 4 | editPackageJson(json => { 5 | json.scripts = undefined; 6 | json.tests = undefined; 7 | json.devDependencies = undefined; 8 | json.keywords.push("gulpplugin"); 9 | json.name = "gulp-esm-to-cjs"; 10 | json.dependencies["esm-to-cjs"] = "*"; 11 | }); 12 | copyFile("./src/gulp.js", "./index.js", str => { 13 | return str.replace("./core", "esm-to-cjs"); 14 | }); 15 | } catch (err) { 16 | console.error(err); 17 | restorePackageJson(); 18 | } 19 | -------------------------------------------------------------------------------- /scripts/restore.js: -------------------------------------------------------------------------------- 1 | const { restorePackageJson } = require("./utils"); 2 | restorePackageJson(); 3 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require("fs"); 2 | const path = require("path"); 3 | 4 | const dir = process.cwd(); 5 | 6 | function editPackageJson(transform) { 7 | const src = path.join(dir, "./package.backup.json"); 8 | const dest = path.join(dir, "./package.json"); 9 | const content = readFileSync(src, "utf8"); 10 | const json = JSON.parse(content); 11 | transform(json); 12 | writeFileSync(dest, JSON.stringify(json, null, 2), "utf8"); 13 | } 14 | 15 | function copyFile(src, dest, transform) { 16 | src = path.join(dir, src); 17 | dest = path.join(dir, dest); 18 | let content = readFileSync(src, "utf8"); 19 | if (transform && typeof transform === "function") { 20 | content = transform(content); 21 | } 22 | writeFileSync(dest, content, "utf8"); 23 | } 24 | 25 | function backupPackageJson() { 26 | copyFile("./package.json", "./package.backup.json"); 27 | } 28 | 29 | function restorePackageJson() { 30 | copyFile("./package.backup.json", "./package.json"); 31 | } 32 | 33 | module.exports.copyFile = copyFile; 34 | module.exports.editPackageJson = editPackageJson; 35 | module.exports.backupPackageJson = backupPackageJson; 36 | module.exports.restorePackageJson = restorePackageJson; 37 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | module.exports.runTransform = runTransform; 2 | 3 | // gives parsing errors better description 4 | let LOOKING_FOR; 5 | // length/distance defaults during parsing 6 | const DISTANCE = 6; 7 | 8 | const defaultOptions = { 9 | quote: "double", 10 | lenDestructure: 60, 11 | lenModuleName: 20, 12 | lenIdentifier: 20, 13 | indent: 2 14 | }; 15 | 16 | function runTransform(str, options = {}) { 17 | options = { ...defaultOptions, ...options }; 18 | options.quote = options.quote === "single" ? "'" : '"'; 19 | const buffer = []; 20 | const exportBuffer = { 21 | items: [], 22 | requires: [] 23 | }; 24 | let pos = 0; 25 | 26 | for (const token of tokenize(str, options)) { 27 | buffer.push(str.slice(pos, token.start)); 28 | buffer.push(transform(token, str, exportBuffer, options)); 29 | pos = token.end + 1; 30 | } 31 | 32 | // add rest of input 33 | pos = skipNewLines(str, pos); 34 | buffer.push(str.slice(pos, str.length)); 35 | 36 | if (exportBuffer.items.length) { 37 | const indent = " ".repeat(options.indent); 38 | // add module.exports 39 | for (const item of exportBuffer.requires) { 40 | buffer.push(item); 41 | } 42 | buffer.push("\nmodule.exports = {\n"); 43 | const exportNames = exportBuffer.items.map( 44 | item => `${indent}${item[0]}${item[1] ? `: ${item[1]}` : ""}` 45 | ); 46 | buffer.push(exportNames.join(",\n")); 47 | buffer.push("\n}"); 48 | } 49 | 50 | buffer.push("\n"); // end file 51 | return buffer.join(""); 52 | } 53 | 54 | function transform(token, str, exportBuffer, { indent }) { 55 | indent = " ".repeat(indent); 56 | 57 | const { type } = token; 58 | switch (type) { 59 | case "import": { 60 | const identifiers = token.modules.map(s => s.join(": ")).join(", "); 61 | return `const { ${identifiers} } = require(${token.moduleName})`; 62 | } 63 | case "import*": 64 | case "importDefault": { 65 | const { identifier, moduleName } = token; 66 | return `const ${identifier} = require(${moduleName})${ 67 | token.isDefaultImport ? ".default" : "" 68 | }`; 69 | } 70 | case "awaitImport": { 71 | return `require(${token.moduleName})`; 72 | } 73 | case "export": { 74 | exportBuffer.items.push(token.modules); 75 | return ""; 76 | } 77 | case "reExport": { 78 | const { moduleName } = token; 79 | 80 | if (token.modules.length === 1) { 81 | const [original, alias] = token.modules[0]; 82 | exportBuffer.items.push([ 83 | alias ? alias : original, 84 | `require(${moduleName}).${original}` 85 | ]); 86 | return; 87 | } 88 | 89 | exportBuffer.requires.push("const {\n"); 90 | const names = token.modules 91 | .map(([original]) => `${indent}${original}: __${original}__`) 92 | .join(",\n"); 93 | exportBuffer.requires.push(names); 94 | exportBuffer.requires.push(`\n} = require(${moduleName});`); 95 | 96 | for (const [original, alias] of token.modules) { 97 | exportBuffer.items.push([alias ? alias : original, `__${original}__`]); 98 | } 99 | 100 | return ""; 101 | } 102 | case "reExportImported": { 103 | exportBuffer.items.push(...token.modules); 104 | return ""; 105 | } 106 | default: 107 | throw new Error("should not reach here"); 108 | } 109 | } 110 | 111 | String.prototype.indexWithin = indexWithin; 112 | 113 | function* tokenize(str, options) { 114 | const { quote, lenDestructure, lenModuleName, lenIdentifier } = options; 115 | 116 | let start = 0; 117 | let pos; 118 | 119 | const types = new Map([ 120 | ["import", "import "], 121 | ["export", "export "], 122 | ["awaitImport", "await import("] 123 | ]); 124 | 125 | while (types.size !== 0) { 126 | // look for first matching pattern 127 | pos = Number.POSITIVE_INFINITY; 128 | let type; 129 | for (const t of types.keys()) { 130 | const idx = str.indexOf(types.get(t), start); 131 | if (idx === -1) { 132 | types.delete(t); 133 | } else if (idx < pos) { 134 | pos = idx; 135 | type = t; 136 | } 137 | } 138 | 139 | switch (type) { 140 | case "import": 141 | yield handleImport(); 142 | break; 143 | case "export": 144 | yield handleExport(); 145 | break; 146 | case "awaitImport": 147 | yield handleAwaitImport(); 148 | break; 149 | } 150 | } 151 | 152 | // import { ... } from "MODULE" 153 | // import * as IDENTIFIER from "MODULE" 154 | function handleImport() { 155 | LOOKING_FOR = "import names"; 156 | const braceStart = str.indexWithin("{", pos + 7, DISTANCE, false); 157 | // 7 === "import ".length 158 | if (braceStart === -1) { 159 | // try to see if it's `import *` 160 | return handleImportStar(); 161 | } 162 | 163 | const braceEnd = str.indexWithin("}", braceStart + 1, lenDestructure); 164 | 165 | LOOKING_FOR = "name of imported module"; 166 | let moduleStart = str.indexWithin("from ", braceEnd + 1, DISTANCE); 167 | moduleStart = str.indexWithin(quote, moduleStart + 1, 5); 168 | const moduleEnd = str.indexWithin(quote, moduleStart + 1, lenModuleName); 169 | 170 | start = moduleEnd + 1; 171 | return { 172 | type: "import", 173 | start: pos, 174 | end: moduleEnd, 175 | modules: destructureModules(str.slice(braceStart, braceEnd + 1)), 176 | moduleName: str.slice(moduleStart, moduleEnd + 1) 177 | }; 178 | } 179 | 180 | // await import(...) 181 | function handleAwaitImport() { 182 | LOOKING_FOR = "name of imported module for await import()"; 183 | const moduleStart = str.indexWithin("(", pos + 12, 10) + 1; 184 | // 12 === "await import".length 185 | const moduleEnd = str.indexWithin(")", moduleStart + 1, lenIdentifier) - 1; 186 | 187 | start = moduleEnd + 2; 188 | return { 189 | type: "awaitImport", 190 | start: pos, 191 | end: moduleEnd + 1, 192 | moduleName: str.slice(moduleStart, moduleEnd + 1) 193 | }; 194 | } 195 | 196 | // export [default] const IDENTIFIER = ... 197 | // export [default] function IDENTIFIER(...) ... 198 | // export [default] class IDENTIFIER ... 199 | // export { ... } >> handleReExport() 200 | // export { ... } from "MODULE" >> handleReExport() 201 | function handleExport() { 202 | LOOKING_FOR = "export pattern"; 203 | let skipStart = pos + "export ".length; 204 | 205 | if (str.indexWithin("{", skipStart, 5, false) !== -1) { 206 | return handleReExport(); 207 | } else if (str.indexWithin("*", skipStart, 5, false) !== -1) { 208 | return handleExportStar(); 209 | } 210 | 211 | LOOKING_FOR = "identifier type (function|class|const) for export"; 212 | 213 | if (str.indexWithin("async ", skipStart, DISTANCE, false) !== -1) { 214 | skipStart += 6; // 6 === "async ".length; 215 | } 216 | 217 | let isDefaultExport = false; 218 | if (str.indexWithin("default ", skipStart, DISTANCE, false) !== -1) { 219 | skipStart += 8; // 8 === "default ".length; 220 | isDefaultExport = true; 221 | } 222 | 223 | const typeEnd = str.indexWithin(" ", skipStart, 9); 224 | // 9 === "function".length + 1 225 | const exportType = str.slice(skipStart, typeEnd); 226 | 227 | LOOKING_FOR = "export identifiers"; 228 | const identifierStart = 229 | str.indexWithin(" ", skipStart + exportType.length, 5) + 1; 230 | const identifierEnd = 231 | str.indexWithin( 232 | exportType === "function" ? "(" : " ", 233 | identifierStart, 234 | lenIdentifier 235 | ) - 1; 236 | 237 | const end = pos + 6 + (isDefaultExport ? 8 : 0); 238 | // 6 == "export".length, 8 === "default ".length; 239 | 240 | const modules = isDefaultExport 241 | ? ["default", str.slice(identifierStart, identifierEnd + 1)] 242 | : [str.slice(identifierStart, identifierEnd + 1)]; 243 | 244 | start = end + 1; 245 | return { 246 | type: "export", 247 | start: pos, 248 | end, 249 | modules 250 | }; 251 | } 252 | 253 | // import * as IDENTIFIER from "MODULE" 254 | function handleImportStar() { 255 | LOOKING_FOR = "import name for import*"; 256 | let identifierStart = str.indexWithin("* as ", pos + 7, DISTANCE, false); 257 | if (identifierStart === -1) { 258 | return handleDefaultImport(); 259 | } 260 | identifierStart += 5; 261 | // 7 === "import ".length, 5 === "* as ".length 262 | const identifierEnd = str.indexWithin( 263 | " ", 264 | identifierStart + 1, 265 | lenIdentifier 266 | ); 267 | 268 | LOOKING_FOR = "name of imported module for import*"; 269 | let moduleStart = 270 | str.indexWithin("from ", identifierEnd + 1) + "from".length; 271 | moduleStart = str.indexWithin(quote, moduleStart + 1); 272 | const moduleEnd = str.indexWithin(quote, moduleStart + 1, lenModuleName); 273 | 274 | start = moduleEnd + 1; 275 | return { 276 | type: "import*", 277 | start: pos, 278 | end: moduleEnd, 279 | identifier: str.slice(identifierStart, identifierEnd), 280 | moduleName: str.slice(moduleStart, moduleEnd + 1) 281 | }; 282 | } 283 | 284 | // import IDENTIFIER from "MODULE" 285 | function handleDefaultImport() { 286 | LOOKING_FOR = "import name for default import"; 287 | const identifierStart = pos + 7; // 7 === "import ".length 288 | const identifierEnd = str.indexWithin(" ", identifierStart, DISTANCE); 289 | 290 | LOOKING_FOR = "name of imported module for import*"; 291 | let moduleStart = 292 | str.indexWithin("from ", identifierEnd + 1) + "from".length; 293 | moduleStart = str.indexWithin(quote, moduleStart + 1); 294 | const moduleEnd = str.indexWithin(quote, moduleStart + 1, lenModuleName); 295 | 296 | start = moduleEnd + 1; 297 | return { 298 | type: "importDefault", 299 | start: pos, 300 | end: moduleEnd, 301 | isDefaultImport: true, 302 | identifier: str.slice(identifierStart, identifierEnd), 303 | moduleName: str.slice(moduleStart, moduleEnd + 1) 304 | }; 305 | } 306 | 307 | // export { ... } from "..." | export { ... } 308 | function handleReExport() { 309 | LOOKING_FOR = "export pattern for re-export"; 310 | const braceStart = str.indexWithin("{", pos + "export ".length, 5); 311 | const braceEnd = str.indexWithin("}", braceStart + 1, lenDestructure); 312 | 313 | LOOKING_FOR = "name of re-exported module"; 314 | let moduleStart = str.indexWithin("from ", braceEnd + 1, 10, false); 315 | 316 | if (moduleStart === -1) { 317 | // export { ... } 318 | const end = skipNewLines(str, braceEnd); 319 | 320 | start = end + 1; 321 | return { 322 | type: "reExportImported", 323 | start: pos, 324 | end, 325 | modules: destructureModules(str.slice(braceStart, braceEnd + 1)) 326 | }; 327 | } 328 | 329 | moduleStart = str.indexWithin(quote, moduleStart, "from ".length + 4); 330 | const moduleEnd = str.indexWithin(quote, moduleStart + 1, lenModuleName); 331 | 332 | const end = skipNewLines(str, moduleEnd); 333 | 334 | start = end + 1; 335 | return { 336 | type: "reExport", 337 | start: pos, 338 | end, 339 | modules: destructureModules(str.slice(braceStart, braceEnd + 1)), 340 | moduleName: str.slice(moduleStart, moduleEnd + 1) 341 | }; 342 | } 343 | 344 | function handleExportStar() { 345 | throw new Error("not implemented"); 346 | } 347 | 348 | function destructureModules(objLiteral) { 349 | return objLiteral 350 | .trim() 351 | .slice(1, -1) 352 | .split(/,\s*/) 353 | .map(i => i.trim()) 354 | .filter(i => i) 355 | .map(i => i.split(/\s*\bas\b\s*/)); 356 | } 357 | } 358 | 359 | // this is same as indexOf, but can stop searching earlier 360 | function indexWithin(needle, from, within = 99, throws = true) { 361 | for (let i = from, L = from + within, j = 0; i < L; ++i) { 362 | if (this.charCodeAt(i) === needle.charCodeAt(j)) { 363 | while (j < needle.length) { 364 | if (this.charCodeAt(i + j) === needle.charCodeAt(j)) { 365 | ++j; 366 | } else { 367 | j = 0; 368 | break; 369 | } 370 | } 371 | if (j === needle.length) { 372 | return i; 373 | } 374 | } 375 | } 376 | if (throws) { 377 | throw new Error( 378 | `ParseError: Failed to find \`${needle}\` within ${within} characters from position ${from}` + 379 | (LOOKING_FOR ? ` while looking for ${LOOKING_FOR}` : "") + 380 | "\n\nINPUT STRING:" + 381 | `\n${"*".repeat(20)}\n` + 382 | this + 383 | `\n${"*".repeat(20)}\n` 384 | ); 385 | } else { 386 | return -1; 387 | } 388 | } 389 | 390 | function skipNewLines(str, i) { 391 | if (str.charAt(i + 1) === ";") ++i; 392 | while (i < str.length && /\s/.test(str.charAt(i))) { 393 | ++i; 394 | } 395 | return i; 396 | } 397 | -------------------------------------------------------------------------------- /src/gulp.js: -------------------------------------------------------------------------------- 1 | const through = require("through2"); 2 | const { runTransform } = require("./core"); 3 | 4 | const PLUGIN_NAME = "gulp-esm-to-cjs"; 5 | 6 | module.exports = options => { 7 | return through.obj(function(file, enc, cb) { 8 | if (file.isNull()) { 9 | return cb(null, file); 10 | } 11 | 12 | if (file.isStream()) { 13 | this.emit( 14 | "error", 15 | new Error(PLUGIN_NAME + " => " + "Streams not supported!") 16 | ); 17 | return cb(null); 18 | } 19 | 20 | try { 21 | const output = runTransform(file.contents.toString(), options); 22 | file.contents = Buffer.from(output); 23 | cb(null, file); 24 | } catch (error) { 25 | cb(new Error(PLUGIN_NAME + " => " + error.message)); 26 | } 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /test/bad.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require("fs"); 2 | const { join } = require("path"); 3 | 4 | const { runTransform } = require("../src/core"); 5 | 6 | module.exports = main; 7 | 8 | function main(stats) { 9 | const str = readFileSync(join(__dirname, "./fixtures/bad.md"), "utf8"); 10 | const failing = []; 11 | for (const fixture of getFixtures(str)) { 12 | ++stats.total; 13 | if (fixture.skip) continue; 14 | ++stats.run; 15 | try { 16 | runTransform(fixture.input, fixture.options); 17 | ++stats.failed; 18 | failing.push(fixture); 19 | } catch (err) { 20 | ++stats.passed; 21 | console.log(`✔ throws when ${fixture.title}`); 22 | } 23 | } 24 | printFailing(failing); 25 | } 26 | 27 | function printFailing(failing) { 28 | const colors = { 29 | bgRed: "\x1b[41m", 30 | fgRed: "\x1b[31m", 31 | fgWhite: "\x1b[37m", 32 | fgGreen: "\x1b[32m", 33 | fgYellow: "\x1b[33m", 34 | reset: "\x1b[0m" 35 | }; 36 | const str = []; 37 | const style2 = "=".repeat(60); 38 | failing.forEach(fixture => { 39 | str.push( 40 | [ 41 | "", 42 | colors.fgRed + style2, 43 | `>> bad.txt #${fixture.line} (test #${fixture.testId})`, 44 | ` @ ${fixture.title}`, 45 | "expected to throw", 46 | style2 + colors.reset 47 | ].join("\n") 48 | ); 49 | console.log(str.join("\n")); 50 | }); 51 | } 52 | 53 | function* getFixtures(str) { 54 | const lines = str.split("\n"); 55 | for (let i = 0, length = lines.length, testId = 0; i < length;) { 56 | const lineStart = i; 57 | 58 | const optionsStart = lines.indexOf("``` json", i); 59 | const optionsEnd = lines.indexOf("```", optionsStart + 1); 60 | const { title, skip, ...options } = JSON.parse( 61 | lines.slice(optionsStart + 1, optionsEnd).join("\n") 62 | ); 63 | 64 | const inputStart = lines.indexOf("``` javascript", optionsEnd + 1); 65 | const inputEnd = lines.indexOf("```", inputStart + 1); 66 | const input = lines.slice(inputStart + 1, inputEnd).join("\n"); 67 | 68 | ++testId; 69 | 70 | i = lines.indexOf("---", inputEnd); 71 | 72 | yield { 73 | skip: skip === true, 74 | line: lineStart + 1, 75 | title, 76 | input, 77 | options, 78 | testId 79 | }; 80 | 81 | if (i === -1) break; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/fixtures/bad.md: -------------------------------------------------------------------------------- 1 | ``` 2 | --- : test case starts 3 | ```json : options 4 | ```javascript : input 5 | --- : test case ends 6 | ``` 7 | --- 8 | ``` json 9 | { "title": "long module name" } 10 | ``` 11 | ``` javascript 12 | import { foo } from "some-really-long-module-name-that-we-did-not-allow-in-options"; 13 | ``` 14 | --- 15 | ``` json 16 | { "title": "too long identifer list" } 17 | ``` 18 | ``` javascript 19 | import { some_really_long_module_name_that_we_did_not_allow_in_options } from "foo"; 20 | ``` 21 | --- 22 | ``` json 23 | { "title": "far away keywords" } 24 | ``` 25 | ``` javascript 26 | import { 27 | 28 | foo: bar, 29 | baz: 30 | 31 | 32 | 33 | 34 | 35 | 36 | "from" 37 | 38 | } from aha; 39 | ``` 40 | --- 41 | ``` json 42 | { "title": "bad syntax" } 43 | ``` 44 | ``` javascript 45 | import foo: bar, 46 | baz: "from" 47 | } from {aha}; 48 | ``` 49 | --- 50 | ``` json 51 | { "title": "bad syntax 2 - why would you do that!" } 52 | ``` 53 | ``` javascript 54 | import {}; 55 | ``` 56 | --- 57 | ``` json 58 | { "title": "not implemented" } 59 | ``` 60 | ``` javascript 61 | export * from "aha"; 62 | ``` 63 | --- 64 | ``` json 65 | { "title": "bad option - quoteType" } 66 | ``` 67 | ``` javascript 68 | import { foo } from 'bar'; 69 | ``` 70 | --- 71 | ``` json 72 | { "title": "bad syntax 3" } 73 | ``` 74 | ``` javascript 75 | import { foo } frOm 'bar'; 76 | ``` 77 | *** 78 | -------------------------------------------------------------------------------- /test/fixtures/supported.md: -------------------------------------------------------------------------------- 1 | ``` 2 | --- : test case starts 3 | ```json : options 4 | ```javascript : input 5 | ```javascript : expected output 6 | --- : test case ends 7 | ``` 8 | --- 9 | ``` json 10 | { "title": "nothing to modify" } 11 | ``` 12 | ``` javascript 13 | const foo = 5; 14 | ``` 15 | ``` javascript 16 | const foo = 5; 17 | ``` 18 | --- 19 | ``` json 20 | { "title": "simple import" } 21 | ``` 22 | ``` javascript 23 | import { foo } from "bar"; 24 | ``` 25 | ``` javascript 26 | const { foo } = require("bar"); 27 | ``` 28 | --- 29 | ``` json 30 | { "title": "is okay without semicolons" } 31 | ``` 32 | ``` javascript 33 | import { foo } from "bar" 34 | 35 | import * as bar from "bar"; 36 | 37 | export { foo as bar, baz } from "aha"; 38 | ``` 39 | ``` javascript 40 | const { foo } = require("bar") 41 | 42 | const bar = require("bar"); 43 | 44 | const { 45 | foo: __foo__, 46 | baz: __baz__ 47 | } = require("aha"); 48 | module.exports = { 49 | bar: __foo__, 50 | baz: __baz__ 51 | } 52 | ``` 53 | --- 54 | ``` json 55 | {"title": "import with rename" } 56 | ``` 57 | ``` javascript 58 | import { a, b as c, d } from "bar"; 59 | ``` 60 | ``` javascript 61 | const { a, b: c, d } = require("bar"); 62 | ``` 63 | --- 64 | ``` json 65 | {"title": "await import" } 66 | ``` 67 | ``` javascript 68 | const foo = await import("bar"); 69 | ``` 70 | ``` javascript 71 | const foo = require("bar"); 72 | ``` 73 | --- 74 | ``` json 75 | {"title": "import *" } 76 | ``` 77 | ``` javascript 78 | import * as path from "path"; 79 | ``` 80 | ``` javascript 81 | const path = require("path"); 82 | ``` 83 | --- 84 | ``` json 85 | {"title": "default import" } 86 | ``` 87 | ``` javascript 88 | import foo from "bar"; 89 | ``` 90 | ``` javascript 91 | const foo = require("bar").default; 92 | ``` 93 | --- 94 | ``` json 95 | {"title": "await import with module name as variable" } 96 | ``` 97 | ``` javascript 98 | const foo = await import(bar); 99 | ``` 100 | ``` javascript 101 | const foo = require(bar); 102 | ``` 103 | --- 104 | ``` json 105 | {"title": "export a const" } 106 | ``` 107 | ``` javascript 108 | export const foo = 5; 109 | ``` 110 | ``` javascript 111 | const foo = 5; 112 | module.exports = { 113 | foo 114 | } 115 | ``` 116 | --- 117 | ``` json 118 | {"title": "default export" } 119 | ``` 120 | ``` javascript 121 | export default const foo = 5; 122 | export const bar = 6; 123 | ``` 124 | ``` javascript 125 | const foo = 5; 126 | const bar = 6; 127 | module.exports = { 128 | default: foo, 129 | bar 130 | } 131 | ``` 132 | --- 133 | ``` json 134 | {"title": "export a function" } 135 | ``` 136 | ``` javascript 137 | export function foo(blah) {} 138 | ``` 139 | ``` javascript 140 | function foo(blah) {} 141 | module.exports = { 142 | foo 143 | } 144 | ``` 145 | --- 146 | ``` json 147 | {"title": "export an async function" } 148 | ``` 149 | ``` javascript 150 | export async function foo(blah) { 151 | 152 | } 153 | ``` 154 | ``` javascript 155 | async function foo(blah) { 156 | 157 | } 158 | module.exports = { 159 | foo 160 | } 161 | ``` 162 | --- 163 | ``` json 164 | {"title": "export a class" } 165 | ``` 166 | ``` javascript 167 | export class foo {}; 168 | ``` 169 | ``` javascript 170 | class foo {}; 171 | module.exports = { 172 | foo 173 | } 174 | ``` 175 | --- 176 | ``` json 177 | {"title": "single re-export" } 178 | ``` 179 | ``` javascript 180 | export { foo } from "bar"; 181 | ``` 182 | ``` javascript 183 | module.exports = { 184 | foo: require("bar").foo 185 | } 186 | ``` 187 | --- 188 | ``` json 189 | {"title": "single re-export with rename" } 190 | ``` 191 | ``` javascript 192 | export { foo as bar } from "baz"; 193 | ``` 194 | ``` javascript 195 | module.exports = { 196 | bar: require("baz").foo 197 | } 198 | ``` 199 | --- 200 | ``` json 201 | {"title": "multiple re-exports" } 202 | ``` 203 | ``` javascript 204 | export { foo, bar } from "baz"; 205 | ``` 206 | ``` javascript 207 | const { 208 | foo: __foo__, 209 | bar: __bar__ 210 | } = require("baz"); 211 | module.exports = { 212 | foo: __foo__, 213 | bar: __bar__ 214 | } 215 | ``` 216 | --- 217 | ``` json 218 | {"title": "multiple re-exports with rename" } 219 | ``` 220 | ``` javascript 221 | export { foo as wow, bar } from "lorem"; 222 | ``` 223 | ``` javascript 224 | const { 225 | foo: __foo__, 226 | bar: __bar__ 227 | } = require("lorem"); 228 | module.exports = { 229 | wow: __foo__, 230 | bar: __bar__ 231 | } 232 | ``` 233 | --- 234 | ``` json 235 | {"title": "multiple re-exports from module with name having non-alphanum characters", "lenModuleName": 40 } 236 | ``` 237 | ``` javascript 238 | export { foo, bar } from "./module/aha/w-o-w/d.json"; 239 | ``` 240 | ``` javascript 241 | const { 242 | foo: __foo__, 243 | bar: __bar__ 244 | } = require("./module/aha/w-o-w/d.json"); 245 | module.exports = { 246 | foo: __foo__, 247 | bar: __bar__ 248 | } 249 | ``` 250 | --- 251 | ``` json 252 | {"title": "import in multiple lines" } 253 | ``` 254 | ``` javascript 255 | import { 256 | a, 257 | b as c, 258 | d 259 | } from "bar"; 260 | ``` 261 | ``` javascript 262 | const { a, b: c, d } = require("bar"); 263 | ``` 264 | --- 265 | ``` json 266 | {"title": "multiple imports" } 267 | ``` 268 | ``` javascript 269 | import { a, b as c, d, eas } from "foo"; 270 | import { e, f, g } from "bar"; 271 | ``` 272 | ``` javascript 273 | const { a, b: c, d, eas } = require("foo"); 274 | const { e, f, g } = require("bar"); 275 | ``` 276 | --- 277 | ``` json 278 | {"title": "multiple exports" } 279 | ``` 280 | ``` javascript 281 | export const foo = 5; 282 | export function bar(some, ...args) { 283 | 284 | } 285 | ``` 286 | ``` javascript 287 | const foo = 5; 288 | function bar(some, ...args) { 289 | 290 | } 291 | module.exports = { 292 | foo, 293 | bar 294 | } 295 | ``` 296 | --- 297 | ``` json 298 | {"title": "allows setting options", "indent": 4 } 299 | ``` 300 | ``` javascript 301 | export { foo, bar } from "module"; 302 | ``` 303 | ``` javascript 304 | const { 305 | foo: __foo__, 306 | bar: __bar__ 307 | } = require("module"); 308 | module.exports = { 309 | foo: __foo__, 310 | bar: __bar__ 311 | } 312 | ``` 313 | --- 314 | ``` json 315 | {"title": "whitespace processing - maps input line to line" } 316 | ``` 317 | ``` javascript 318 | export const foo = 5; 319 | 320 | 321 | export const bar = 6; 322 | 323 | 324 | 325 | 326 | 327 | ``` 328 | ``` javascript 329 | const foo = 5; 330 | 331 | 332 | const bar = 6; 333 | 334 | 335 | 336 | 337 | 338 | module.exports = { 339 | foo, 340 | bar 341 | } 342 | ``` 343 | --- 344 | ``` json 345 | {"title": "all supported types mixed" } 346 | ``` 347 | ``` javascript 348 | import { a, b as c, d } from "m1"; 349 | import { 350 | e, 351 | f as g, 352 | h 353 | } from "m1/2"; 354 | import * as path from "path"; 355 | import foo from "bar"; 356 | 357 | const i = await import("m2"); 358 | 359 | export const j = 5; 360 | 361 | export { k } from "m3"; 362 | 363 | export class l { 364 | constructor(name) { 365 | this.name = name; 366 | } 367 | }; 368 | 369 | export function m() { 370 | 371 | } 372 | 373 | export { n, o as p } from "m4"; 374 | export { q }; 375 | export { s as r }; 376 | export default const bar = 10; 377 | 378 | ``` 379 | ``` javascript 380 | const { a, b: c, d } = require("m1"); 381 | const { e, f: g, h } = require("m1/2"); 382 | const path = require("path"); 383 | const foo = require("bar").default; 384 | 385 | const i = require("m2"); 386 | 387 | const j = 5; 388 | 389 | 390 | 391 | class l { 392 | constructor(name) { 393 | this.name = name; 394 | } 395 | }; 396 | 397 | function m() { 398 | 399 | } 400 | 401 | 402 | 403 | 404 | const bar = 10; 405 | const { 406 | n: __n__, 407 | o: __o__ 408 | } = require("m4"); 409 | module.exports = { 410 | j, 411 | k: require("m3").k, 412 | l, 413 | m, 414 | n: __n__, 415 | p: __o__, 416 | q, 417 | s: r, 418 | default: bar 419 | } 420 | ``` 421 | *** 422 | -------------------------------------------------------------------------------- /test/supported.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require("fs"); 2 | const { join } = require("path"); 3 | const Table = require("easy-table"); 4 | 5 | const { runTransform } = require("../src/core"); 6 | 7 | module.exports = main; 8 | 9 | function main(stats) { 10 | const fixtures = readFileSync( 11 | join(__dirname, "./fixtures/supported.md"), 12 | "utf8" 13 | ); 14 | const failing = []; 15 | for (const fixture of getFixtures(fixtures)) { 16 | ++stats.total; 17 | if (fixture.skip) continue; 18 | ++stats.run; 19 | const result = runTransform(fixture.input, fixture.options).trim(); 20 | if (result === fixture.expected.trim()) { 21 | console.log(`✔ ${fixture.title}`); 22 | ++stats.passed; 23 | } else { 24 | ++stats.failed; 25 | fixture.fail = true; 26 | fixture.actual = result; 27 | failing.push(fixture); 28 | } 29 | } 30 | printFailing(failing); 31 | } 32 | 33 | function printFailing(failing) { 34 | const str = []; 35 | const colors = { 36 | bgRed: "\x1b[41m", 37 | fgRed: "\x1b[31m", 38 | fgWhite: "\x1b[37m", 39 | fgGreen: "\x1b[32m", 40 | fgYellow: "\x1b[33m", 41 | reset: "\x1b[0m" 42 | }; 43 | failing.forEach(fixture => { 44 | const expectation = fixture.expected.split("\n"); 45 | const reality = fixture.actual.split("\n"); 46 | const maxLen = Math.max(expectation.length, reality.length); 47 | reality.push(..." ".repeat(maxLen - reality.length).split(" ")); 48 | expectation.push(..." ".repeat(maxLen - expectation.length).split(" ")); 49 | 50 | const table = new Table(); 51 | 52 | let printLen = 0, 53 | printLen1 = 0, 54 | printLen2 = 0; 55 | expectation.forEach((_, i) => { 56 | const color = expectation[i] !== reality[i] ? colors.bgRed : ""; 57 | table.cell("expectation", color + expectation[i] + colors.reset); 58 | table.cell("reality", color + reality[i] + colors.reset); 59 | printLen1 = Math.max(printLen1, expectation[i].length); 60 | printLen2 = Math.max(printLen2, reality[i].length); 61 | printLen = Math.max(printLen, printLen1 + printLen2 + 2); 62 | table.newRow(); 63 | }); 64 | 65 | const style1 = "-".repeat(printLen); 66 | const style2 = "=".repeat(printLen); 67 | 68 | str.push( 69 | [ 70 | "", 71 | colors.fgRed + style2, 72 | `>> supported.txt #${fixture.line} (test #${fixture.testId})`, 73 | ` @ ${fixture.title}`, 74 | style1 + colors.reset, 75 | fixture.input, 76 | style1, 77 | table.toString(), 78 | colors.fgRed + style2 + colors.reset 79 | ].join("\n") 80 | ); 81 | console.log(str.join("\n")); 82 | }); 83 | } 84 | 85 | function* getFixtures(str) { 86 | const lines = str.split("\n"); 87 | for (let i = 7, length = lines.length, testId = 0; i < length;) { 88 | const lineStart = i; 89 | 90 | const optionsStart = lines.indexOf("``` json", i); 91 | const optionsEnd = lines.indexOf("```", optionsStart + 1); 92 | const { title, skip, ...options } = JSON.parse( 93 | lines.slice(optionsStart + 1, optionsEnd).join("\n") 94 | ); 95 | 96 | const inputStart = lines.indexOf("``` javascript", optionsEnd + 1); 97 | const inputEnd = lines.indexOf("```", inputStart + 1); 98 | const input = lines.slice(inputStart + 1, inputEnd).join("\n"); 99 | 100 | const expectedStart = lines.indexOf("``` javascript", inputEnd + 1); 101 | const expectedEnd = lines.indexOf("```", expectedStart + 1); 102 | const expected = lines.slice(expectedStart + 1, expectedEnd).join("\n"); 103 | 104 | ++testId; 105 | 106 | i = lines.indexOf("---", expectedEnd); 107 | 108 | yield { 109 | skip, 110 | line: lineStart + 1, 111 | title, 112 | input, 113 | options, 114 | expected, 115 | testId 116 | }; 117 | 118 | if (i === -1) break; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const supported = require("./supported"); 2 | const bad = require("./bad"); 3 | 4 | const stats = { 5 | total: 0, 6 | run: 0, 7 | passed: 0, 8 | failed: 0, 9 | failing: [] 10 | }; 11 | 12 | supported(stats); 13 | bad(stats); 14 | 15 | if (stats.failed) { 16 | console.log( 17 | [ 18 | "", 19 | "Summary:", 20 | `❌ Passed: ${stats.passed} / ${stats.total}`, 21 | stats.run !== stats.total 22 | ? ` (⚠ Skipped: ${stats.total - stats.run})` 23 | : "" 24 | ].join("\n") 25 | ); 26 | process.exit(1); 27 | } else { 28 | console.log(`\n✅ ${stats.passed} of ${stats.total} tests passed.`); 29 | if (stats.run !== stats.total) { 30 | console.log(`⚠ Skipped: ${stats.total - stats.run}`); 31 | } 32 | } 33 | --------------------------------------------------------------------------------