diff --git a/package-lock.json b/package-lock.json index 0d7407b..adc2f8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1330,6 +1330,12 @@ "integrity": "sha512-0JKYQRatHdzijO/ni7JV5eHUJWaMRpGvwiABk8U5iAk5Corm0yLNEfYGNkZWYc+wCyCKKpg0+TsZIvP8AymIYA==", "dev": true }, + "@types/pify": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/pify/-/pify-3.0.2.tgz", + "integrity": "sha512-a5AKF1/9pCU3HGMkesgY6LsBdXHUY3WU+I2qgpU0J+I8XuJA1aFr59eS84/HP0+dxsyBSNbt+4yGI2adUpHwSg==", + "dev": true + }, "@types/shelljs": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.5.tgz", @@ -1765,8 +1771,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -1842,7 +1847,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2302,8 +2306,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "2.0.0", @@ -4084,8 +4087,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.2.8", @@ -4948,7 +4950,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5305,7 +5306,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -5314,8 +5314,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", @@ -6972,7 +6971,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7528,7 +7526,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -7754,8 +7751,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -7806,6 +7802,11 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", @@ -9803,8 +9804,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "1.0.3", diff --git a/package.json b/package.json index faec828..4975b4c 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@types/js-yaml": "^3.12.1", "@types/lodash.mergewith": "^4.6.6", "@types/object-hash": "^1.2.0", + "@types/pify": "^3.0.2", "@types/signal-exit": "^3.0.0", "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^1.6.0", @@ -120,11 +121,13 @@ "errorish": "^0.2.1", "find-up": "^3.0.0", "fs-extra": "^7.0.1", + "glob": "^7.1.3", "js-yaml": "^3.13.1", "lodash.mergewith": "^4.6.1", "loglevel": "^1.6.1", "manage-path": "^2.0.0", "object-hash": "^1.3.1", + "pify": "^4.0.1", "promist": "^0.5.3", "signal-exit": "^3.0.2", "slimconf": "^0.9.0", diff --git a/src/state/index.ts b/src/state/index.ts index 2e1e708..4f5855c 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -44,8 +44,11 @@ export default { async setScope(name: string): Promise { const definition = await scope(name); if (definition) { - states.internal.scopes.push(definition.name); + // keep track of scope branches + states.internal.scopes = states.internal.scopes.concat(definition.names); + // set current directory as the the one of the scope this.setBase({ file: null, directory: definition.directory }); + // reset options this.setOptions(); } }, diff --git a/src/state/scope.ts b/src/state/scope.ts deleted file mode 100644 index 9111a79..0000000 --- a/src/state/scope.ts +++ /dev/null @@ -1,19 +0,0 @@ -import state from './index'; -import logger from '~/utils/logger'; -import { IScopeDefinition } from './types'; - -export default async function scope( - scope: string -): Promise { - const paths = await state.paths(); - if (scope === 'root') { - if (paths.root) { - return { name: 'root', directory: paths.root.directory }; - } else { - logger.debug('root scope was not found and was assigned to self'); - return null; - } - } - - throw Error(`Scope ${scope} was not found`); -} diff --git a/src/state/scope/children/from-globs.ts b/src/state/scope/children/from-globs.ts new file mode 100644 index 0000000..ac2e6fb --- /dev/null +++ b/src/state/scope/children/from-globs.ts @@ -0,0 +1,80 @@ +import path from 'path'; +import fs from 'fs-extra'; +import glob from 'glob'; +import pify from 'pify'; +import { parallel } from 'promist'; +import { exists } from '~/utils/file'; +import { FILE_NAME, FILE_EXT } from '~/constants'; +import { IChild } from '../../types'; + +export default async function getChildrenFromGlobs( + patterns: string[], + directory: string +): Promise { + const arrs = await parallel.map(patterns, (pattern) => + fromGlob(pattern, directory) + ); + + const dirs = arrs.reduce((acc: string[], arr: string[]) => { + return acc.concat(arr); + }, []); + + // filter and make into IChild + return filter(dirs).map((dir) => ({ + // absolute path + directory: path.join(directory, dir), + matcher(name: string) { + return dir.includes(name); + } + })); +} + +export async function fromGlob( + pattern: string, + directory: string +): Promise { + return parallel.filter( + await pify(glob)(pattern, { cwd: directory }), + async (dir: string) => { + // get absolute path + dir = path.join(directory, dir); + + // select only directories + const stat = await fs.stat(dir); + if (!stat.isDirectory()) return false; + + // select only directories that have a package.json + // or a kpo configuration file + const toFind = ['package.json'] + .concat(FILE_EXT.map((ext) => FILE_NAME + ext)) + .map((file) => path.join(dir, file)); + + for (let file of toFind) { + if (await exists(file)) return true; + } + + return false; + } + ); +} + +/** + * Filter directories: select only the first one in depth + * in which a configuration file was found. + * If we have /foo and /foo/bar, only /foo will be selected + */ +export function filter(dirs: string[]): string[] { + dirs = dirs.sort(); + let i = 1; + while (i < dirs.length) { + const current = dirs[i]; + const previous = dirs[i - 1]; + if (current.slice(0, previous.length) === previous) { + dirs = dirs.slice(0, i).concat(dirs.slice(i + 1)); + } else { + i++; + } + } + + return dirs; +} diff --git a/src/state/scope/children/from-map.ts b/src/state/scope/children/from-map.ts new file mode 100644 index 0000000..7e4967a --- /dev/null +++ b/src/state/scope/children/from-map.ts @@ -0,0 +1,15 @@ +import path from 'path'; +import { IOfType } from '~/types'; +import { IChild } from '../../types'; + +export default function getChildrenFromMap( + map: IOfType, + directory: string +): IChild[] { + return Object.entries(map).map(([key, value]) => ({ + directory: path.isAbsolute(value) ? value : path.join(directory, value), + matcher(name: string): boolean { + return name === key; + } + })); +} diff --git a/src/state/scope/children/index.ts b/src/state/scope/children/index.ts new file mode 100644 index 0000000..b48c2b0 --- /dev/null +++ b/src/state/scope/children/index.ts @@ -0,0 +1,39 @@ +import path from 'path'; +import fs from 'fs-extra'; +import logger from '~/utils/logger'; +import { exists } from '~/utils/file'; +import { rejects } from 'errorish'; +import getChildrenFromGlobs from './from-globs'; +import getChildrenFromMap from './from-map'; +import { IChild } from '../../types'; +import { TChildrenDefinition } from '~/types'; + +export default async function getChildren( + directory: string, + definition?: TChildrenDefinition +): Promise { + logger.debug('obtaining children'); + + if (definition) { + logger.debug('children found in options'); + + if (Array.isArray(definition)) { + return getChildrenFromGlobs(definition, directory); + } + return getChildrenFromMap(definition, directory); + } + + const lerna = (await exists(path.join(directory, 'lerna.json'))) + ? await fs.readJSON(path.join(directory, 'lerna.json')).catch(rejects) + : null; + + if (lerna) { + logger.debug('lerna file found'); + if (lerna.packages) { + return getChildrenFromGlobs(lerna.packages, directory); + } + } + + logger.debug('no children found'); + return []; +} diff --git a/src/state/scope/index.ts b/src/state/scope/index.ts new file mode 100644 index 0000000..0069b1d --- /dev/null +++ b/src/state/scope/index.ts @@ -0,0 +1,38 @@ +import state from '../index'; +import logger from '~/utils/logger'; +import getChildren from './children'; +import { IScopeDefinition } from '../types'; + +export default async function scope( + scope: string +): Promise { + const paths = await state.paths(); + + // root scope + if (scope === 'root') { + if (paths.root) { + return { names: ['root'], directory: paths.root.directory }; + } else { + logger.debug('root scope was not found and was assigned to self'); + return null; + } + } + + // child scopes + const children = await getChildren(paths.directory, state.get('children')); + const matches = children + .filter((child) => child.matcher(scope)) + .map((child) => child.directory); + + if (matches.length) { + logger.debug(`scopes found for ${scope}:\n${matches.join('\n')}`); + + if (matches.length > 1) { + throw Error(`Several scopes matched name "${scope}"`); + } + + return { names: [scope], directory: matches[0] }; + } + + throw Error(`Scope ${scope} was not found`); +} diff --git a/src/state/types.ts b/src/state/types.ts index 542386e..a7dc4a1 100644 --- a/src/state/types.ts +++ b/src/state/types.ts @@ -23,6 +23,11 @@ export interface ILoaded { } export interface IScopeDefinition { - name: string; + names: string[]; directory: string; } + +export interface IChild { + directory: string; + matcher: (scope: string) => boolean; +} diff --git a/src/types.ts b/src/types.ts index 9cdd2c9..0bdd2fd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -34,5 +34,7 @@ export interface IBaseOptions extends ICoreOptions { export interface IScopeOptions extends ICoreOptions { root?: string | null; - children?: IOfType; + children?: TChildrenDefinition; } + +export type TChildrenDefinition = IOfType | string[];