From 4b5a3ee1dfefe44fbadfa1ea6daa4c9933a092b0 Mon Sep 17 00:00:00 2001 From: dylanonelson Date: Sun, 11 Feb 2018 21:58:18 -0500 Subject: [PATCH 1/4] misc(generator): allow local paths to generators --- SCAFFOLDING.md | 7 ++++- lib/commands/serve.js | 4 ++- lib/utils/npm-packages-exists.js | 25 +++++++++++++----- lib/utils/npm-packages-exists.spec.js | 31 ++++++++++++++++++++++ lib/utils/package-manager.js | 30 +++++++++++++++++++++- lib/utils/package-manager.spec.js | 32 +++++++++++++++++++++-- lib/utils/resolve-packages.js | 37 ++++++++++++++++++++++----- 7 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 lib/utils/npm-packages-exists.spec.js diff --git a/SCAFFOLDING.md b/SCAFFOLDING.md index c428f5bea43..d0fef8fcfb1 100644 --- a/SCAFFOLDING.md +++ b/SCAFFOLDING.md @@ -14,7 +14,12 @@ 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 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). + +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-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..c99fa0bbf79 100644 --- a/lib/utils/package-manager.spec.js +++ b/lib/utils/package-manager.spec.js @@ -1,5 +1,7 @@ "use strict"; +const path = require("path"); + jest.mock("cross-spawn"); jest.mock("fs"); @@ -27,6 +29,11 @@ describe("package-manager", () => { ); } + function mockSpawnErrorTwice() { + mockSpawnErrorOnce(); + mockSpawnErrorOnce(); + } + spawn.sync.mockReturnValue(defaultSyncResult); it("should return 'yarn' from getPackageManager if it's installed", () => { @@ -65,7 +72,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 +84,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 +94,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 = path.join(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..a26b771c511 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,36 @@ 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}.`); + console.log("\nError:\n"); + console.error(chalk.bold.red(err)); + process.exitCode = 1; + } + + 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 +83,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); }); } From 7a8c02d385aa4444d66cd3317de5b41b6df88b28 Mon Sep 17 00:00:00 2001 From: dylanonelson Date: Fri, 2 Mar 2018 20:43:07 -0500 Subject: [PATCH 2/4] misc(generator): allow local paths to generators --- SCAFFOLDING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SCAFFOLDING.md b/SCAFFOLDING.md index d0fef8fcfb1..359badf91ad 100644 --- a/SCAFFOLDING.md +++ b/SCAFFOLDING.md @@ -19,7 +19,7 @@ webpack-addon?](https://github.com/ev1stensberg/webpack-addons-demo). 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. +If the package is on your local filesystem, it can be named whatever you want. Pass the path to the package. ## API From 0b346b1ec65e7b47342854ee3f5fec447a7c5257 Mon Sep 17 00:00:00 2001 From: dylanonelson Date: Fri, 2 Mar 2018 22:18:29 -0500 Subject: [PATCH 3/4] misc(generator): allow local paths to generators --- lib/utils/is-local-path.js | 19 +++++++++++++++++++ lib/utils/is-local-path.spec.js | 24 ++++++++++++++++++++++++ lib/utils/npm-packages-exists.js | 6 +++--- lib/utils/npm-packages-exists.spec.js | 5 ----- lib/utils/resolve-packages.js | 8 ++++---- 5 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 lib/utils/is-local-path.js create mode 100644 lib/utils/is-local-path.spec.js diff --git a/lib/utils/is-local-path.js b/lib/utils/is-local-path.js new file mode 100644 index 00000000000..c05b0b1420a --- /dev/null +++ b/lib/utils/is-local-path.js @@ -0,0 +1,19 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +/** + * Attempts to detect whether the string is a local path regardless of its + * existence by checking its format. The point is to distinguish between + * paths and modules on the npm registry. This will fail for non-existent + * local Windows paths that begin with a drive letter, e.g. C:..\generator.js, + * but will succeed for any existing files and any absolute paths. + * + * @param {String} str - string to check + * @returns {Boolean} whether the string could be a path to a local file or directory + */ + +module.exports = function(str) { + return ((path.isAbsolute(str) || /^\./.test(str)) || fs.existsSync(str)); +}; diff --git a/lib/utils/is-local-path.spec.js b/lib/utils/is-local-path.spec.js new file mode 100644 index 00000000000..32acbaf8384 --- /dev/null +++ b/lib/utils/is-local-path.spec.js @@ -0,0 +1,24 @@ +"use strict"; + +const isLocalPath = require("./is-local-path"); +const path = require("path"); + +describe("is-local-path", () => { + it("returns true for paths beginning in the current directory", () => { + const p = path.resolve(".", "test", "dir"); + expect(isLocalPath(p)).toBe(true); + }); + + it("returns true for absolute paths", () => { + const p = path.join("/", "test", "dir"); + expect(isLocalPath(p)).toBe(true); + }); + + it("returns false for npm packages names", () => { + expect(isLocalPath("webpack-addons-ylvis")).toBe(false); + }); + + it("returns false for scoped npm package names", () => { + expect(isLocalPath("@webpack/test")).toBe(false); + }); +}); diff --git a/lib/utils/npm-packages-exists.js b/lib/utils/npm-packages-exists.js index d6ffcd26581..7a9fdb8b5b1 100644 --- a/lib/utils/npm-packages-exists.js +++ b/lib/utils/npm-packages-exists.js @@ -1,6 +1,6 @@ "use strict"; const chalk = require("chalk"); -const fs = require("fs"); +const isLocalPath = require("./is-local-path"); const npmExists = require("./npm-exists"); const resolvePackages = require("./resolve-packages").resolvePackages; @@ -22,8 +22,8 @@ module.exports = function npmPackagesExists(pkg) { } pkg.forEach(addon => { - // The addon is a path to a local folder; no validation is necessary - if (fs.existsSync(addon)) { + if (isLocalPath(addon)) { + // If the addon is a path to a local folder, no name validation is necessary. acceptedPackages.push(addon); resolvePackagesIfReady(); return; diff --git a/lib/utils/npm-packages-exists.spec.js b/lib/utils/npm-packages-exists.spec.js index 02029fc94e7..19cfab3db24 100644 --- a/lib/utils/npm-packages-exists.spec.js +++ b/lib/utils/npm-packages-exists.spec.js @@ -1,7 +1,5 @@ -const fs = require("fs"); const npmPackagesExists = require("./npm-packages-exists"); -jest.mock("fs"); jest.mock("./npm-exists"); jest.mock("./resolve-packages"); @@ -9,18 +7,15 @@ 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(() => { diff --git a/lib/utils/resolve-packages.js b/lib/utils/resolve-packages.js index a26b771c511..7ed4fb2b582 100644 --- a/lib/utils/resolve-packages.js +++ b/lib/utils/resolve-packages.js @@ -1,6 +1,5 @@ "use strict"; -const fs = require("fs"); const path = require("path"); const chalk = require("chalk"); @@ -8,6 +7,7 @@ const creator = require("../init/index").creator; const getPathToGlobalPackages = require("./package-manager") .getPathToGlobalPackages; +const isLocalPath = require("./is-local-path"); const spawnChild = require("./package-manager").spawnChild; /** @@ -50,7 +50,7 @@ function resolvePackages(pkg) { pkg.forEach(addon => { // Resolve paths to modules on local filesystem - if (fs.existsSync(addon)) { + if (isLocalPath(addon)) { let absolutePath = addon; try { @@ -58,8 +58,8 @@ function resolvePackages(pkg) { require.resolve(absolutePath); packageLocations.push(absolutePath); } catch (err) { - console.log(`Cannot find a valid npm module at ${absolutePath}.`); - console.log("\nError:\n"); + console.log(`Cannot find a generator at ${absolutePath}.`); + console.log("\nReason:\n"); console.error(chalk.bold.red(err)); process.exitCode = 1; } From 3d15ddbadeec5454b65f9f38e9ea231563d8b37a Mon Sep 17 00:00:00 2001 From: Dylan Nelson Date: Wed, 7 Mar 2018 12:46:22 -0500 Subject: [PATCH 4/4] misc(generator): allow local paths to generators --- lib/utils/is-local-path.js | 2 +- lib/utils/npm-packages-exists.js | 10 +++++++--- lib/utils/npm-packages-exists.spec.js | 16 +++++++++++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/utils/is-local-path.js b/lib/utils/is-local-path.js index c05b0b1420a..af17d590594 100644 --- a/lib/utils/is-local-path.js +++ b/lib/utils/is-local-path.js @@ -15,5 +15,5 @@ const path = require("path"); */ module.exports = function(str) { - return ((path.isAbsolute(str) || /^\./.test(str)) || fs.existsSync(str)); + return path.isAbsolute(str) || /^\./.test(str) || fs.existsSync(str); }; diff --git a/lib/utils/npm-packages-exists.js b/lib/utils/npm-packages-exists.js index 7a9fdb8b5b1..fccc1c25e7a 100644 --- a/lib/utils/npm-packages-exists.js +++ b/lib/utils/npm-packages-exists.js @@ -4,6 +4,8 @@ const isLocalPath = require("./is-local-path"); const npmExists = require("./npm-exists"); const resolvePackages = require("./resolve-packages").resolvePackages; +const WEBPACK_ADDON_PREFIX = "webpack-addons"; + /** * * Loops through an array and checks if a package is registered @@ -30,12 +32,14 @@ module.exports = function npmPackagesExists(pkg) { } // The addon is on npm; validate name and existence - // eslint-disable-next-line - if (addon.length <= 14 || addon.slice(0, 14) !== "webpack-addons") { + if ( + addon.length <= WEBPACK_ADDON_PREFIX.length || + addon.slice(0, WEBPACK_ADDON_PREFIX.length) !== WEBPACK_ADDON_PREFIX + ) { throw new TypeError( chalk.bold(`${addon} isn't a valid name.\n`) + chalk.red( - "\nIt should be prefixed with 'webpack-addons', but have different suffix.\n" + `\nIt should be prefixed with '${WEBPACK_ADDON_PREFIX}', but have different suffix.\n` ) ); } diff --git a/lib/utils/npm-packages-exists.spec.js b/lib/utils/npm-packages-exists.spec.js index 19cfab3db24..784f700502e 100644 --- a/lib/utils/npm-packages-exists.spec.js +++ b/lib/utils/npm-packages-exists.spec.js @@ -8,18 +8,28 @@ const mockResolvePackages = require("./resolve-packages").resolvePackages; describe("npmPackagesExists", () => { test("resolves packages when they are available on the local filesystem", () => { npmPackagesExists(["./testpkg"]); - expect(mockResolvePackages.mock.calls[mockResolvePackages.mock.calls.length - 1][0]).toEqual(["./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", () => { - expect(() => npmPackagesExists(["my-webpack-addon"])).toThrowError(TypeError); + expect(() => npmPackagesExists(["my-webpack-addon"])).toThrowError( + TypeError + ); }); test("resolves packages when they are available on npm", done => { 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"]); + expect( + mockResolvePackages.mock.calls[ + mockResolvePackages.mock.calls.length - 1 + ][0] + ).toEqual(["webpack-addons-foobar"]); done(); }, 10); });