diff --git a/tools/cli/src/index.ts b/tools/cli/src/index.ts index cc12738893..8996c4b5ab 100644 --- a/tools/cli/src/index.ts +++ b/tools/cli/src/index.ts @@ -50,7 +50,12 @@ cli cli .command('link') .description('Link local LeafyGreen packages to a destination app.') - .argument('destination', 'The destination app path') + .argument('[destination]', 'The destination app path') + .option('--to ', 'Alias for `destination`') + .option( + '--from ', + 'When running from a consuming application, defines the source of linked packages', + ) .option('-v --verbose', 'Prints additional information to the console', false) .option('--scope ', 'The NPM organization') .option( diff --git a/tools/link/src/link.ts b/tools/link/src/link.ts index 026872504d..1257b4245f 100644 --- a/tools/link/src/link.ts +++ b/tools/link/src/link.ts @@ -1,42 +1,77 @@ /* eslint-disable no-console */ import { getLGConfig } from '@lg-tools/meta'; import chalk from 'chalk'; -import { spawn } from 'cross-spawn'; import fse from 'fs-extra'; -import { homedir } from 'os'; import path from 'path'; -import { formatLog } from './utils'; +import { createLinkFrom } from './utils/createLinkFrom'; +import { formatLog } from './utils/formatLog'; +import { yarnInstall } from './utils/install'; +import { linkPackageTo } from './utils/linkPackageTo'; +import { PackageDetails } from './utils/types'; interface LinkOptions { packages: Array; scope: string; verbose: boolean; + to?: string; + from?: string; } const ignorePackages = ['mongo-nav']; -export async function linkPackages(destination: string, opts: LinkOptions) { - const { verbose, scope: scopeFlag, packages } = opts; +export async function linkPackages( + dest: string | undefined, + opts: LinkOptions, +) { + const { verbose, scope: scopeFlag, packages, to, from } = opts; + const rootDir = process.cwd(); - const relativeDestination = path.relative(rootDir, destination); + + if (!to && !dest && !from) { + console.error('Error linking. Must provide either a destination or source'); + } + + const destination = path.resolve(path.join(rootDir, dest || to || '.')); + const source = path.resolve(from ? path.join(rootDir, from) : rootDir); // Check if the destination exists if ( - !(fse.existsSync(destination) && fse.lstatSync(destination).isDirectory()) + !( + destination && + fse.existsSync(destination) && + fse.lstatSync(destination).isDirectory() + ) ) { throw new Error( - `Can't find the directory ${formatLog.path(relativeDestination)}.`, + `Can't find the directory ${formatLog.path(destination ?? '')}.`, + ); + } + + if (dest ?? to) { + console.log( + chalk.green(`Linking packages to ${formatLog.path(destination)} ...`), ); } - console.log( - chalk.green( - `Linking packages to ${formatLog.path(relativeDestination)} ...`, - ), - ); + if (from) { + console.log( + chalk.green(`Linking packages from ${formatLog.path(source)} ...`), + ); + } - const { scopes: availableScopes } = getLGConfig(); + const { scopes: availableScopes } = getLGConfig(source); + + verbose && + console.log({ + availableScopes, + dest, + to, + from, + destination, + source, + rootDir, + }); const linkPromises: Array> = []; @@ -44,8 +79,8 @@ export async function linkPackages(destination: string, opts: LinkOptions) { if (!scopeFlag || scopeFlag.includes(scopeName)) { linkPromises.push( linkPackagesForScope( - scopeName, - scopePath as string, + { scopeName, scopePath }, + source, destination, packages, verbose, @@ -60,8 +95,8 @@ export async function linkPackages(destination: string, opts: LinkOptions) { } async function linkPackagesForScope( - scopeName: string, - scopePath: string, + { scopeName, scopePath }: Pick, + source: string, destination: string, packages?: Array, verbose?: boolean, @@ -86,6 +121,7 @@ async function linkPackagesForScope( packages.some(pkgFlag => pkgFlag.includes(installedPkg))), ); + /** Create links */ console.log( chalk.gray( ` Creating links to ${formatLog.scope(scopeName)} packages...`, @@ -93,9 +129,15 @@ async function linkPackagesForScope( ); await Promise.all( packagesToLink.map(pkg => { - createYarnLinkForPackage(scopeName, scopePath, pkg, verbose); + createLinkFrom( + source, + { scopeName, scopePath, packageName: pkg }, + verbose, + ); }), ); + + /** Connect link */ console.log( chalk.gray( ` Connecting links for ${formatLog.scope( @@ -105,7 +147,14 @@ async function linkPackagesForScope( ); await Promise.all( packagesToLink.map((pkg: string) => - linkPackageToDestination(scopeName, pkg, destination, verbose), + linkPackageTo( + destination, + { + scopeName, + packageName: pkg, + }, + verbose, + ), ), ); } else { @@ -124,102 +173,11 @@ async function linkPackagesForScope( // TODO: Prompt user to install instead of just running it await yarnInstall(destination); await linkPackagesForScope( - scopeName, - scopePath, + { scopeName, scopePath }, destination, + source, packages, verbose, ); } } - -/** - * Runs the yarn link command in a leafygreen-ui package directory - * @returns Promise that resolves when the yarn link command has finished - */ -function createYarnLinkForPackage( - scopeName: string, - scopePath: string, - packageName: string, - verbose?: boolean, -): Promise { - const scopeSrc = scopePath; - return new Promise(resolve => { - const packagesDirectory = findDirectory(process.cwd(), scopeSrc); - - if (packagesDirectory) { - verbose && - console.log( - 'Creating link for:', - chalk.green(`${scopeName}/${packageName}`), - ); - spawn('yarn', ['link'], { - cwd: path.join(packagesDirectory, packageName), - stdio: verbose ? 'inherit' : 'ignore', - }) - .on('close', resolve) - .on('error', () => { - throw new Error(`Couldn't create link for package: ${packageName}`); - }); - } else { - throw new Error( - `Can't find a ${scopeSrc} directory in ${process.cwd()} or any of its parent directories.`, - ); - } - }); -} - -/** - * Runs the yarn link command in the destination directory - * @returns Promise that resolves when the yarn link command has finished - */ -function linkPackageToDestination( - scopeName: string, - packageName: string, - destination: string, - verbose?: boolean, -): Promise { - const fullPackageName = `${scopeName}/${packageName}`; - return new Promise(resolve => { - verbose && console.log('Linking package:', chalk.blue(fullPackageName)); - spawn('yarn', ['link', fullPackageName], { - cwd: destination, - stdio: verbose ? 'inherit' : 'ignore', - }) - .on('close', resolve) - .on('error', () => { - throw new Error(`Couldn't link package: ${fullPackageName}`); - }); - }); -} - -function findDirectory( - startDir: string, - targetDir: string, -): string | undefined { - const testDir = path.join(startDir, targetDir); - - if (fse.existsSync(testDir) && fse.lstatSync(testDir).isDirectory()) { - return testDir; - } else { - const parentDir = path.join(startDir, '..'); - - // If we haven't reached the users home directory, recursively look for the packages directory - if (parentDir !== homedir()) { - return findDirectory(path.join(startDir, '..'), targetDir); - } - } -} - -function yarnInstall(path: string) { - return new Promise((resolve, reject) => { - spawn('yarn', ['install'], { - cwd: path, - stdio: 'ignore', - }) - .on('close', resolve) - .on('error', () => { - throw new Error(`Error installing packages`); - }); - }); -} diff --git a/tools/link/src/unlink.ts b/tools/link/src/unlink.ts index a0efe24236..da795230eb 100644 --- a/tools/link/src/unlink.ts +++ b/tools/link/src/unlink.ts @@ -5,7 +5,7 @@ import { spawn } from 'cross-spawn'; import fse from 'fs-extra'; import path from 'path'; -import { formatLog } from './utils'; +import { formatLog } from './utils/formatLog'; interface UnlinkOpts { verbose: boolean; diff --git a/tools/link/src/utils/createLinkFrom.ts b/tools/link/src/utils/createLinkFrom.ts new file mode 100644 index 0000000000..aac32c692f --- /dev/null +++ b/tools/link/src/utils/createLinkFrom.ts @@ -0,0 +1,42 @@ +/* eslint-disable no-console */ +import chalk from 'chalk'; +import { spawn } from 'cross-spawn'; +import path from 'path'; + +import { findDirectory } from './findDirectory'; +import { PackageDetails } from './types'; + +/** + * Runs the yarn link command in a leafygreen-ui package directory + * @returns Promise that resolves when the yarn link command has finished + */ +export function createLinkFrom( + source: string, + { scopeName, scopePath, packageName }: PackageDetails, + verbose?: boolean, +): Promise { + const scopeSrc = scopePath; + return new Promise(resolve => { + const packagesDirectory = findDirectory(process.cwd(), scopeSrc); + + if (packagesDirectory) { + verbose && + console.log( + 'Creating link for:', + chalk.green(`${scopeName}/${packageName}`), + ); + spawn('yarn', ['link'], { + cwd: path.join(packagesDirectory, packageName), + stdio: verbose ? 'inherit' : 'ignore', + }) + .on('close', resolve) + .on('error', () => { + throw new Error(`Couldn't create link for package: ${packageName}`); + }); + } else { + throw new Error( + `Can't find a ${scopeSrc} directory in ${process.cwd()} or any of its parent directories.`, + ); + } + }); +} diff --git a/tools/link/src/utils/findDirectory.ts b/tools/link/src/utils/findDirectory.ts new file mode 100644 index 0000000000..c6a3ac8418 --- /dev/null +++ b/tools/link/src/utils/findDirectory.ts @@ -0,0 +1,21 @@ +import fse from 'fs-extra'; +import { homedir } from 'os'; +import path from 'path'; + +export function findDirectory( + startDir: string, + targetDir: string, +): string | undefined { + const testDir = path.join(startDir, targetDir); + + if (fse.existsSync(testDir) && fse.lstatSync(testDir).isDirectory()) { + return testDir; + } else { + const parentDir = path.join(startDir, '..'); + + // If we haven't reached the users home directory, recursively look for the packages directory + if (parentDir !== homedir()) { + return findDirectory(path.join(startDir, '..'), targetDir); + } + } +} diff --git a/tools/link/src/utils.ts b/tools/link/src/utils/formatLog.ts similarity index 100% rename from tools/link/src/utils.ts rename to tools/link/src/utils/formatLog.ts diff --git a/tools/link/src/utils/install.ts b/tools/link/src/utils/install.ts new file mode 100644 index 0000000000..3d03bb6613 --- /dev/null +++ b/tools/link/src/utils/install.ts @@ -0,0 +1,14 @@ +import { spawn } from 'cross-spawn'; + +export function yarnInstall(path: string) { + return new Promise((resolve, reject) => { + spawn('yarn', ['install'], { + cwd: path, + stdio: 'ignore', + }) + .on('close', resolve) + .on('error', () => { + throw new Error(`Error installing packages`); + }); + }); +} diff --git a/tools/link/src/utils/linkPackageTo.ts b/tools/link/src/utils/linkPackageTo.ts new file mode 100644 index 0000000000..3b6a675cbd --- /dev/null +++ b/tools/link/src/utils/linkPackageTo.ts @@ -0,0 +1,28 @@ +import chalk from 'chalk'; +import { spawn } from 'cross-spawn'; + +import { PackageDetails } from './types'; + +/** + * Runs the yarn link command in the destination directory + * @returns Promise that resolves when the yarn link command has finished + */ +export function linkPackageTo( + destination: string, + { scopeName, packageName }: Pick, + verbose?: boolean, +): Promise { + const fullPackageName = `${scopeName}/${packageName}`; + return new Promise(resolve => { + // eslint-disable-next-line no-console + verbose && console.log('Linking package:', chalk.blue(fullPackageName)); + spawn('yarn', ['link', fullPackageName], { + cwd: destination, + stdio: verbose ? 'inherit' : 'ignore', + }) + .on('close', resolve) + .on('error', () => { + throw new Error(`Couldn't link package: ${fullPackageName}`); + }); + }); +} diff --git a/tools/link/src/utils/types.ts b/tools/link/src/utils/types.ts new file mode 100644 index 0000000000..7919b5e544 --- /dev/null +++ b/tools/link/src/utils/types.ts @@ -0,0 +1,5 @@ +export interface PackageDetails { + scopeName: string; + packageName: string; + scopePath: string; +} diff --git a/tools/meta/src/getLGConfig.ts b/tools/meta/src/getLGConfig.ts index e59d6cbd4a..2a4849b667 100644 --- a/tools/meta/src/getLGConfig.ts +++ b/tools/meta/src/getLGConfig.ts @@ -11,8 +11,8 @@ export const LGConfigFileName = 'lg.json'; * * @returns The LG config object for the current repository */ -export const getLGConfig = (): LGConfig => { - const rootDir = process.cwd(); +export const getLGConfig = (dir?: string): LGConfig => { + const rootDir = dir ?? process.cwd(); const lgConfigPath = path.resolve(rootDir, LGConfigFileName); // Check if an lg.json exists