Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Link to & from #1917

Merged
merged 5 commits into from
Aug 10, 2023
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
7 changes: 6 additions & 1 deletion tools/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <destination>', 'Alias for `destination`')
.option(
'--from <source>',
'When running from a consuming application, defines the source of linked packages',
)
.option('-v --verbose', 'Prints additional information to the console', false)
.option('--scope <name>', 'The NPM organization')
.option(
Expand Down
184 changes: 71 additions & 113 deletions tools/link/src/link.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,86 @@
/* 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<string>;
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<Promise<void>> = [];

for (const [scopeName, scopePath] of Object.entries(availableScopes)) {
if (!scopeFlag || scopeFlag.includes(scopeName)) {
linkPromises.push(
linkPackagesForScope(
scopeName,
scopePath as string,
{ scopeName, scopePath },
source,
destination,
packages,
verbose,
Expand All @@ -60,8 +95,8 @@ export async function linkPackages(destination: string, opts: LinkOptions) {
}

async function linkPackagesForScope(
scopeName: string,
scopePath: string,
{ scopeName, scopePath }: Pick<PackageDetails, 'scopeName' | 'scopePath'>,
source: string,
destination: string,
packages?: Array<string>,
verbose?: boolean,
Expand All @@ -86,16 +121,23 @@ async function linkPackagesForScope(
packages.some(pkgFlag => pkgFlag.includes(installedPkg))),
);

/** Create links */
console.log(
chalk.gray(
` Creating links to ${formatLog.scope(scopeName)} packages...`,
),
);
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(
Expand All @@ -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 {
Expand All @@ -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<void> {
const scopeSrc = scopePath;
return new Promise<void>(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 <packageName> command in the destination directory
* @returns Promise that resolves when the yarn link <packageName> command has finished
*/
function linkPackageToDestination(
scopeName: string,
packageName: string,
destination: string,
verbose?: boolean,
): Promise<void> {
const fullPackageName = `${scopeName}/${packageName}`;
return new Promise<void>(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`);
});
});
}
2 changes: 1 addition & 1 deletion tools/link/src/unlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
42 changes: 42 additions & 0 deletions tools/link/src/utils/createLinkFrom.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const scopeSrc = scopePath;
return new Promise<void>(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.`,
);
}
});
}
21 changes: 21 additions & 0 deletions tools/link/src/utils/findDirectory.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
File renamed without changes.
14 changes: 14 additions & 0 deletions tools/link/src/utils/install.ts
Original file line number Diff line number Diff line change
@@ -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`);
});
});
}
28 changes: 28 additions & 0 deletions tools/link/src/utils/linkPackageTo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import chalk from 'chalk';
import { spawn } from 'cross-spawn';

import { PackageDetails } from './types';

/**
* Runs the yarn link <packageName> command in the destination directory
* @returns Promise that resolves when the yarn link <packageName> command has finished
*/
export function linkPackageTo(
destination: string,
{ scopeName, packageName }: Pick<PackageDetails, 'scopeName' | 'packageName'>,
verbose?: boolean,
): Promise<void> {
const fullPackageName = `${scopeName}/${packageName}`;
return new Promise<void>(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}`);
});
});
}
5 changes: 5 additions & 0 deletions tools/link/src/utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface PackageDetails {
scopeName: string;
packageName: string;
scopePath: string;
}
Loading
Loading