├── .gitignore ├── .huskyrc ├── .lintstagedrc ├── .npmrc ├── README.md ├── bin └── cherry-pick.js ├── lib └── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "{lib,bin}/**/*.js": [ 3 | "prettier --no-semi --use-tabs --single-quote --trailing-comma es5 --write", 4 | "git add" 5 | ], 6 | "*.md": [ 7 | "prettier --prose-wrap always --write", 8 | "git add" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cherry-pick 🍒⛏📦 2 | 3 | Build tool to generate proxy directories with `package.json` files such as this: 4 | 5 | ```json 6 | { 7 | "name": "redux-saga/effects", 8 | "private": true, 9 | "main": "../lib/effects.js", 10 | "module": "../es/effects.js" 11 | } 12 | ``` 13 | 14 | ## Why? 15 | 16 | When it comes to "main" entry points of our libraries we have an _easy_ way for 17 | supporting both CJS & ESM files with respectively `"main"` and `"module"` fields 18 | in `package.json`. This allows resolution algorithms to chose a file with the 19 | best format _automatically_. However if we have multiple files in a package and 20 | we want all of them to be importable we often suggest to users doing it like 21 | this: 22 | 23 | ```js 24 | import module from "package/lib/module"; 25 | ``` 26 | 27 | There are problems with this approach: 28 | 29 | * it is often encouraging people to import files authored in CJS format, which 30 | if produced with tools like [`babel`](https://github.com/babel/babel) has i.e. 31 | interop helper functions deoptimizing imported file size when comparing to the 32 | same file authored in ESM format. Also `webpack` just bails out on CJS files 33 | when trying to optimize your application size with techniques such as 34 | tree-shaking & scope hoisting (a.k.a module concatenation). 35 | * it is exposing **internal directory structure** to the user. Why `lib` is in 36 | the requested path? If you ship both CJS & ESM directories to `npm` and if 37 | users would like to import appropriate file depending on the tool they are 38 | "forced" to remember this and switch between importing the same thing with 39 | paths like `package/lib/module` and `package/es/module`. This is a mental 40 | overhead that can be avoided. 41 | 42 | This technique was also described by me in more details in 43 | [this article](https://developers.livechatinc.com/blog/how-to-create-javascript-libraries-in-2018-part-2#proxy-directories). 44 | 45 | ## CLI Options 46 | 47 | ### default 48 | 49 | ``` 50 | cherry-pick [input-dir] 51 | 52 | Create proxy directories 53 | 54 | Commands: 55 | cherry-pick [input-dir] Create proxy directories [default] 56 | cherry-pick clean [input-dir] Cleanup generated directories 57 | 58 | Options: 59 | --help, -h Show help [boolean] 60 | --version, -v Show version number [boolean] 61 | --cjs-dir [default: "lib"] 62 | --esm-dir [default: "es"] 63 | --types-dir 64 | --cwd [default: "."] 65 | --input-dir [default: "src"] 66 | ``` 67 | 68 | ### clean 69 | 70 | ``` 71 | cherry-pick clean [input-dir] 72 | 73 | Cleanup generated directories 74 | 75 | Options: 76 | --help, -h Show help [boolean] 77 | --version, -v Show version number [boolean] 78 | --cwd [default: "."] 79 | --input-dir [default: "src"] 80 | ``` 81 | 82 | ## JS API 83 | 84 | `cherry-pick` exports a `default` method which creates proxy directories and 85 | `clean` which removes them. Both accepts the same options as corresponding CLI 86 | commands, only they are camelCased. 87 | 88 | ```js 89 | const { default: cherryPick, clean } = require("cherry-pick"); 90 | 91 | cherryPick({ inputDir: "source" }) 92 | .then(cherryPicked => 93 | console.log(`Created proxy directories: ${cherryPicked.join(", ")}`) 94 | ) 95 | .then(() => clean({ inputDir: "source" })) 96 | .then(removed => 97 | console.log(`Removed proxy directories: ${Removed.join(", ")}`) 98 | ); 99 | ``` 100 | -------------------------------------------------------------------------------- /bin/cherry-pick.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const yargs = require('yargs') 4 | const chalk = require('chalk') 5 | const { default: cherryPick, clean } = require('..') 6 | 7 | const noop = () => {} 8 | 9 | const DONE_LABEL = chalk.green.inverse(' DONE ') 10 | 11 | yargs 12 | .command( 13 | '$0 [input-dir]', 14 | 'Create proxy directories', 15 | yargs => 16 | yargs 17 | .default('input-dir', 'src') 18 | .option('name') 19 | .option('cjs-dir', { default: 'lib' }) 20 | .option('esm-dir', { default: 'es' }) 21 | .option('types-dir') 22 | .option('cwd', { default: '.' }), 23 | options => 24 | cherryPick(options).then(files => 25 | console.log( 26 | `\n🍒 ⛏ 📦 ${DONE_LABEL} Created proxy directories: ${files.join( 27 | ', ' 28 | )}.\n` 29 | ) 30 | ) 31 | ) 32 | .command( 33 | 'clean [input-dir]', 34 | 'Cleanup generated directories', 35 | yargs => yargs.default('input-dir', 'src').option('cwd', { default: '.' }), 36 | options => 37 | clean(options).then(files => 38 | console.log( 39 | `\n🍒 ⛏ 🗑 ${DONE_LABEL} Removed proxy directories: ${files.join( 40 | ', ' 41 | )}.\n` 42 | ) 43 | ) 44 | ) 45 | .help() 46 | .alias('help', 'h') 47 | .version() 48 | .alias('version', 'v') 49 | .strict().argv 50 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { promisify } = require('util') 4 | const glob = require('tiny-glob') 5 | const readPkgUp = require('read-pkg-up') 6 | 7 | const readDir = promisify(fs.readdir) 8 | const mkDir = promisify(fs.mkdir) 9 | const rimraf = promisify(require('rimraf')) 10 | const stat = promisify(fs.stat) 11 | const writeFile = promisify(fs.writeFile) 12 | 13 | const isFile = path => 14 | stat(path) 15 | .then(stats => stats.isFile()) 16 | .catch(() => false) 17 | 18 | const withDefaults = ( 19 | { cwd = '.', ...options } = {}, 20 | additionalDefaults = {} 21 | ) => ({ 22 | inputDir: 'src', 23 | cwd: path.resolve(process.cwd(), cwd), 24 | ...additionalDefaults, 25 | ...options, 26 | }) 27 | 28 | const noop = () => {} 29 | 30 | const findFiles = async ({ cwd, inputDir }) => { 31 | const filePaths = await glob( 32 | path.join(inputDir, '!(index).{js,jsx,ts,tsx}'), 33 | { cwd } 34 | ) 35 | return filePaths 36 | .filter(f => !f.endsWith('.d.ts')) 37 | .map(filePath => path.basename(filePath).replace(/\.(js|ts)x?$/, '')) 38 | } 39 | 40 | const pkgCache = new WeakMap() 41 | 42 | const getPkgName = async options => { 43 | if (options.name != null) { 44 | return options.name 45 | } 46 | if (pkgCache.has(options)) { 47 | return pkgCache.get(options) 48 | } 49 | const result = await readPkgUp({ cwd: options.cwd }) 50 | if (!result) { 51 | throw new Error( 52 | 'Could not determine package name. No `name` option was passed and no package.json was found relative to: ' + 53 | options.cwd 54 | ) 55 | } 56 | const pkgName = result.package.name 57 | pkgCache.set(options, pkgName) 58 | return pkgName 59 | } 60 | 61 | const fileProxy = async (options, file) => { 62 | const { cwd, cjsDir, esmDir, typesDir } = options 63 | const pkgName = await getPkgName(options) 64 | 65 | const proxyPkg = { 66 | name: `${pkgName}/${file}`, 67 | private: true, 68 | main: path.join('..', cjsDir, `${file}.js`), 69 | module: path.join('..', esmDir, `${file}.js`), 70 | } 71 | 72 | if (typeof typesDir === 'string') { 73 | proxyPkg.types = path.join('..', typesDir, `${file}.d.ts`) 74 | } else if (await isFile(path.join(cwd, `${file}.d.ts`))) { 75 | proxyPkg.types = path.join('..', `${file}.d.ts`) 76 | // try the esm path in case types are located with each 77 | } else if (await isFile(path.join(cwd, esmDir, `${file}.d.ts`))) { 78 | proxyPkg.types = path.join('..', esmDir, `${file}.d.ts`) 79 | } 80 | 81 | return JSON.stringify(proxyPkg, null, 2) + '\n' 82 | } 83 | 84 | const cherryPick = async inputOptions => { 85 | const options = withDefaults(inputOptions, { 86 | cjsDir: 'lib', 87 | esmDir: 'es', 88 | }) 89 | 90 | const files = await findFiles(options) 91 | 92 | await Promise.all( 93 | files.map(async file => { 94 | const proxyDir = path.join(options.cwd, file) 95 | await mkDir(proxyDir).catch(noop) 96 | await writeFile( 97 | `${proxyDir}/package.json`, 98 | await fileProxy(options, file) 99 | ) 100 | }) 101 | ) 102 | 103 | return files 104 | } 105 | 106 | const clean = async inputOptions => { 107 | const options = withDefaults(inputOptions) 108 | const files = await findFiles(options) 109 | await Promise.all( 110 | files.map(async file => rimraf(path.join(options.cwd, file))) 111 | ) 112 | return files 113 | } 114 | 115 | module.exports.default = cherryPick 116 | module.exports.clean = clean 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cherry-pick", 3 | "version": "0.5.0", 4 | "description": "🍒⛏📦 Build tool to generate proxy directories.", 5 | "main": "./lib/index.js", 6 | "bin": "./bin/cherry-pick.js", 7 | "files": [ 8 | "bin", 9 | "lib" 10 | ], 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "release:patch": "npm version patch && npm publish && git push --follow-tags", 14 | "release:minor": "npm version minor && npm publish && git push --follow-tags", 15 | "release:major": "npm version major && npm publish && git push --follow-tags" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/Andarist/cherry-pick.git" 20 | }, 21 | "keywords": [ 22 | "build", 23 | "tool", 24 | "es", 25 | "commonjs", 26 | "cjs", 27 | "cherry-pick" 28 | ], 29 | "author": "Mateusz Burzyński (https://github.com/Andarist)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/Andarist/cherry-pick/issues" 33 | }, 34 | "homepage": "https://github.com/Andarist/cherry-pick#readme", 35 | "dependencies": { 36 | "chalk": "^2.4.2", 37 | "read-pkg-up": "^6.0.0", 38 | "rimraf": "^2.6.3", 39 | "tiny-glob": "^0.2.6", 40 | "yargs": "^13.2.4" 41 | }, 42 | "devDependencies": { 43 | "husky": "^2.5.0", 44 | "lint-staged": "^8.2.1", 45 | "prettier": "^1.18.2" 46 | } 47 | } 48 | --------------------------------------------------------------------------------