diff --git a/LICENSE b/LICENSE index 87cdf8b..96d744b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Eric Clemmons +Copyright (c) 2016 Eric Clemmons Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +19,3 @@ 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. - diff --git a/README.md b/README.md index 72638b3..6404ab8 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,21 @@ -# npm-install-loader +# npm-install-webpack-plugin -> Webpack loader to automatically npm install & save dependencies. +> Webpack plugin that automatically **installs & saves missing dependencies** +> while you work! +Seamless works with: +- [x] Javascript + (e.g. `require`, `import`) +- [x] CSS + (e.g. `@import "~bootstrap"`) +- [x] Webpack loaders + (e.g. `babel-loader`, `file-loader`, etc.) -[![travis build](https://img.shields.io/travis/ericclemmons/npm-install-loader.svg)](https://travis-ci.org/ericclemmons/npm-install-loader) -[![Coverage Status](https://coveralls.io/repos/ericclemmons/npm-install-loader/badge.svg?branch=master&service=github)](https://coveralls.io/github/ericclemmons/npm-install-loader?branch=master) -[![version](https://img.shields.io/npm/v/npm-install-loader.svg)](http://npm.im/npm-install-loader) -[![downloads](https://img.shields.io/npm/dm/npm-install-loader.svg)](http://npm-stat.com/charts.html?package=npm-install-loader) -[![MIT License](https://img.shields.io/npm/l/npm-install-loader.svg)](http://opensource.org/licenses/MIT) +[![travis build](https://img.shields.io/travis/ericclemmons/npm-install-webpack-plugin.svg)](https://travis-ci.org/ericclemmons/npm-install-webpack-plugin) +[![Coverage Status](https://coveralls.io/repos/ericclemmons/npm-install-webpack-plugin/badge.svg?branch=master&service=github)](https://coveralls.io/github/ericclemmons/npm-install-webpack-plugin?branch=master) +[![version](https://img.shields.io/npm/v/npm-install-webpack-plugin.svg)](http://npm.im/npm-install-webpack-plugin) +[![downloads](https://img.shields.io/npm/dm/npm-install-webpack-plugin.svg)](http://npm-stat.com/charts.html?package=npm-install-webpack-plugin) +[![MIT License](https://img.shields.io/npm/l/npm-install-webpack-plugin.svg)](http://opensource.org/licenses/MIT) - - - @@ -18,59 +26,42 @@ build script & server just to install a dependency you didn't know you needed until now. Instead, use `require` or `import` how you normally would and `npm install` -will happen automatically install missing dependencies between reloads. +will happen **automatically install & save missing dependencies** while you work! ### Usage In your `webpack.config.js`: ```js -module: { - postLoaders: [ - { - exclude: /node_modules/, - loader: "npm-install-loader", - test: /\.js$/, - }, - ], -} +plugins: [ + new NpmInstallPlugin(), +], ``` -This will ensure that any other loaders -(e.g. `eslint-loader`, `babel-loader`, etc.) have completed. - -### Saving - -This loader simply runs `npm install [modules]`. - -I recommend creating an `.npmrc` file -in the root of your project with: +**If you have an `.npmrc` file in your project, +those arguments will be used:** -```ini +``` save=true +save-exact=true ``` -This will automatically save any dependencies anytime you run `npm install` (no need to pass `--save`). - -**Alternatively**, you can provide CLI arguments that get added directly to `npm install`: +Alternatively, you can provide your own arguments to `npm install`: ```js -postLoaders: [ - { - exclude: /node_modules/, - loader: "npm-install-loader", - query: { - cli: { - registry: "..." // --registry='...' - save: true, // --save - saveExact: true, // --save-exact - }, - }, - test: /\.js$/, - }, +plugins: [ + new NpmInstallPlugin({ + ... + cacheMin: 999999 // --cache-min=999999 (prefer NPM cached version) + registry: "..." // --registry="..." + save: true, // --save + saveDev: true, // --save-dev + saveExact: true, // --save-exact + ... + }), ], - +``` ### License -> MIT License 2015 © Eric Clemmons +> MIT License 2016 © Eric Clemmons diff --git a/example/.babelrc b/example/.babelrc new file mode 100644 index 0000000..add9397 --- /dev/null +++ b/example/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": ["react", "es2015", "stage-0"], + "env": { + "development": { + "presets": ["react-hmre"] + } + } +} diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1 @@ +build diff --git a/example/.npmrc b/example/.npmrc new file mode 100644 index 0000000..b37ac74 --- /dev/null +++ b/example/.npmrc @@ -0,0 +1,3 @@ +cache-min=9999999 +save=true +save-exact=true diff --git a/example/.nvmrc b/example/.nvmrc new file mode 100644 index 0000000..268fccb --- /dev/null +++ b/example/.nvmrc @@ -0,0 +1 @@ +v5.5.0 diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..681d361 --- /dev/null +++ b/example/README.md @@ -0,0 +1,29 @@ +# `npm-install-webpack-plugin` Example + +> This example shows how it's possible to start with a minimal project setup +> and rapidly develop by auto-installing both Javascript & CSS dependencies +> without ever leaving the editor. + +## Usage + +It's recommended to install the same version of Node this is tested against: + +```shell +$ nvm install +$ nvm use +``` + +Install dependencies required to start working: + +```shell +$ npm install +``` + +Start the server that (that doesn't do anything yet): + +```shell +$ npm start +``` + +Finally, open , make changes to `server.js` +and watch dependencies get installed as you go! diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..a1b0214 --- /dev/null +++ b/example/package.json @@ -0,0 +1,19 @@ +{ + "private": true, + "devDependencies": { + "babel-preset-es2015": "6.3.13", + "babel-preset-react": "6.3.13", + "babel-preset-react-hmre": "1.0.1", + "babel-preset-stage-0": "6.3.13", + "reload-server-webpack-plugin": "1.0.1", + "webpack": "1.12.11" + }, + "scripts": { + "link": "(cd .. && npm link .) && npm link npm-install-webpack-plugin", + "postinstall": "npm run link", + "start": "webpack --config webpack.config.server.babel.js --watch" + }, + "dependencies": { + "react": "0.14.6" + } +} diff --git a/example/src/client.js b/example/src/client.js new file mode 100644 index 0000000..1d2c860 --- /dev/null +++ b/example/src/client.js @@ -0,0 +1,6 @@ +import React from "react"; +import DOM from "react-dom"; + +import App from "./components/app"; + +DOM.render(, document.getElementById("app")); diff --git a/example/src/components/App.css b/example/src/components/App.css new file mode 100644 index 0000000..b1a1c72 --- /dev/null +++ b/example/src/components/App.css @@ -0,0 +1 @@ +@import "~bootswatch/lumen/bootstrap.css"; diff --git a/example/src/components/App.js b/example/src/components/App.js new file mode 100644 index 0000000..4f96a77 --- /dev/null +++ b/example/src/components/App.js @@ -0,0 +1,36 @@ +import React from "react"; + +import "./App.css"; + +export default class App extends React.Component { + render() { + return ( +
+
+

