├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .npm 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Damian Kaczmarek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### If you use `@` in your linked module names, there is a risk this module will cause data loss 2 | See this issue https://github.com/Rush/link-module-alias/issues/3 3 | 4 | # link-module-alias 5 | 6 | Setup private modules within your repo to get away from error-prone typing of long relative paths like these: 7 | ```js 8 | require('../../../../some/very/deep/module') 9 | ``` 10 | 11 | Just create an alias and do it cleanly: 12 | 13 | ```js 14 | var module = require('@deep/module') 15 | // Or ES6 16 | import module from '@deep/module' 17 | ``` 18 | 19 | You can setup aliases both to individual files and to directories. 20 | 21 | **WARNING** Use this module only in final applications. It will not work inside published npm packages. 22 | 23 | ## Install 24 | 25 | ``` 26 | npm i --save-dev link-module-alias 27 | ``` 28 | 29 | ## Usage 30 | 31 | Add your custom configuration to your `package.json` (in your application's root), and setup automatic initialization in your scripts section. 32 | 33 | Note: you can use `@` in front of your module but before of the possible data loss https://github.com/Rush/link-module-alias/issues/3 34 | 35 | ```js 36 | "scripts": { 37 | "postinstall": "link-module-alias" 38 | }, 39 | // Aliases 40 | "_moduleAliases": { 41 | "~root" : ".", // Application's root 42 | "~deep" : "src/some/very/deep/directory/or/file", 43 | "@my_module" : "lib/some-file.js", // can be @ - but see above comment and understand the associated risk 44 | "something" : "src/foo", // Or without ~. Actually, it could be any string 45 | "module-with-typings": { 46 | "main": "src/path/to/module.js", 47 | "typings" "src/path/to/module/typings.d.ts" // you can specify typings for files (it works with aliases to files only) 48 | } 49 | } 50 | ``` 51 | 52 | If you encounter issues with installing modules, you may want to set up the preinstall script as well: 53 | ```js 54 | "scripts": { 55 | "postinstall": "link-module-alias", 56 | "preinstall": "command -v link-module-alias && link-module-alias clean || true" 57 | } 58 | ``` 59 | 60 | ## How does it work? 61 | 62 | - For aliases to directories, we create symlinks with `fs.symlink` 63 | - For aliases to files, we create proxy modules with a package.json containing `"main"` that points to the target file 64 | 65 | ## Background 66 | 67 | This module it's almost a drop in replacement for another package https://www.npmjs.com/package/module-alias - use module `module-alias` if you like runtime require hooks and use `link-module-alias` if you want good compatibility with your IDE and no runtime hacks. 68 | 69 | The key differentiator of `link-module-alias` is creating all the module links statically in form of symlinks and proxy packages inside `node_modules`, there is no hacky require hook and you don't need to load any supporting packages. 70 | 71 | The key motivator to create `link-module-alias` was to fix the issue with module aliases not resolving in VS Code. https://github.com/ilearnio/module-alias/issues/19 72 | 73 | ## License 74 | 75 | MIT. Attribution to the `module-alias` for parts for the README and original idea. 76 | 77 | # Contributors 78 | 79 | [@kwburnett](https://github.com/kwburnett) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Author: Damian "Rush" Kaczmarek 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const packageJson = require('./package.json'); 8 | 9 | const moduleAliases = packageJson._moduleAliases; 10 | if(!moduleAliases) { 11 | console.error(`_moduleAliases in package.json is empty, skipping`); 12 | process.exit(0); 13 | } 14 | 15 | const { promisify } = require('util'); 16 | 17 | const stat = promisify(fs.stat); 18 | const lstat = promisify(fs.lstat); 19 | const writeFile = promisify(fs.writeFile); 20 | const unlink = promisify(fs.unlink); 21 | const readdir = promisify(fs.readdir); 22 | const symlink = promisify(fs.symlink); 23 | const mkdir = promisify(fs.mkdir); 24 | const rmdir = promisify(fs.rmdir); 25 | 26 | const chalk = require('chalk'); 27 | 28 | const LINK_ALIAS_PREFIX = '.link-module-alias-'; 29 | const LINK_ALIAS_NESTED_SEPARATOR = '--'; 30 | const DIR_LINK_TYPE = ((process.platform === 'win32') ? 'junction' : 'dir'); 31 | 32 | async function tryUnlink(path) { 33 | try { 34 | return await unlink(path); 35 | } catch (err) { 36 | if (err.code !== 'ENOENT') { 37 | throw err; 38 | } 39 | } 40 | }; 41 | 42 | async function tryRmdir(path) { 43 | try { 44 | return await rmdir(path); 45 | } catch (err) { 46 | if (err.code !== 'ENOENT') { 47 | throw err; 48 | } 49 | } 50 | }; 51 | 52 | function addColor({moduleName, type, target}) { 53 | if(type === 'none') { 54 | return chalk.red(moduleName) + ` -> ${chalk.bold(chalk.red('ALREADY EXISTS'))}`; 55 | } else if(type === 'symlink') { 56 | return chalk.cyan(moduleName) + ` -> ${chalk.bold(target)}`; 57 | } else if(type === 'proxy') { 58 | return chalk.green(moduleName) + ` -> ${chalk.bold(target)}`; 59 | } 60 | return `${moduleName} `; 61 | } 62 | 63 | function addColorUnlink({moduleName, type}) { 64 | if(type === 'none') { 65 | moduleName = chalk.red(moduleName); 66 | } else if(type === 'symlink') { 67 | moduleName = chalk.cyan(moduleName); 68 | } else if(type === 'proxy') { 69 | moduleName = chalk.green(moduleName); 70 | } 71 | return moduleName; 72 | } 73 | 74 | function getModuleAlias(moduleName) { 75 | // Replace any nested alias names with "--" 76 | return `${LINK_ALIAS_PREFIX}${moduleName.replace(/\//g, LINK_ALIAS_NESTED_SEPARATOR)}`; 77 | } 78 | 79 | function getModuleNameFromAliasFile(aliasFileName) { 80 | // See if this matches the prefix and return the module name, if present 81 | const m = aliasFileName.match(new RegExp(`^\\${LINK_ALIAS_PREFIX}(.*)`)); // RegExp = /^\.link-module-alias-(.*)/ 82 | return m && m[1].replace(new RegExp(LINK_ALIAS_NESTED_SEPARATOR, 'g'), '/'); // RegExp = /--/g 83 | } 84 | 85 | async function exists(filename) { 86 | try { 87 | await stat(filename); 88 | return true; 89 | } catch(err) { 90 | return false; 91 | } 92 | } 93 | 94 | async function unlinkModule(moduleName) { 95 | const moduleDir = path.join('node_modules', moduleName); 96 | let statKey; 97 | try { 98 | statKey = await lstat(moduleDir); 99 | } catch(err) {} 100 | 101 | let type; 102 | if(statKey && statKey.isSymbolicLink()) { 103 | await tryUnlink(moduleDir); 104 | await tryUnlink(path.join('node_modules', getModuleAlias(moduleName))); 105 | type = 'symlink'; 106 | } else if(statKey) { 107 | await tryUnlink(path.join(moduleDir, 'package.json')); 108 | await tryRmdir(moduleDir); 109 | await tryUnlink(path.join('node_modules', getModuleAlias(moduleName))); 110 | type = 'proxy'; 111 | } else { 112 | type = 'none'; 113 | } 114 | return { moduleName, type }; 115 | } 116 | 117 | async function linkModule(moduleName) { 118 | const moduleDir = path.join('node_modules', moduleName); 119 | const moduleExists = await exists(moduleDir); 120 | const linkExists = moduleExists && await exists(path.join('node_modules', getModuleAlias(moduleName))); 121 | const moduleAlias = moduleAliases[moduleName]; 122 | const isSimpleAlias = typeof moduleAlias === 'string'; 123 | const typings = isSimpleAlias ? undefined : moduleAlias.typings; 124 | const target = isSimpleAlias ? moduleAlias : moduleAlias.main; 125 | 126 | if (moduleName.match(/^@/) && !packageJson._moduleAliasIgnoreWarning) { 127 | console.warn( 128 | chalk.yellow(`WARNING! Using @ in front of your module name ${moduleName} may cause data loss. Please read this issue thread https://github.com/Rush/link-module-alias/issues/3 and make a backup before executing any npm/yarn commands. The issue ultimately can be blamed on npm/yarn. You've been warned.`) + 129 | ` -- you can disable this warning by setting _moduleAliasIgnoreWarning to true in your package.json` 130 | ); 131 | } 132 | 133 | let type; 134 | if(moduleExists && !linkExists) { 135 | console.error(chalk.red(`Module ${moduleName} already exists and wasn't created by us, skipping`)); 136 | type = 'none'; 137 | return { moduleName, type, target }; 138 | } else if(linkExists) { 139 | await unlinkModule(moduleName); 140 | } 141 | 142 | if(target.match(/\.js$/)) { 143 | // console.log(`Target ${target} is a direct link, creating proxy require`); 144 | await mkdir(moduleDir); 145 | 146 | const packageJsonObj = { 147 | name: moduleName, 148 | main: path.join('../../', target), 149 | }; 150 | 151 | if (typings) { 152 | packageJsonObj.typings = path.join('../../', typings); 153 | } 154 | 155 | await writeFile(path.join(moduleDir, 'package.json'), JSON.stringify(packageJsonObj, null, 2)); 156 | type = 'proxy'; 157 | } else { 158 | const stat = fs.lstatSync(target); 159 | if(!stat.isDirectory()) { 160 | console.log(`Target ${target} is not a directory, skipping ...`); 161 | type = 'none'; 162 | return { moduleName, type, target }; 163 | } 164 | // Check if there is a nested alias 165 | let directoryUp = ''; 166 | if (moduleName.includes('/')) { 167 | // For every subdirectory, we need to come up in our link to ensure the right directories are linked 168 | directoryUp = '../'.repeat((moduleName.match(/\//g) || []).length); 169 | // If every directory is already made, mkdir will throw an error 170 | try { 171 | // We need to create every directory except the last 172 | let parentDirectories = moduleName.substr(0, moduleName.lastIndexOf('/')); 173 | await mkdir(path.join('node_modules', parentDirectories), { recursive: true }); 174 | } catch (err) { 175 | if (err.code !== 'EEXISTS') { 176 | throw err; 177 | } 178 | } 179 | } 180 | await symlink(path.join('../', directoryUp, target), moduleDir, DIR_LINK_TYPE); 181 | type = 'symlink'; 182 | } 183 | await writeFile(path.join('node_modules', getModuleAlias(moduleName)), ''); 184 | return { moduleName, type, target }; 185 | } 186 | 187 | async function linkModules() { 188 | try { await mkdir('node_modules'); } catch(err) {} 189 | const modules = await Promise.all(Object.keys(moduleAliases).map(async key => { 190 | return linkModule(key); 191 | })); 192 | console.log('link-module-alias:', modules.map(addColor).join(', ')); 193 | } 194 | 195 | async function unlinkModules() { 196 | const nodeModulesExists = await exists('node_modules'); 197 | if(!nodeModulesExists) { 198 | return; 199 | } 200 | const allModules = await readdir('node_modules'); 201 | 202 | const modules = allModules.map(getModuleNameFromAliasFile).filter(v => !!v); 203 | 204 | const unlinkedModules = await Promise.all(modules.map(mod => { 205 | return unlinkModule(mod); 206 | })); 207 | if(unlinkedModules.length) { 208 | console.log('link-module-alias: Cleaned ', unlinkedModules.filter(v => { 209 | return v.type !== 'none'; 210 | }).map(addColorUnlink).join(' ')); 211 | } else { 212 | console.log('link-module-alias: No modules to clean'); 213 | } 214 | } 215 | 216 | if(process.argv[2] === 'clean') { 217 | unlinkModules(); 218 | } else { 219 | linkModules(); 220 | } 221 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "link-module-alias", 3 | "version": "1.2.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ansi-styles": { 8 | "version": "3.2.1", 9 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 10 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 11 | "requires": { 12 | "color-convert": "^1.9.0" 13 | } 14 | }, 15 | "chalk": { 16 | "version": "2.4.1", 17 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", 18 | "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", 19 | "requires": { 20 | "ansi-styles": "^3.2.1", 21 | "escape-string-regexp": "^1.0.5", 22 | "supports-color": "^5.3.0" 23 | } 24 | }, 25 | "color-convert": { 26 | "version": "1.9.1", 27 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", 28 | "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", 29 | "requires": { 30 | "color-name": "^1.1.1" 31 | } 32 | }, 33 | "color-name": { 34 | "version": "1.1.3", 35 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 36 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 37 | }, 38 | "escape-string-regexp": { 39 | "version": "1.0.5", 40 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 41 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 42 | }, 43 | "has-flag": { 44 | "version": "3.0.0", 45 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 46 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 47 | }, 48 | "supports-color": { 49 | "version": "5.4.0", 50 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 51 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 52 | "requires": { 53 | "has-flag": "^3.0.0" 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "link-module-alias", 3 | "version": "1.2.1", 4 | "description": "Create permanent links for _moduleAliases", 5 | "bin": "index.js", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": "github:Rush/link-module-alias", 11 | "author": "Damian Kaczmarek ", 12 | "contributors": [ 13 | "Kevin Burnett ", 14 | "Klaus Sevensleeper" 15 | ], 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/Rush/link-module-alias/issues" 19 | }, 20 | "homepage": "https://github.com/Rush/link-module-alias#readme", 21 | "engines": { 22 | "node": "> 8.0.0" 23 | }, 24 | "dependencies": { 25 | "chalk": "^2.4.1" 26 | } 27 | } 28 | --------------------------------------------------------------------------------