Skip to content

Commit

Permalink
misc(generator): Allow local paths to generators (#265)
Browse files Browse the repository at this point in the history
* misc(generator): allow local paths to generators

* misc(generator): allow local paths to generators

* misc(generator): allow local paths to generators

* misc(generator): allow local paths to generators
  • Loading branch information
dylanonelson authored and evenstensberg committed Mar 8, 2018
1 parent d5c287b commit 8011b5a
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 18 deletions.
7 changes: 6 additions & 1 deletion SCAFFOLDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 path to the package.

## API

Expand Down
19 changes: 19 additions & 0 deletions lib/utils/is-local-path.js
Original file line number Diff line number Diff line change
@@ -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);
};
24 changes: 24 additions & 0 deletions lib/utils/is-local-path.spec.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
33 changes: 25 additions & 8 deletions lib/utils/npm-packages-exists.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"use strict";
const chalk = require("chalk");
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
Expand All @@ -14,21 +17,38 @@ 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
if (addon.length <= 14 || addon.slice(0, 14) !== "webpack-addons") {
if (isLocalPath(addon)) {
// If the addon is a path to a local folder, no name validation is necessary.
acceptedPackages.push(addon);
resolvePackagesIfReady();
return;
}

// The addon is on npm; validate name and existence
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`
)
);
}

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);
Expand All @@ -38,9 +58,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);
});
};
36 changes: 36 additions & 0 deletions lib/utils/npm-packages-exists.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const npmPackagesExists = require("./npm-packages-exists");

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", () => {
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", () => {
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"]);
done();
}, 10);
});
});
30 changes: 29 additions & 1 deletion lib/utils/package-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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
};
32 changes: 30 additions & 2 deletions lib/utils/package-manager.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use strict";

const path = require("path");

jest.mock("cross-spawn");
jest.mock("fs");

Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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",
Expand All @@ -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);
Expand All @@ -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"));
});
});
37 changes: 31 additions & 6 deletions lib/utils/resolve-packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

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 isLocalPath = require("./is-local-path");
const spawnChild = require("./package-manager").spawnChild;

/**
Expand Down Expand Up @@ -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 (isLocalPath(addon)) {
let absolutePath = addon;

try {
absolutePath = path.resolve(process.cwd(), addon);
require.resolve(absolutePath);
packageLocations.push(absolutePath);
} catch (err) {
console.log(`Cannot find a generator at ${absolutePath}.`);
console.log("\nReason:\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..");
Expand All @@ -55,15 +83,12 @@ function resolvePackages(pkg) {
}
})
.catch(err => {
console.log("Package Couldn'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);
});
}

Expand Down

0 comments on commit 8011b5a

Please sign in to comment.