├── .gitattributes ├── .travis.yml ├── .gitignore ├── .editorconfig ├── .eslintrc ├── mit.license ├── package.json ├── bin └── tsdm ├── readme.md ├── test └── index.spec.js └── src └── index.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | build 5 | node_modules 6 | ncp-debug.log 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard"], 3 | "rules": { 4 | "max-len": [2, 80], 5 | "space-before-function-paren": 0, 6 | "semi": [2, "always"], 7 | "no-extra-semi": 2, 8 | "semi-spacing": [2, { "before": false, "after": true }] 9 | }, 10 | "env": { 11 | "mocha": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /mit.license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Stanley Shyiko 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsdm", 3 | "description": "No worries TypeScript definition manager", 4 | "version": "0.1.0-3", 5 | "author": "Stanley Shyiko ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/shyiko/tsdm" 10 | }, 11 | "bin": "./bin/tsdm", 12 | "main": "./src/index.js", 13 | "dependencies": { 14 | "async": "^1.5.2", 15 | "chalk": "^1.1.1", 16 | "findup": "^0.1.5", 17 | "lodash.assign": "^4.0.2", 18 | "lodash.pluck": "^3.1.2", 19 | "mkdirp": "^0.5.1", 20 | "rcfg": "^0.1.0", 21 | "read-package-tree": "^5.1.2", 22 | "resolve": "^1.1.7", 23 | "rimraf": "^2.5.1", 24 | "yargs": "^3.32.0" 25 | }, 26 | "devDependencies": { 27 | "chai": "^3.4.1", 28 | "eslint": "^1.10.3", 29 | "eslint-config-standard": "^4.4.0", 30 | "eslint-plugin-standard": "^1.3.1", 31 | "mocha": "^2.3.4", 32 | "mock-fs": "^3.7.0", 33 | "sinon": "^1.17.3" 34 | }, 35 | "scripts": { 36 | "test": "eslint src/**/*.js test/**/*.js && mocha -R spec" 37 | }, 38 | "keywords": [ 39 | "typescript", 40 | "typings", 41 | "defintions", 42 | "tsd", 43 | "manager" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /bin/tsdm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | var rewire = require('../src').rewire; 5 | var up = require('findup'); 6 | var chalk = require('chalk'); 7 | var yargs = require('yargs') 8 | .usage('No worries TypeScript declaration manager (https://github.com/shyiko/tsdm)' + 9 | '\n\nUsage: tsdm') 10 | .command('rewire', 'Rewire type definition providers ' + 11 | '(should be called after `npm install`) (aliases: r)') 12 | .help('h').alias('h', 'help') 13 | .version(function () { return require('../package').version; }) 14 | .strict(); 15 | 16 | up(process.cwd(), 'package.json', function (err, projectRoot) { 17 | if (err) { 18 | console.error('Couldn\'t resolve project root (' + err.message + ')'); 19 | process.exit(5); 20 | } 21 | var command = process.argv[2]; 22 | switch (command) { 23 | case 'r': 24 | case 'rewire': 25 | rewire({path: projectRoot}) 26 | .on('wired', function (e) { 27 | switch (e.type) { 28 | case 'ambient': 29 | var pkg = e.pkg; 30 | var ts = pkg.typescript; 31 | var def = ts && [].concat(ts.definition || ts.definitions) 32 | .map(function (name) { 33 | return path.basename(name); 34 | }); 35 | console.log(chalk.green('Linked ') + 36 | chalkifyPath(path.relative(process.cwd(), pkg._where)) + 37 | chalk.gray((def ? ' (' + def.join(', ') + ')' : ''))); 38 | break; 39 | case 'scoped': 40 | console.log(chalk.green('Updated ') + 41 | chalkifyPath(path.relative(process.cwd(), e.file))); 42 | break; 43 | default: 44 | throw new Error(); 45 | } 46 | }) 47 | .on('warn', function (e) { 48 | console.warn('WARN: ' + e); 49 | }) 50 | .on('error', function (err) { 51 | console.error(err.stack); 52 | process.exit(4); 53 | }); 54 | break; 55 | default: 56 | yargs.showHelp(); 57 | if (command) { 58 | console.error('Unknown command `' + command + '`.'); 59 | } 60 | process.exit(3); 61 | } 62 | }); 63 | 64 | function chalkifyPath(p) { 65 | var d; 66 | if (~(d = p.lastIndexOf(path.sep))) { 67 | p = chalk.gray(p.slice(0, d + 1)) + p.slice(d + 1) 68 | } 69 | return p; 70 | } 71 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 | [![Build Status](https://travis-ci.org/shyiko/tsdm.svg?branch=master)](https://travis-ci.org/shyiko/tsdm) 8 | 9 | No worries TypeScript definition manager. 10 | 11 | > \* experimental 12 | 13 | ## Why? 14 | * No dependency on [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) GitHub repo. No PRs to send. No PRs to accept. 15 | * ... and no custom registries either. Everything is in NPM. 16 | * No [fear of hitting GitHub rate-limit](https://github.com/DefinitelyTyped/tsd#i-hit-the-github-rate-limit-now-what). 17 | * No need to commit `typings/**` or [full of opaque hashes](https://github.com/DefinitelyTyped/tsd/blob/master/tsd.json) `tsd.json`. 18 | * No `/// `s all over your code. 19 | * No special `.json`. `package.json` is all you need. 20 | * Easy version management. 21 | * One responsibility - wiring type definitions in. Installation, shinkwrapping, etc. is all offloaded on to `npm`. 22 | * Nothing to learn. If you know how to use `npm` - you're pretty much all set. 23 | 24 | ## Installation 25 | 26 | ```sh 27 | npm install -g tsdm 28 | ``` 29 | 30 | ## Usage 31 | 32 | For any package that doesn't come with typings 33 | out-of-the-box use `npm` to install external definition (e.g. `npm install --save-dev ...`). 34 | After that - run `tsdm rewire`. That's it. 35 | 36 | ```sh 37 | npm i retyped-react-tsd-ambient --save-dev && tsdm rewire 38 | ``` 39 | 40 | > NOTE that `compilerOptions.moduleResolution` has to be set to 41 | [node](https://github.com/Microsoft/TypeScript/wiki/Typings-for-npm-packages) (in your tsconfig.json) 42 | 43 | > Most (if not all) [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) typings are available through [retyped](https://github.com/retyped). 44 | [npmsearch](http://npmsearch.com/?q=keywords:tsd%20AND%20chai) is an excellent place to find many more. 45 | 46 | > If you need a quick way to add declaration for the module that doesn't yet have definition available on `npm` you 47 | can add it to `/.tsdm.d.ts`. This is meant a temporary solution only. Please consider contributing missing 48 | typings back to the community. 49 | 50 | **DEMO @** [shyiko/typescript-starter-pack](https://github.com/shyiko/typescript-starter-pack) (coming soon) 51 | 52 | ## License 53 | 54 | [MIT License](https://github.com/shyiko/tsdm/blob/master/mit.license) 55 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | var mock = require('mock-fs'); 2 | var expect = require('chai').expect; 3 | var sinon = require('sinon'); 4 | var fs = require('fs'); 5 | var tsdm = require('../src'); 6 | 7 | describe('tsdm', function () { 8 | describe('#rewire()', function () { 9 | it('should generate typings/tsd.d.ts referencing ambient definitions', 10 | function (cb) { 11 | mock({ 12 | '/project': { 13 | 'package.json': JSON.stringify({name: 'project', version: '0.1.0'}), 14 | 'node_modules': { 15 | 'a-tsd': { 16 | 'package.json': 17 | JSON.stringify({name: 'a-tsd', version: '0.1.0', 18 | typescript: {definition: 'a.d.ts'}}), 19 | 'a.d.ts': 'declare module "a" { export default 0; }' 20 | } 21 | } 22 | } 23 | }); 24 | var spy = sinon.spy(); 25 | tsdm.rewire({path: '/project'}) 26 | .on('wired', spy) 27 | .on('error', sinon.stub().throws()) 28 | .on('end', function () { 29 | expect(spy.callCount).to.be.equal(1); 30 | // using fs.*Sync is okay because of mock-fs binding 31 | var data = fs.readFileSync('/project/typings/tsd.d.ts', 'utf8'); 32 | expect(data.split('\n')).to.contain( 33 | '/// '); 34 | cb(); 35 | }); 36 | }); 37 | it('should symlink ambient definitions if "symlink" mode is on', 38 | function (cb) { 39 | mock({ 40 | '/project': { 41 | 'package.json': JSON.stringify({name: 'project', version: '0.1.0'}), 42 | 'node_modules': { 43 | 'a-tsd': { 44 | 'package.json': 45 | JSON.stringify({name: 'a-tsd', version: '0.1.0', 46 | typescript: {definition: 'a.d.ts'}}), 47 | 'a.d.ts': 'declare module "a" { export default 0; }' 48 | } 49 | }, 50 | '.tsdmrc': JSON.stringify({symlink: true}) 51 | } 52 | }); 53 | var spy = sinon.spy(); 54 | tsdm.rewire({path: '/project'}) 55 | .on('wired', spy) 56 | .on('error', sinon.stub().throws()) 57 | .on('end', function () { 58 | expect(spy.callCount).to.be.equal(1); 59 | // using fs.*Sync is okay because of mock-fs binding 60 | var data = fs.readFileSync('/project/typings/tsd.d.ts', 'utf8'); 61 | expect(data.split('\n')) 62 | .to.contain('/// '); 63 | expect(fs.lstatSync('/project/typings/a-tsd').isSymbolicLink()) 64 | .to.be.true; 65 | cb(); 66 | }); 67 | }); 68 | it('should rewire scoped `typings` in place', 69 | function (cb) { 70 | mock({ 71 | '/project': { 72 | 'package.json': JSON.stringify({name: 'project', version: '0.1.0'}), 73 | 'node_modules': { 74 | 'a-tsd': { 75 | 'package.json': 76 | JSON.stringify({name: 'a-tsd', version: '0.1.0', 77 | typings: 'a.d.ts', typingsScope: 'a'}), 78 | 'a.d.ts': 'export default class A { static field: string }' 79 | }, 80 | 'a': { 81 | 'package.json': 82 | JSON.stringify({name: 'a', version: '0.1.0'}), 83 | 'index.js': '' // otherwise 'resolve' module will fail 84 | } 85 | } 86 | } 87 | }); 88 | var spy = sinon.spy(); 89 | tsdm.rewire({path: '/project'}) 90 | .on('wired', spy) 91 | .on('error', sinon.stub().throws()) 92 | .on('end', function () { 93 | expect(spy.callCount).to.be.equal(1); 94 | // using fs.*Sync is okay because of mock-fs binding 95 | var data = fs.readFileSync('/project/node_modules/a/package.json', 96 | 'utf8'); 97 | expect(JSON.parse(data).typings).to.be.equal('../a-tsd/a.d.ts'); 98 | cb(); 99 | }); 100 | }); 101 | }); 102 | afterEach(function () { 103 | mock.restore(); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var rpt = require('read-package-tree'); 2 | var rimraf = require('rimraf'); 3 | var mkdirp = require('mkdirp'); 4 | var rslv = require('resolve'); 5 | var async = require('async'); 6 | var up = require('findup'); 7 | var pluck = require('lodash.pluck'); 8 | var assign = require('lodash.assign'); 9 | var rc = require('rcfg'); 10 | var fs = require('fs'); 11 | var path = require('path'); 12 | var util = require('util'); 13 | var def = Boolean; 14 | var EventEmitter = require('events').EventEmitter; 15 | 16 | var debug = util.debuglog('tsdm'); 17 | 18 | // todo: proxy npm commands for auto rewire? 19 | // fixme: if scoped-declaration provider is removed - corresponding node_module 20 | // will still contain invalid reference 21 | 22 | /** 23 | * @param {string} path 24 | * @param {function(pkg): boolean} filter 25 | * @param cb {function(err, pkg[])} 26 | */ 27 | function list(path, filter, cb) { 28 | var rootNode; 29 | rpt(path, function (node) { 30 | return (rootNode || (rootNode = node)) === node; 31 | }, function (err, data) { 32 | if (err) { 33 | return cb(err); 34 | } 35 | var defModules = data.children 36 | .reduce(function (r, node) { 37 | var pkg = node.package; 38 | if (filter(pkg)) { 39 | // _id: '@' 40 | pkg._where = node.realpath; 41 | r.push(pkg); 42 | } 43 | return r; 44 | }, []) 45 | .sort(function (l, r) { return l.name.localeCompare(r.name); }); 46 | cb(null, defModules); 47 | }); 48 | } 49 | 50 | /** 51 | * @param {string} dir 52 | * @param {function(stats, path): boolean} filter 53 | * @param cb {function(err)} 54 | */ 55 | function rm(dir, filter, cb) { 56 | fs.readdir(dir, function (err, fileNames) { 57 | if (err) { 58 | return cb(err); 59 | } 60 | async.each(fileNames, function (fileName, cb) { 61 | var file = path.join(dir, fileName); 62 | fs.lstat(file, function (err, stats) { 63 | if (err) { 64 | return cb(err); 65 | } 66 | if (filter(stats, file)) { 67 | fs.unlink(file, cb); 68 | } else { 69 | cb(); 70 | } 71 | }); 72 | }, cb); 73 | }); 74 | } 75 | 76 | /** 77 | * @param {string[]} src 78 | * @param {string} dst 79 | * @param {function(err)} cb 80 | */ 81 | function link(src, dst, cb) { 82 | async.each(src, function (src, cb) { 83 | fs.symlink(src, path.join(dst, path.basename(src)), cb); 84 | }, cb); 85 | } 86 | 87 | /** 88 | * @param {string} file 89 | * @param {string[]} refs 90 | * @param {function(err)} cb 91 | */ 92 | function commitRefs(file, refs, cb) { 93 | var data = [ 94 | '// Autogenerated, do not edit. All changes will be lost.\n' 95 | ].concat( 96 | refs.map(function (ref) { 97 | return '/// '; 98 | }) 99 | ).join('\n'); 100 | fs.writeFile(file, data, cb); 101 | } 102 | 103 | function rewireAmbient(o, cb) { 104 | var self = this; 105 | list(o.path, 106 | function (pkg) { 107 | var t = pkg.typescript; 108 | return t && (t.definition || t.definitions); 109 | }, 110 | function (err, pkgs) { 111 | if (err) { 112 | return cb(err); 113 | } 114 | var defRoot = path.join(o.path, 'typings'); 115 | if (!pkgs.length) { 116 | return rimraf(defRoot, cb); 117 | } 118 | pkgs.forEach(function (pkg) { 119 | debug('Found ambient typings provider ' + 120 | path.relative(process.cwd(), pkg._where)); 121 | }); 122 | async.series([ 123 | // create typings/ directory 124 | mkdirp.bind(null, defRoot), 125 | // delete previous symlinks (if any) 126 | rm.bind(null, defRoot, function (st) { return st.isSymbolicLink(); }), 127 | // create typings/* symlinks (disabled by default) 128 | // required in case of "excluded" node_modules/ + WebStorm 11 129 | o.symlink && link.bind(null, pluck(pkgs, '_where'), defRoot), 130 | // update typings/tsd.d.ts 131 | function (cb) { 132 | pkgs.forEach(function (pkg) { 133 | self.emit('wired', {type: 'ambient', pkg: pkg}); 134 | }); 135 | var refs = pkgs.reduce(function (r, pkg) { 136 | var def = [].concat(pkg.typescript.definition || 137 | pkg.typescript.definitions); 138 | return r.concat(def.map(function (def) { 139 | return path.join(o.symlink ? pkg.name 140 | : path.relative(defRoot, pkg._where), def); 141 | })); 142 | }, []); 143 | var override = path.join(o.path, '.tsdm.d.ts'); 144 | fs.stat(override, function (err, stats) { 145 | if (!err && stats.isFile()) { 146 | refs.push(path.relative(defRoot, override)); 147 | self.emit('wired', {type: 'ambient', 148 | pkg: {_where: override}}); 149 | } 150 | commitRefs(path.join(defRoot, 'tsd.d.ts'), refs, cb); 151 | }); 152 | } 153 | ].filter(def), cb); 154 | }); 155 | } 156 | 157 | function rewireScoped(o, cb) { 158 | var self = this; 159 | list(o.path, 160 | function (pkg) { return pkg.typings && pkg.typingsScope; }, 161 | function (err, pkgs) { 162 | if (err) { 163 | return cb(err); 164 | } 165 | pkgs.forEach(function (pkg) { 166 | debug('Found typings provider ' + 167 | path.relative(process.cwd(), pkg._where)); 168 | }); 169 | // resolve external type declarations 170 | async.map(pkgs, function (pkg, cb) { 171 | rslv(pkg.typingsScope, {basedir: pkg._where}, 172 | function (err, location) { 173 | if (err) { 174 | return cb(err); 175 | } 176 | up(location, 'package.json', function (err, dir) { 177 | if (err) { 178 | return cb(err); 179 | } 180 | var base = path.relative(dir, pkg._where); 181 | cb(null, { 182 | source: path.join(pkg._where, 'package.json'), 183 | path: dir, 184 | typings: [].concat(pkg.typings).map(function (t) { 185 | return path.join(base, t); 186 | }) 187 | }); 188 | }); 189 | }); 190 | }, function (err, pp) { 191 | if (err) { 192 | return cb(err); 193 | } 194 | // update packages with external type declarations 195 | async.each(pp, function (p, cb) { 196 | var file = path.join(p.path, 'package.json'); 197 | fs.readFile(file, function (err, data) { 198 | if (err) { 199 | return cb(err); 200 | } 201 | var json; 202 | try { 203 | json = JSON.parse(data); 204 | } catch (e) { 205 | return cb(new Error('Failed to parse ' + file + 206 | ' (not a valid JSON)')); 207 | } 208 | json._tsdm || (json._tsdm = json.typings || true); 209 | if (p.typings.length > 1) { 210 | self.emit('warn', p.source + ' contains multiple typings. ' + 211 | 'Only the first one will be wired in'); 212 | } 213 | json.typings = p.typings[0]; 214 | fs.writeFile(file, JSON.stringify(json, null, 2), 215 | function (err) { 216 | if (err) { 217 | return cb(err); 218 | } 219 | self.emit('wired', {type: 'scoped', file: file}); 220 | cb(); 221 | }); 222 | }); 223 | }, cb); 224 | }); 225 | }); 226 | } 227 | 228 | module.exports = { 229 | rewire: function (o) { 230 | var ee = new EventEmitter(); 231 | process.nextTick(function () { 232 | async.waterfall([ 233 | rc.bind(null, 'tsdm', {cwd: o.path}), 234 | function (rc, cb) { 235 | debug('Loaded RC ' + JSON.stringify(rc)); 236 | ee.emit('rc', rc); 237 | var cfg = assign(rc, o); 238 | async.parallel([ 239 | rewireAmbient.bind(ee, cfg), rewireScoped.bind(ee, cfg) 240 | ], cb); 241 | } 242 | ], function (err) { 243 | err && ee.emit('error', err); 244 | ee.emit('end'); 245 | }); 246 | }); 247 | return ee; 248 | } 249 | }; 250 | --------------------------------------------------------------------------------