+ + npm-install-webpack-plugin + +

+
+ +
+

+ It Works! +

+ +

+ Webpack has successfully compiled the application JS & CSS. +

+ + + View on Github + +
+
+ ); + } +} diff --git a/example/src/server.js b/example/src/server.js new file mode 100644 index 0000000..e027278 --- /dev/null +++ b/example/src/server.js @@ -0,0 +1,30 @@ +import express from "express"; +import webpack from "webpack"; + +import client from "../webpack.config.client.babel"; + +const compiler = webpack(client); + +export default express() + .get("/", (req, res) => res.send(` + +
+ Waiting on client.js to execute... +
+ + + `)) + .use(require("webpack-dev-middleware")(compiler, { + noInfo: true, + publicPath: client.output.publicPath, + quiet: false, + })) + .use(require("webpack-hot-middleware")(compiler)) + .listen(3000, (err) => { + if (err) { + return console.error(err); + } + + console.info("Listening on http://localhost:3000/") + }) +; diff --git a/example/webpack.config.babel.js b/example/webpack.config.babel.js new file mode 100644 index 0000000..0c50a9e --- /dev/null +++ b/example/webpack.config.babel.js @@ -0,0 +1,32 @@ +import NpmInstallPlugin from "npm-install-webpack-plugin"; +import path from "path"; + +export const defaults = { + context: process.cwd(), + + externals: [], + + module: { + loaders: [ + { test: /\.css$/, loader: "style-loader" }, + { test: /\.css$/, loader: "css-loader", query: { localIdentName: "[name]-[local]--[hash:base64:5]" } }, + { test: /\.eot$/, loader: "file-loader" }, + { test: /\.js$/, loader: "babel-loader", query: { cacheDirectory: true }, exclude: /node_modules/ }, + { test: /\.json$/, loader: "json-loader" }, + { test: /\.(png|jpg)$/, loader: "url-loader", query: { limit: 8192 } }, // Inline base64 URLs for <= 8K images + { test: /\.svg$/, loader: "url-loader", query: { mimetype: "image/svg+xml" } }, + { test: /\.ttf$/, loader: "url-loader", query: { mimetype: "application/octet-stream" } }, + { test: /\.(woff|woff2)$/, loader: "url-loader", query: { mimetype: "application/font-woff" } }, + ], + }, + + output: { + chunkFilename: "[id].[hash:5]-[chunkhash:7].js", + devtoolModuleFilenameTemplate: "[absolute-resource-path]", + filename: "[name].js", + }, + + plugins: [ + new NpmInstallPlugin(), + ], +}; diff --git a/example/webpack.config.client.babel.js b/example/webpack.config.client.babel.js new file mode 100644 index 0000000..0fe4308 --- /dev/null +++ b/example/webpack.config.client.babel.js @@ -0,0 +1,29 @@ +import path from "path"; +import webpack from "webpack"; + +import { defaults } from "./webpack.config.babel"; + +export default { + ...defaults, + + entry: { + client: [ + "webpack-hot-middleware/client?reload=true", + "./src/client.js", + ], + }, + + output: { + ...defaults.output, + libaryTarget: "var", + path: path.join(defaults.context, "build/client"), + publicPath: "/", + }, + + plugins: [ + ...defaults.plugins, + new webpack.HotModuleReplacementPlugin(), + ], + + target: "web", +} diff --git a/example/webpack.config.server.babel.js b/example/webpack.config.server.babel.js new file mode 100644 index 0000000..36fdca7 --- /dev/null +++ b/example/webpack.config.server.babel.js @@ -0,0 +1,31 @@ +import ReloadServerPlugin from "reload-server-webpack-plugin"; +import path from "path"; + +import { defaults } from "./webpack.config.babel"; + +export default { + ...defaults, + + entry: { + server: "./src/server.js", + }, + + externals: [ + ...defaults.externals, + // Every non-relative module is external + /^[a-z\-0-9]+$/, + ], + + output: { + ...defaults.output, + libraryTarget: "commonjs2", + path: path.join(defaults.context, "build/server"), + }, + + plugins: [ + ...defaults.plugins, + new ReloadServerPlugin({ script: "./build/server/server.js" }), + ], + + target: "node", +} diff --git a/index.js b/index.js index 61fe0ef..c12b35f 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports = require("./src/loader.js"); +module.exports = require("./src/plugin.js"); diff --git a/package.json b/package.json index 1d73f97..0cc26d5 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "npm-install-loader", + "name": "npm-install-webpack-plugin", "version": "1.1.1", "description": "Webpack loader to automatically npm install & save dependencies.", "main": "index.js", @@ -18,30 +18,28 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/ericclemmons/webpack-npm-install-loader.git" + "url": "git+https://github.com/ericclemmons/npm-install-webpack-plugin.git" }, "keywords": [ "webpack", - "webpack-loader", + "webpack-plugin", "npm", "install" ], "author": "Eric Clemmons ", "license": "MIT", "bugs": { - "url": "https://github.com/ericclemmons/webpack-npm-install-loader/issues" + "url": "https://github.com/ericclemmons/npm-install-webpack-plugin/issues" }, - "homepage": "https://github.com/ericclemmons/webpack-npm-install-loader#readme", + "homepage": "https://github.com/ericclemmons/npm-install-webpack-plugin#readme", "dependencies": { - "babel-core": "^6.3.26", "cross-spawn": "^2.1.4", - "loader-utils": "^0.2.12", - "lodash.kebabcase": "^3.0.1" + "lodash.kebabcase": "^3.1.0" }, "devDependencies": { "coveralls": "^2.11.6", "expect": "^1.13.4", "mocha": "^2.3.4", - "nyc": "^5.0.1" + "nyc": "^5.3.0" } } diff --git a/src/installer.js b/src/installer.js index 9e69c63..e9011f0 100644 --- a/src/installer.js +++ b/src/installer.js @@ -4,57 +4,71 @@ var kebabCase = require("lodash.kebabcase"); var path = require("path"); var util = require("util"); -module.exports.check = function(dependencies, dirs) { - var missing = []; - dependencies.forEach(function(dependency) { - // Ignore relative modules, which aren't installed by NPM - if (/^\./.test(dependency)) { - return; - } +var INTERNAL = /^\./; // Match "./client", "../something", etc. +var EXTERNAL = /^[a-z\-0-9]+$/; // Match "react", "path", "fs", etc. + +module.exports.check = function(request) { + var namespaced = request.charAt(0) === "@"; + var dep = request.split("/") + .slice(0, namespaced ? 2 : 1) + .join("/") + ; + + // Ignore relative modules, which aren't installed by NPM + if (!dep.match(EXTERNAL) && !namespaced) { + return; + } - // Only look for the dependency directory - dependency = dependency.split('/')[0]; + try { + var pkgPath = require.resolve(path.join(process.cwd(), "package.json")); + var pkg = require(pkgPath); - // Bail early if we've already determined this is a missing dependency - if (missing.indexOf(dependency) !== -1) { - return; - } + // Remove cached copy for future checks + delete require.cache[pkgPath]; + } catch(e) { + throw e; + } - try { - // Ignore dependencies that are resolveable - require.resolve(dependency); + var hasDep = pkg.dependencies && pkg.dependencies[dep]; + var hasDevDep = pkg.devDependencies && pkg.devDependencies[dep]; - return; - } catch(e) { - var modulePaths = (dirs || []).map(function(dir) { - return path.resolve(dir, dependency); - }); + // Bail early if we've already installed this dependency + if (hasDep || hasDevDep) { + return; + } - // Check all module directories for dependency directory - while (modulePaths.length) { - var modulePath = modulePaths.shift(); + // Ignore linked modules + try { + var stats = fs.lstatSync(path.join(process.cwd(), "node_modules", dep)); - try { - // If it exists, Webpack can find it - fs.statSync(modulePath); + if (stats.isSymbolicLink()) { + return; + } + } catch(e) { + // Module exists in node_modules, but isn't symlinked + } - return; - } catch(e) {} - } + // Ignore NPM global modules (e.g. "path", "fs", etc.) + try { + var resolved = require.resolve(dep); - // Dependency must be missing - missing.push(dependency); + // Global modules resolve to their name, not an actual path + if (resolved.match(EXTERNAL)) { + return; } - }); - return missing; + } catch(e) { + // Module is not resolveable + } + + return dep; } -module.exports.install = function install(dependencies, options) { - if (!dependencies || !dependencies.length) { - return undefined; +module.exports.install = function install(dep, options) { + if (!dep) { + return; } - var args = ["install"].concat(dependencies); + var args = ["install"].concat([dep]).filter(Boolean); if (options) { for (option in options) { @@ -73,8 +87,9 @@ module.exports.install = function install(dependencies, options) { } } - var suffix = dependencies.length === 1 ? "y" : "ies"; - console.info("Installing missing dependenc%s %s...", suffix, dependencies.join(", ")); + console.info("Installing `%s`...", dep); + + var output = spawn.sync("npm", args, { stdio: "inherit" }); - return spawn.sync("npm", args, { stdio: "inherit" }); + return output; }; diff --git a/src/loader.js b/src/loader.js deleted file mode 100644 index 471dc58..0000000 --- a/src/loader.js +++ /dev/null @@ -1,30 +0,0 @@ -var installer = require("./installer"); -var loaderUtils = require("loader-utils"); -var path = require("path"); -var parser = require("./parser"); -var util = require("util"); - -module.exports = function loader(source, map) { - if (this.cacheable) { - this.cacheable(); - } - - var context = this.options.context || process.cwd(); - var resolve = this.options.resolve; - - var dependencies = parser.parse(source); - - var modulePaths = [].concat( - resolve.root || [], - resolve.modulesDirectories || [] - ).map(function(dir) { - return path.resolve(context, dir); - }); - - var missing = installer.check(dependencies, modulePaths); - var query = loaderUtils.parseQuery(this.query); - - installer.install(missing, query.cli); - - this.callback(null, source, map); -}; diff --git a/src/parser.js b/src/parser.js deleted file mode 100644 index ba74cde..0000000 --- a/src/parser.js +++ /dev/null @@ -1,30 +0,0 @@ -var babel = require("babel-core"); - -function parseNode(node) { - if (!node) { - return; - } - - if (node.type === "CallExpression" && node.callee.name === "require") { - return node.arguments[0].value; - } - - if (node.type === "ExpressionStatement") { - return parseNode(node.expression); - } - - if (node.type === "ImportDeclaration") { - return node.source.value; - } - - if (node.type === "VariableDeclaration") { - return parseNode(node.declarations[0].init); - } -}; - -module.exports.parse = function parse(source) { - var result = babel.transform(source); - var dependencies = result.ast.program.body.map(parseNode).filter(Boolean); - - return dependencies; -}; diff --git a/src/plugin.js b/src/plugin.js new file mode 100644 index 0000000..0190dc9 --- /dev/null +++ b/src/plugin.js @@ -0,0 +1,54 @@ +var path = require("path"); + +var installer = require("./installer"); + +function NpmInstallPlugin(options) { + this.options = options || {}; +} + +NpmInstallPlugin.prototype.apply = function(compiler) { + // Plugin needs to intercept module resolution before the "official" resolve + compiler.plugin("normal-module-factory", this.listenToFactory); + + // Install loaders on demand + compiler.resolvers.loader.plugin("module", this.resolveLoader.bind(this)); + + // Install project dependencies on demand + compiler.resolvers.normal.plugin("module", this.resolveModule.bind(this)); +}; + +NpmInstallPlugin.prototype.listenToFactory = function(factory) { + factory.plugin("before-resolve", function(result, next) { + // Trigger early-module resolution + factory.resolvers.normal.resolve(result.context, result.request, function(err, filepath) { + next(null, result); + }); + }); +}; + +NpmInstallPlugin.prototype.resolve = function(result) { + var dep = installer.check(result.request); + + if (dep) { + installer.install(dep, this.options.cli); + } + + return dep; +}; + +NpmInstallPlugin.prototype.resolveModule = function(result, next) { + // Only install direct dependencies, not sub-dependencies + if (!result.path.match("node_modules")) { + this.resolve(result); + } + + next(); +}; + +NpmInstallPlugin.prototype.resolveLoader = function(result, next) { + this.resolve(result); + + next(); +}; + +module.exports = NpmInstallPlugin; diff --git a/test/installer.test.js b/test/installer.test.js index 93cff9e..aab87c9 100644 --- a/test/installer.test.js +++ b/test/installer.test.js @@ -1,50 +1,109 @@ -var spawn = require("cross-spawn"); var expect = require("expect"); +var fs = require("fs"); +var spawn = require("cross-spawn"); + var installer = require("../src/installer"); describe("installer", function() { describe(".check", function() { - context("given resolveable dependencies", function() { - it("should return []", function() { - expect(installer.check(["expect", "mocha"])).toEqual([]); + context("given a local module", function() { + it("should return undefined", function() { + expect(installer.check("./foo")).toBe(undefined); + }); + }); + + context("when process.cwd() is missing package.json", function() { + before(function() { + this.cwd = process.cwd(); + + process.chdir(__dirname); + }); + + after(function() { + process.chdir(this.cwd); + }); + + it("should throw", function() { + expect(function() { + installer.check("anything"); + }).toThrow(/Cannot find module/); }); }); - context("given relative paths", function() { - it("should return []", function() { - expect(installer.check(["./something"])).toEqual([]); + context("given a dependency in package.json", function() { + it("should return undefined", function() { + expect(installer.check("cross-spawn")).toBe(undefined); }); }); - context("given un-resolveable dependencies", function() { - it("should return them", function() { - expect(installer.check(["does-not-exist"])).toEqual(["does-not-exist"]); + context("given a devDependency in package.json", function() { + it("should return undefined", function() { + expect(installer.check("expect")).toBe(undefined); }); + }); - context("that import deeply", function() { - it("should return their module name", function() { - expect(installer.check(["does-not-exist/lib/test"])).toEqual(["does-not-exist"]); + context("given a linked dependency", function() { + beforeEach(function() { + this.lstatSync = expect.spyOn(fs, "lstatSync").andReturn({ + isSymbolicLink: function() { + return true; + }, }); }); - context("that import multiple times from the same module", function() { - it("should return their module name once", function() { - expect(installer.check(["d-n-e/a", "d-n-e/b"])).toEqual(["d-n-e"]); - }); + afterEach(function() { + this.lstatSync.restore(); }); - context("that exists in alternative directories", function() { - it("should return []", function() { - expect(installer.check(["test"], [process.cwd()])).toEqual([]); - }); + it("should return undefined", function() { + expect(installer.check("something-linked")).toBe(undefined); + expect(this.lstatSync.calls.length).toBe(1); + expect(this.lstatSync.calls[0].arguments).toEqual([ + [process.cwd(), "node_modules", "something-linked"].join("/"), + ]); }); }); + context("given a global module", function() { + it("should return undefined", function () { + expect(installer.check("path")).toBe(undefined); + }); + }); + + context("given a module", function() { + it("should return module", function() { + expect(installer.check("react")).toBe("react"); + }); + }); + + context("given a module/and/path", function() { + it("should return module", function() { + expect(installer.check("react/proptypes")).toBe("react"); + }); + }); + + context("given a @namespaced/module", function() { + it("should return @namespaced/module", function() { + expect(installer.check("@namespaced/module")).toBe("@namespaced/module"); + }); + }); + + context("given a module already installed, but not saved", function() { + it("should return module", function() { + expect(installer.check("yargs")).toBe("yargs"); + }); + }); + + context("given a webpack !!loader/module", function() { + it("should return undefined", function() { + expect(installer.check("!!./css-loader/index.js',")).toBe(undefined); + }); + }) }); describe(".install", function() { beforeEach(function() { - this.spy = expect.spyOn(spawn, "sync"); + this.sync = expect.spyOn(spawn, "sync"); expect.spyOn(console, "info"); }); @@ -53,46 +112,40 @@ describe("installer", function() { expect.restoreSpies(); }); - context("given falsey values", function() { + context("given a falsey value", function() { it("should return undefined", function() { expect(installer.install()).toEqual(undefined); expect(installer.install(0)).toEqual(undefined); + expect(installer.install(false)).toEqual(undefined); + expect(installer.install(null)).toEqual(undefined); expect(installer.install("")).toEqual(undefined); - expect(installer.install([])).toEqual(undefined); - }); - }); - - context("given empty dependencies", function() { - it("should return undefined", function() { - expect(installer.install([])).toEqual(undefined); }); }); - context("given dependencies", function() { - it("should install them", function() { - var result = installer.install(["foo", "bar"]); + context("given a dependency", function() { + it("should install it", function() { + var result = installer.install("foo"); - expect(this.spy).toHaveBeenCalled(); - expect(this.spy.calls.length).toEqual(1); - expect(this.spy.calls[0].arguments[0]).toEqual("npm"); - expect(this.spy.calls[0].arguments[1]).toEqual(["install", "foo", "bar"]); + expect(this.sync).toHaveBeenCalled(); + expect(this.sync.calls.length).toEqual(1); + expect(this.sync.calls[0].arguments[0]).toEqual("npm"); + expect(this.sync.calls[0].arguments[1]).toEqual(["install", "foo"]); }); context("given options", function() { it("should pass them to child process", function() { - var result = installer.install(["foo", "bar"], { + var result = installer.install("foo", { save: true, saveExact: false, registry: "https://registry.npmjs.com/", }); - expect(this.spy).toHaveBeenCalled(); - expect(this.spy.calls.length).toEqual(1); - expect(this.spy.calls[0].arguments[0]).toEqual("npm"); - expect(this.spy.calls[0].arguments[1]).toEqual([ + expect(this.sync).toHaveBeenCalled(); + expect(this.sync.calls.length).toEqual(1); + expect(this.sync.calls[0].arguments[0]).toEqual("npm"); + expect(this.sync.calls[0].arguments[1]).toEqual([ "install", "foo", - "bar", "--save", "--registry='https://registry.npmjs.com/'", ]); diff --git a/test/loader.test.js b/test/loader.test.js deleted file mode 100644 index 84c570b..0000000 --- a/test/loader.test.js +++ /dev/null @@ -1,74 +0,0 @@ -var expect = require("expect"); -var installer = require("../src/installer"); -var loader = require("../src/loader"); -var path = require("path"); - -describe("loader", function() { - beforeEach(function() { - expect.spyOn(console, "info"); - - this.cacheable = function() {}; - this.callback = expect.createSpy(); - this.check = expect.spyOn(installer, "check").andCallThrough(); - this.install = expect.spyOn(installer, "install"); - this.map = "map"; - - this.source = [ - "require('mocha')", - "import expect from 'expect'", - "import * as missing from 'missing'", - ].join("\n"); - - this.options = { - context: process.cwd(), - resolve: { - root: ["test"], - modulesDirectories: ["node_modules"], - }, - }; - - this.query = '?{"cli":{"save":true,"saveExact":true}}'; - - loader.call(this, this.source, this.map); - }); - - afterEach(function() { - expect.restoreSpies(); - }); - - it("should check resolve.root & resolve.modulesDirectories", function() { - expect(this.check).toHaveBeenCalled(); - expect(this.check.calls[0].arguments).toEqual([ - ["mocha", "expect", "missing"], - [ - path.join(this.options.context, "test"), - path.join(this.options.context, "node_modules"), - ], - ]); - }); - - context("when calling .install", function() { - it("should pass dependencies as the first argument", function() { - expect(this.install).toHaveBeenCalled(); - expect(this.install.calls[0].arguments[0]).toEqual(["missing"]); - }); - - it("should pass args as the second argument", function() { - expect(this.install).toHaveBeenCalled(); - expect(this.install.calls[0].arguments[1]).toEqual({ - save: true, - saveExact: true, - }); - }); - }); - - - it("should callback source & map", function() { - expect(this.callback).toHaveBeenCalled(); - expect(this.callback.calls[0].arguments).toEqual([ - null, - this.source, - this.map, - ]); - }); -}); diff --git a/test/parser.test.js b/test/parser.test.js deleted file mode 100644 index 65a09b0..0000000 --- a/test/parser.test.js +++ /dev/null @@ -1,67 +0,0 @@ -var babel = require("babel-core"); -var expect = require("expect"); -var parse = require("../src/parser").parse; - -describe("parser", function() { - describe(".parse", function() { - context("given statements", function() { - it("should return []", function() { - expect(parse(null)).toEqual([]); - expect(parse(undefined)).toEqual([]); - expect(parse(0)).toEqual([]); - expect(parse("")).toEqual([]); - expect(parse("var foo = 'bar';")).toEqual([]); - }); - }); - - context("given source that with NULLs in the AST", function() { - beforeEach(function() { - this.transform = expect.spyOn(babel, "transform").andReturn({ - ast: { - program: { - body: [null], - }, - }, - }); - }); - - afterEach(function() { - this.transform.restore(); - }); - - it("should return []", function() { - expect(parse("something with NULLs")).toEqual([]); - }); - }); - - context("given: require('foo')", function() { - it("should return 'foo'", function() { - expect(parse("require('foo')")).toEqual(["foo"]); - }); - }); - - context("given: let a = require('foo')", function() { - it("should return 'foo'", function() { - expect(parse("let a = require('foo')")).toEqual(["foo"]); - }); - }); - - context("given: const { a, b } = require('foo')", function() { - it("should return 'foo'", function() { - expect(parse("const { a, b } = require('foo')")).toEqual(["foo"]); - }); - }); - - context("given: import * as a from 'foo'", function() { - it("should return 'foo'", function() { - expect(parse("import * as a from 'foo'")).toEqual(["foo"]); - }); - }); - - context("given: import a from 'foo'", function() { - it("should return 'foo'", function() { - expect(parse("import a from 'foo'")).toEqual(["foo"]); - }); - }); - }); -}); diff --git a/test/plugin.test.js b/test/plugin.test.js new file mode 100644 index 0000000..92f69f9 --- /dev/null +++ b/test/plugin.test.js @@ -0,0 +1,208 @@ +var expect = require("expect"); +var installer = require("../src/installer"); +var Plugin = require("../src/plugin"); + +describe("plugin", function() { + beforeEach(function() { + this.options = { + cli: { + save: true, + saveDev: false, + }, + }; + + this.plugin = new Plugin(this.options); + }); + + it("should accept options", function() { + expect(this.plugin.options).toEqual(this.options); + }); + + describe(".apply", function() { + before(function() { + this.compiler = { + plugin: expect.createSpy(), + resolvers: { + loader: { plugin: expect.createSpy() }, + normal: { plugin: expect.createSpy() }, + }, + }; + + this.plugin.apply(this.compiler); + }); + + after(function() { + expect.restoreSpies(); + }); + + it("should hook into `normal-module-factory`", function() { + expect(this.compiler.plugin.calls.length).toBe(1); + expect(this.compiler.plugin.calls[0].arguments).toEqual([ + "normal-module-factory", + this.plugin.listenToFactory + ]); + }); + + it("should hook into loader resolvers", function() { + expect(this.compiler.resolvers.loader.plugin.calls.length).toBe(1); + expect(this.compiler.resolvers.loader.plugin.calls[0].arguments).toEqual([ + "module", + this.plugin.resolveLoader.bind(this.plugin) + ]); + }); + + it("should hook into normal resolvers", function() { + expect(this.compiler.resolvers.normal.plugin.calls.length).toBe(1); + expect(this.compiler.resolvers.normal.plugin.calls[0].arguments).toEqual([ + "module", + this.plugin.resolveModule.bind(this.plugin) + ]); + }); + }); + + describe(".listenToFactory", function() { + before(function() { + this.next = expect.createSpy().andCall(function(err, result) { + return result; + }); + + this.result = { + context: "/", + request: "foo", + path: "node_modules" + }; + + this.factory = { + plugin: expect.createSpy().andCall(function(event, factoryPlugin) { + factoryPlugin(this.result, this.next); + }.bind(this)), + + resolvers: { + normal: { + resolve: expect.createSpy().andCall(function(context, request, callback) { + callback(null, [context, request].join("/")); + }.bind(this)), + }, + }, + }; + + this.plugin.listenToFactory(this.factory); + }); + + it("should hook into `before-resolve`", function() { + expect(this.factory.plugin.calls.length).toBe(1); + expect(this.factory.plugin.calls[0].arguments[0]).toBe("before-resolve"); + }); + + it("should immediately resolve", function() { + expect(this.factory.resolvers.normal.resolve.calls.length).toBe(1); + }); + + it("should pass result through", function() { + expect(this.next.calls.length).toBe(1); + expect(this.next.calls[0].arguments).toEqual([null, this.result]); + }); + }); + + describe(".resolve", function() { + beforeEach(function() { + this.check = expect.spyOn(installer, "check"); + this.install = expect.spyOn(installer, "install"); + this.result = { request: "foo "}; + }); + + afterEach(function() { + this.check.restore(); + this.install.restore(); + }); + + it("should check if request is installed", function() { + this.plugin.resolve(this.result); + + expect(this.check.calls.length).toBe(1); + expect(this.check.calls[0].arguments).toEqual([this.result.request]); + }); + + it("should return the value of the check", function() { + this.check.andReturn(this.result.request); + + var result = this.plugin.resolve(this.result); + + expect(this.check.calls.length).toBe(1); + expect(this.check.calls[0].arguments).toEqual([this.result.request]); + + expect(result).toEqual(this.result.request); + }); + + it("should not install if it exists", function() { + this.plugin.resolve(this.result); + + expect(this.install.calls.length).toEqual(0); + }); + + it("should install if missing", function() { + this.check.andReturn(this.result.request); + this.plugin.resolve(this.result); + + expect(this.install.calls.length).toBe(1); + expect(this.install.calls[0].arguments).toEqual([ + this.result.request, + this.options.cli + ]); + }); + }); + + describe(".resolveModule", function() { + beforeEach(function() { + this.resolve = expect.spyOn(this.plugin, "resolve"); + this.next = expect.createSpy(); + }); + + afterEach(function() { + this.resolve.restore(); + this.next.restore(); + }); + + it("should call .resolve if direct dependency", function() { + var result = { path: "/", request: "foo" }; + + this.plugin.resolveModule(result, this.next); + + expect(this.resolve.calls.length).toBe(1); + expect(this.resolve.calls[0].arguments).toEqual([result]); + expect(this.next.calls.length).toBe(1); + expect(this.next.calls[0].arguments).toEqual([]); + }); + + it("should call not .resolve if sub-dependency", function() { + var result = { path: "node_modules", request: "foo" }; + + this.plugin.resolveModule(result, this.next); + + expect(this.resolve.calls.length).toBe(0); + expect(this.next.calls.length).toBe(1); + expect(this.next.calls[0].arguments).toEqual([]); + }); + }); + + describe(".resolveLoader", function() { + beforeEach(function() { + this.resolve = expect.spyOn(this.plugin, "resolve"); + this.next = expect.createSpy(); + }); + + afterEach(function() { + this.resolve.restore(); + this.next.restore(); + }); + + it("should call .resolve", function() { + var result = { path: "node_modules", request: "foo" }; + + this.plugin.resolveLoader(result, this.next); + + expect(this.resolve.calls.length).toBe(1); + expect(this.resolve.calls[0].arguments).toEqual([result]); + }); + }); +});