From 0f0fe3ec835eed33b1467fc7efe5efb903eaa2dc Mon Sep 17 00:00:00 2001 From: dylanonelson Date: Sun, 11 Feb 2018 21:58:18 -0500 Subject: [PATCH] misc(generator): allow local paths to generators --- SCAFFOLDING.md | 6 ++++- lib/commands/serve.js | 4 +++- lib/utils/npm-packages-exists.js | 25 +++++++++++++++----- lib/utils/npm-packages-exists.spec.js | 31 ++++++++++++++++++++++++ lib/utils/package-exists.js | 22 +++++++++++++++++ lib/utils/package-manager.js | 30 ++++++++++++++++++++++- lib/utils/package-manager.spec.js | 30 +++++++++++++++++++++-- lib/utils/resolve-packages.js | 34 ++++++++++++++++++++++----- 8 files changed, 165 insertions(+), 17 deletions(-) create mode 100644 lib/utils/npm-packages-exists.spec.js create mode 100644 lib/utils/package-exists.js diff --git a/SCAFFOLDING.md b/SCAFFOLDING.md index c428f5bea43..26d9e600551 100644 --- a/SCAFFOLDING.md +++ b/SCAFFOLDING.md @@ -14,7 +14,11 @@ Before writing a `webpack-cli` scaffold, think about what you're trying to achie ## webpack-addons-yourpackage -In order for `webpack-cli` to compile your package, it relies on a prefix of `webpack-addons`. The package must also be published on npm. If you are curious about how you can create your very own `addon`, please read [How do I compose a webpack-addon?](https://github.com/ev1stensberg/webpack-addons-demo). +In order for `webpack-cli` to compile your package, it must be available on npm or on your local filesystem. + +If the package is on npm, its name must have a prefix of `webpack-addons`. + +If the package is on your local filesystem, it can be named whatever you want. Pass the name of the package as a relative path to its root directory. ## API diff --git a/lib/commands/serve.js b/lib/commands/serve.js index d9c26af8b39..3992101b764 100644 --- a/lib/commands/serve.js +++ b/lib/commands/serve.js @@ -71,7 +71,9 @@ function serve() { : []; if (hasDevServerDep.length) { - let WDSPath = getRootPathModule("node_modules/webpack-dev-server/bin/webpack-dev-server.js"); + let WDSPath = getRootPathModule( + "node_modules/webpack-dev-server/bin/webpack-dev-server.js" + ); if (!WDSPath) { console.log( "\n", diff --git a/lib/utils/npm-packages-exists.js b/lib/utils/npm-packages-exists.js index 65c38e80eaa..d6ffcd26581 100644 --- a/lib/utils/npm-packages-exists.js +++ b/lib/utils/npm-packages-exists.js @@ -1,5 +1,6 @@ "use strict"; const chalk = require("chalk"); +const fs = require("fs"); const npmExists = require("./npm-exists"); const resolvePackages = require("./resolve-packages").resolvePackages; @@ -14,8 +15,22 @@ const resolvePackages = require("./resolve-packages").resolvePackages; module.exports = function npmPackagesExists(pkg) { let acceptedPackages = []; + + function resolvePackagesIfReady() { + if (acceptedPackages.length === pkg.length) + return resolvePackages(acceptedPackages); + } + pkg.forEach(addon => { - //eslint-disable-next-line + // The addon is a path to a local folder; no validation is necessary + if (fs.existsSync(addon)) { + acceptedPackages.push(addon); + resolvePackagesIfReady(); + return; + } + + // The addon is on npm; validate name and existence + // eslint-disable-next-line if (addon.length <= 14 || addon.slice(0, 14) !== "webpack-addons") { throw new TypeError( chalk.bold(`${addon} isn't a valid name.\n`) + @@ -24,11 +39,12 @@ module.exports = function npmPackagesExists(pkg) { ) ); } + npmExists(addon) .then(moduleExists => { if (!moduleExists) { Error.stackTraceLimit = 0; - throw new TypeError("Package isn't registered on npm."); + throw new TypeError(`Cannot resolve location of package ${addon}.`); } if (moduleExists) { acceptedPackages.push(addon); @@ -38,9 +54,6 @@ module.exports = function npmPackagesExists(pkg) { console.error(err.stack || err); process.exit(0); }) - .then(_ => { - if (acceptedPackages.length === pkg.length) - return resolvePackages(acceptedPackages); - }); + .then(resolvePackagesIfReady); }); }; diff --git a/lib/utils/npm-packages-exists.spec.js b/lib/utils/npm-packages-exists.spec.js new file mode 100644 index 00000000000..02029fc94e7 --- /dev/null +++ b/lib/utils/npm-packages-exists.spec.js @@ -0,0 +1,31 @@ +const fs = require("fs"); +const npmPackagesExists = require("./npm-packages-exists"); + +jest.mock("fs"); +jest.mock("./npm-exists"); +jest.mock("./resolve-packages"); + +const mockResolvePackages = require("./resolve-packages").resolvePackages; + +describe("npmPackagesExists", () => { + test("resolves packages when they are available on the local filesystem", () => { + fs.existsSync.mockReturnValueOnce(true); + npmPackagesExists(["./testpkg"]); + expect(mockResolvePackages.mock.calls[mockResolvePackages.mock.calls.length - 1][0]).toEqual(["./testpkg"]); + }); + + test("throws a TypeError when an npm package name doesn't include the prefix", () => { + fs.existsSync.mockReturnValueOnce(false); + expect(() => npmPackagesExists(["my-webpack-addon"])).toThrowError(TypeError); + }); + + test("resolves packages when they are available on npm", done => { + fs.existsSync.mockReturnValueOnce(false); + require("./npm-exists").mockImplementation(() => Promise.resolve(true)); + npmPackagesExists(["webpack-addons-foobar"]); + setTimeout(() => { + expect(mockResolvePackages.mock.calls[mockResolvePackages.mock.calls.length - 1][0]).toEqual(["webpack-addons-foobar"]); + done(); + }, 10); + }); +}); diff --git a/lib/utils/package-exists.js b/lib/utils/package-exists.js new file mode 100644 index 00000000000..eae03343a25 --- /dev/null +++ b/lib/utils/package-exists.js @@ -0,0 +1,22 @@ +"use strict"; + +const fs = require("fs"); +const npmExists = require("./npm-exists"); + +/** + * + * Checks whether a package exists locally or on npm + * + * @param {String} pkg - Name or location of package + * @returns {Promise} exists - Returns true or false, + * based on if it exists or not + */ + +module.exports = function packageExists(pkg) { + const isValidLocalPath = fs.existsSync(pkg); + if (isValidLocalPath) { + return Promise.resolve(true); + } + + return npmExists(pkg); +}; diff --git a/lib/utils/package-manager.js b/lib/utils/package-manager.js index 4f2fead6d56..415eafef016 100644 --- a/lib/utils/package-manager.js +++ b/lib/utils/package-manager.js @@ -48,7 +48,8 @@ function spawnYarn(pkg, isNew) { */ function spawnChild(pkg) { - const pkgPath = path.resolve(globalPath, pkg); + const rootPath = getPathToGlobalPackages(); + const pkgPath = path.resolve(rootPath, pkg); const packageManager = getPackageManager(); const isNew = !fs.existsSync(pkgPath); @@ -71,7 +72,34 @@ function getPackageManager() { return "yarn"; } +/** + * + * Returns the path to globally installed + * npm packages, depending on the available + * package manager determined by `getPackageManager` + * + * @returns {String} path - Path to global node_modules folder + */ +function getPathToGlobalPackages() { + const manager = getPackageManager(); + + if (manager === "yarn") { + try { + const yarnDir = spawn + .sync("yarn", ["global", "dir"]) + .stdout.toString() + .trim(); + return path.join(yarnDir, "node_modules"); + } catch (e) { + // Default to the global npm path below + } + } + + return globalPath; +} + module.exports = { getPackageManager, + getPathToGlobalPackages, spawnChild }; diff --git a/lib/utils/package-manager.spec.js b/lib/utils/package-manager.spec.js index e45b920fb4a..ef70e7d7234 100644 --- a/lib/utils/package-manager.spec.js +++ b/lib/utils/package-manager.spec.js @@ -27,6 +27,11 @@ describe("package-manager", () => { ); } + function mockSpawnErrorTwice() { + mockSpawnErrorOnce(); + mockSpawnErrorOnce(); + } + spawn.sync.mockReturnValue(defaultSyncResult); it("should return 'yarn' from getPackageManager if it's installed", () => { @@ -65,7 +70,7 @@ describe("package-manager", () => { it("should spawn npm install from spawnChild", () => { const packageName = "some-pkg"; - mockSpawnErrorOnce(); + mockSpawnErrorTwice(); packageManager.spawnChild(packageName); expect(spawn.sync).toHaveBeenLastCalledWith( "npm", @@ -77,7 +82,7 @@ describe("package-manager", () => { it("should spawn npm update from spawnChild", () => { const packageName = "some-pkg"; - mockSpawnErrorOnce(); + mockSpawnErrorTwice(); fs.existsSync.mockReturnValueOnce(true); packageManager.spawnChild(packageName); @@ -87,4 +92,25 @@ describe("package-manager", () => { { stdio: "inherit" } ); }); + + it("should return the yarn global dir from getPathToGlobalPackages if yarn is installed", () => { + const yarnDir = "/Users/test/.config/yarn/global"; + // Mock confirmation that yarn is installed + spawn.sync.mockReturnValueOnce(defaultSyncResult); + // Mock stdout of `yarn global dir` + spawn.sync.mockReturnValueOnce({ + stdout: { + toString: () => `${yarnDir}\n` + } + }); + const globalPath = packageManager.getPathToGlobalPackages(); + const expected = `${yarnDir}/node_modules`; + expect(globalPath).toBe(expected); + }); + + it("should return the npm global dir from getPathToGlobalPackages if yarn is not installed", () => { + mockSpawnErrorOnce(); + const globalPath = packageManager.getPathToGlobalPackages(); + expect(globalPath).toBe(require("global-modules")); + }); }); diff --git a/lib/utils/resolve-packages.js b/lib/utils/resolve-packages.js index 5f4f2308706..af2febeb72b 100644 --- a/lib/utils/resolve-packages.js +++ b/lib/utils/resolve-packages.js @@ -1,11 +1,13 @@ "use strict"; +const fs = require("fs"); const path = require("path"); const chalk = require("chalk"); -const globalPath = require("global-modules"); const creator = require("../init/index").creator; +const getPathToGlobalPackages = require("./package-manager") + .getPathToGlobalPackages; const spawnChild = require("./package-manager").spawnChild; /** @@ -41,10 +43,33 @@ function resolvePackages(pkg) { let packageLocations = []; + function invokeGeneratorIfReady() { + if (packageLocations.length === pkg.length) + return creator(packageLocations); + } + pkg.forEach(addon => { + // Resolve paths to modules on local filesystem + if (fs.existsSync(addon)) { + let absolutePath = addon; + + try { + absolutePath = path.resolve(process.cwd(), addon); + require.resolve(absolutePath); + packageLocations.push(absolutePath); + } catch (err) { + console.log(`Cannot find a valid npm module at ${absolutePath}.`); + } + + invokeGeneratorIfReady(); + return; + } + + // Resolve modules on npm registry processPromise(spawnChild(addon)) .then(_ => { try { + const globalPath = getPathToGlobalPackages(); packageLocations.push(path.resolve(globalPath, addon)); } catch (err) { console.log("Package wasn't validated correctly.."); @@ -55,15 +80,12 @@ function resolvePackages(pkg) { } }) .catch(err => { - console.log("Package Coudln't be installed, aborting.."); + console.log("Package couldn't be installed, aborting.."); console.log("\nReason: \n"); console.error(chalk.bold.red(err)); process.exitCode = 1; }) - .then(_ => { - if (packageLocations.length === pkg.length) - return creator(packageLocations); - }); + .then(invokeGeneratorIfReady); }); }