Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 131 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const fs = require('fs-extra');
const opta = require('opta');
const parseList = require('safe-parse-list');
const { create, load } = require('@npmcli/package-json');
const mapWorkspaces = require('@npmcli/map-workspaces');
const { Loggerr } = require('loggerr');
const packageName = require('./lib/package-name');
const git = require('./lib/git');
Expand Down Expand Up @@ -123,6 +124,22 @@ function initOpts () {
}
},

workspaceRoot: {
type: 'string',
flag: {
key: 'workspace-root'
},
prompt: {
message: 'Workspace Root:',
when: (promptInput, defaultWhen, allInput) => {
if (allInput.workspaceRoot !== allInput.cwd) {
return true;
}
return defaultWhen;
}
}
},

type: {
type: 'string',
prompt: {
Expand Down Expand Up @@ -255,6 +272,50 @@ async function readPackageJson (options, { log } = {}) {
log.error(e);
}

// Check to see if we are in a monorepo context
if (!pkg.workspaces) {
let rootPkg;
let rootDir = opts.workspaceRoot;
if (!rootDir) {
// Limit to git root if in a checked out repo
const gitRoot = await git.repositoryRoot(opts.cwd);

const [_rootPkg, _rootDir] = await npm.findWorkspaceRoot(opts.cwd, gitRoot);
rootPkg = _rootPkg;
rootDir = _rootDir;
} else {
try {
rootPkg = await npm.readPkg(rootDir);
} catch (e) {
if (e.code !== 'ENOENT') {
throw e;
}
// ignore
}
}

// If we are *not* inside what looks like an existing monorepo setup,
// check what packages may show up and suggest them as defaults
// for the `workspaces` key
if (rootDir === opts.cwd) {
const maybeWorkspaces = await npm.searchForWorkspaces(opts.cwd, pkg);
if (maybeWorkspaces.size) {
pkg.workspaces = [];
for (const wsDir of maybeWorkspaces.values()) {
pkg.workspaces.push(path.relative(opts.cwd, wsDir));
}
}
} else {
// We are in a workspace context, don't ask about
// workspaces and set the workspaceRoot
options.overrides({
workspaces: null,
workspaceRoot: rootDir,
workspaceRootPkg: rootPkg
});
}
}

let author;
if (!pkg || !pkg.author) {
const gitAuthor = await git.author({ cwd: opts.cwd });
Expand Down Expand Up @@ -285,7 +346,8 @@ async function readPackageJson (options, { log } = {}) {
repository: repo,
keywords: pkg.keywords,
scripts: pkg.scripts,
license: pkg.license
license: pkg.license,
workspaces: pkg.workspaces
});

return packageInstance.update(pkg);
Expand Down Expand Up @@ -364,26 +426,86 @@ module.exports.write = write;
// TODO: look at https://npm.im/json-file-plus for writing
async function write (opts, pkg, { log } = {}) {
const pkgPath = path.resolve(opts.cwd, 'package.json');

// Ensure directory exists
await fs.mkdirp(path.dirname(pkgPath));

// Write package json
log.info(`Writing package.json\n${pkgPath}`);
await pkg.save();

// If we dont have workspaceRootPkg then we are
// already working on the root workspace package.json
// which means we don't need to do anything with updating
// the monorepo
let workspaceRelativePath = null;
if (opts.workspaceRoot && opts.workspaceRootPkg) {
workspaceRelativePath = path.relative(opts.workspaceRoot, opts.cwd);

// Check if this wis already part of the workspace
const ws = await mapWorkspaces({
pkg: opts.workspaceRootPkg,
cwd: opts.workspaceRoot
});
if (Array.from(ws.values()).includes(opts.cwd)) {
log.debug('Workspaces globs already match the new package path, no update necessary');
} else {
log.info('Adding new package to workspace root package.json');
const rootPkg = await load(opts.workspaceRoot);
rootPkg.update({
workspaces: [...(opts.workspaceRootPkg.workspaces || []), workspaceRelativePath]
});
await rootPkg.save();
}

log.info('Running install', {
directory: opts.workspaceRoot || opts.cwd
});
const out = await npm.install(null, {
directory: opts.workspaceRoot || opts.cwd
});
if (out.stderr) {
log.error(out.stderr);
}
log.debug(out.stdout);
}

// Run installs
if (opts.dependencies && opts.dependencies.length) {
log.info('Installing dependencies', opts.dependencies);
await npm.install(opts.dependencies, {
log.info('Installing dependencies', {
save: 'prod',
directory: opts.workspaceRoot || opts.cwd,
exact: !!opts.saveExact,
workspace: workspaceRelativePath
});
const out = await npm.install(opts.dependencies, {
save: 'prod',
directory: opts.cwd,
exact: !!opts.saveExact
directory: opts.workspaceRoot || opts.cwd,
exact: !!opts.saveExact,
workspace: workspaceRelativePath
});
if (out.stderr) {
log.error(out.stderr);
}
log.debug(out.stdout);
}
if (opts.devDependencies && opts.devDependencies.length) {
log.info('Installing dev dependencies', opts.devDependencies);
await npm.install(opts.devDependencies, {
log.info('Installing dev dependencies', {
save: 'dev',
directory: opts.workspaceRoot || opts.cwd,
exact: !!opts.saveExact,
workspace: workspaceRelativePath
});
const out = await npm.install(opts.devDependencies, {
save: 'dev',
directory: opts.cwd,
exact: !!opts.saveExact
directory: opts.workspaceRoot || opts.cwd,
exact: !!opts.saveExact,
workspace: workspaceRelativePath
});
if (out.stderr) {
log.error(out.stderr);
}
log.debug(out.stdout);
}

// Read full package back to return
Expand Down
Empty file added lib/find-package-json.js
Empty file.
24 changes: 24 additions & 0 deletions lib/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,27 @@ module.exports.repository = async function (cwd, pkg) {
return pkg.repository.url;
}
};

module.exports.repositoryRoot = async function repositoryRoot (cwd) {
try {
// Since we call this *before* creating the new pacakge directory in a monorepo,
// we need to find the first real directory above `cwd` because otherwise this
// command will succeede but use the system cwd instead
let _cwd = cwd;
while (_cwd !== '/') {
try {
const p = await fs.realpath(_cwd);
await fs.access(p, fs.constants.W_OK | fs.constants.R_OK);
_cwd = p;
break;
} catch (e) {
_cwd = path.dirname(_cwd);
}
}

const out = await execFile('git', ['rev-parse', '--show-toplevel'], { cwd: _cwd });
return out.stdout.trim();
} catch (e) {
// Ignore errors
}
};
97 changes: 87 additions & 10 deletions lib/npm.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
'use strict';
const { promisify } = require('util');
const path = require('path');
const fs = require('fs/promises');
const execFile = promisify(require('child_process').execFile);
const npa = require('npm-package-arg');
const semver = require('semver');
const validateNpmPackageName = require('validate-npm-package-name');
const mapWorkspaces = require('@npmcli/map-workspaces');

// Remove npm env vars from the commands, this
// is so it respects the directory it is run in,
Expand All @@ -18,23 +21,30 @@ const env = Object.keys(process.env).reduce((e, key) => {

module.exports.install = install;
function install (deps = [], opts = {}) {
if (!deps || !deps.length) {
if (deps !== null && (!deps || !deps.length)) {
return Promise.resolve();
}

let args = ['i'];
if (opts.save === false) {
args.push('--no-save');
} else {
args.push(`--save-${opts.save || 'prod'}`);
if (opts.exact) {
args.push('--save-exact');

// If we are installing deps, respect those flags
if (deps !== null) {
if (opts.save === false) {
args.push('--no-save');
} else {
args.push(`--save-${opts.save || 'prod'}`);
if (opts.exact) {
args.push('--save-exact');
}
if (opts.bundle) {
args.push('--save-bundle');
}
}
if (opts.bundle) {
args.push('--save-bundle');
if (opts.workspace) {
args.push('-w', opts.workspace);
}
args = args.concat(deps);
}
args = args.concat(deps);

return execFile('npm', args, {
env,
Expand Down Expand Up @@ -123,3 +133,70 @@ function validatePackageName (name) {
}
return true;
}

module.exports.readPkg = readPkg;
async function readPkg (cwd) {
return JSON.parse(await fs.readFile(path.join(cwd, 'package.json')));
}

async function findNearestPackageJson (cwd, root = '/', filter = (dir, pkg) => !!pkg) {
let pkg;
let _cwd = cwd;
while (_cwd) {
try {
pkg = await readPkg(_cwd);
if (filter(_cwd, pkg)) {
break;
}
} catch (e) {
// ignore
}

if (_cwd === root) {
break;
}
_cwd = path.dirname(_cwd);
}
if (!pkg) {
return [null, cwd];
}
return [pkg, _cwd];
}

module.exports.findWorkspaceRoot = findWorkspaceRoot;
async function findWorkspaceRoot (cwd, root = '/') {
const [firstPkg, dir] = await findNearestPackageJson(cwd, root);

// No package.json found inside root
if (!firstPkg) {
return [null, cwd];
}

// The closest directory with a package.json looks like a workspace root
if (firstPkg.workspaces || root === dir) {
return [firstPkg, dir];
}

const [workspacePkg, dir2] = await findNearestPackageJson(path.dirname(dir), root, (d, pkg) => {
return !!pkg?.workspaces;
});

// Found what looks like a workspace root
if (workspacePkg) {
return [workspacePkg, dir2];
}

// No package in root defining a workspace, so return the first
return [firstPkg, dir];
}

module.exports.searchForWorkspaces = searchForWorkspaces;
async function searchForWorkspaces (cwd = process.cwd(), pkg, pattern = '**/*') {
return mapWorkspaces({
pkg: {
...pkg,
workspaces: [pattern]
},
cwd
});
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@
},
"license": "MIT",
"dependencies": {
"@npmcli/map-workspaces": "^5.0.3",
"@npmcli/name-from-folder": "^2.0.0",
"@npmcli/package-json": "^5.0.0",
"fs-extra": "^11.1.1",
"loggerr": "^3.0.0",
"npm-package-arg": "^11.0.1",
"opta": "^1.0.0",
"opta": "^1.1.5",
"safe-parse-list": "^0.1.1",
"validate-npm-package-name": "^5.0.0"
},
Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/monorepo-no-workspaces/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@test/monorepo-no-workspaces",
"version": "0.0.0",
"description": "A test monorepo",
"scripts": {
"test": "exit 0"
},
"author": "Test <tester@example.com>",
"license": "ISC"
}
10 changes: 10 additions & 0 deletions test/fixtures/monorepo-no-workspaces/packages/foo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@test/monorepo-foo",
"version": "1.0.0",
"description": "A test monorepo package named foo",
"scripts": {
"test": "exit 0"
},
"author": "Test <tester@example.com>",
"license": "ISC"
}
Loading