diff --git a/.babelrc b/.babelrc deleted file mode 100644 index fb05a4b..0000000 --- a/.babelrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "presets": ["es2015"], - "plugins": ["transform-function-bind", "transform-object-rest-spread"], - "env": { - "test": { - "plugins": [["__coverage__", {"only": "src/"}]] - } - } -} diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 1494d51..0000000 --- a/.eslintrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "airbnb-base", - "parser": "babel-eslint", - "rules": { - "comma-dangle": ["error", "never"], - "global-require": "off", - "indent": ["error", "tab", {"SwitchCase": 1}], - "newline-per-chained-call": "off", - "no-case-declarations": "off", - "no-console": "off", - "no-nested-ternary": "warn", - "no-unused-vars": "warn", - "no-use-before-define": ["error", {"functions": false}], - "object-curly-spacing": ["error", "never"], - "semi": ["error", "never"], - "space-before-function-paren": ["error", {"anonymous": "always", "named": "always"}] - } -} diff --git a/.gitignore b/.gitignore index f6550ce..c9f6725 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,2 @@ -.nyc_output/ -.tmp/ -coverage/ -docs/ -node_modules/ -lib/ -logs/ -*.log +ied +node_modules diff --git a/.travis.yml b/.travis.yml index 31f6343..69d2f6d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,22 +1,7 @@ -language: node_js -sudo: false -node_js: -- '6' -- '5' -- '4' -- '0.12' -- '0.11' -- iojs -script: -- time npm run test:spec -- time npm run test:e2e -- npm run lint -- npm run test:coverage -deploy: - provider: npm - email: alexander.gugel@gmail.com - api_key: - secure: W184bMn/asJVqhR5d/ldn5n9c3XIBeBKFvgJWr0vEJawWqp+9cqwD4UJ4eb94WawjZyQ2y/mjSdZH7hdCqBV9wNeXRBvfwKVkR+ctUq6kMLmjBOWj0nWXBrhQ7X0+Ado/rV+WJpkxuXT9ZFGDe5De4f1XZm8dgnfaVw4pqf4YN6MmY9aLcovtyszbygDY1lqMUjbYAuy4IH6OWBZE30RbQTSqTeBCiwHms/gO2Y8lEagPq/XNsu63BJJzHV1O88zFdzv/mePnZoSYTtmo2CMOsCbr74Mq92YiYjFPy4j/v8L+wQiowqaoWyKNNrQ87B+vMWCFdchB1AW93MIcwVAFaBZ1znI+dbkzCjrUt4y0tMaclNdz++SxC+P6MyuqFnOSYrhEHJYvjWNzQS0dgV4G1dAmAfqTyAZ972YhfX38Z6piBabVmE3HRQF6HyvOt7T7rgY07nupfiBn9VRWOW7AzSjYq5lV1uI/AZZ2qFvaEKxhSbvZYTn4R3+qLyuh+zBcTGCgCTXPnyG9H+9jMgo70KGxBRoRrCZ24e0kwA75IIxilzC5EUoJ9XGVYBIz2A4p7iv19+keZSUkMCxchn7CF76HajJ2jLLqW/8hbDAC5ZtswIJ520kVg/jXccQFDWB9WHpXUc1aX5qm1YY2nGYcjmmKwLT2wrp7kDbRArXTTA= - on: - tags: true - repo: alexanderGugel/ied +language: go + +go: + - 1.x + - 1.6 + - 1.7.x + - master diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..fd707aa --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,956 @@ +2.3.6 / 2016-11-08 +=================== + + * Merge branch 'distinct-key' + * Use distinctKey again + +v2.3.5 / 2016-11-07 +=================== + + * 2.3.5 + * Remove ::distinctKey usage + * Update History +2.3.5 / 2016-11-07 +=================== + + * Remove ::distinctKey usage + +v2.3.4 / 2016-11-07 +=================== + + * Use fixed version of RxJS + +v2.3.3 / 2016-09-26 +=================== + + * Merge pull request #173 from alindeman/storage-dir + * config.storageDir is no longer present + +v2.3.3 / 2016-09-26 +=================== + + * config.storageDir is no longer present + +v2.3.2 / 2016-08-14 +=================== + + * Fix broken install for package using caret version + * Fix linting issues + * Add proxy guide + +2.3.1 / 2016-07-29 +================== + +This release reverts the `.cas` change, which broke the resolve function used +for checking if a dependency was already installed. + + * Revert "Add configurable storage directory that defaults to `node_modules/.cas` (fixes #96)" + +v2.3.0 / 2016-07-27 +=================== + +This release includes support for Git dependencies and a `--prod` flag used for +excluding `devDependencies`. + + * Merge pull request #157 from alexanderGugel/prod-flag + * Add --production flag + * Remove save-exact + * Merge pull request #155 from mgcrea/feat-resolve-git-remote + * Add support for git public/private remotes + +v2.2.0 / 2016-07-26 +================== + + * Release using CI + * Merge pull request #156 from alexanderGugel/default-shell + * Print available commands in `ied sh` + * Fix .babelrc formatting + * Default shell to $SHELL + +v2.1.1 / 2016-07-23 +================== + +Simplify testing on Windows. + + * Merge pull request #154 from ArjanSchouten/master + * Added cross-env so tests can be runned on Windows + * Add AUTHORS + +v2.1.0 / 2016-07-08 +================== + +This version includes some major improvements to ied, including support for +additional installation targets and major tooling changes. + +Linting rules, as well as tests have been extended. + +Special thanks to @mgcrea for adding an installation mechanism for GitHub and +tarball dependencies. + + * Merge pull request #148 from mgcrea/patch-npm-update + * Merge pull request #144 from mgcrea/feat-custom-storage + * Update dependencies + * Merge pull request #146 from mgcrea/fix-git-pkg-spec + * Refactor hosted resolve to support multiple providers, add bitbucket support + * Add configurable storage directory that defaults to `node_modules/.cas` (fixes #96) + * Merge pull request #143 from mgcrea/fix-tests + * Remove .only call to properly test all cases + * Merge pull request #142 from mgcrea/fix-github-targets + * Fix and properly test both `file:` and `github:` targets + * Test link#getSymlinks + * Merge pull request #138 from mgcrea/refactor-tests + * Improve coverage with babel-plugin-__coverage__ + * Use ./lib/cmd.js instead of npm in Makefile + * Revert for-of to forEach switch + * Rename test/specs to test/spec + * Split specs and e2e tests, add code coverage via nyc + * Refactor eslint + * Refactor unit tests + * Merge pull request #137 from mgcrea/chore-prune-npm + * Prune and update dependencies + * Merge pull request #133 from mgcrea/wip-file-github + * Include Inch CI docs badge + * Properly check semver ranges on explicit local installs (fixes #130) + * Add support for exotic package types (file, hosted, and tarball) + +v2.0.5 / 2016-06-21 +=================== + + * 2.0.5 + * Reinit History + * Merge pull request #134 from mgcrea/patch-build-node-dtrace-provider + * Merge pull request #135 from mgcrea/patch-debuglog + * Merge pull request #136 from mgcrea/chore-travis + * Test against node@6, only test latest minor + * Add support for NODE_DEBUG=* to debug everything + * Build packages in a serialized fashion to prevent race conditions + * Fix build for specific npm-related edge cases + * Merge pull request #131 from mgcrea/patch-source-map + * Merge pull request #132 from mgcrea/fix-debuglog + * Fix debuglog arguments handling + * Add inline source maps to compiled lib + * Merge pull request #128 from SiegfriedEhret/patch-1 + * Update README.md + +v2.0.4 / 2016-05-29 +=================== + + * 2.0.4 + * Use RxJS for version and help command + * Merge branch 'run-cmd' + * Fix config tests + * Rewrite run command using RxJS + * Use RxJS for unlink command + * Use RxJS for link command + * Decouple registry from config + * Make test a .PHONY task + * Clean up Makefile + * Fix registry tests + * Add .github PR and issue templates + * Use assert.AssertionError instead of custom errors + * Refactor debuglog + * Add ASCII logo to README + * Add lint to Makefile; Put docs into ./docs + * Clean up .gitignore + +v2.0.3 / 2016-05-27 +=================== + + * 2.0.3 + * Remove TODO + * Merge pull request #126 from alexanderGugel/fix-retry + * Fix retry config var + +v2.0.2 / 2016-05-27 +=================== + + * 2.0.2 + * Add missing dev dependency for mocha tests + +v2.0.1 / 2016-05-27 +=================== + + * 2.0.1 + * Remove spaces before commands + +v2.0.0 / 2016-05-27 +=================== + + * 2.0.0 + * Add CHANGELOG.md + * Build node_modules before lib + * Clean up dependencies + * Remove duplicate test from USAGE + * Merge branch 'next' + * Add .files to package.json + * Add prepublish script + * Fix dead link lib -> src + * Fix usage doc + * Remove FAQ + * Remove standard style + * Fix Makefile + * Define argv types + * Fetch registry version document + * Modularize resolve and install + * Remove ls command + * Add debuglog + * Rewrite + +v1.1.1 / 2016-04-06 +=================== + + * 1.1.1 + * Merge pull request #103 from just-boris/patch-1 + * change script phase + +v1.1.0 / 2016-03-29 +=================== + + * 1.1.0 + * Merge pull request #95 from JoshSchreuder/fix-windows-support + * Merge pull request #87 from just-boris/master + * Merge pull request #94 from Arnavion/patch-1 + * Use junction as symlink type on Windows + * expose 'run' module + * setup Travis to build native packages for Node.js 4 and later + * add tests on install logic + * support native packages + * invoke lifecycle scripts on install + * Fix travis badge in README to show status of master branch + +v1.0.6 / 2016-03-08 +=================== + + * 1.0.6 + * Merge pull request #90 from just-boris/fix-cache-init + * do not pass rest of arguments from cache.init() + * expose: Remove expose command + * Refactor and cleanup installCmd + * Handle installCmd error in bin/cmd + * Move cache init logic to cache + * Remove unneeded mkdirp before installCmd + * Refactor install_cmd + * link: forceSymlink for local installs + * Reformat link + +v1.0.5 / 2016-02-22 +=================== + + * 1.0.5 + * Merge branch 'refactor' + * Remove obsolete async.series + * Remove unused ignore_error.js helper + +v1.0.4 / 2016-02-17 +=================== + + * 1.0.4 + * Minor refactor of install utils + +v1.0.3 / 2016-02-17 +=================== + + * 1.0.3 + * Merge pull request #86 from alexanderGugel/fix-tty + * Correctly handle case where stderr is not a TTY + * Refactor resolve to switch + * Fix typo + +v1.0.2 / 2016-02-10 +=================== + + * 1.0.2 + * Merge pull request #84 from alexanderGugel/dev + * Extract force_symlink out into force-symlink package + +v1.0.1 / 2016-02-10 +=================== + + * 1.0.1 + * Merge pull request #83 from alexanderGugel/dev + * Add unit tests for config + * Add missing 'use strict's + * Refactor and test cache + +v1.0.0 / 2016-02-01 +=================== + + * 1.0.0 + * Merge pull request #81 from just-boris/master + * resolve dependency version as tag closes #74 + +v0.4.11 / 2016-01-30 +==================== + + * 0.4.11 + * use resolved package version as uid + +v0.4.10 / 2016-01-30 +==================== + + * 0.4.10 + * Merge pull request #66 from rstacruz/sepia + * Ignore fixtures + * Perform rm -rf of test artifacts before npm test + * Use sepia + +v0.4.9 / 2016-01-29 +=================== + + * 0.4.9 + * chmod scripts + +v0.4.8 / 2016-01-29 +=================== + + * 0.4.8 + * Remove WIP from README + +v0.4.7 / 2016-01-28 +=================== + + * 0.4.7 + * Make requestRetries configurable via IED_REQUEST_RETRIES env var + +v0.4.6 / 2016-01-28 +=================== + + * 0.4.6 + * Use got for ping command + +v0.4.5 / 2016-01-27 +=================== + + * 0.4.5 + * Refactor env in run command + +v0.4.4 / 2016-01-27 +=================== + + * 0.4.4 + * Merge pull request #77 from dickeyxxx/url-resolve + * resolve registry urls + * fix https registries with ping + +v0.4.3 / 2016-01-25 +=================== + + * 0.4.3 + * Merge pull request #64 from rstacruz/bash-only + * Always use sh + * Merge pull request #71 from rstacruz/patch-1 + * Update TODO.md + * Always use bash (#62) + +v0.4.2 / 2016-01-25 +=================== + + * 0.4.2 + * Merge pull request #73 from just-boris/master + * gather bin information from package.json + +v0.4.1 / 2016-01-21 +=================== + + * 0.4.1 + * Merge pull request #60 from just-boris/patch-1 + * Skip root folder for cached tarballs as well as in fetch + +v0.4.0 / 2016-01-18 +=================== + + * 0.4.0 + * Merge pull request #58 from mrmlnc/fix-init-error-handler + * ooops, fix output error for init + * Merge pull request #57 from mrmlnc/init-error-handler + * User-friendly error output when exiting from the initialization process + * Merge pull request #56 from mrmlnc/fix-54 + * the correct path to the home directory in Windows (fix #54) + * Merge pull request #53 from just-boris/tarballs + * Merge pull request #50 from just-boris/master + * install gulp v4 as example of tarball package + * use got instead of raw http + * strip root folder in tarballs + * resolve and download tarballs + * do not catch errors during callback dispatching + +v0.3.6 / 2015-12-10 +=================== + + * 0.3.6 + * Cleanup README and TODO + +v0.3.5 / 2015-12-03 +=================== + + * 0.3.5 + * Default to HTTPS registry + +v0.3.4 / 2015-11-27 +=================== + + * 0.3.4 + * Fix Makefile to link into $HOME/.node_modules + * Revert "Remove Makefile" + * No longer hardcode scoped modules in expose() + +v0.3.3 / 2015-11-22 +=================== + + * 0.3.3 + * Fix install sub-command for scoped packages + * Test semver of installed package + * Remove Makefile + +v0.3.2 / 2015-11-21 +=================== + + * 0.3.2 + * Merge branch 'node_v0' + * Use custom debuglog + * Resolve linting issues + +v0.3.1 / 2015-11-21 +=================== + + * 0.3.1 + * Fix resolve tests + +v0.3.0 / 2015-11-21 +=================== + + * 0.3.0 + * Add support for running behind a proxy + +v0.2.5 / 2015-11-20 +=================== + + * 0.2.5 + * Remove broken badge + +v0.2.4 / 2015-11-20 +=================== + + * 0.2.4 + * Use correct npm badge in README + +v0.2.3 / 2015-11-20 +=================== + + * 0.2.3 + * Add CODE_OF_CONDUCT, CONTRIBUTING; Improve README + * Merge pull request #31 from nisaacson/adds-https-support + * Pull request feedback + * Adds tests for https protocol + * Fixes protocol string check + * Fixes naming conflict + * Adds https support and reworks linter/test scripts + +v0.2.2 / 2015-11-19 +=================== + + * 0.2.2 + * Fix run sub-command + * Cleanup and refactor install + * Add integration test for install + +v0.2.1 / 2015-11-18 +=================== + + * 0.2.1 + * Add support for scoped modules + +v0.1.1 / 2015-11-17 +=================== + + * 0.1.1 + * Refactor linking functionality + * Add Gitter badge + * Throw LOCKED error during redundant install + +v0.1.0 / 2015-11-16 +=================== + + * 0.1.0 + * Add link and unlink sub-command + * Make USAGE.txt and README consistent + * Merge pull request #9 from chaconnewu/master + * Add one more example usage in README.md + +v0.0.5 / 2015-11-16 +=================== + + * 0.0.5 + * Add linter + +v0.0.4 / 2015-11-16 +=================== + + * 0.0.4 + +v0.0.3 / 2015-11-16 +=================== + + * 0.0.3 + * Use node_modules/.tmp instead of $TMPDIR + * Make statusCode error messages more useful + +v0.0.2 / 2015-11-15 +=================== + + * 0.0.2 + * Merge pull request #3 from twhid/patch-1 + * Fixed the typo + +v0.0.1 / 2015-11-15 +=================== + + * 0.0.1 + * Fix bug where fetch never called cb + * Rename to ied + * Add init sub-command + * Add expose sub-command + * Correctly call cb after closed cache write stream + * Add caching layer to `fetch` step + * Add config sub-command + * Update USAGE docs + * Refactor composed install function + +atomic / 2015-11-10 +=================== + + * Make installation of packages atomic + * Add --registry flag + * Extract out sub-commands + * Add ping sub-command + * Add global config file + * Add run command (experimental WIP) + * Ignore example dir + * Properly require packages from within themselves + * Make callback an optional param + * Add sh command (experimental) + * Fix flickering progress bar bug + * Add --only flag + * Add --save, --save-dev flag + * Fix build process (now requires sub-commands) + * Use content-length for progress bar + * Remove unsupported Node versions from CI + * Place sub-dependencies higher up in node_modules + * Add basic progress bar + * Split out install into generic install and expose + * Refactor resolve and download + * Fix Makefile no work on consecutive installs + * Improve build process + * Use minimist + * Update node_modules.tar + * Cleanup package.json + * Install devDependencies of package.json + * Add CI + * Merge branch 'tests' + * Add more tests + +test / 2015-10-24 +================= + + + +v2.0.5 / 2016-06-21 +================== + + * Merge pull request #134 from mgcrea/patch-build-node-dtrace-provider + * Merge pull request #135 from mgcrea/patch-debuglog + * Merge pull request #136 from mgcrea/chore-travis + * Test against node@6, only test latest minor + * Add support for NODE_DEBUG=* to debug everything + * Build packages in a serialized fashion to prevent race conditions + * Fix build for specific npm-related edge cases + * Merge pull request #131 from mgcrea/patch-source-map + * Merge pull request #132 from mgcrea/fix-debuglog + * Fix debuglog arguments handling + * Add inline source maps to compiled lib + * Merge pull request #128 from SiegfriedEhret/patch-1 + * Update README.md + +v2.0.4 / 2016-05-29 +=================== + + * 2.0.4 + * Use RxJS for version and help command + * Merge branch 'run-cmd' + * Fix config tests + * Rewrite run command using RxJS + * Use RxJS for unlink command + * Use RxJS for link command + * Decouple registry from config + * Make test a .PHONY task + * Clean up Makefile + * Fix registry tests + * Add .github PR and issue templates + * Use assert.AssertionError instead of custom errors + * Refactor debuglog + * Add ASCII logo to README + * Add lint to Makefile; Put docs into ./docs + * Clean up .gitignore + +v2.0.3 / 2016-05-27 +=================== + + * 2.0.3 + * Remove TODO + * Merge pull request #126 from alexanderGugel/fix-retry + * Fix retry config var + +v2.0.2 / 2016-05-27 +=================== + + * 2.0.2 + * Add missing dev dependency for mocha tests + +v2.0.1 / 2016-05-27 +=================== + + * 2.0.1 + * Remove spaces before commands + +v2.0.0 / 2016-05-27 +=================== + + * 2.0.0 + * Add CHANGELOG.md + * Build node_modules before lib + * Clean up dependencies + * Remove duplicate test from USAGE + * Merge branch 'next' + * Add .files to package.json + * Add prepublish script + * Fix dead link lib -> src + * Fix usage doc + * Remove FAQ + * Remove standard style + * Fix Makefile + * Define argv types + * Fetch registry version document + * Modularize resolve and install + * Remove ls command + * Add debuglog + * Rewrite + +v1.1.1 / 2016-04-06 +=================== + + * 1.1.1 + * Merge pull request #103 from just-boris/patch-1 + * change script phase + +v1.1.0 / 2016-03-29 +=================== + + * 1.1.0 + * Merge pull request #95 from JoshSchreuder/fix-windows-support + * Merge pull request #87 from just-boris/master + * Merge pull request #94 from Arnavion/patch-1 + * Use junction as symlink type on Windows + * expose 'run' module + * setup Travis to build native packages for Node.js 4 and later + * add tests on install logic + * support native packages + * invoke lifecycle scripts on install + * Fix travis badge in README to show status of master branch + +v1.0.6 / 2016-03-08 +=================== + + * 1.0.6 + * Merge pull request #90 from just-boris/fix-cache-init + * do not pass rest of arguments from cache.init() + * expose: Remove expose command + * Refactor and cleanup installCmd + * Handle installCmd error in bin/cmd + * Move cache init logic to cache + * Remove unneeded mkdirp before installCmd + * Refactor install_cmd + * link: forceSymlink for local installs + * Reformat link + +v1.0.5 / 2016-02-22 +=================== + + * 1.0.5 + * Merge branch 'refactor' + * Remove obsolete async.series + * Remove unused ignore_error.js helper + +v1.0.4 / 2016-02-17 +=================== + + * 1.0.4 + * Minor refactor of install utils + +v1.0.3 / 2016-02-17 +=================== + + * 1.0.3 + * Merge pull request #86 from alexanderGugel/fix-tty + * Correctly handle case where stderr is not a TTY + * Refactor resolve to switch + * Fix typo + +v1.0.2 / 2016-02-10 +=================== + + * 1.0.2 + * Merge pull request #84 from alexanderGugel/dev + * Extract force_symlink out into force-symlink package + +v1.0.1 / 2016-02-10 +=================== + + * 1.0.1 + * Merge pull request #83 from alexanderGugel/dev + * Add unit tests for config + * Add missing 'use strict's + * Refactor and test cache + +v1.0.0 / 2016-02-01 +=================== + + * 1.0.0 + * Merge pull request #81 from just-boris/master + * resolve dependency version as tag closes #74 + +v0.4.11 / 2016-01-30 +==================== + + * 0.4.11 + * use resolved package version as uid + +v0.4.10 / 2016-01-30 +==================== + + * 0.4.10 + * Merge pull request #66 from rstacruz/sepia + * Ignore fixtures + * Perform rm -rf of test artifacts before npm test + * Use sepia + +v0.4.9 / 2016-01-29 +=================== + + * 0.4.9 + * chmod scripts + +v0.4.8 / 2016-01-29 +=================== + + * 0.4.8 + * Remove WIP from README + +v0.4.7 / 2016-01-28 +=================== + + * 0.4.7 + * Make requestRetries configurable via IED_REQUEST_RETRIES env var + +v0.4.6 / 2016-01-28 +=================== + + * 0.4.6 + * Use got for ping command + +v0.4.5 / 2016-01-27 +=================== + + * 0.4.5 + * Refactor env in run command + +v0.4.4 / 2016-01-27 +=================== + + * 0.4.4 + * Merge pull request #77 from dickeyxxx/url-resolve + * resolve registry urls + * fix https registries with ping + +v0.4.3 / 2016-01-25 +=================== + + * 0.4.3 + * Merge pull request #64 from rstacruz/bash-only + * Always use sh + * Merge pull request #71 from rstacruz/patch-1 + * Update TODO.md + * Always use bash (#62) + +v0.4.2 / 2016-01-25 +=================== + + * 0.4.2 + * Merge pull request #73 from just-boris/master + * gather bin information from package.json + +v0.4.1 / 2016-01-21 +=================== + + * 0.4.1 + * Merge pull request #60 from just-boris/patch-1 + * Skip root folder for cached tarballs as well as in fetch + +v0.4.0 / 2016-01-18 +=================== + + * 0.4.0 + * Merge pull request #58 from mrmlnc/fix-init-error-handler + * ooops, fix output error for init + * Merge pull request #57 from mrmlnc/init-error-handler + * User-friendly error output when exiting from the initialization process + * Merge pull request #56 from mrmlnc/fix-54 + * the correct path to the home directory in Windows (fix #54) + * Merge pull request #53 from just-boris/tarballs + * Merge pull request #50 from just-boris/master + * install gulp v4 as example of tarball package + * use got instead of raw http + * strip root folder in tarballs + * resolve and download tarballs + * do not catch errors during callback dispatching + +v0.3.6 / 2015-12-10 +=================== + + * 0.3.6 + * Cleanup README and TODO + +v0.3.5 / 2015-12-03 +=================== + + * 0.3.5 + * Default to HTTPS registry + +v0.3.4 / 2015-11-27 +=================== + + * 0.3.4 + * Fix Makefile to link into $HOME/.node_modules + * Revert "Remove Makefile" + * No longer hardcode scoped modules in expose() + +v0.3.3 / 2015-11-22 +=================== + + * 0.3.3 + * Fix install sub-command for scoped packages + * Test semver of installed package + * Remove Makefile + +v0.3.2 / 2015-11-21 +=================== + + * 0.3.2 + * Merge branch 'node_v0' + * Use custom debuglog + * Resolve linting issues + +v0.3.1 / 2015-11-21 +=================== + + * 0.3.1 + * Fix resolve tests + +v0.3.0 / 2015-11-21 +=================== + + * 0.3.0 + * Add support for running behind a proxy + +v0.2.5 / 2015-11-20 +=================== + + * 0.2.5 + * Remove broken badge + +v0.2.4 / 2015-11-20 +=================== + + * 0.2.4 + * Use correct npm badge in README + +v0.2.3 / 2015-11-20 +=================== + + * 0.2.3 + * Add CODE_OF_CONDUCT, CONTRIBUTING; Improve README + * Merge pull request #31 from nisaacson/adds-https-support + * Pull request feedback + * Adds tests for https protocol + * Fixes protocol string check + * Fixes naming conflict + * Adds https support and reworks linter/test scripts + +v0.2.2 / 2015-11-19 +=================== + + * 0.2.2 + * Fix run sub-command + * Cleanup and refactor install + * Add integration test for install + +v0.2.1 / 2015-11-18 +=================== + + * 0.2.1 + * Add support for scoped modules + +v0.1.1 / 2015-11-17 +=================== + + * 0.1.1 + * Refactor linking functionality + * Add Gitter badge + * Throw LOCKED error during redundant install + +v0.1.0 / 2015-11-16 +=================== + + * 0.1.0 + * Add link and unlink sub-command + * Make USAGE.txt and README consistent + * Merge pull request #9 from chaconnewu/master + * Add one more example usage in README.md + +v0.0.5 / 2015-11-16 +=================== + + * 0.0.5 + * Add linter + +v0.0.4 / 2015-11-16 +=================== + + * 0.0.4 + +v0.0.3 / 2015-11-16 +=================== + + * 0.0.3 + * Use node_modules/.tmp instead of $TMPDIR + * Make statusCode error messages more useful + +v0.0.2 / 2015-11-15 +=================== + + * 0.0.2 + * Merge pull request #3 from twhid/patch-1 + * Fixed the typo + +v0.0.1 / 2015-11-15 +=================== + + * 0.0.1 + * Fix bug where fetch never called cb + * Rename to ied + * Add init sub-command + * Add expose sub-command + * Correctly call cb after closed cache write stream + * Add caching layer to `fetch` step + * Add config sub-command + * Update USAGE docs + * Refactor composed install function diff --git a/Makefile b/Makefile index 71db06c..35cfa10 100644 --- a/Makefile +++ b/Makefile @@ -1,58 +1,15 @@ -# TODO Actually we could just use `npm link`, but we don't really want to rely -# on npm. Ideally we would even check-in the dependencies, but then people -# wouldn't take us seriously. +.PHONY: build test vet fmt -CURRENT_DIR = $(shell pwd) -INSTALL_DIR = $(HOME)/.node_modules -BIN_DIR = /usr/local/bin -BIN = ied -DEPS_BIN_DIR = ./node_modules/.bin -SRC = $(wildcard src/*.js) -LIB = $(SRC:src/%.js=lib/%.js) +default: build -.PHONY: link install uninstall clean lint dev watch test - -# http://blog.jgc.org/2015/04/the-one-line-you-should-add-to-every.html -print-%: ; @echo $*=$($*) - -lib: $(LIB) node_modules -lib/%.js: src/%.js - mkdir -p $(@D) - $(DEPS_BIN_DIR)/babel $< -o $@ - -node_modules: package.json - npm install - -install_dirs: - mkdir -p $(INSTALL_DIR) - mkdir -p $(BIN_DIR) - -link: install_dirs - ln -s $(CURRENT_DIR) $(INSTALL_DIR)/$(BIN) - ln -s $(INSTALL_DIR)/ied/lib/cmd.js $(BIN_DIR)/ied - -install: node_modules install_dirs - cp -R $(CURRENT_DIR) $(INSTALL_DIR)/$(BIN) - chmod +x $(INSTALL_DIR)/$(BIN) - ln -s $(INSTALL_DIR)/ied/lib/cmd.js $(BIN_DIR)/ied - -uninstall: - rm -rf $(INSTALL_DIR)/$(BIN) $(BIN_DIR)/$(BIN) - -clean: - rm -rf lib test/test - -docs: src - $(DEPS_BIN_DIR)/esdoc -c esdoc.json +build: + @go build -v test: - ./lib/cmd.js test - -lint: - ./lib/cmd.js run lint + @go test -v -dev: - ./lib/cmd.js start +vet: + @go vet -v -watch: - ./lib/cmd.js compile:watch +fmt: + @gofmt -w *.go diff --git a/USAGE.txt b/USAGE.txt deleted file mode 100644 index 4c9fd1f..0000000 --- a/USAGE.txt +++ /dev/null @@ -1,45 +0,0 @@ - ied is a package manager for Node. - - Usage: - - ied [command] [arguments] - - The commands are: - - install fetch packages and dependencies - run run a package.json script - shell enter a sub-shell with augmented PATH - ping check if the registry is up - config print the used config - init initialize a new package - link link the current package or into it - unlink unlink the current package or from it - start runs `ied run start` - stop runs `ied run stop` - build runs `ied run build` - test runs `ied run test` - - Flags: - -h, --help show usage information - -v, --version print the current version - -S, --save update package.json dependencies - -D, --save-dev update package.json devDependencies - -O, --save-optional update package.json optionalDependencies - -r, --registry use a custom registry - (default: http://registry.npmjs.org/) - -b, --build execute lifecycle scripts upon completion - (e.g. postinstall) - -prod, --production only install dependencies and optionalDependencies - - Example: - ied install - ied install - ied install @ - ied install @ - - Can specify one or more: ied install semver@^5.0.1 tape - If no argument is supplied, installs dependencies from package.json. - Sub-commands can also be called via their shorthand aliases. - - README: https://github.com/alexanderGugel/ied - ISSUES: https://github.com/alexanderGugel/ied/issues \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..d384580 --- /dev/null +++ b/config.go @@ -0,0 +1,73 @@ +package main + +import ( + "errors" + "github.com/Sirupsen/logrus" + "gopkg.in/yaml.v2" + "io/ioutil" + "net/url" + "os/user" + "path/filepath" +) + +// Config encapsulates global configuration options. +type Config struct { + Registry string `yaml:"Registry"` + LogLevel string `yaml:"Log Level"` + LogFormat string `yaml:"Log Format"` +} + +// NewDefaultConfig creates a new configuration, populated with sensible +// defaults. +func NewDefaultConfig() *Config { + return &Config{ + Registry: "https://registry.npmjs.com", + LogLevel: "info", + LogFormat: "json", + } +} + +// configFilename returns the path from which the default ied config should be +// read. By default this is the .ied.yaml file in the user's home directory. +func configFilename() (string, error) { + usr, err := user.Current() + if err != nil { + return "", err + } + return filepath.Join(usr.HomeDir, ".ied.yaml"), nil +} + +// LoadConfig reads the user's config from the provided filename. This function +// always returns a config. User-defined configuration records override +// predefined config defaults. +func LoadConfig(filename string) (*Config, error) { + config := NewDefaultConfig() + raw, err := ioutil.ReadFile(filename) + if err != nil { + return config, err + } + + if err := yaml.Unmarshal(raw, config); err != nil { + return config, err + } + return config, nil +} + +// Validate checks it the provided config is valid. It ensures that the provided +// registry URL is syntactically valid and the log level exists. +func (c *Config) Validate() error { + if c.Registry == "" { + return errors.New("missing registry") + } + if _, err := url.Parse(c.Registry); err != nil { + return errors.New("invalid registry url") + } + if _, err := logrus.ParseLevel(c.LogLevel); err != nil { + return errors.New("invalid log level") + } + if c.LogFormat != "json" && + c.LogFormat != "text" { + return errors.New("invalid log format") + } + return nil +} diff --git a/config_cmd.go b/config_cmd.go new file mode 100644 index 0000000..dea4eac --- /dev/null +++ b/config_cmd.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/Sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +func configCmd(c *Config) { + str, err := yaml.Marshal(c) + if err != nil { + logrus.Fatalf("failed to serialize cofnig: %v", err) + } + logrus.Println(string(str)) +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..add050c --- /dev/null +++ b/config_test.go @@ -0,0 +1,87 @@ +package main + +import ( + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestNewDefaultConfig(t *testing.T) { + config := NewDefaultConfig() + assert.Equal(t, config.Registry, "https://registry.npmjs.com") + assert.Equal(t, config.LogLevel, "info") + assert.Equal(t, config.LogFormat, "json") +} + +func TestLoadConfig(t *testing.T) { + dir, err := ioutil.TempDir("", "TestLoadConfig") + assert.NoError(t, err) + + defer os.RemoveAll(dir) + + filename := filepath.Join(dir, "config.yaml") + raw := []byte(`Registry: https://registry.npmjs.com +Log Level: warn`) + err = ioutil.WriteFile(filename, raw, os.ModePerm) + assert.NoError(t, err) + + config, err := LoadConfig(filename) + assert.NoError(t, err) + assert.Equal(t, *config, Config{ + Registry: "https://registry.npmjs.com", + LogLevel: "warn", + LogFormat: "json", + }) +} + +func TestConfigValidateValid(t *testing.T) { + configs := []Config{ + Config{ + Registry: "https://registry.npmjs.com", + LogLevel: "warn", + LogFormat: "json", + }, + Config{ + Registry: "https://registry.npmjs.com", + LogLevel: "warn", + LogFormat: "text", + }, + Config{ + Registry: "https://registry.npmjs.com", + LogLevel: "info", + LogFormat: "json", + }, + } + + for _, config := range configs { + err := config.Validate() + assert.NoError(t, err, "expected %v to be valid", config) + } +} + +func TestConfigValidateInvalid(t *testing.T) { + configs := []Config{ + Config{ + Registry: "", + LogLevel: "warn", + LogFormat: "json", + }, + Config{ + Registry: "https://registry.npmjs.com", + LogLevel: "invalid", + LogFormat: "json", + }, + Config{ + Registry: "https://registry.npmjs.com", + LogLevel: "warn", + LogFormat: "invalid", + }, + } + + for _, config := range configs { + err := config.Validate() + assert.Error(t, err, "expected %v to be invalid", config) + } +} diff --git a/e2e_test.go b/e2e_test.go new file mode 100644 index 0000000..90bfc1b --- /dev/null +++ b/e2e_test.go @@ -0,0 +1,134 @@ +package main + +import ( + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" + "os/exec" + "testing" +) + +func testPackage(name string, t *testing.T) { + dir, err := ioutil.TempDir("", "TestE2E-"+name) + assert.NoError(t, err) + defer os.RemoveAll(dir) + + config := NewDefaultConfig() + resolver := initResolver(config) + store := initStore(dir, resolver) + + err = store.Install(nil, name, "*") + assert.NoError(t, err) + + path, err := exec.LookPath("node") + assert.NoError(t, err) + + cmd := exec.Command(path, "-e", `console.log(require("`+name+`"))`) + cmd.Dir = dir + + out, err := cmd.CombinedOutput() + assert.NoError(t, err, string(out)) +} + +// Each package installation should be reported individually. + +func TestInstallLodash(t *testing.T) { testPackage("lodash", t) } +func TestInstallRequest(t *testing.T) { testPackage("request", t) } +func TestInstallAsync(t *testing.T) { testPackage("async", t) } +func TestInstallUnderscore(t *testing.T) { testPackage("underscore", t) } +func TestInstallExpress(t *testing.T) { testPackage("express", t) } +func TestInstallBluebird(t *testing.T) { testPackage("bluebird", t) } +func TestInstallChalk(t *testing.T) { testPackage("chalk", t) } +func TestInstallCommander(t *testing.T) { testPackage("commander", t) } +func TestInstallDebug(t *testing.T) { testPackage("debug", t) } +func TestInstallMoment(t *testing.T) { testPackage("moment", t) } +func TestInstallMkdirp(t *testing.T) { testPackage("mkdirp", t) } +func TestInstallReact(t *testing.T) { testPackage("react", t) } +func TestInstallColors(t *testing.T) { testPackage("colors", t) } +func TestInstallQ(t *testing.T) { testPackage("q", t) } +func TestInstallThrough2(t *testing.T) { testPackage("through2", t) } +func TestInstallYeomanGenerator(t *testing.T) { testPackage("yeoman-generator", t) } +func TestInstallGlob(t *testing.T) { testPackage("glob", t) } +func TestInstallMinimist(t *testing.T) { testPackage("minimist", t) } +func TestInstallGulpUtil(t *testing.T) { testPackage("gulp-util", t) } +func TestInstallBodyParser(t *testing.T) { testPackage("body-parser", t) } +func TestInstallFsExtra(t *testing.T) { testPackage("fs-extra", t) } +func TestInstallCoffeeScript(t *testing.T) { testPackage("coffee-script", t) } +func TestInstallJquery(t *testing.T) { testPackage("jquery", t) } +func TestInstallReactDom(t *testing.T) { testPackage("react-dom", t) } +func TestInstallCheerio(t *testing.T) { testPackage("cheerio", t) } +func TestInstallBabelRuntime(t *testing.T) { testPackage("babel-runtime", t) } +func TestInstallYargs(t *testing.T) { testPackage("yargs", t) } +func TestInstallNodeUuid(t *testing.T) { testPackage("node-uuid", t) } +func TestInstallGulp(t *testing.T) { testPackage("gulp", t) } +func TestInstallOptimist(t *testing.T) { testPackage("optimist", t) } +func TestInstallWinston(t *testing.T) { testPackage("winston", t) } +func TestInstallClassnames(t *testing.T) { testPackage("classnames", t) } +func TestInstallYosay(t *testing.T) { testPackage("yosay", t) } +func TestInstallObjectAssign(t *testing.T) { testPackage("object-assign", t) } +func TestInstallSemver(t *testing.T) { testPackage("semver", t) } +func TestInstallSocketIo(t *testing.T) { testPackage("socket.io", t) } +func TestInstallRimraf(t *testing.T) { testPackage("rimraf", t) } +func TestInstallRedis(t *testing.T) { testPackage("redis", t) } +func TestInstallEmberCliBabel(t *testing.T) { testPackage("ember-cli-babel", t) } +func TestInstallBabelPresetEs2015(t *testing.T) { testPackage("babel-preset-es2015", t) } +func TestInstallSuperagent(t *testing.T) { testPackage("superagent", t) } +func TestInstallBabelCore(t *testing.T) { testPackage("babel-core", t) } +func TestInstallHandlebars(t *testing.T) { testPackage("handlebars", t) } +func TestInstallMongoose(t *testing.T) { testPackage("mongoose", t) } +func TestInstallMongodb(t *testing.T) { testPackage("mongodb", t) } +func TestInstallAwsSdk(t *testing.T) { testPackage("aws-sdk", t) } +func TestInstallMocha(t *testing.T) { testPackage("mocha", t) } +func TestInstallInquirer(t *testing.T) { testPackage("inquirer", t) } +func TestInstallCo(t *testing.T) { testPackage("co", t) } +func TestInstallJade(t *testing.T) { testPackage("jade", t) } +func TestInstallShelljs(t *testing.T) { testPackage("shelljs", t) } +func TestInstallExtend(t *testing.T) { testPackage("extend", t) } +func TestInstallUuid(t *testing.T) { testPackage("uuid", t) } +func TestInstallXml2js(t *testing.T) { testPackage("xml2js", t) } +func TestInstallJsYaml(t *testing.T) { testPackage("js-yaml", t) } +func TestInstallEjs(t *testing.T) { testPackage("ejs", t) } +func TestInstallUglifyJs(t *testing.T) { testPackage("uglify-js", t) } +func TestInstallMime(t *testing.T) { testPackage("mime", t) } +func TestInstallChai(t *testing.T) { testPackage("chai", t) } +func TestInstallWebpack(t *testing.T) { testPackage("webpack", t) } +func TestInstallUnderscoreString(t *testing.T) { testPackage("underscore.string", t) } +func TestInstallMorgan(t *testing.T) { testPackage("morgan", t) } +func TestInstallJoi(t *testing.T) { testPackage("joi", t) } +func TestInstallMarked(t *testing.T) { testPackage("marked", t) } +func TestInstallCookieParser(t *testing.T) { testPackage("cookie-parser", t) } +func TestInstallBrowserify(t *testing.T) { testPackage("browserify", t) } +func TestInstallXtend(t *testing.T) { testPackage("xtend", t) } +func TestInstallEs6Promise(t *testing.T) { testPackage("es6-promise", t) } +func TestInstallGrunt(t *testing.T) { testPackage("grunt", t) } +func TestInstallBabelPolyfill(t *testing.T) { testPackage("babel-polyfill", t) } +func TestInstallPromise(t *testing.T) { testPackage("promise", t) } +func TestInstallMysql(t *testing.T) { testPackage("mysql", t) } +func TestInstallWs(t *testing.T) { testPackage("ws", t) } +func TestInstallRedux(t *testing.T) { testPackage("redux", t) } +func TestInstallThrough(t *testing.T) { testPackage("through", t) } +func TestInstallPath(t *testing.T) { testPackage("path", t) } +func TestInstallImmutable(t *testing.T) { testPackage("immutable", t) } +func TestInstallRamda(t *testing.T) { testPackage("ramda", t) } +func TestInstallNan(t *testing.T) { testPackage("nan", t) } +func TestInstallRequestPromise(t *testing.T) { testPackage("request-promise", t) } +func TestInstallPrompt(t *testing.T) { testPackage("prompt", t) } +func TestInstallRxjs(t *testing.T) { testPackage("rxjs", t) } +func TestInstallAngular(t *testing.T) { testPackage("angular", t) } +func TestInstallMinimatch(t *testing.T) { testPackage("minimatch", t) } +func TestInstallBunyan(t *testing.T) { testPackage("bunyan", t) } +func TestInstallLess(t *testing.T) { testPackage("less", t) } +func TestInstallBabelLoader(t *testing.T) { testPackage("babel-loader", t) } +func TestInstallGulpRename(t *testing.T) { testPackage("gulp-rename", t) } +func TestInstallConnect(t *testing.T) { testPackage("connect", t) } +func TestInstallPostcss(t *testing.T) { testPackage("postcss", t) } +func TestInstallEslint(t *testing.T) { testPackage("eslint", t) } +func TestInstallMeow(t *testing.T) { testPackage("meow", t) } +func TestInstallQs(t *testing.T) { testPackage("qs", t) } +func TestInstallChokidar(t *testing.T) { testPackage("chokidar", t) } +func TestInstallBabelPresetReact(t *testing.T) { testPackage("babel-preset-react", t) } +func TestInstallReactRedux(t *testing.T) { testPackage("react-redux", t) } +func TestInstallInherits(t *testing.T) { testPackage("inherits", t) } +func TestInstallPassport(t *testing.T) { testPackage("passport", t) } +func TestInstallSocketIoClient(t *testing.T) { testPackage("socket.io-client", t) } +func TestInstallReactRouter(t *testing.T) { testPackage("react-router", t) } diff --git a/esdoc.json b/esdoc.json deleted file mode 100644 index 5f1bede..0000000 --- a/esdoc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "source": "./src", - "destination": "./docs", - "plugins": [ - {"name": "esdoc-es7-plugin"} - ] -} diff --git a/guides/proxy.md b/guides/proxy.md deleted file mode 100644 index d50656d..0000000 --- a/guides/proxy.md +++ /dev/null @@ -1,29 +0,0 @@ -## Using `ied` behind a proxy - -`ied` can easily be configured to run behind a (corporate) proxy server. -Both HTTP and HTTPS proxies are supported. Optionally a username and password -can be supplied as part of the URL. - -`ied` does not use a system-wide configuration file, but instead uses -environment variables. - -In order force `ied` to use a corporate proxy, set the `IED_PROXY` environment -variable to the URL of your proxy server. Alternatively you can set -`http_proxy`, which might already be set depending on your system: - -```bash -export IED_PROXY=http://user:pass@proxy.server.com:3128 -``` - -Since corporate proxies tend to throttle connections, concurrent installations -might be less reliable. Thankfully `ied` is quite resistant in those scenarios, -but if needed you can still set the `IED_REQUEST_RETRIES` environment variable, -which instructs `ied` to retry failed (= timed out) requests. -`IED_REQUEST_RETRIES` defaults to 10 requests: - -```bash -# defaults to 10 -export IED_REQUEST_RETRIES=13 -``` - -For in-line documentation, see [`config.js`](../src/config.js). diff --git a/help_cmd.go b/help_cmd.go new file mode 100644 index 0000000..f94073e --- /dev/null +++ b/help_cmd.go @@ -0,0 +1,32 @@ +package main + +import "github.com/Sirupsen/logrus" + +var usage = ` + ied is a package manager for CommonJS packages. + + Usage: + ied [] + + Commands: + install fetch packages and dependencies + ping check if the registry is up + config print the used config + + Example: + ied install + ied install + ied install @ + ied install @ + + Can specify one or more: ied install semver@^5.0.1 tape + If no argument is supplied, installs dependencies from package.json. + Sub-commands can also be called via their shorthand aliases. + + README: https://github.com/alexanderGugel/ied + ISSUES: https://github.com/alexanderGugel/ied/issues +` + +func helpCmd() { + logrus.Println(usage) +} diff --git a/install_cmd.go b/install_cmd.go new file mode 100644 index 0000000..9326c89 --- /dev/null +++ b/install_cmd.go @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/Sirupsen/logrus" + "path/filepath" +) + +func initResolver(config *Config) Resolver { + registry := NewRegistry(config.Registry) + local := NewLocal() + return NewMultiResolver(local, registry) +} + +func initStore(wd string, resolver Resolver) *Store { + dir := filepath.Join(wd, "node_modules") + store := NewStore(dir, resolver) + if err := store.Init(); err != nil { + logrus.Fatalf("failed to init store: %v", err) + } + return store +} + +func installCmd(wd string, config *Config, args []string) { + resolver := initResolver(config) + store := initStore(wd, resolver) + + for _, name := range args { + if err := store.Install(nil, name, "*"); err != nil { + logrus.Printf("failed to install %v: %v", name, err) + } + } +} diff --git a/local.go b/local.go new file mode 100644 index 0000000..0fdbdba --- /dev/null +++ b/local.go @@ -0,0 +1,65 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" +) + +// LocalPkg represents a package that has already been installed in the specific +// project. It considers a package to be installed if a corresponding +// package.json file exists. +type LocalPkg struct { + Shasum string + Dependencies map[string]string `json:"dependencies"` +} + +// Deps returns a map of sub-dependencies. +func (l *LocalPkg) Deps() map[string]string { + return l.Dependencies +} + +// ID returns the implied shasum of the package, which can be retrieved from +// the symbolic link's target path. +func (l *LocalPkg) ID() string { + return l.Shasum +} + +// DownloadInto does nothing, since the package - by definition - has already +// been downloaded. +func (l *LocalPkg) DownloadInto(string) error { + return nil +} + +// Local is a resolver strategy used for handling already installed packages. +type Local struct{} + +// NewLocal creates a new local installation strategy. +func NewLocal() *Local { + return &Local{} +} + +// Resolve resolves an already installed (= local) dependency to a corresponding +// package. +func (l *Local) Resolve(dir, name, version string) (Pkg, error) { + link, err := os.Readlink(dir) + if err != nil { + return nil, nil + } + + raw, err := ioutil.ReadFile(filepath.Join(link, "package.json")) + if err != nil { + // Symlink exists, but package hasn't been downloaded. Implicitly + // delegate to registry resolver, which will install the package. + return nil, nil + } + + pkg := &LocalPkg{} + if err := json.Unmarshal(raw, &pkg); err != nil { + return nil, err + } + pkg.Shasum = filepath.Base(filepath.Dir(link)) + + return pkg, nil +} diff --git a/local_test.go b/local_test.go new file mode 100644 index 0000000..d5d8df4 --- /dev/null +++ b/local_test.go @@ -0,0 +1,61 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLocalPkgDeps(t *testing.T) { + pkg := &LocalPkg{ + Dependencies: map[string]string{ + "tape": "1.2.3", + "browserify": "1.2.3", + }, + } + + deps := pkg.Deps() + assert.Equal(t, deps, pkg.Dependencies) +} + +func TestLocalPkgID(t *testing.T) { + pkg := &LocalPkg{Shasum: "123"} + id := pkg.ID() + assert.Equal(t, id, pkg.Shasum) +} + +func TestLocalPkgDownloadInto(t *testing.T) { + pkg := &LocalPkg{} + err := pkg.DownloadInto("") + assert.NoError(t, err) +} + +func TestLocalResolve(t *testing.T) { + dir, err := ioutil.TempDir("", "TestLocalResolve") + assert.NoError(t, err) + + defer os.RemoveAll(dir) + + err = os.MkdirAll(filepath.Join(dir, "node_modules", "shasum", "package"), os.ModePerm) + assert.NoError(t, err, "failed to create package dir structure") + + err = os.Symlink( + filepath.Join(dir, "node_modules", "shasum", "package"), + filepath.Join(dir, "node_modules", "some-package"), + ) + assert.NoError(t, err, "failed to create package symlink") + + pkgFile := filepath.Join(dir, "node_modules", "shasum", "package", "package.json") + raw := []byte("{\"name\": \"some-package\"}\n") + err = ioutil.WriteFile(pkgFile, raw, os.ModePerm) + assert.NoError(t, err, "failed to write package.json") + + local := NewLocal() + pkg, err := local.Resolve(filepath.Join(dir, "node_modules", "some-package"), "", "") + assert.NoError(t, err, "failed to resolve dependency") + + assert.Equal(t, pkg.ID(), "shasum") +} diff --git a/log_formatter.go b/log_formatter.go new file mode 100644 index 0000000..8c21f7c --- /dev/null +++ b/log_formatter.go @@ -0,0 +1,150 @@ +// See https://github.com/sirupsen/logrus/blob/master/text_formatter.go +package main + +import ( + "bytes" + "fmt" + "github.com/Sirupsen/logrus" + "runtime" + "sort" + "strings" +) + +const ( + nocolor = 0 + red = 31 + green = 32 + yellow = 33 + blue = 34 + gray = 37 +) + +var isColorTerminal bool + +func init() { + isColorTerminal = logrus.IsTerminal() && runtime.GOOS != "windows" +} + +type LogFormatter struct{} + +func (f *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) { + var b *bytes.Buffer + var keys []string = make([]string, 0, len(entry.Data)) + for k := range entry.Data { + keys = append(keys, k) + } + + sort.Strings(keys) + if entry.Buffer != nil { + b = entry.Buffer + } else { + b = &bytes.Buffer{} + } + + prefixFieldClashes(entry.Data) + + if isColorTerminal { + f.printColored(b, entry, keys) + } else { + f.appendKeyValue(b, "level", entry.Level.String()) + if entry.Message != "" { + f.appendKeyValue(b, "msg", entry.Message) + } + for _, key := range keys { + f.appendKeyValue(b, key, entry.Data[key]) + } + } + + b.WriteByte('\n') + return b.Bytes(), nil +} + +func (f *LogFormatter) printColored( + b *bytes.Buffer, + entry *logrus.Entry, + keys []string, +) { + var levelColor int + switch entry.Level { + case logrus.DebugLevel: + levelColor = gray + case logrus.WarnLevel: + levelColor = yellow + case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel: + levelColor = red + default: + levelColor = blue + } + + levelText := strings.ToUpper(entry.Level.String())[0:4] + fmt.Fprintf( + b, + "\x1b[%dm%s\x1b[0m %-44s ", + levelColor, + levelText, + entry.Message, + ) + + for _, k := range keys { + v := entry.Data[k] + fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k) + f.appendValue(b, v) + } +} + +func needsQuoting(text string) bool { + for _, ch := range text { + if !((ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == '-' || ch == '.') { + return true + } + } + return false +} + +func (f *LogFormatter) appendKeyValue( + b *bytes.Buffer, + key string, + value interface{}, +) { + b.WriteString(key) + b.WriteByte('=') + f.appendValue(b, value) + b.WriteByte(' ') +} + +func (f *LogFormatter) appendValue(b *bytes.Buffer, value interface{}) { + switch value := value.(type) { + case string: + if !needsQuoting(value) { + b.WriteString(value) + } else { + fmt.Fprintf(b, "%q", value) + } + case error: + errmsg := value.Error() + if !needsQuoting(errmsg) { + b.WriteString(errmsg) + } else { + fmt.Fprintf(b, "%q", errmsg) + } + default: + fmt.Fprint(b, value) + } +} + +func prefixFieldClashes(data logrus.Fields) { + if t, ok := data["time"]; ok { + data["fields.time"] = t + } + + if m, ok := data["msg"]; ok { + data["fields.msg"] = m + } + + if l, ok := data["level"]; ok { + data["fields.level"] = l + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..ed104c2 --- /dev/null +++ b/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "github.com/Sirupsen/logrus" + "os" +) + +func readConfig() *Config { + filename, err := configFilename() + if err != nil { + logrus.Warnf("failed getting config filename: %v", err) + } + + config, err := LoadConfig(filename) + if err != nil { + logrus.Warnf("failed loading config: %v", err) + } + + if err := config.Validate(); err != nil { + logrus.Fatalf("failed loading config: %v", err) + } + + return config +} + +func setLogLevel(config *Config) { + level, err := logrus.ParseLevel(config.LogLevel) + if err != nil { + logrus.Warnf("failed to parse log level: %v", err) + } else { + logrus.SetLevel(level) + } +} + +func setLogFormatter() { + logrus.SetFormatter(&LogFormatter{}) +} + +func getWd() string { + wd, err := os.Getwd() + if err != nil { + logrus.Fatalf("failed to get working directory: %v", err) + } + return wd +} + +func main() { + config := readConfig() + setLogLevel(config) + setLogFormatter() + wd := getWd() + + if len(os.Args) < 2 { + helpCmd() + return + } + + switch os.Args[1] { + case "i": + fallthrough + case "install": + installCmd(wd, config, os.Args[2:]) + case "ping": + pingCmd(config) + case "config": + configCmd(config) + default: + helpCmd() + } +} diff --git a/package.json b/package.json deleted file mode 100644 index 34b1cf4..0000000 --- a/package.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "name": "ied", - "version": "2.3.6", - "bin": { - "ied": "lib/cmd.js" - }, - "dependencies": { - "easy-table": "^1.0.0", - "force-symlink": "0.0.2", - "gunzip-maybe": "^1.3.1", - "init-package-json": "^1.9.4", - "lodash.frompairs": "^4.0.1", - "lodash.memoize": "^4.1.0", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "needle": "1.0.0", - "node-gyp": "^3.4.0", - "node-pre-gyp": "^0.6.29", - "node-uuid": "^1.4.7", - "npm-package-arg": "^4.2.0", - "ora": "^0.2.3", - "rimraf": "^2.5.3", - "rxjs": "5.0.0-rc.1", - "semver": "^5.2.0", - "source-map-support": "^0.4.1", - "tar-fs": "^1.13.0" - }, - "devDependencies": { - "babel-cli": "^6.10.1", - "babel-core": "^6.10.4", - "babel-eslint": "^6.1.0", - "babel-plugin-__coverage__": "^11.0.0", - "babel-plugin-transform-function-bind": "^6.8.0", - "babel-plugin-transform-object-rest-spread": "^6.8.0", - "babel-preset-es2015": "^6.9.0", - "babel-register": "^6.9.0", - "cross-env": "^2.0.0", - "esdoc": "^0.4.7", - "esdoc-es7-plugin": "0.0.3", - "eslint": "^2.13.1", - "eslint-config-airbnb-base": "^3.0.1", - "eslint-plugin-import": "^1.10.2", - "mocha": "^2.5.3", - "nyc": "^6.6.1", - "resolve": "^1.1.7", - "rimraf": "^2.5.3", - "sinon": "^1.17.4" - }, - "scripts": { - "start": "npm run test:spec -- --watch", - "test": "npm run test:spec && npm run test:e2e", - "test:e2e": "rimraf .tmp/test/*; cross-env NODE_ENV=test mocha test/e2e", - "test:spec": "cross-env NODE_ENV=test mocha test/spec", - "test:coverage": "nyc --reporter=lcov npm run test:spec -- --reporter dot && nyc report", - "compile": "rimraf lib/*; cross-env NODE_ENV=production babel src/ -d lib/", - "compile:watch": "npm run compile -- -w", - "dev": "rimraf lib/*; cross-env NODE_ENV=development babel src/ -d lib/ -s", - "dev:watch": "npm run dev -- -w", - "lint": "eslint src/ test/", - "prepublish": "npm run compile" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/alexanderGugel/ied.git" - }, - "author": "Alexander Gugel ", - "license": "MIT", - "bugs": { - "url": "https://github.com/alexanderGugel/ied/issues" - }, - "homepage": "https://github.com/alexanderGugel/ied#readme", - "description": "An alternative package manager for Node.", - "files": [ - "lib", - "USAGE.txt" - ], - "nyc": { - "sourceMap": false, - "instrument": false - } -} diff --git a/ping_cmd.go b/ping_cmd.go new file mode 100644 index 0000000..4b48f41 --- /dev/null +++ b/ping_cmd.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/Sirupsen/logrus" +) + +func pingCmd(config *Config) { + registry := NewRegistry(config.Registry) + logrus.Infof("📡 pinging %q…", config.Registry) + err := registry.Ping() + if err != nil { + logrus.Fatalf("👎 registry down: %v", err) + } + logrus.Info("👍 registry up") +} diff --git a/pkg.go b/pkg.go new file mode 100644 index 0000000..d53aae4 --- /dev/null +++ b/pkg.go @@ -0,0 +1,13 @@ +package main + +// Pkg represents a package. +type Pkg interface { + // Deps returns a map of sub-dependencies. + Deps() map[string]string + + // ID returns an unique identifier of this package. + ID() string + + // DownloadInto fetches and unpacks the package. + DownloadInto(dir string) error +} diff --git a/registry.go b/registry.go new file mode 100644 index 0000000..5c562fe --- /dev/null +++ b/registry.go @@ -0,0 +1,95 @@ +package main + +import ( + "archive/tar" + "compress/gzip" + "encoding/json" + "fmt" + "net/http" + "path/filepath" +) + +// RegistryPkg represents a package.json document resolved from a specific +// registry. +type RegistryPkg struct { + Name string `json:"name"` + Version string `json:"version"` + Dependencies map[string]string `json:"dependencies"` + DevDependencies map[string]string `json:"devDependencies"` + PeerDependencies map[string]string `json:"peerDependencies"` + Dist struct { + Tarball string `json:"tarball"` + Shasum string `json:"shasum"` + } `json:"dist"` +} + +// Deps returns a map of sub-dependencies. +func (p *RegistryPkg) Deps() map[string]string { + return p.Dependencies +} + +// ID returns the shasum of the tarball of the package. +func (p *RegistryPkg) ID() string { + return p.Dist.Shasum +} + +// DownloadInto fetches the package from the registry and unpacks it. +func (p *RegistryPkg) DownloadInto(dir string) error { + r, err := http.Get(p.Dist.Tarball) + if err != nil { + return err + } + defer r.Body.Close() + + reader, err := gzip.NewReader(r.Body) + if err != nil { + return err + } + defer reader.Close() + + return Untar( + filepath.Join(dir, p.ID()), + tar.NewReader(reader), + ) +} + +// Registry represents a CommonJS complaint registry server. +type Registry struct { + RootURL string +} + +// NewRegistry creates a new registry. +func NewRegistry(rootURL string) *Registry { + return &Registry{rootURL} +} + +// Ping checks if the registry is available by hitting the /-/ping endpoint. +func (r *Registry) Ping() error { + res, err := http.Get(r.RootURL + "/-/ping") + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", res.StatusCode) + } + return nil +} + +// Resolve fetches the package version document of a specified dependency. +func (r *Registry) Resolve(dir, name, version string) (Pkg, error) { + url := r.RootURL + "/" + name + "/" + version + res, err := http.Get(url) + if err != nil { + return nil, err + } + defer res.Body.Close() + + pkg := &RegistryPkg{} + err = json.NewDecoder(res.Body).Decode(pkg) + if err != nil { + return nil, err + } + + return pkg, nil +} diff --git a/registry_test.go b/registry_test.go new file mode 100644 index 0000000..3af8630 --- /dev/null +++ b/registry_test.go @@ -0,0 +1,37 @@ +package main + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRegistryPkgDeps(t *testing.T) { + pkg := &RegistryPkg{ + Dependencies: map[string]string{ + "browserify": "1.2.3", + "tape": "~1.0.0", + }, + } + deps := pkg.Deps() + assert.Equal(t, deps, pkg.Dependencies) +} + +func TestRegistryPkgID(t *testing.T) { + pkg := &RegistryPkg{ + Dist: struct { + Tarball string `json:"tarball"` + Shasum string `json:"shasum"` + }{ + Tarball: "", + Shasum: "shasum", + }, + } + id := pkg.ID() + assert.Equal(t, id, pkg.Dist.Shasum, "should use shasum as unique id") +} + +func TestNewRegistry(t *testing.T) { + rootURL := "http://registry.npmjs.com/" + registry := NewRegistry(rootURL) + assert.Equal(t, registry.RootURL, rootURL) +} diff --git a/resolver.go b/resolver.go new file mode 100644 index 0000000..f020d3b --- /dev/null +++ b/resolver.go @@ -0,0 +1,38 @@ +package main + +// Resolver needs to implement a strategy for resolving dependencies. +type Resolver interface { + // Resolve resolves a specific dependency (typically retrieved from the + // dependencies field or explicitly specified by user) to a package that can + // be downloaded and linked. + Resolve(dir, name, version string) (Pkg, error) +} + +// MultiResolver wraps multiple different resolvers, but is itself a resolver. +type MultiResolver struct { + resolvers []Resolver +} + +// NewMultiResolver wraps multiple resolvers and creates a new MultiResolver. +func NewMultiResolver(resolvers ...Resolver) MultiResolver { + return MultiResolver{ + resolvers: resolvers, + } +} + +// Resolve resolves the specified dependency using all available resolvers. If +// the dependency couldn't be resolved, nil will be returned as a package. +func (r MultiResolver) Resolve(dir, name, version string) (Pkg, error) { + var pkg Pkg + for _, resolver := range r.resolvers { + var err error + pkg, err = resolver.Resolve(dir, name, version) + if err != nil { + return nil, err + } + if pkg != nil { + break + } + } + return pkg, nil +} diff --git a/resolver_test.go b/resolver_test.go new file mode 100644 index 0000000..5464226 --- /dev/null +++ b/resolver_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +type MockResolver struct { + Name string + Version string + Dir string + Pkg Pkg + Err error +} + +func (r MockResolver) Resolve(dir, name, version string) (Pkg, error) { + if dir == r.Dir && name == r.Name && version == r.Version { + return r.Pkg, r.Err + } + return nil, nil +} + +type StubPkg struct { + Name string +} + +func (s StubPkg) Deps() map[string]string { + return make(map[string]string) +} + +func (s StubPkg) ID() string { + return s.Name +} + +func (s StubPkg) DownloadInto(dir string) error { + return nil +} + +func TestMultiResolverResolveOneResolver(t *testing.T) { + resolverA := MockResolver{ + Name: "a", + Version: "1", + Dir: "/a", + Pkg: StubPkg{"a"}, + Err: nil, + } + + multiResolver := NewMultiResolver(resolverA) + pkg, err := multiResolver.Resolve("/a", "a", "1") + + assert.NoError(t, err) + assert.Equal(t, pkg, resolverA.Pkg) +} + +func TestMultiResolverResolveTwoResolvers(t *testing.T) { + resolverA := MockResolver{ + Name: "a", + Version: "1", + Dir: "/a", + Pkg: StubPkg{"a"}, + Err: nil, + } + resolverB := MockResolver{ + Name: "b", + Version: "2", + Dir: "/b", + Pkg: StubPkg{"b"}, + Err: nil, + } + + multiResolver := NewMultiResolver(resolverA, resolverB) + pkg, err := multiResolver.Resolve("/a", "a", "1") + + assert.NoError(t, err) + assert.Equal(t, pkg, resolverA.Pkg) + + pkg, err = multiResolver.Resolve("/b", "b", "2") + assert.NoError(t, err) + assert.Equal(t, pkg, resolverB.Pkg) + + pkg, err = multiResolver.Resolve("/b", "b", "3") + assert.NoError(t, err) + assert.Equal(t, pkg, nil) +} diff --git a/src/build.js b/src/build.js deleted file mode 100644 index 8ee77b7..0000000 --- a/src/build.js +++ /dev/null @@ -1,114 +0,0 @@ -import path from 'path' -import {ArrayObservable} from 'rxjs/observable/ArrayObservable' -import {Observable} from 'rxjs/Observable' -import {_do} from 'rxjs/operator/do' -import {map} from 'rxjs/operator/map' -import {concatMap} from 'rxjs/operator/concatMap' -import {mergeMap} from 'rxjs/operator/mergeMap' -import {filter} from 'rxjs/operator/filter' -import {every} from 'rxjs/operator/every' -import {spawn} from 'child_process' - -import * as config from './config' - -import debuglog from './debuglog' -const log = debuglog('build') - -/** - * names of lifecycle scripts that should be run as part of the installation - * process of a specific package (= properties of `scripts` object in - * `package.json`). - * @type {Array.} - * @readonly - */ -export const LIFECYCLE_SCRIPTS = [ - 'preinstall', - 'install', - 'postinstall' -] - -/** - * error class used for representing an error that occurs due to a lifecycle - * script that exits with a non-zero status code. - */ -export class FailedBuildError extends Error { - /** - * create instance. - */ - constructor () { - super('failed to build one or more dependencies that exited with != 0') - this.name = FailedBuildError - } -} - -/** - * build a dependency by executing the given lifecycle script. - * @param {String} nodeModules - absolute path of the `node_modules` directory. - * @param {Object} dep - dependency to be built. - * @param {String} dep.target - relative location of the target directory. - * @param {String} dep.script - script to be executed (usually using `sh`). - * @return {Observable} - observable sequence of the returned exit code. - */ -export function build (nodeModules, dep) { - const {target, script} = dep - log(`executing "${script}" from ${target}`) - - return Observable.create((observer) => { - // some packages do expect a defined `npm_execpath` env - // eg. https://github.com/chrisa/node-dtrace-provider/blob/v0.6.0/scripts/install.js#L19 - const env = {npm_execpath: '', ...process.env} - - env.PATH = [ - path.join(nodeModules, target, 'node_modules', '.bin'), - path.resolve(__dirname, '..', 'node_modules', '.bin'), - process.env.PATH - ].join(path.delimiter) - - const childProcess = spawn(config.sh, [config.shFlag, script], { - cwd: path.join(nodeModules, target, 'package'), - env, - stdio: 'inherit' - // shell: true // does break `dtrace-provider@0.6.0` build - }) - childProcess.on('error', (error) => { - observer.error(error) - }) - childProcess.on('close', (code) => { - observer.next(code) - observer.complete() - }) - }) -} - -/** - * extract lifecycle scripts from supplied dependency. - * @param {Dep} dep - dependency to be parsed. - * @return {Array.} - array of script targets to be executed. - */ -export function parseLifecycleScripts ({target, pkgJson: {scripts = {}}}) { - const results = [] - for (let i = 0; i < LIFECYCLE_SCRIPTS.length; i++) { - const name = LIFECYCLE_SCRIPTS[i] - const script = scripts[name] - if (script) results.push({target, script}) - } - return results -} - -/** - * run all lifecycle scripts upon completion of the installation process. - * ensures that all scripts exit with 0 (success), otherwise an error will be - * thrown. - * @param {String} nodeModules - `node_modules` base directory. - * @return {Observable} - empty observable sequence that will be completed once - * all lifecycle scripts have been executed. - */ -export function buildAll (nodeModules) { - return this - ::map(parseLifecycleScripts) - ::mergeMap((scripts) => ArrayObservable.create(scripts)) - ::concatMap((script) => build(nodeModules, script)) - ::every((code) => code === 0) - ::filter((ok) => !ok) - ::_do(() => { throw new FailedBuildError() }) -} diff --git a/src/cache.js b/src/cache.js deleted file mode 100644 index 97a59b2..0000000 --- a/src/cache.js +++ /dev/null @@ -1,81 +0,0 @@ -import {Observable} from 'rxjs/Observable' -import {mergeMap} from 'rxjs/operator/mergeMap' -import {retryWhen} from 'rxjs/operator/retryWhen' -import gunzip from 'gunzip-maybe' -import tar from 'tar-fs' -import fs from 'fs' -import path from 'path' -import uuid from 'node-uuid' -import * as util from './util' -import * as config from './config' - -/** - * initialize the cache. - * @return {Observable} - observable sequence that will be completed once the - * base directory of the cache has been created. - */ -export function init () { - return util.mkdirp(path.join(config.cacheDir, '.tmp')) -} - -/** - * get a random temporary filename. - * @return {String} - temporary filename. - */ -export function getTmp () { - const filename = path.join(config.cacheDir, '.tmp', uuid.v4()) - return filename -} - -/** - * open a write stream into a temporarily cached file for caching a new - * package. - * @return {WriteStream} - Write Stream - */ -export function write () { - return fs.createWriteStream(getTmp()) -} - -/** - * open a read stream to a cached dependency. - * @param {String} id - id (unique identifier) of the cached tarball. - * @return {ReadStream} - Read Stream - */ -export function read (id) { - const filename = path.join(config.cacheDir, id) - return fs.createReadStream(filename) -} - -/** - * extract a dependency from the cache. - * @param {String} dest - pathname into which the cached dependency should be - * extracted. - * @param {String} id - id (unique identifier) of the cached tarball. - * @return {Observable} - observable sequence that will be completed once - * the cached dependency has been fetched. - */ -export function extract (dest, id) { - return Observable.create((observer) => { - const tmpDest = getTmp() - const untar = tar.extract(tmpDest, {strip: 1}) - - const completeHandler = () => { - observer.next(tmpDest) - observer.complete() - } - const errorHandler = (err) => observer.error(err) - - this.read(id).on('error', errorHandler) - .pipe(gunzip()).on('error', errorHandler) - .pipe(untar).on('error', errorHandler) - .on('finish', completeHandler) - }) - ::mergeMap((tmpDest) => util.rename(tmpDest, dest) - ::retryWhen((errors) => errors::mergeMap((error) => { - if (error.code !== 'ENOENT') { - throw error - } - return util.mkdirp(path.dirname(dest)) - })) - ) -} diff --git a/src/cache_cmd.js b/src/cache_cmd.js deleted file mode 100644 index 63e8723..0000000 --- a/src/cache_cmd.js +++ /dev/null @@ -1,27 +0,0 @@ -import rimraf from 'rimraf' -import path from 'path' -import * as config from './config' - -/** - * print help if invoked without any further sub-command, empty the cache - * directory (delete it) if invoked via `ied cache clean`. - * @param {String} cwd - current working directory. - * @param {Object} argv - parsed command line arguments. - */ -export default function cacheCmd (cwd, argv) { - switch (argv._[1]) { - // `ied cache clean` - case 'clean': - const shasum = argv._[2] - if (shasum) { - rimraf.sync(path.join(config.cacheDir, shasum)) - } else { - rimraf.sync(config.cacheDir) - } - break - // `ied cache` - default: - const helpCmd = require('./help_cmd').default - helpCmd(cwd, argv) - } -} diff --git a/src/cmd.js b/src/cmd.js deleted file mode 100755 index 7d3f398..0000000 --- a/src/cmd.js +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env node - -import minimist from 'minimist' -import * as config from './config' -if (['development', 'test'].indexOf(process.env.NODE_ENV) !== -1) { - require('source-map-support').install() -} - -const alias = { - h: 'help', - v: 'version', - S: 'save', - D: 'save-dev', - O: 'save-optional', - r: 'registry', - b: 'build', - prod: 'production' -} - -const string = [ - 'registry' -] - -const boolean = [ - 'help', - 'version', - 'save', - 'save-dev', - 'save-optional', - 'build', - 'production' -] - -const cwd = process.cwd() -const argv = minimist(process.argv.slice(2), {alias, string, boolean}) - -if (argv.registry) { - config.registry = argv.registry -} - -let installCmd -let shellCmd -let runCmd -let pingCmd -let configCmd -let initCmd -let linkCmd -let unlinkCmd -let helpCmd -let versionCmd -let cacheCmd - -(() => { - if (argv.help) { - helpCmd = require('./help_cmd').default - helpCmd().subscribe() - return - } - - if (argv.version) { - versionCmd = require('./version_cmd').default - versionCmd().subscribe() - return - } - - const [subCommand] = argv._ - - switch (subCommand) { - case 'i': - case 'install': - installCmd = require('./install_cmd').default - installCmd(cwd, argv).subscribe() - break - case 'sh': - case 'shell': - shellCmd = require('./shell_cmd').default - shellCmd(cwd).subscribe() - break - case 'r': - case 'run': - case 'run-script': - runCmd = require('./run_cmd').default - runCmd(cwd, argv).subscribe() - break - case 't': - case 'test': - case 'start': - case 'build': - case 'stop': - runCmd = require('./run_cmd').default - runCmd(cwd, {...argv, _: ['run'].concat(argv._)}).subscribe() - break - case 'ping': - pingCmd = require('./ping_cmd').default - pingCmd() - break - case 'conf': - case 'config': - configCmd = require('./config_cmd').default - configCmd() - break - case 'init': - initCmd = require('./init_cmd').default - initCmd(cwd, argv) - break - case 'link': - linkCmd = require('./link_cmd').default - linkCmd(cwd, argv).subscribe() - break - case 'unlink': - unlinkCmd = require('./unlink_cmd').default - unlinkCmd(cwd, argv).subscribe() - break - case 'cache': - cacheCmd = require('./cache_cmd').default - cacheCmd(cwd, argv) - break - case 'version': - versionCmd = require('./version_cmd').default - versionCmd().subscribe() - break - default: - helpCmd = require('./help_cmd').default - helpCmd().subscribe() - } -})() diff --git a/src/config.js b/src/config.js deleted file mode 100644 index 2f18005..0000000 --- a/src/config.js +++ /dev/null @@ -1,99 +0,0 @@ -import path from 'path' - -const {env, platform, execPath} = process - -/** - * boolean value indicating whether or not we're running on `win32` (Windows). - * @type {Boolean} - */ -export const isWindows = platform === 'win32' - -/** - * absolute location of the user's home directory. - * @type {String} - */ -export const home = env[isWindows ? 'USERPROFILE' : 'HOME'] - -/** - * registry endpoint configured via `IED_REGISTRY`, defaults to the npm - * registry [`https://registry.npmjs.org/`]('https://registry.npmjs.org/'). - * @type {String} - */ -export const registry = env.IED_REGISTRY || 'https://registry.npmjs.org/' - -/** - * cache directory used for storing downloaded package tarballs. configurable - * via `IED_CACHE_DIR`, default to `.ied_cache` in the user's home directory. - * @type {String} - */ -export const cacheDir = env.IED_CACHE_DIR || path.join(home, '.ied_cache') - -/** - * directory used for globally installed `node_modules`. - * configurable via `IED_GLOBAL_NODE_MODULES`, default to `.node_modules` in - * the user's home directory. - * @type {String} - */ -export const globalNodeModules = env.IED_GLOBAL_NODE_MODULES || - path.join(home, '.node_modules') - -/** - * similar to {@link globalNodeModules}. directory used for symlinks of - * globally linked executables. - * configurable via `IED_GLOBAL_BIN`, default to parent of `process.execPath` - * (location of `node` binary). - * @type {String} - */ -export const globalBin = env.IED_GLOBAL_BIN || path.resolve(execPath, '..') - -/** - * proxy server endpoint. can be set via `IED_PROXY` or `http_proxy`. optional - * and default to `null`. - * @type {String|null} - */ -export const proxy = env.IED_PROXY || env.http_proxy || null - -/** - * how often `ied` should retry HTTP requests before indicating failure. - * defaults to `5` requests. can be set via `IED_REQUEST_RETRIES`. - * @type {Number} - */ -export const retries = parseInt(env.IED_REQUEST_RETRIES, 10) || 5 - -/** - * shell command used for executing lifecycle scripts (such as `postinstall`). - * platform dependent: default to `cmd` on Windows, otherwise use `sh`. - * can be overridden using `IED_SH`. - * @type {String} - */ -export const sh = env.IED_SH || env.SHELL || (platform === 'win32' ? env.comspec || 'cmd' : 'sh') - -/** - * additional flags supplied to the `sh` executable. platform dependent: - * default to `/d /s /c` on Windows, otherwise use `-c`. - * can be overridden using `IED_SH_FLAG`. - * @type {String} - */ -export const shFlag = env.IED_SH_FLAG || (isWindows ? '/d /s /c' : '-c') - -/** - * bearer token used for downloading access restricted packages (scoped - * modules). this token will be set as `Authorization` header field on all - * subsequent HTTP requests to the registry, thus exposing a potential - * **security** risk. - * @type {String|null} - */ -export const bearerToken = env.IED_BEARER_TOKEN || null - -/** - * HTTP options supplied as part of all subsequent requests. used for exporting - * generic HTTP options that contain the proxy configuration (if set) and the - * `Authorization` header. - * @type {Object} - */ -export const httpOptions = { - proxy, - headers: bearerToken - ? {authorization: `Bearer ${bearerToken}`} - : {} -} diff --git a/src/config_cmd.js b/src/config_cmd.js deleted file mode 100644 index 907f1fc..0000000 --- a/src/config_cmd.js +++ /dev/null @@ -1,18 +0,0 @@ -import * as config from './config' -import Table from 'easy-table' - -/** - * print the used configuration object as an ASCII table. - */ -export default function configCmd () { - const table = new Table() - const keys = Object.keys(config) - for (const key of keys) { - const value = config[key] - table.cell('key', key) - table.cell('value', value) - table.newRow() - } - - console.log(table.toString()) -} diff --git a/src/debuglog.js b/src/debuglog.js deleted file mode 100644 index f27180c..0000000 --- a/src/debuglog.js +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// 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: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// 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. - -import util from 'util' - -const debugs = {} -let debugEnv - -/** - * Node's `debuglog` function. Not available on older Node versions, therefore - * copied in for convenience: - * - * This is used to create a function which conditionally writes to stderr based - * on the existence of a `NODE_DEBUG` environment variable. If the `section` - * name appears in that environment variable, then the returned function will - * be similar to `console.error()`. If not, then the returned function is a - * no-op. - * - * @param {String} set - the section of the program to be debugged. - * @return {Function} - the logging function. - * @see https://nodejs.org/api/util.html#util_util_debuglog_section - */ -function debuglog (set) { - if (debugEnv === undefined) { - debugEnv = process.env.NODE_DEBUG || '' - } - const upperSet = set.toUpperCase() - if (!debugs[upperSet]) { - if (debugEnv === '*' || new RegExp(`\\b${upperSet}\\b`, 'i').test(debugEnv)) { - const pid = process.pid - debugs[upperSet] = (...args) => { - const msg = util.format(...args) - console.error('%s %d: %s', upperSet, pid, msg) - } - } else { - debugs[upperSet] = Function.prototype - } - } - return debugs[upperSet] -} - -export default debuglog diff --git a/src/git.js b/src/git.js deleted file mode 100644 index 5c72893..0000000 --- a/src/git.js +++ /dev/null @@ -1,69 +0,0 @@ - -import {spawn} from 'child_process' -import {Observable} from 'rxjs/Observable' -import {mergeMap} from 'rxjs/operator/mergeMap' -import {retryWhen} from 'rxjs/operator/retryWhen' -import {getTmp} from './cache' -import * as util from './util' -import * as config from './config' -import path from 'path' - -import debuglog from './debuglog' - -const log = debuglog('git') - -function prefixGitArgs () { - return process.platform === 'win32' ? ['-c', 'core.longpaths=true'] : [] -} - -function spawnGit (args) { - log(`executing git with args ${args}`) - const fullArgs = prefixGitArgs().concat(args || []) - return spawn('git', fullArgs) -} - -/** - * clone a git repository. - * @param {String} repo - git repository to clone. - * @param {String} ref - git reference to checkout. - * @return {Observable} - observable sequence that will be completed once - * the git repository has been cloned. - */ -export function clone (repo, ref) { - return Observable.create((observer) => { - const tmpDest = getTmp() - - const completeHandler = () => { - observer.next(tmpDest) - observer.complete() - } - const errorHandler = (err) => { - log(`failed to clone repository from ${repo}`) - observer.error(err) - } - const args = ['clone', '-b', ref, repo, tmpDest, '--single-branch'] - log(`cloning git repository from ${repo}`) - const git = spawnGit(args) - git.on('close', code => (code ? errorHandler() : completeHandler())) - }) -} - -/** - * extract a cloned git repository to destination. - * @param {String} dest - pathname into which the cloned repository should be - * extracted. - * @return {Observable} - observable sequence that will be completed once - * the cloned repository has been extracted. - */ -export function extract (dest) { - const {pkgJson, target} = this - const tmpDest = pkgJson.dist.path - const where = path.join(dest, target, 'package') - return util.rename(tmpDest, where) - ::retryWhen((errors) => errors::mergeMap((error) => { - if (error.code !== 'ENOENT') { - throw error - } - return util.mkdirp(path.dirname(dest)) - })) -} diff --git a/src/help_cmd.js b/src/help_cmd.js deleted file mode 100644 index 502b076..0000000 --- a/src/help_cmd.js +++ /dev/null @@ -1,13 +0,0 @@ -import path from 'path' -import {readFile} from './util' -import {_do} from 'rxjs/operator/do' - -/** - * print the `USAGE` document. can be invoked using `ied help` or implicitly as - * a fall back. - * @return {Observable} - observable sequence of `USAGE`. - */ -export default function helpCmd () { - const filename = path.join(__dirname, '../USAGE.txt') - return readFile(filename, 'utf8')::_do(console.log) -} diff --git a/src/init_cmd.js b/src/init_cmd.js deleted file mode 100644 index bd57b08..0000000 --- a/src/init_cmd.js +++ /dev/null @@ -1,22 +0,0 @@ -import init from 'init-package-json' -import path from 'path' -import * as config from './config' - -/** - * initialize a new `package.json` file. - * @param {String} cwd - current working directory. - * @see https://www.npmjs.com/package/init-package-json - */ -export default function initCmd (cwd) { - const initFile = path.resolve(config.home, '.ied-init') - - init(cwd, initFile, (err) => { - if (err) { - if (err.message === 'canceled') { - console.log('init canceled!') - return - } - throw err - } - }) -} diff --git a/src/install.js b/src/install.js deleted file mode 100644 index e6f41a5..0000000 --- a/src/install.js +++ /dev/null @@ -1,436 +0,0 @@ -import crypto from 'crypto' -import path from 'path' -import url from 'url' -import {ArrayObservable} from 'rxjs/observable/ArrayObservable' -import {EmptyObservable} from 'rxjs/observable/EmptyObservable' -import {Observable} from 'rxjs/Observable' -import {_finally} from 'rxjs/operator/finally' -import {concatStatic} from 'rxjs/operator/concat' -import {distinctKey} from 'rxjs/operator/distinctKey' -import {expand} from 'rxjs/operator/expand' -import {map} from 'rxjs/operator/map' -import {_catch} from 'rxjs/operator/catch' -import {mergeMap} from 'rxjs/operator/mergeMap' -import {retry} from 'rxjs/operator/retry' -import {skip} from 'rxjs/operator/skip' -import {satisfies} from 'semver' -import needle from 'needle' -import assert from 'assert' -import npa from 'npm-package-arg' -import memoize from 'lodash.memoize' - -import * as cache from './cache' -import * as config from './config' -import * as registry from './registry' -import * as git from './git' -import * as util from './util' -import * as progress from './progress' -import {normalizeBin, parseDependencies} from './pkg_json' - -import debuglog from './debuglog' - -const log = debuglog('install') -const cachedNpa = memoize(npa) - -/** - * properties of project-level `package.json` files that will be checked for - * dependencies. - * @type {Array.} - * @readonly - */ -export const ENTRY_DEPENDENCY_FIELDS = [ - 'dependencies', - 'devDependencies', - 'optionalDependencies' -] - -/** - * properties of `package.json` of sub-dependencies that will be checked for - * dependences. - * @type {Array.} - * @readonly - */ -export const DEPENDENCY_FIELDS = [ - 'dependencies', - 'optionalDependencies' -] - -/** - * error class used for representing an error that occurs due to a lifecycle - * script that exits with a non-zero status code. - */ -export class LocalConflictError extends Error { - /** - * create instance. - * @param {String} name - name of the dependency. - * @param {String} version - local version. - * @param {String} expected - expected version. - */ - constructor (name, version, expected) { - super(`Local version ${name}@${version} does not match required version @${expected}`) - this.name = 'LocalConflictError' - } -} - -/** - * resolve a dependency's `package.json` file from the local file system. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {String} name - name of the dependency. - * @param {String} version - version of the dependency. - * @param {Boolean} isExplicit - whether the install command asks for an explicit install. - * @return {Observable} - observable sequence of `package.json` objects. - */ -export function resolveLocal (nodeModules, parentTarget, name, version, isExplicit) { - const linkname = path.join(nodeModules, parentTarget, 'node_modules', name) - const mockFetch = () => EmptyObservable.create() - log(`resolving ${linkname} from node_modules`) - - // support `file:` with symlinks - if (version.substr(0, 5) === 'file:') { - log(`resolved ${name}@${version} as local symlink`) - const isScoped = name.charAt(0) === '@' - const src = path.join(parentTarget, isScoped ? '..' : '', version.substr(5)) - const dst = path.join('node_modules', parentTarget, 'node_modules', name) - return util.forceSymlink(src, dst)::_finally(progress.complete) - } - - return util.readlink(linkname)::mergeMap((rel) => { - const target = path.basename(path.dirname(rel)) - const filename = path.join(linkname, 'package.json') - log(`reading package.json from ${filename}`) - - return util.readFileJSON(filename)::map((pkgJson) => { - if (isExplicit && !satisfies(pkgJson.version, version)) { - throw new LocalConflictError(name, pkgJson.version, version) - } - return {parentTarget, pkgJson, target, name, fetch: mockFetch} - }) - }) -} - -/** - * resolve a dependency's `package.json` file from a remote registry. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {String} name - name of the dependency. - * @param {String} version - version of the dependency. - * @return {Observable} - observable sequence of `package.json` objects. - */ -export function resolveRemote (nodeModules, parentTarget, name, version) { - const source = `${name}@${version}` - log(`resolving ${source} from remote registry`) - - const parsedSpec = cachedNpa(source) - - switch (parsedSpec.type) { - case 'range': - case 'version': - case 'tag': - return resolveFromNpm(nodeModules, parentTarget, parsedSpec) - case 'remote': - return resolveFromTarball(nodeModules, parentTarget, parsedSpec) - case 'hosted': - return resolveFromHosted(nodeModules, parentTarget, parsedSpec) - case 'git': - return resolveFromGit(nodeModules, parentTarget, parsedSpec) - default: - throw new Error(`Unknown package spec: ${parsedSpec.type} for ${name}`) - } -} - -/** - * resolve a dependency's `package.json` file from the npm registry. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {Object} parsedSpec - parsed package name and specifier. - * @return {Observable} - observable sequence of `package.json` objects. - */ -export function resolveFromNpm (nodeModules, parentTarget, parsedSpec) { - const {raw, name, type, spec} = parsedSpec - log(`resolving ${raw} from npm`) - const options = {...config.httpOptions, retries: config.retries} - return registry.match(name, spec, options)::map((pkgJson) => { - const target = pkgJson.dist.shasum - log(`resolved ${raw} to tarball shasum ${target} from npm`) - return {parentTarget, pkgJson, target, name, type, fetch} - }) -} - -/** - * resolve a dependency's `package.json` file from an url tarball. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {Object} parsedSpec - parsed package name and specifier. - * @return {Observable} - observable sequence of `package.json` objects. - */ -export function resolveFromTarball (nodeModules, parentTarget, parsedSpec) { - const {raw, name, type, spec} = parsedSpec - log(`resolving ${raw} from tarball`) - return Observable.create((observer) => { - // create shasum from url for storage - const hash = crypto.createHash('sha1') - hash.update(raw) - const shasum = hash.digest('hex') - const pkgJson = {name, dist: {tarball: spec, shasum}} - log(`resolved ${raw} to uri shasum ${shasum} from tarball`) - observer.next({parentTarget, pkgJson, target: shasum, name, type, fetch}) - observer.complete() - }) -} - -/** - * resolve a dependency's `package.json` file from an hosted GitHub-like registry. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {Object} parsedSpec - parsed package name and specifier. - * @return {Observable} - observable sequence of `package.json` objects. - */ -export function resolveFromHosted (nodeModules, parentTarget, parsedSpec) { - const {raw, name, type, hosted} = parsedSpec - log(`resolving ${raw} from ${hosted.type}`) - - const [provider, shortcut] = hosted.shortcut.split(':') - const [repo, ref = 'master'] = shortcut.split('#') - - const options = {...config.httpOptions, retries: config.retries} - // create shasum from directUrl for storage - const hash = crypto.createHash('sha1') - hash.update(hosted.directUrl) - const shasum = hash.digest('hex') - - let tarball - switch (hosted.type) { - case 'github': - tarball = url.resolve('https://codeload.github.com', `${repo}/tar.gz/${ref}`) - break - case 'bitbucket': - tarball = url.resolve('https://bitbucket.org', `${repo}/get/${ref}.tar.gz`) - break - default: - throw new Error(`Unknown hosted type: ${hosted.type} for ${name}`) - } - - return registry.fetch(hosted.directUrl, options) - ::map(({body}) => JSON.parse(body)) - ::map(pkgJson => { - pkgJson.dist = {tarball, shasum} // eslint-disable-line no-param-reassign - log(`resolved ${name}@${ref} to directUrl shasum ${shasum} from ${provider}`) - return {parentTarget, pkgJson, target: shasum, name: pkgJson.name, type, fetch} - }) -} - -/** - * resolve a dependency's `package.json` file from a git endpoint. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - relative parent's node_modules path. - * @param {Object} parsedSpec - parsed package name and specifier. - * @return {Observable} - observable sequence of `package.json` objects. - */ -export function resolveFromGit (nodeModules, parentTarget, parsedSpec) { - const {raw, type, spec} = parsedSpec - log(`resolving ${raw} from git`) - - const [protocol, host] = spec.split('://') - const [repo, ref = 'master'] = host.split('#') - - // create shasum from spec for storage - const hash = crypto.createHash('sha1') - hash.update(spec) - const shasum = hash.digest('hex') - let repoPath - - return git.clone(repo, ref) - ::mergeMap(tmpDest => { - repoPath = tmpDest - return util.readFileJSON(path.resolve(tmpDest, 'package.json')) - }) - ::map(pkgJson => { - const name = pkgJson.name - pkgJson.dist = {shasum, path: repoPath} // eslint-disable-line no-param-reassign - log(`resolved ${name}@${ref} to spec shasum ${shasum} from git`) - return {parentTarget, pkgJson, target: shasum, name, type, fetch: git.extract} - }) -} - -/** - * resolve an individual sub-dependency based on the parent's target and the - * current working directory. - * @param {String} nodeModules - `node_modules` base directory. - * @param {String} parentTarget - target path used for determining the sub- - * dependency's path. - * @param {Boolean} isExplicit - whether the install command asks for an explicit install. - * @return {Obserable} - observable sequence of `package.json` root documents - * wrapped into dependency objects representing the resolved sub-dependency. - */ -export function resolve (nodeModules, parentTarget, isExplicit) { - return this::mergeMap(([name, version]) => { - progress.add() - progress.report(`resolving ${name}@${version}`) - log(`resolving ${name}@${version}`) - - return resolveLocal(nodeModules, parentTarget, name, version, isExplicit) - ::_catch((error) => { - if (error.name !== 'LocalConflictError' && error.code !== 'ENOENT') { - throw error - } - log(`failed to resolve ${name}@${version} from local ${parentTarget} via ${nodeModules}`) - return resolveRemote(nodeModules, parentTarget, name, version, isExplicit) - }) - ::_finally(progress.complete) - }) -} - -/** - * resolve all dependencies starting at the current working directory. - * @param {String} nodeModules - `node_modules` base directory. - * @param {Object} [targets=Object.create(null)] - resolved / active targets. - * @param {Boolean} isExplicit - whether the install command asks for an explicit install. - * @return {Observable} - an observable sequence of resolved dependencies. - */ -export function resolveAll (nodeModules, targets = Object.create(null), isExplicit) { - return this::expand(({target, pkgJson, isProd = false}) => { - // cancel when we get into a circular dependency - if (target in targets) { - log(`aborting due to circular dependency ${target}`) - return EmptyObservable.create() - } - - targets[target] = true // eslint-disable-line no-param-reassign - - // install devDependencies of entry dependency (project-level) - const fields = (target === '..' && !isProd) - ? ENTRY_DEPENDENCY_FIELDS - : DEPENDENCY_FIELDS - - log(`extracting ${fields} from ${target}`) - - const dependencies = parseDependencies(pkgJson, fields) - - return ArrayObservable.create(dependencies) - ::resolve(nodeModules, target, isExplicit) - }) -} - -function resolveSymlink (src, dst) { - const relSrc = path.relative(path.dirname(dst), src) - return [relSrc, dst] -} - -function getBinLinks (dep) { - const {pkgJson, parentTarget, target} = dep - const binLinks = [] - const bin = normalizeBin(pkgJson) - const names = Object.keys(bin) - for (let i = 0; i < names.length; i++) { - const name = names[i] - const src = path.join('node_modules', target, 'package', bin[name]) - const dst = path.join('node_modules', parentTarget, 'node_modules', '.bin', name) - binLinks.push([src, dst]) - } - return binLinks -} - -function getDirectLink (dep) { - const {parentTarget, target, name} = dep - const src = path.join('node_modules', target, 'package') - const dst = path.join('node_modules', parentTarget, 'node_modules', name) - return [src, dst] -} - -/** - * symlink the intermediate results of the underlying observable sequence - * @return {Observable} - empty observable sequence that will be completed - * once all dependencies have been symlinked. - */ -export function linkAll () { - return this - ::mergeMap((dep) => [getDirectLink(dep), ...getBinLinks(dep)]) - ::map(([src, dst]) => resolveSymlink(src, dst)) - ::mergeMap(([src, dst]) => { - log(`symlinking ${src} -> ${dst}`) - return util.forceSymlink(src, dst) - }) -} - -function checkShasum (shasum, expected, tarball) { - assert.equal(shasum, expected, - `shasum mismatch for ${tarball}: ${shasum} <-> ${expected}`) -} - -function download (tarball, expected, type) { - log(`downloading ${tarball}, expecting ${expected}`) - return Observable.create((observer) => { - const shasum = crypto.createHash('sha1') - const response = needle.get(tarball, config.httpOptions) - const cached = response.pipe(cache.write()) - - const errorHandler = (error) => observer.error(error) - const dataHandler = (chunk) => shasum.update(chunk) - const finishHandler = () => { - const actualShasum = shasum.digest('hex') - log(`downloaded ${actualShasum} into ${cached.path}`) - // only actually check shasum integrity for npm tarballs - const expectedShasum = ['range', 'version', 'tag'].indexOf(type) !== -1 ? - actualShasum : expected - observer.next({tmpPath: cached.path, shasum: expectedShasum}) - observer.complete() - } - - response.on('data', dataHandler) - response.on('error', errorHandler) - - cached.on('error', errorHandler) - cached.on('finish', finishHandler) - }) - ::mergeMap(({tmpPath, shasum}) => { - if (expected) { - checkShasum(shasum, expected, tarball) - } - - const newPath = path.join(config.cacheDir, shasum) - return util.rename(tmpPath, newPath) - }) -} - -function fixPermissions (target, bin) { - const execMode = 0o777 & (~process.umask()) - const paths = [] - const names = Object.keys(bin) - for (let i = 0; i < names.length; i++) { - const name = names[i] - paths.push(path.resolve(target, bin[name])) - } - log(`fixing persmissions of ${names} in ${target}`) - return ArrayObservable.create(paths) - ::mergeMap((filepath) => util.chmod(filepath, execMode)) -} - -function fetch (nodeModules) { - const {target, type, pkgJson: {name, bin, dist: {tarball, shasum}}} = this - const where = path.join(nodeModules, target, 'package') - - log(`fetching ${tarball} into ${where}`) - - return util.stat(where)::skip(1)::_catch((error) => { - if (error.code !== 'ENOENT') { - throw error - } - const extracted = cache.extract(where, shasum)::_catch((error) => { // eslint-disable-line - if (error.code !== 'ENOENT') { - throw error - } - return concatStatic( - download(tarball, shasum, type), - cache.extract(where, shasum) - ) - }) - const fixedPermissions = fixPermissions(where, normalizeBin({name, bin})) - return concatStatic(extracted, fixedPermissions) - }) -} - -export function fetchAll (nodeModules) { - const fetchWithRetry = (dep) => dep.fetch(nodeModules)::retry(config.retries) - return this::distinctKey('target')::mergeMap(fetchWithRetry) -} diff --git a/src/install_cmd.js b/src/install_cmd.js deleted file mode 100644 index 93aef9f..0000000 --- a/src/install_cmd.js +++ /dev/null @@ -1,48 +0,0 @@ -import path from 'path' -import {EmptyObservable} from 'rxjs/observable/EmptyObservable' -import {concatStatic} from 'rxjs/operator/concat' -import {publishReplay} from 'rxjs/operator/publishReplay' -import {skip} from 'rxjs/operator/skip' -import {map} from 'rxjs/operator/map' -import {mergeStatic} from 'rxjs/operator/merge' -import {ignoreElements} from 'rxjs/operator/ignoreElements' - -import {resolveAll, fetchAll, linkAll} from './install' -import {init as initCache} from './cache' -import {fromArgv, fromFs, save} from './pkg_json' -import {buildAll} from './build' - -/** - * run the installation command. - * @param {String} cwd - current working directory (absolute path). - * @param {Object} argv - parsed command line arguments. - * @return {Observable} - an observable sequence that will be completed once - * the installation is complete. - */ -export default function installCmd (cwd, argv) { - const isExplicit = argv._.length - 1 - const updatedPkgJSONs = isExplicit ? fromArgv(cwd, argv) : fromFs(cwd) - const isProd = argv.production - - const nodeModules = path.join(cwd, 'node_modules') - - const resolvedAll = updatedPkgJSONs - ::map((pkgJson) => ({pkgJson, target: '..', isProd})) - ::resolveAll(nodeModules, undefined, isExplicit)::skip(1) - ::publishReplay().refCount() - - const initialized = initCache()::ignoreElements() - const linkedAll = resolvedAll::linkAll() - const fetchedAll = resolvedAll::fetchAll(nodeModules) - const installedAll = mergeStatic(linkedAll, fetchedAll) - - const builtAll = argv.build - ? resolvedAll::buildAll(nodeModules) - : EmptyObservable.create() - - const saved = (argv.save || argv['save-dev'] || argv['save-optional']) - ? updatedPkgJSONs::save(cwd) - : EmptyObservable.create() - - return concatStatic(initialized, installedAll, saved, builtAll) -} diff --git a/src/link.js b/src/link.js deleted file mode 100644 index 96663b7..0000000 --- a/src/link.js +++ /dev/null @@ -1,80 +0,0 @@ -import path from 'path' -import * as config from './config' -import {ArrayObservable} from 'rxjs/observable/ArrayObservable' -import {readFileJSON, forceSymlink, unlink} from './util' -import {mergeMap} from 'rxjs/operator/mergeMap' - -/** - * generate the symlinks to be created in order to link to passed in package. - * @param {String} cwd - current working directory. - * @param {Object} pkgJson - `package.json` file to be linked. - * @return {Array.} - an array of tuples representing symbolic links to be - * created. - */ -export function getSymlinks (cwd, pkgJson) { - const libSymlink = [cwd, path.join(config.globalNodeModules, pkgJson.name)] - let bin = pkgJson.bin - if (typeof bin === 'string') { - bin = {} - bin[pkgJson.name] = pkgJson.bin - } - bin = bin || {} - const binSymlinks = Object.keys(bin).map((name) => ([ - path.join(config.globalNodeModules, pkgJson.name, bin[name]), - path.join(config.globalBin, name) - ])) - return [libSymlink].concat(binSymlinks) -} - -/* - * globally expose the package we're currently in (used for `ied link`). - * @param {String} cwd - current working directory. - * @return {Observable} - observable sequence. - */ -export function linkToGlobal (cwd) { - return readFileJSON(path.join(cwd, 'package.json')) - ::mergeMap((pkgJson) => getSymlinks(cwd, pkgJson)) - ::mergeMap(([src, dst]) => forceSymlink(src, dst)) -} - -/** - * links a globally linked package into the package present in the current - * working directory (used for `ied link some-package`). - * the package can be `require`d afterwards. - * `node_modules/.bin` stays untouched. - * @param {String} cwd - current working directory. - * @param {String} name - name of the dependency to be linked. - * @return {Observable} - observable sequence. - */ -export function linkFromGlobal (cwd, name) { - const dst = path.join(cwd, 'node_modules', name) - const src = path.join(config.globalNodeModules, name) - return forceSymlink(src, dst) -} - -/** - * revert the effects of `ied link` by removing the previously created - * symbolic links (used for `ied unlink`). - * @param {String} cwd - current working directory. - * @return {Observable} - observable sequence. - */ -export function unlinkToGlobal (cwd) { - const pkg = require(path.join(cwd, 'package.json')) - const symlinks = getSymlinks(cwd, pkg) - return ArrayObservable.create(symlinks) - ::mergeMap(([src, dst]) => unlink(dst)) -} - -/** - * revert the effects of `ied link some-package` by removing the previously - * created symbolic links from the project's `node_modules` directory (used for - * `ied unlink some-package`). - * @param {String} cwd - current working directory. - * @param {String} name - name of the dependency to be unlinked from the - * project's `node_modules`. - * @return {Observable} - observable sequence. - */ -export function unlinkFromGlobal (cwd, name) { - const dst = path.join(cwd, 'node_modules', name) - return unlink(dst) -} diff --git a/src/link_cmd.js b/src/link_cmd.js deleted file mode 100644 index 7ae6271..0000000 --- a/src/link_cmd.js +++ /dev/null @@ -1,41 +0,0 @@ -import * as config from './config' -import {mkdirp} from './util' -import {linkFromGlobal, linkToGlobal} from './link' -import {concatStatic} from 'rxjs/operator/concat' -import {ArrayObservable} from 'rxjs/observable/ArrayObservable' -import {mergeMap} from 'rxjs/operator/mergeMap' -import path from 'path' - -/** - * can be used in two ways: - * 1. in order to globally _expose_ the current package (`ied link`). - * 2. in order to use a previously globally _exposed_ package (`ied link tap`). - * - * useful for local development when you want to use a dependency in a - * different project without publishing to the npm registry / installing from - * local FS. - * - * create a symlink either in the global `node_modules` directory (`ied link`) - * or in the project's `node_modules` directory (e.g. `ied link tap` would - * create a symlink in `current-project/node_modules/tap` pointing to a - * globally installed tap version). - * - * @param {String} cwd - current working directory. - * @param {Object} argv - parsed command line arguments. - * @return {Observable} - observable sequence. - */ -export default function linkCmd (cwd, argv) { - const names = argv._.slice(1) - - if (names.length) { - const localNodeModules = path.join(cwd, 'node_modules') - const init = mkdirp(localNodeModules) - return concatStatic(init, ArrayObservable.create(names) - ::mergeMap((name) => linkFromGlobal(cwd, name))) - } - - const init = concatStatic( - mkdirp(config.globalNodeModules), - mkdirp(config.globalBin)) - return concatStatic(init, linkToGlobal(cwd)) -} diff --git a/src/ping.js b/src/ping.js deleted file mode 100644 index c3b5f9e..0000000 --- a/src/ping.js +++ /dev/null @@ -1,12 +0,0 @@ -import url from 'url' -import {registry} from './config' -import {httpGetJSON} from './util' - -/** - * ping the pre-configured npm registry by hitting `/-/ping?write=true`. - * @return {Observable} - observable sequence of the returned JSON object. - */ -export function ping () { - const uri = url.resolve(registry, '-/ping?write=true') - return httpGetJSON(uri) -} diff --git a/src/ping_cmd.js b/src/ping_cmd.js deleted file mode 100644 index 96598c5..0000000 --- a/src/ping_cmd.js +++ /dev/null @@ -1,9 +0,0 @@ -import {ping} from './ping' - -/** - * ping the registry and print the received response. - * @return {Subscription} - subscription to the {@link ping} command. - */ -export default function pingCmd () { - return ping().subscribe(console.log) -} diff --git a/src/pkg_json.js b/src/pkg_json.js deleted file mode 100644 index bda52e1..0000000 --- a/src/pkg_json.js +++ /dev/null @@ -1,152 +0,0 @@ -import path from 'path' -import {ScalarObservable} from 'rxjs/observable/ScalarObservable' -import fromPairs from 'lodash.frompairs' -import {map} from 'rxjs/operator/map' -import {mergeMap} from 'rxjs/operator/mergeMap' -import {_catch} from 'rxjs/operator/catch' - -import * as util from './util' - -/** - * merge dependency fields. - * @param {Object} pkgJson - `package.json` object from which the dependencies - * should be obtained. - * @param {Array.} fields - property names of dependencies to be merged - * together. - * @return {Object} - merged dependencies. - */ -export function mergeDependencies (pkgJson, fields) { - const allDependencies = {} - for (let i = 0; i < fields.length; i++) { - const field = fields[i] - const dependencies = pkgJson[field] || {} - const names = Object.keys(dependencies) - for (let j = 0; j < names.length; j++) { - const name = names[j] - allDependencies[name] = dependencies[name] - } - } - return allDependencies -} - -/** - * extract an array of bundled dependency names from the passed in - * `package.json`. uses the `bundleDependencies` and `bundledDependencies` - * properties. - * @param {Object} pkgJson - plain JavaScript object representing a - * `package.json` file. - * @return {Array.} - array of bundled dependency names. - */ -export function parseBundleDependencies (pkgJson) { - const bundleDependencies = [] - .concat(pkgJson.bundleDependencies || []) - .concat(pkgJson.bundledDependencies || []) - return bundleDependencies -} - -/** - * extract specified dependencies from a specific `package.json`. - * @param {Object} pkgJson - plain JavaScript object representing a - * `package.json` file. - * @param {Array.} fields - array of dependency fields to be followed. - * @return {Array} - array of dependency pairs. - */ -export function parseDependencies (pkgJson, fields) { - // bundleDependencies and bundledDependencies are optional. we need to - // exclude those form the final [name, version] pairs that we're - // generating. - const bundleDependencies = parseBundleDependencies(pkgJson) - const allDependencies = mergeDependencies(pkgJson, fields) - const names = Object.keys(allDependencies) - const results = [] - for (let i = 0; i < names.length; i++) { - const name = names[i] - if (bundleDependencies.indexOf(name) === -1) { - results.push([name, allDependencies[name]]) - } - } - return results -} - -/** - * normalize the `bin` property in `package.json`, which could either be a - * string, object or undefined. - * @param {Object} pkgJson - plain JavaScript object representing a - * `package.json` file. - * @return {Object} - normalized `bin` property. - */ -export function normalizeBin (pkgJson) { - switch (typeof pkgJson.bin) { - case 'string': return ({[pkgJson.name]: pkgJson.bin}) - case 'object': return pkgJson.bin - default: return {} - } -} - -/** - * create an instance by reading a `package.json` from disk. - * @param {String} baseDir - base directory of the project. - * @return {Observabel} - an observable sequence of an `EntryDep`. - */ -export function fromFs (baseDir) { - const filename = path.join(baseDir, 'package.json') - return util.readFileJSON(filename) -} - -export function updatePkgJson (pkgJson, diff) { - const updatedPkgJson = {...pkgJson} - const fields = Object.keys(diff) - for (const field of fields) { - updatedPkgJson[field] = { - ...updatedPkgJson[field], - ...diff[field] - } - } - return updatedPkgJson -} - -export function save (baseDir) { - const filename = path.join(baseDir, 'package.json') - - return this - ::mergeMap((diff) => fromFs(baseDir) - ::_catch(() => ScalarObservable.create({})) - ::map((pkgJson) => updatePkgJson(pkgJson, diff)) - ) - ::map((pkgJson) => JSON.stringify(pkgJson, null, '\t')) - ::mergeMap((pkgJson) => util.writeFile(filename, pkgJson, 'utf8')) -} - -/** - * create an instance by parsing the explicit dependencies supplied via - * command line arguments. - * @param {String} baseDir - base directory of the project. - * @param {Array} argv - command line arguments. - * @return {Observabel} - an observable sequence of an `EntryDep`. - */ -export function fromArgv (baseDir, argv) { - const pkgJson = parseArgv(argv) - return ScalarObservable.create(pkgJson) -} - -/** - * parse the command line arguments and create the dependency field of a - * `package.json` file from it. - * @param {Array} argv - command line arguments. - * @return {NullPkgJSON} - package.json created from explicit dependencies - * supplied via command line arguments. - */ -export function parseArgv (argv) { - const names = argv._.slice(1) - - const nameVersionPairs = fromPairs(names.map((target) => { - const nameVersion = /^(@?.+?)(?:@(.+)?)?$/.exec(target) - return [nameVersion[1], nameVersion[2] || '*'] - })) - - const field = argv['save-dev'] ? 'devDependencies' - : argv['save-optional'] ? 'optionalDependencies' - : 'dependencies' - - return {[field]: nameVersionPairs} -} diff --git a/src/progress.js b/src/progress.js deleted file mode 100644 index f9db564..0000000 --- a/src/progress.js +++ /dev/null @@ -1,44 +0,0 @@ -import ora from 'ora' - -const spinner = ora({ - spinner: 'clock' -}) - -let completed = 0 -let added = 0 -let status = '' - -// start the spinner on startup -spinner.start() - -/** - * add one or more scheduled tasks. - * @param {Number} [n=1] - number of scheduled tasks. - */ -export function add (n = 1) { - added += n - report() -} - -/** - * complete a previously scheduled task. stop the spinner when there are no - * outstanding tasks. - * @param {Number} [n=1] - number of tasks that have been completed. - */ -export function complete (n = 1) { - completed += n - if (added === completed) spinner.stop() - else report() -} - -/** - * log the progress by updating the status message, percentage and spinner. - * @param {String} [_status] - optional (updated) status message. defaults to - * the previous status message. - * @see https://www.npmjs.org/package/ora - */ -export function report (_status = status) { - status = _status - const progress = Math.round((completed / added) * 100 * 100) / 100 - spinner.text = `${progress}% ${status}` -} diff --git a/src/registry.js b/src/registry.js deleted file mode 100644 index a7d6a4c..0000000 --- a/src/registry.js +++ /dev/null @@ -1,99 +0,0 @@ -import url from 'url' -import {map} from 'rxjs/operator/map' -import {_do} from 'rxjs/operator/do' -import {retry} from 'rxjs/operator/retry' -import {publishReplay} from 'rxjs/operator/publishReplay' -import {httpGet} from './util' -import assert from 'assert' - -/** - * default registry URL to be used. can be overridden via options on relevant - * functions. - * @type {String} - */ -export const DEFAULT_REGISTRY = 'https://registry.npmjs.org/' - -/** - * default number of retries to attempt before failing to resolve to a package - * @type {Number} - */ -export const DEFAULT_RETRIES = 5 - -/** - * register of pending and completed HTTP requests mapped to their respective - * observable sequences. - * @type {Object} - */ -export const requests = Object.create(null) - -/** - * clear the internal cache used for pending and completed HTTP requests. - */ -export function reset () { - const uris = Object.keys(requests) - for (const uri of uris) { - delete requests[uri] - } -} - -/** - * ensure that the registry responded with an accepted HTTP status code - * (`200`). - * @param {String} uri - URI used for retrieving the supplied response. - * @param {Number} resp - HTTP response object. - * @throws {assert.AssertionError} if the status code is not 200. - */ -export function checkStatus (uri, resp) { - const {statusCode, body: {error}} = resp - assert.equal(statusCode, 200, `error status code ${uri}: ${error}`) -} - -/** - * escape the given package name, which can then be used as part of the package - * root URL. - * @param {String} name - package name. - * @return {String} - escaped package name. - */ -export function escapeName (name) { - const isScoped = name.charAt(0) === '@' - const escapedName = isScoped - ? `@${encodeURIComponent(name.substr(1))}` - : encodeURIComponent(name) - return escapedName -} - -/** - * HTTP GET the resource at the supplied URI. if a request to the same URI has - * already been made, return the cached (pending) request. - * @param {String} uri - endpoint to fetch data from. - * @param {Object} [options = {}] - optional HTTP and `retries` options. - * @return {Observable} - observable sequence of pending / completed request. - */ -export function fetch (uri, options = {}) { - const {retries = DEFAULT_RETRIES, ...needleOptions} = options - const existingRequest = requests[uri] - - if (existingRequest) { - return existingRequest - } - const newRequest = httpGet(uri, needleOptions) - ::_do((resp) => checkStatus(uri, resp)) - ::retry(retries)::publishReplay().refCount() - requests[uri] = newRequest - return newRequest -} - -/** - * resolve a package defined via an ambiguous semantic version string to a - * specific `package.json` file. - * @param {String} name - package name. - * @param {String} version - semantic version string or tag name. - * @param {Object} options - HTTP request options. - * @return {Observable} - observable sequence of the `package.json` file. - */ -export function match (name, version, options = {}) { - const escapedName = escapeName(name) - const {registry = DEFAULT_REGISTRY, ...fetchOptions} = options - const uri = url.resolve(registry, `${escapedName}/${version}`) - return fetch(uri, fetchOptions)::map(({body}) => body) -} diff --git a/src/run_cmd.js b/src/run_cmd.js deleted file mode 100644 index acde388..0000000 --- a/src/run_cmd.js +++ /dev/null @@ -1,71 +0,0 @@ -import path from 'path' -import assert from 'assert' -import {readFile, entries} from './util' -import {concatMap} from 'rxjs/operator/concatMap' -import {Observable} from 'rxjs' -import {map} from 'rxjs/operator/map' -import {filter} from 'rxjs/operator/filter' -import {reduce} from 'rxjs/operator/reduce' -import {_do} from 'rxjs/operator/do' -import {spawn} from 'child_process' -import {sh, shFlag} from './config' - -/** - * run a `package.json` script and the related pre- and postscript. - * @param {String} cwd - current working directory. - * @param {Object} argv - command line arguments. - * @return {Observable} - observable sequence. - */ -export default function runCmd (cwd, argv) { - const scripts = argv._.slice(1) - - const pkgJson = readFile(path.join(cwd, 'package.json'), 'utf8') - ::map(JSON.parse) - - // if no script(s) have been specified, log out the available scripts. - if (!scripts.length) { - return pkgJson::_do(logAvailable) - } - - const PATH = [path.join(cwd, 'node_modules/.bin'), process.env.PATH] - .join(path.delimiter) - const env = {...process.env, PATH} - const runOptions = {env, stdio: 'inherit'} - - return pkgJson::map(({scripts = {}}) => scripts)::entries() // eslint-disable-line no-shadow - ::filter(([name]) => ~scripts.indexOf(name)) - ::concatMap(([name, script]) => run(script, runOptions) - ::map((code) => ([name, script, code]))) - ::_do(logCode) - ::reduce((codes, [name, script, code]) => codes + code, 0) - ::_do((code) => assert.equal(code, 0, 'exit status != 0')) -} - -export function run (script, options = {}) { - return Observable.create((observer) => { - const args = [shFlag, script] - const childProcess = spawn(sh, args, options) - childProcess.on('close', (code) => { - observer.next(code) - observer.complete() - }) - childProcess.on('error', (err) => { - observer.error(err) - }) - }) -} - -function logCode ([name, script, code]) { - const prefix = `${name}: \`${script}\`` - if (code === 0) console.log(`${prefix} succeeded`) - else console.error(`${prefix} failed (exit status ${code})`) -} - -function logAvailable ({scripts = {}}) { - const available = Object.keys(scripts) - if (available.length) { - console.log(`available scripts: ${available.join(', ')}`) - } else { - console.log('no scripts in package.json') - } -} diff --git a/src/shell_cmd.js b/src/shell_cmd.js deleted file mode 100644 index 5aaf8e1..0000000 --- a/src/shell_cmd.js +++ /dev/null @@ -1,21 +0,0 @@ -import path from 'path' -import * as config from './config' -import {spawn} from 'child_process' -import {readdir} from './util' -import {map} from 'rxjs/operator/map' -import {_do} from 'rxjs/operator/do' - -/** - * enter a new session that has access to the CLIs exposed by the installed - * packages by using an amended `PATH`. - * @param {String} cwd - current working directory. - */ -export default function shellCmd (cwd) { - const binPath = path.join(cwd, 'node_modules/.bin') - const env = Object.create(process.env) - env.PATH = [binPath, process.env.PATH].join(path.delimiter) - - return readdir(binPath) - ::_do(cmds => console.log('\nadded', cmds.join(', '), '\n')) - ::map(() => spawn(config.sh, [], {stdio: 'inherit', env})) -} diff --git a/src/unlink_cmd.js b/src/unlink_cmd.js deleted file mode 100644 index 6b3a67b..0000000 --- a/src/unlink_cmd.js +++ /dev/null @@ -1,20 +0,0 @@ -import {ArrayObservable} from 'rxjs/observable/ArrayObservable' -import {mergeMap} from 'rxjs/operator/mergeMap' -import {unlinkFromGlobal, unlinkToGlobal} from './link' - -/** - * unlink one or more previously linked dependencies. can be invoked via - * `ied unlink`. E.g. `ied unlink browserify tap webpack` would unlink all - * _three_ dependencies. - * @param {String} cwd - current working directory. - * @param {Object} argv - parsed command line arguments. - * @return {Observable} - observable sequence. - */ -export default function unlinkCmd (cwd, argv) { - const names = argv._.slice(1) - - return names.length - ? ArrayObservable.create(names) - ::mergeMap((name) => unlinkFromGlobal(cwd, name)) - : unlinkToGlobal(cwd) -} diff --git a/src/util.js b/src/util.js deleted file mode 100644 index 41bb78f..0000000 --- a/src/util.js +++ /dev/null @@ -1,132 +0,0 @@ -import {Observable} from 'rxjs/Observable' -import fs from 'fs' -import _mkdirp from 'mkdirp' -import _forceSymlink from 'force-symlink' -import needle from 'needle' -import {map} from 'rxjs/operator/map' -import {mergeMap} from 'rxjs/operator/mergeMap' -import * as config from './config' - -/** - * given an arbitrary asynchronous function that accepts a callback function, - * wrap the outer asynchronous function into an observable sequence factory. - * invoking the returned generated function is going to return a new **cold** - * observable sequence. - * @param {Function} fn - function to be wrapped. - * @param {thisArg} [thisArg] - optional context. - * @return {Function} - cold observable sequence factory. - */ -export function createObservableFactory (fn, thisArg) { - return (...args) => - Observable.create((observer) => { - fn.apply(thisArg, [...args, (error, ...results) => { - if (error) { - observer.error(error) - } else { - results.forEach(result => observer.next(result)) - observer.complete() - } - }]) - }) -} - -/** - * send a GET request to the given HTTP endpoint by passing the supplied - * arguments to [`needle`](https://www.npmjs.com/package/needle). - * @return {Observable} - observable sequence of a single response object. - */ -export function httpGet (...args) { - return Observable.create((observer) => { - needle.get(...args, (error, response) => { - if (error) observer.error(error) - else { - observer.next(response) - observer.complete() - } - }) - }) -} - -/** - * GETs JSON documents from an HTTP endpoint. - * @param {String} url - endpoint to which the GET request should be made - * @return {Object} An observable sequence of the fetched JSON document. - */ -export function httpGetJSON (url) { - return Observable.create((observer) => { - needle.get(url, config.httpOptions, (error, response) => { - if (error) observer.error(error) - else { - observer.next(response.body) - observer.complete() - } - }) - }) -} - -/** @type {Function} Observable wrapper function around `fs.readFile`. */ -export const readFile = createObservableFactory(fs.readFile, fs) - -/** @type {Function} Observable wrapper function around `fs.writeFile`. */ -export const writeFile = createObservableFactory(fs.writeFile, fs) - -/** @type {Function} Observable wrapper function around `fs.stat`. */ -export const stat = createObservableFactory(fs.stat, fs) - -/** @type {Function} Observable wrapper function around `fs.rename`. */ -export const rename = createObservableFactory(fs.rename, fs) - -/** @type {Function} Observable wrapper function around `fs.readlink`. */ -export const readlink = createObservableFactory(fs.readlink, fs) - -/** @type {Function} Observable wrapper function around `fs.readdir`. */ -export const readdir = createObservableFactory(fs.readdir, fs) - -/** @type {Function} Observable wrapper function around `fs.chmod`. */ -export const chmod = createObservableFactory(fs.chmod, fs) - -/** @type {Function} Observable wrapper function around `fs.unlink`. */ -export const unlink = createObservableFactory(fs.unlink, fs) - -/** @type {Function} Observable wrapper function around -[`force-symlink`](https://www.npmjs.org/package/force-symlink). */ -export const forceSymlink = createObservableFactory(_forceSymlink) - -/** @type {Function} Observable wrapper function around -[`mkdirp`](https://www.npmjs.com/package/mkdirp). */ -export const mkdirp = createObservableFactory(_mkdirp) - -/** - * equivalent to `Map#entries` for observables, but operates on objects. - * @return {Observable} - observable sequence of pairs. - */ -export function entries () { - return this::mergeMap((object) => { - const results = [] - const keys = Object.keys(object) - for (const key of keys) { - results.push([key, object[key]]) - } - return results - }) -} - -/** - * read a UTF8 encoded JSON file from disk. - * @param {String} file - filename to be used. - * @return {Observable} - observable sequence of a single object representing - * the read JSON file. - */ -export function readFileJSON (file) { - return readFile(file, 'utf8')::map(JSON.parse) -} - -/** - * set the terminal title using the required ANSI escape codes. - * @param {String} title - title to be set. - */ -export function setTitle (title) { - process.stdout.write( - `${String.fromCharCode(27)}]0;${title}${String.fromCharCode(7)}` - ) -} diff --git a/src/version_cmd.js b/src/version_cmd.js deleted file mode 100644 index de0e022..0000000 --- a/src/version_cmd.js +++ /dev/null @@ -1,15 +0,0 @@ -import {readFileJSON} from './util' -import path from 'path' -import {map} from 'rxjs/operator/map' -import {_do} from 'rxjs/operator/do' - -/** - * display the version number. - * @return {Subscription} - a subscription that logs the versio number to the - * console. - */ -export default function versionCmd () { - return readFileJSON(path.join(__dirname, '../package.json')) - ::map(({version}) => version) - ::_do((version) => console.log(`ied version ${version}`)) -} diff --git a/store.go b/store.go new file mode 100644 index 0000000..3d46161 --- /dev/null +++ b/store.go @@ -0,0 +1,125 @@ +package main + +import ( + "fmt" + "github.com/Sirupsen/logrus" + "github.com/hashicorp/go-multierror" + "os" + "path/filepath" + "sync" +) + +// UnresolvedError records an error from a failed package installation. +type UnresolvedError struct { + Pkg Pkg + name string + version string +} + +func (e UnresolvedError) Error() string { + return fmt.Sprintf("failed to resolve %s@%s", e.name, e.version) +} + +// Store keeps track of the currently installed dependencies. +type Store struct { + Pkgs map[string]Pkg + Resolver Resolver + Dir string + mutex sync.Mutex +} + +// NewStore creates a new store. +func NewStore(dir string, resolver Resolver) *Store { + return &Store{ + Pkgs: make(map[string]Pkg), + Dir: dir, + Resolver: resolver, + } +} + +// Init creates the store's base directory if it doesn't already exist. +func (s *Store) Init() error { + return os.MkdirAll(s.Dir, os.ModePerm) +} + +// Register adds a package to the store. Returns true if the package has been +// added. +func (s *Store) Register(pkg Pkg) (bool, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + id := pkg.ID() + if s.Pkgs[id] != nil { + return false, nil + } + + dir := filepath.Join(s.Dir, id, "node_modules") + err := os.MkdirAll(dir, os.ModePerm) + s.Pkgs[id] = pkg + return true, err +} + +func getTargetPath(dir string, to Pkg) string { + // This directory might not exist yet, but it doesn't have to, since it's + // where the symbolic link *points* to. + return filepath.Join(dir, to.ID(), "package") +} + +func getLinkPath(dir string, from Pkg, name string) string { + if from == nil { + return filepath.Join(dir, name) + } + prefix := filepath.Join(from.ID(), "node_modules") + return filepath.Join(dir, prefix, name) +} + +// Install recursively installs a package into the store. +func (s *Store) Install(from Pkg, name string, version string) error { + logrus.Infof("installing %s@%s", name, version) + + linkPath := getLinkPath(s.Dir, from, name) + pkg, err := s.Resolver.Resolve(linkPath, name, version) + if err != nil { + return err + } + if pkg == nil { + return UnresolvedError{pkg, name, version} + } + + targetPath := getTargetPath(s.Dir, pkg) + + if err := os.Symlink(targetPath, linkPath); err != nil { + // TODO Handle error (might already exist). + } + + if ok, err := s.Register(pkg); !ok || err != nil { + // Package is already being installed. + return err + } + + errs := make(chan error) + defer close(errs) + + deps := pkg.Deps() + + install := func(name, version string) { + errs <- s.Install(pkg, name, version) + } + + // Install sub-dependency. + for name, version := range deps { + go install(name, version) + } + + // Download dependency. + go func() { errs <- pkg.DownloadInto(s.Dir) }() + + var result *multierror.Error + for i := 0; i < len(deps)+1; i++ { + if err := <-errs; err != nil { + result = multierror.Append(result, err) + } + } + + return result.ErrorOrNil() +} diff --git a/test/.eslintrc b/test/.eslintrc deleted file mode 100644 index 8c12db9..0000000 --- a/test/.eslintrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "./../.eslintrc", - "parser": "babel-eslint", - "env": { - mocha: true - }, - "rules": { - "func-names": "off", - "no-underscore-dangle": "off" - } -} diff --git a/test/e2e/install.e2e.js b/test/e2e/install.e2e.js deleted file mode 100644 index 0267b91..0000000 --- a/test/e2e/install.e2e.js +++ /dev/null @@ -1,191 +0,0 @@ -import assert from 'assert' -import {spawn} from 'child_process' -import path from 'path' -import mkdirp from 'mkdirp' -import resolve from 'resolve' -import rimraf from 'rimraf' - -const targets = [ - 'browserify', - 'express', - 'karma', - 'bower', - 'cordova', - 'coffee-script', - 'gulp', - 'forever', - 'grunt', - 'less', - 'tape' -] - -const buildTargets = [ - 'dtrace-provider' -] - -const localTargets = [ - 'mocha@file:../../../node_modules/mocha' -] - -const hostedTargets = [ - 'gulp@github:gulpjs/gulp#4.0', - 'double-ended-queue@git+https://github.com/petkaantonov/deque.git', - 'bitbucket-api@git+https://bitbucket.org/hgarcia/node-bitbucket-api.git' -] - -const base = path.join(__dirname, '../../.tmp/test') -const ied = path.join(__dirname, '../../lib/cmd') - -describe('e2e install', () => { - targets.forEach((target) => { - describe(`ied install ${target}`, function () { - const cwd = path.join(base, target) - this.timeout(60 * 1000) - - before((done) => { - rimraf(cwd, done) - }) - - before((done) => { - mkdirp(cwd, done) - }) - - before((done) => { - spawn('node', [ied, 'install', target], { - cwd, - stdio: 'inherit' - }) - .on('error', done) - .on('close', (code) => { - assert.equal(code, 0) - done() - }) - }) - - it(`should make ${target} require\'able`, (done) => { - resolve(target, {basedir: cwd}, (err, res) => { - assert.ifError(err) - assert.notEqual(res.indexOf(cwd), -1) - require(res) - done() - }) - }) - }) - }) -}) - -describe('e2e install & build', () => { - buildTargets.forEach((target) => { - describe(`ied install ${target} --build`, function () { - const cwd = path.join(base, target) - this.timeout(60 * 1000) - - before((done) => { - rimraf(cwd, done) - }) - - before((done) => { - mkdirp(cwd, done) - }) - - before((done) => { - spawn('node', [ied, 'install', '--build', target], { - cwd, - stdio: 'inherit' - }) - .on('error', done) - .on('close', (code) => { - assert.equal(code, 0) - done() - }) - }) - - it(`should make ${target} require\'able`, (done) => { - resolve(target, {basedir: cwd}, (err, res) => { - assert.ifError(err) - assert.notEqual(res.indexOf(cwd), -1) - require(res) - done() - }) - }) - }) - }) -}) - -describe('e2e local install', () => { - localTargets.forEach((target) => { - describe(`ied install ${target}`, function () { - const [name] = target.split('@') - const cwd = path.join(base, name) - this.timeout(60 * 1000) - - before((done) => { - rimraf(cwd, done) - }) - - before((done) => { - mkdirp(cwd, done) - }) - - before((done) => { - spawn('node', [ied, 'install', target], { - cwd, - stdio: 'inherit' - }) - .on('error', done) - .on('close', (code) => { - assert.equal(code, 0) - done() - }) - }) - - it(`should make ${name} require\'able`, (done) => { - resolve(name, {basedir: cwd}, (err, res) => { - assert.ifError(err) - assert.notEqual(res.indexOf(cwd), -1) - require(res) - done() - }) - }) - }) - }) -}) - -describe('e2e hosted install', () => { - hostedTargets.forEach((target) => { - describe(`ied install ${target}`, function () { - const [name] = target.split('@') - const cwd = path.join(base, name) - this.timeout(60 * 1000) - - before((done) => { - rimraf(cwd, done) - }) - - before((done) => { - mkdirp(cwd, done) - }) - - before((done) => { - spawn('node', [ied, 'install', target], { - cwd, - stdio: 'inherit' - }) - .on('error', done) - .on('close', (code) => { - assert.equal(code, 0) - done() - }) - }) - - it(`should make ${name} require\'able`, (done) => { - resolve(name, {basedir: cwd}, (err, res) => { - assert.ifError(err) - assert.notEqual(res.indexOf(cwd), -1) - require(res) - done() - }) - }) - }) - }) -}) diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index 017a516..0000000 --- a/test/mocha.opts +++ /dev/null @@ -1,3 +0,0 @@ ---ui bdd ---compilers js:babel-register ---recursive diff --git a/test/spec/cache.spec.js b/test/spec/cache.spec.js deleted file mode 100644 index bd19e5c..0000000 --- a/test/spec/cache.spec.js +++ /dev/null @@ -1,114 +0,0 @@ -import assert from 'assert' -import fs from 'fs' -import sinon from 'sinon' -import stream from 'stream' -import tar from 'tar-fs' -import path from 'path' -import uuid from 'node-uuid' -import {Observable} from 'rxjs/Observable' - -import * as cache from '../../src/cache' -import * as config from '../../src/config' -import * as util from '../../src/util' - -describe('cache', () => { - const sandbox = sinon.sandbox.create() - - afterEach(() => sandbox.restore()) - - describe('init', () => { - it('should return Observable', () => { - assert.equal(cache.init().constructor, Observable) - }) - - it('should mkdirp the cache directory', () => { - const o = Observable.create() - sandbox.stub(util, 'mkdirp').returns(o) - cache.init() - sinon.assert.calledOnce(util.mkdirp) - sinon.assert.calledWithExactly(util.mkdirp, path.join(config.cacheDir, '.tmp')) - }) - }) - - describe('write', () => { - it('should open a WriteStream to random temporary location in cacheDir', () => { - const writeStream = {} - const randomId = '123' - sandbox.stub(uuid, 'v4').returns(randomId) - sandbox.stub(fs, 'createWriteStream').returns(writeStream) - assert.equal(cache.write(), writeStream, 'should return result of fs.createWriteStream') - sinon.assert.calledWithExactly(fs.createWriteStream, `${config.cacheDir}/.tmp/${randomId}`) - }) - }) - - describe('getTmp', () => { - it('should return a temporary directory', () => { - const randomId = '123' - sandbox.stub(uuid, 'v4').returns(randomId) - const tmpDir = path.join(config.cacheDir, '.tmp') - assert.ok(cache.getTmp().match(tmpDir)) - assert.ok(cache.getTmp(), path.join(tmpDir, randomId)) - }) - }) - - describe('read', () => { - it('should open a ReadStream to specified shasum in cacheDir', () => { - const readStream = {} - const shasum = '5e2f6970611f079c7cf857de1dc7aa1b480de7a5' - sandbox.stub(fs, 'createReadStream').returns(readStream) - assert.equal(cache.read(shasum), readStream, 'should return result of fs.createReadStream') - sinon.assert.calledWithExactly(fs.createReadStream, `${config.cacheDir}/${shasum}`) - }) - }) - - describe('extract', () => { - it('should return cold Observable', () => { - sandbox.spy(cache, 'read') - const shasum = '5e2f6970611f079c7cf857de1dc7aa1b480de7a5' - assert(cache.extract('./', shasum).constructor, Observable) - sinon.assert.notCalled(cache.read) - }) - - context('when cache.read read stream emits an error', () => { - it('should throw an error', () => { - const readStream = new stream.Readable() - sandbox.stub(cache, 'read').returns(readStream) - const expectedError = new Error() - - const next = sandbox.stub() - const error = sandbox.stub() - const complete = sandbox.stub() - - cache.extract().subscribe(next, error, complete) - readStream.emit('error', expectedError) - - sinon.assert.notCalled(next) - sinon.assert.notCalled(complete) - sinon.assert.calledOnce(error) - sinon.assert.calledWithExactly(error, expectedError) - }) - }) - - context('when tar.extract read stream emits an error', () => { - it('should thorw an error', () => { - const readStream = new stream.Readable() - sandbox.stub(cache, 'read').returns(new stream.Readable()) - sandbox.stub(tar, 'extract').returns(readStream) - - const expectedError = new Error() - - const next = sandbox.stub() - const error = sandbox.stub() - const complete = sandbox.stub() - - cache.extract().subscribe(next, error, complete) - readStream.emit('error', expectedError) - - sinon.assert.notCalled(next) - sinon.assert.notCalled(complete) - sinon.assert.calledOnce(error) - sinon.assert.calledWithExactly(error, expectedError) - }) - }) - }) -}) diff --git a/test/spec/config.spec.js b/test/spec/config.spec.js deleted file mode 100644 index 2eaa276..0000000 --- a/test/spec/config.spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import assert from 'assert' -import * as config from '../../src/config' - -describe('config', () => { - describe('isWindows', () => { - it('should be boolean', () => { - assert.equal(typeof config.isWindows, 'boolean') - }) - }) - - describe('home', () => { - it('should be string', () => { - assert.equal(typeof config.home, 'string') - }) - }) - - describe('registry', () => { - it('should be string', () => { - assert.equal(typeof config.registry, 'string') - }) - }) - - describe('cacheDir', () => { - it('should be string', () => { - assert.equal(typeof config.cacheDir, 'string') - }) - }) - - describe('globalNodeModules', () => { - it('should be string', () => { - assert.equal(typeof config.globalNodeModules, 'string') - }) - }) - - describe('globalBin', () => { - it('should be string', () => { - assert.equal(typeof config.globalBin, 'string') - }) - }) - - describe('httpProxy', () => { - it('should be string or null', () => { - if (config.httpProxy) { - assert.equal(typeof config.httpProxy, 'string') - } else { - assert.equal(config.httpProxy, null) - } - }) - }) - - describe('httpsProxy', () => { - it('should be string or null', () => { - if (config.httpProxy) { - assert.equal(typeof config.httpsProxy, 'string') - } else { - assert.equal(config.httpsProxy, null) - } - }) - }) - - describe('retries', () => { - it('should be number', () => { - assert.equal(typeof config.retries, 'number') - }) - }) - - describe('sh', () => { - it('should be string', () => { - assert.equal(typeof config.sh, 'string') - }) - }) -}) diff --git a/test/spec/config_cmd.spec.js b/test/spec/config_cmd.spec.js deleted file mode 100644 index 0c40c64..0000000 --- a/test/spec/config_cmd.spec.js +++ /dev/null @@ -1,18 +0,0 @@ -import sinon from 'sinon' -import * as config from '../../src/config' -import configCmd from '../../src/config_cmd' - -describe('configCmd', () => { - const sandbox = sinon.sandbox.create() - - afterEach(() => sandbox.restore()) - - it('should print all config variables', () => { - sandbox.stub(console, 'log') - configCmd() - Object.keys(config).forEach(key => { - sinon.assert.calledWith(console.log, sinon.match(key)) - sinon.assert.calledWith(console.log, sinon.match(String(config[key]))) - }) - }) -}) diff --git a/test/spec/init_cmd.spec.js b/test/spec/init_cmd.spec.js deleted file mode 100644 index d5ed8f1..0000000 --- a/test/spec/init_cmd.spec.js +++ /dev/null @@ -1,8 +0,0 @@ -import assert from 'assert' -import initCmd from '../../src/init_cmd' - -describe('initCmd', () => { - it('should be a function', () => { - assert.equal(typeof initCmd, 'function') - }) -}) diff --git a/test/spec/link.spec.js b/test/spec/link.spec.js deleted file mode 100644 index 432f8d3..0000000 --- a/test/spec/link.spec.js +++ /dev/null @@ -1,76 +0,0 @@ -import assert from 'assert' -import sinon from 'sinon' -import * as config from '../../src/config' -import * as link from '../../src/link' - -describe('link', () => { - const sandbox = sinon.sandbox.create() - - afterEach(() => sandbox.restore()) - - describe('getSymlinks', () => { - const scenarios = [ - [ - { - name: 'tap' - }, - [ - ['/cwd', `${config.globalNodeModules}/tap`] - ] - ], - [ - { - name: 'tap', - bin: 'cmd.js' - }, - [ - ['/cwd', `${config.globalNodeModules}/tap`], - [`${config.globalNodeModules}/tap/cmd.js`, `${config.globalBin}/tap`] - ] - ], - [ - { - name: 'tap', - bin: {tap: 'bin/cmd'} - }, - [ - ['/cwd', `${config.globalNodeModules}/tap`], - [`${config.globalNodeModules}/tap/bin/cmd`, `${config.globalBin}/tap`] - ] - ], - [ - { - name: 'tap', - bin: { - tap: 'bin/cmd', - tap2: 'bin/cmd2' - } - }, - [ - ['/cwd', `${config.globalNodeModules}/tap`], - [`${config.globalNodeModules}/tap/bin/cmd`, `${config.globalBin}/tap`], - [`${config.globalNodeModules}/tap/bin/cmd2`, `${config.globalBin}/tap2`] - ] - ], - [ - { - name: 'tap', - bin: {tap: 'bin/cmd'} - }, - [ - ['/cwd', `${config.globalNodeModules}/tap`], - [`${config.globalNodeModules}/tap/bin/cmd`, `${config.globalBin}/tap`] - ] - ] - ] - - scenarios.forEach(([bin, expected]) => { - context(`when pkgJSON = ${JSON.stringify(bin)}`, () => { - it(`should return ${JSON.stringify(expected)}`, () => { - const actual = link.getSymlinks('/cwd', bin) - assert.deepEqual(actual, expected) - }) - }) - }) - }) -}) diff --git a/test/spec/pkg_json.spec.js b/test/spec/pkg_json.spec.js deleted file mode 100644 index d00a13b..0000000 --- a/test/spec/pkg_json.spec.js +++ /dev/null @@ -1,208 +0,0 @@ -import assert from 'assert' -import sinon from 'sinon' -import {ScalarObservable} from 'rxjs/observable/ScalarObservable' -import * as util from '../../src/util' -import * as pkgJson from '../../src/pkg_json' - -describe('pkgJson', () => { - const sandbox = sinon.sandbox.create() - - afterEach(() => sandbox.restore()) - - describe('mergeDependencies', () => { - it('should merge dependency fields', () => { - const _pkgJson = { - dependencies: { - tap: '1.0.1', - browserify: '1.0.9' - }, - devDependencies: { - tap: '2.5.0', - 'is-array': '2.1.2' - } - } - const fields = ['dependencies', 'devDependencies'] - const result = pkgJson.mergeDependencies(_pkgJson, fields) - assert.deepEqual(result, { - tap: '2.5.0', - browserify: '1.0.9', - 'is-array': '2.1.2' - }) - }) - }) - - describe('parseBundleDependencies', () => { - it('should return array of bundled dependencies', () => { - const _pkgJson = { - bundleDependencies: ['tap', 'is-array'], - bundledDependencies: ['browserify'] - } - const result = pkgJson.parseBundleDependencies(_pkgJson) - assert.deepEqual(result, ['tap', 'is-array', 'browserify']) - }) - }) - - describe('parseDependencies', () => { - it('should extract specified dependencies', () => { - const _pkgJson = { - dependencies: { - tap: '1.0.1', - browserify: '1.0.9' - }, - devDependencies: { - tap: '2.5.0', - 'is-array': '2.1.2' - } - } - const fields = ['dependencies', 'devDependencies'] - const result = pkgJson.parseDependencies(_pkgJson, fields) - assert.deepEqual(result, [ - ['tap', '2.5.0'], - ['browserify', '1.0.9'], - ['is-array', '2.1.2'] - ]) - }) - - it('should ignore bundled dependencies', () => { - const _pkgJson = { - dependencies: { - tap: '1.0.1', - browserify: '1.0.9', - 'is-array': '2.1.2' - }, - bundleDependencies: ['tap'], - bundledDependencies: ['is-array'] - } - const fields = ['dependencies'] - const result = pkgJson.parseDependencies(_pkgJson, fields) - assert.deepEqual(result, [['browserify', '1.0.9']]) - }) - }) - - describe('normalizeBin', () => { - context('when bin is a string', () => { - it('should set name as key', () => { - const name = 'some-package' - const bin = 'some-file.js' - const _pkgJson = {name, bin} - const result = pkgJson.normalizeBin(_pkgJson) - assert.deepEqual(result, {[name]: bin}) - }) - }) - - context('when bin is an object', () => { - it('should return bin', () => { - const bin = {'some-command': 'some-file.js'} - const _pkgJson = {bin} - const result = pkgJson.normalizeBin(_pkgJson) - assert.deepEqual(result, bin) - }) - }) - - context('when bin is undefined', () => { - it('should return empty object', () => { - const _pkgJson = {} - const result = pkgJson.normalizeBin(_pkgJson) - assert.deepEqual(result, {}) - }) - }) - - context('when bin is of some other type', () => { - it('should return empty object', () => { - const _pkgJson = {bin: 123} - const result = pkgJson.normalizeBin(_pkgJson) - assert.deepEqual(result, {}) - }) - }) - }) - - describe('fromFs', () => { - it('should read in package.json', () => { - const _pkgJson = {name: 'some-package'} - const readStub = sandbox.stub(util, 'readFileJSON') - readStub.returns(ScalarObservable.create(_pkgJson)) - const next = sinon.spy() - const error = sinon.spy() - const complete = sinon.spy() - pkgJson.fromFs('/some/dir').subscribe(next, error, complete) - sinon.assert.calledOnce(next) - sinon.assert.notCalled(error) - sinon.assert.calledOnce(complete) - sinon.assert.calledWith(next, _pkgJson) - sinon.assert.calledWith(readStub, '/some/dir/package.json') - }) - }) - - describe('updatePkgJson', () => { - it('should patch package.json', () => { - const dependencies = {tap: '1.0.0', 'is-array': '2.0.0'} - const devDependencies = {browserify: '1.0.0', express: '0.0.1'} - const _pkgJson = {dependencies, devDependencies} - const diff = { - dependencies: { - ava: '1.0.0' - }, - devDependencies: { - browserify: '2.0.0', - connect: '0.0.1' - } - } - const result = pkgJson.updatePkgJson(_pkgJson, diff) - assert.notEqual(result, _pkgJson) - assert.deepEqual(result, { - dependencies: { - ava: '1.0.0', - 'is-array': '2.0.0', - tap: '1.0.0' - }, - devDependencies: { - browserify: '2.0.0', - connect: '0.0.1', - express: '0.0.1' - } - }) - }) - }) - - describe('fromArgv', () => { - it('should return ScalarObservable', () => { - const result = pkgJson.fromArgv('/', {_: ['install', 'tap']}) - assert.ok(result._isScalar) - }) - - it('should create pkgJSON by parsing argv', () => { - const _pkgJson = {dependencies: {}} - sandbox.stub(pkgJson, 'parseArgv').returns(_pkgJson) - const next = sinon.spy() - const error = sinon.spy() - const complete = sinon.spy() - pkgJson.fromArgv('/cwd', {_: []}) - .subscribe(next, error, complete) - sinon.assert.calledOnce(next) - sinon.assert.calledWith(next, _pkgJson) - sinon.assert.notCalled(error) - sinon.assert.calledOnce(complete) - }) - }) - - describe('parseArgv', () => { - context('when --save-dev', () => { - it('should add to devDependencies', () => { - const result = pkgJson.parseArgv({_: [null, 'tap@1.0.0'], 'save-dev': true}) - assert.deepEqual(result, {devDependencies: {tap: '1.0.0'}}) - }) - }) - context('when --save-optional', () => { - it('should add to optionalDependencies', () => { - const result = pkgJson.parseArgv({_: [null, 'tap@1.0.0'], 'save-optional': true}) - assert.deepEqual(result, {optionalDependencies: {tap: '1.0.0'}}) - }) - }) - context('when --save', () => { - it('should add to dependencies', () => { - const result = pkgJson.parseArgv({_: [null, 'tap@1.0.0'], save: true}) - assert.deepEqual(result, {dependencies: {tap: '1.0.0'}}) - }) - }) - }) -}) diff --git a/test/spec/registry.spec.js b/test/spec/registry.spec.js deleted file mode 100644 index c9878da..0000000 --- a/test/spec/registry.spec.js +++ /dev/null @@ -1,89 +0,0 @@ -import sinon from 'sinon' -import url from 'url' -import assert from 'assert' -import {EmptyObservable} from 'rxjs/observable/EmptyObservable' -import * as registry from '../../src/registry' - -describe('registry', () => { - const sandbox = sinon.sandbox.create() - - afterEach(() => sandbox.restore()) - afterEach(() => registry.reset()) - - describe('DEFAULT_REGISTRY', () => { - it('should be HTTPS URL', () => { - assert.equal(typeof registry.DEFAULT_REGISTRY, 'string') - assert.equal(url.parse(registry.DEFAULT_REGISTRY).protocol, 'https:') - }) - }) - - describe('DEFAULT_RETRIES', () => { - it('should be a number', () => { - assert.equal(typeof registry.DEFAULT_RETRIES, 'number') - assert(registry.DEFAULT_RETRIES >= 0) - }) - }) - - describe('escapeName', () => { - context('when name is scoped', () => { - it('should preserve "@"', () => { - const escapedName = registry.escapeName('@hello/world') - assert.equal(escapedName, '@hello%2Fworld') - }) - }) - context('when name is not scoped', () => { - it('should be identity', () => { - const escapedName = registry.escapeName('world') - assert.equal(escapedName, 'world') - }) - }) - }) - - describe('checkStatus', () => { - context('when statusCode is not 200', () => { - it('should throw an error', () => { - const response = {statusCode: 400, body: {error: 'Some error'}} - assert.throws(() => { - registry.checkStatus('http://example.com', response) - }) - }) - }) - context('when statusCode is 200', () => { - it('should not throw an error', () => { - const response = {statusCode: 200, body: {}} - registry.checkStatus('http://example.com', response) - }) - }) - }) - - describe('requests', () => { - it('should be a clean object', () => { - assert.deepEqual(registry.requests, Object.create(null)) - }) - }) - - describe('reset', () => { - context('when requests have been cached', () => { - beforeEach(() => { - registry.requests['http://example.com'] = {} - registry.requests['http://example2.com'] = {} - }) - it('should delete all cached requests', () => { - registry.reset() - assert.deepEqual(registry.requests, Object.create(null)) - }) - }) - }) - - describe('fetch', () => { - context('when request has already been made', () => { - it('should return pending request', () => { - const uri = 'https://example.com/example' - const pendingRequest = EmptyObservable.create() - registry.requests[uri] = pendingRequest - const request = registry.fetch(uri) - assert.equal(request, pendingRequest) - }) - }) - }) -}) diff --git a/test/spec/shell_cmd.spec.js b/test/spec/shell_cmd.spec.js deleted file mode 100644 index 1462346..0000000 --- a/test/spec/shell_cmd.spec.js +++ /dev/null @@ -1,50 +0,0 @@ -import assert from 'assert' -import sinon from 'sinon' -import shellCmd from '../../src/shell_cmd' -import childProcess from 'child_process' -import * as config from '../../src/config' -import * as util from '../../src/util' -import {ScalarObservable} from 'rxjs/observable/ScalarObservable' - -describe('shellCmd', () => { - const sandbox = sinon.sandbox.create() - afterEach(() => sandbox.restore()) - - beforeEach(() => { - sandbox.stub(childProcess, 'spawn') - sandbox.stub(util, 'readdir') - sandbox.spy(console, 'log') - }) - - it('should spawn child process', () => { - util.readdir.returns(ScalarObservable.create([])) - shellCmd('/cwd').subscribe() - - sinon.assert.calledOnce(childProcess.spawn) - sinon.assert.calledWith(childProcess.spawn, config.sh, [], { - stdio: 'inherit', - env: sinon.match.has('PATH') - }) - - const {env: {PATH}} = childProcess.spawn.getCall(0).args[2] - assert.equal(PATH.indexOf('/cwd/node_modules/.bin:'), 0) - }) - - it('should add node_modules/.bin to PATH', () => { - util.readdir.returns(ScalarObservable.create([])) - shellCmd('/cwd').subscribe() - - const {env: {PATH}} = childProcess.spawn.getCall(0).args[2] - assert.equal(PATH.indexOf('/cwd/node_modules/.bin:'), 0) - }) - - it('should log available commands', () => { - const cmds = ['browserify', 'tape', 'npm'] - util.readdir.returns(ScalarObservable.create(cmds)) - shellCmd('/cwd').subscribe() - const out = console.log.getCall(0).args.join(' ') - for (const cmd of cmds) { - assert.notEqual(out.indexOf(cmd), -1, `should log ${cmd}`) - } - }) -}) diff --git a/test/spec/util.spec.js b/test/spec/util.spec.js deleted file mode 100644 index 9eafc1a..0000000 --- a/test/spec/util.spec.js +++ /dev/null @@ -1,107 +0,0 @@ -import sinon from 'sinon' -import assert from 'assert' -import * as util from '../../src/util' - -describe('util', () => { - const sandbox = sinon.sandbox.create() - afterEach(() => sandbox.restore()) - - describe('createObservableFactory', () => { - context('when wrapping successful callback with arity 1', () => { - it('should callback a single argument', () => { - const fn = sandbox.stub().yields(null, 'some data') - const createObservable = util.createObservableFactory(fn) - const next = sandbox.spy() - const error = sandbox.spy() - const complete = sandbox.spy() - createObservable('endpoint').subscribe(next, error, complete) - sinon.assert.calledWithExactly(next, 'some data') - sinon.assert.calledOnce(next) - sinon.assert.calledOnce(complete) - sinon.assert.notCalled(error) - sinon.assert.calledOnce(fn) - sinon.assert.calledWith(fn, 'endpoint') - }) - }) - - context('when callback is being called with error', () => { - it('should handle callback errors', () => { - const errorObject = new Error() - const fn = sandbox.stub().yields(errorObject) - const createObservable = util.createObservableFactory(fn) - const next = sandbox.spy() - const error = sandbox.spy() - const complete = sandbox.spy() - createObservable('endpoint').subscribe(next, error, complete) - sinon.assert.calledWithExactly(error, errorObject) - sinon.assert.calledOnce(error) - sinon.assert.notCalled(complete) - sinon.assert.notCalled(next) - sinon.assert.calledOnce(fn) - }) - }) - }) - - - describe('readFile', () => { - it('should be an exporter function', () => { - assert.equal(typeof util.readFile, 'function') - }) - }) - - describe('writeFile', () => { - it('should be an exporter function', () => { - assert.equal(typeof util.writeFile, 'function') - }) - }) - - describe('stat', () => { - it('should be an exporter function', () => { - assert.equal(typeof util.stat, 'function') - }) - }) - - describe('rename', () => { - it('should be an exporter function', () => { - assert.equal(typeof util.rename, 'function') - }) - }) - - describe('readlink', () => { - it('should be an exporter function', () => { - assert.equal(typeof util.readlink, 'function') - }) - }) - - describe('chmod', () => { - it('should be an exporter function', () => { - assert.equal(typeof util.chmod, 'function') - }) - }) - - describe('forceSymlink', () => { - it('should be an exporter function', () => { - assert.equal(typeof util.forceSymlink, 'function') - }) - }) - - describe('mkdirp', () => { - it('should be an exporter function', () => { - assert.equal(typeof util.mkdirp, 'function') - }) - }) - - describe('setTitle', () => { - it('should set terminal title', () => { - const title = 'some title' - sandbox.stub(process.stdout, 'write') - util.setTitle(title) - const exepctedTitle = `${String.fromCharCode(27)}]0;${title}${String.fromCharCode(7)}` - sinon.assert.calledOnce(process.stdout.write) - sinon.assert.calledWithExactly(process.stdout.write, exepctedTitle) - - // otherwise it won't be printed - sandbox.restore() - }) - }) -}) diff --git a/util.go b/util.go new file mode 100644 index 0000000..9e13961 --- /dev/null +++ b/util.go @@ -0,0 +1,51 @@ +package main + +import ( + "archive/tar" + "io" + "os" + "path/filepath" +) + +// Untar unpacks a tarball. +func Untar(dir string, reader *tar.Reader) error { + header, err := reader.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + path := filepath.Join(dir, header.Name) + + if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { + return err + } + + switch header.Typeflag { + case tar.TypeDir: + // Create directory. + if err := os.Mkdir(path, os.ModePerm); err != nil { + return err + } + case tar.TypeReg: + // Create file. + writer, err := os.Create(path) + if err != nil { + return err + } + + // Use happy path. We need to close the writer either way. + io.Copy(writer, reader) + writer.Close() + + // Fix file permission. + // TODO Make files executable + if err := os.Chmod(path, os.ModePerm); err != nil { + return err + } + } + + return Untar(dir, reader) +}