Skip to content

Commit

Permalink
Adds multiple package managers to link script (#2167)
Browse files Browse the repository at this point in the history
* run link based on destination package manager

* Create getPackageManager.spec.ts

* adds tests for `installPackages`

* add tests for createLinkFrom

* add tests for linkPackageTo

* add tests for linkPackagesForScope

* lint

* rm old code

* adds changesets

---------

Co-authored-by: Brooke Scarlett Yalof <[email protected]>
  • Loading branch information
TheSonOfThomp and bruugey authored Jan 29, 2024
1 parent c465fff commit 90bba7b
Show file tree
Hide file tree
Showing 15 changed files with 455 additions and 118 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-crabs-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lg-tools/meta': minor
---

Adds `pnpm` as a possible `getPackageManager` return value.
5 changes: 5 additions & 0 deletions .changeset/wicked-donuts-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lg-tools/link': minor
---

Previously, `lg link` would only install & link packages using `yarn`. Now it will check the destination app's package manager (via lock file) and link packages using the correct package manager.
103 changes: 6 additions & 97 deletions tools/link/src/link.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
/* eslint-disable no-console */
import { getLGConfig } from '@lg-tools/meta';
import { exitWithErrorMessage, getLGConfig } from '@lg-tools/meta';
import chalk from 'chalk';
import fse from 'fs-extra';
import path from 'path';

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';
import { linkPackagesForScope } from './utils/linkPackagesForScope';

interface LinkOptions {
packages: Array<string>;
Expand All @@ -18,8 +15,6 @@ interface LinkOptions {
from?: string;
}

const ignorePackages = ['mongo-nav'];

export async function linkPackages(
dest: string | undefined,
opts: LinkOptions,
Expand All @@ -29,7 +24,9 @@ export async function linkPackages(
const rootDir = process.cwd();

if (!to && !dest && !from) {
console.error('Error linking. Must provide either a destination or source');
exitWithErrorMessage(
'Error linking. Must provide either a destination or source',
);
}

const destination = path.resolve(path.join(rootDir, dest || to || '.'));
Expand All @@ -43,7 +40,7 @@ export async function linkPackages(
fse.lstatSync(destination).isDirectory()
)
) {
throw new Error(
exitWithErrorMessage(
`Can't find the directory ${formatLog.path(destination ?? '')}.`,
);
}
Expand Down Expand Up @@ -93,91 +90,3 @@ export async function linkPackages(

console.log(chalk.green('Finished linking packages.'));
}

async function linkPackagesForScope(
{ scopeName, scopePath }: Pick<PackageDetails, 'scopeName' | 'scopePath'>,
source: string,
destination: string,
packages?: Array<string>,
verbose?: boolean,
) {
const node_modulesDir = path.join(destination, 'node_modules');

// The directory where the scope's packages are installed
const installedModulesDir = path.join(destination, 'node_modules', scopeName);

if (fse.existsSync(node_modulesDir)) {
// Check that the destination has scope's packages installed
if (fse.existsSync(installedModulesDir)) {
// Get a list of all the packages in the destination
// Run yarn link on each package
// Run yarn link <packageName> on the destination
const installedLGPackages = fse.readdirSync(installedModulesDir);

const packagesToLink = installedLGPackages.filter(
installedPkg =>
!ignorePackages.includes(installedPkg) &&
(!packages ||
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 => {
createLinkFrom(
source,
{ scopeName, scopePath, packageName: pkg },
verbose,
);
}),
);

/** Connect link */
console.log(
chalk.gray(
` Connecting links for ${formatLog.scope(
scopeName,
)} packages to ${chalk.blue(formatLog.path(destination))}...`,
),
);
await Promise.all(
packagesToLink.map((pkg: string) =>
linkPackageTo(
destination,
{
scopeName,
packageName: pkg,
},
verbose,
),
),
);
} else {
console.error(
chalk.gray(
` Couldn't find ${formatLog.scope(
scopeName,
)} scoped packages installed at ${chalk.blue(
formatLog.path(destination),
)}. Skipping.`,
),
);
}
} else {
console.error(chalk.yellow(`${formatLog.path('node_modules')} not found.`));
// TODO: Prompt user to install instead of just running it
await yarnInstall(destination);
await linkPackagesForScope(
{ scopeName, scopePath },
destination,
source,
packages,
verbose,
);
}
}
49 changes: 49 additions & 0 deletions tools/link/src/utils/createLinkFrom.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ChildProcess } from 'child_process';
import xSpawn from 'cross-spawn';
import fsx from 'fs-extra';
import path from 'path';

import { createLinkFrom } from './createLinkFrom';
import { MockChildProcess } from './mocks.testutils';

describe('tools/link/createLinkFrom', () => {
let spawnSpy: jest.SpyInstance<ChildProcess>;

beforeAll(() => {
fsx.emptyDirSync('./tmp');
fsx.rmdirSync('./tmp/');
fsx.mkdirSync('./tmp/');
fsx.mkdirSync('./tmp/scope');
fsx.mkdirSync('./tmp/scope/test-package');
});

beforeEach(() => {
spawnSpy = jest.spyOn(xSpawn, 'spawn');
spawnSpy.mockImplementation((..._args) => new MockChildProcess());
});

afterEach(() => {
spawnSpy.mockRestore();
fsx.emptyDirSync('./tmp');
});

afterAll(() => {
fsx.rmdirSync('./tmp/');
});

test('calls `npm link` command from package directory', () => {
createLinkFrom(path.resolve('./tmp/'), {
scopeName: '@example',
scopePath: 'scope',
packageName: 'test-package',
});

expect(spawnSpy).toHaveBeenCalledWith(
'npm',
['link'],
expect.objectContaining({
cwd: expect.stringContaining('tmp/scope/test-package'),
}),
);
});
});
23 changes: 18 additions & 5 deletions tools/link/src/utils/createLinkFrom.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,44 @@
/* eslint-disable no-console */
import { getPackageManager, SupportedPackageManager } from '@lg-tools/meta';
import chalk from 'chalk';
import { spawn } from 'cross-spawn';
import path from 'path';

import { findDirectory } from './findDirectory';
import { PackageDetails } from './types';

interface CreateLinkOptions extends PackageDetails {
verbose?: boolean;
packageManager?: SupportedPackageManager;
}

/**
* 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,
source: string = process.cwd(),
{
scopeName,
scopePath,
packageName,
packageManager,
verbose,
}: CreateLinkOptions,
): Promise<void> {
const scopeSrc = scopePath;
return new Promise<void>(resolve => {
const packagesDirectory = findDirectory(process.cwd(), scopeSrc);
const packagesDirectory = findDirectory(source, scopeSrc);
packageManager = packageManager ?? getPackageManager(source);

if (packagesDirectory) {
verbose &&
console.log(
'Creating link for:',
chalk.green(`${scopeName}/${packageName}`),
);
spawn('yarn', ['link'], {

spawn(packageManager, ['link'], {
cwd: path.join(packagesDirectory, packageName),
stdio: verbose ? 'inherit' : 'ignore',
})
Expand Down
47 changes: 47 additions & 0 deletions tools/link/src/utils/install.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ChildProcess } from 'child_process';
import xSpawn from 'cross-spawn';
import fsx from 'fs-extra';

import { installPackages } from './install';
import { MockChildProcess } from './mocks.testutils';

describe('tools/link/utils/install', () => {
let spawnSpy: jest.SpyInstance<ChildProcess>;

beforeAll(() => {
fsx.mkdirSync('./tmp/');
});

beforeEach(() => {
spawnSpy = jest.spyOn(xSpawn, 'spawn');
spawnSpy.mockImplementation((..._args) => new MockChildProcess());
});

afterEach(() => {
spawnSpy.mockRestore();
fsx.emptyDirSync('./tmp');
});

afterAll(() => {
fsx.rmdirSync('./tmp/');
});

test('runs `npm install` command', async () => {
await installPackages('./tmp');
expect(spawnSpy).toHaveBeenCalledWith(
'npm',
['install'],
expect.objectContaining({}),
);
});

test('runs install command using local package manager', async () => {
fsx.createFileSync('./tmp/yarn.lock');
await installPackages('./tmp');
expect(spawnSpy).toHaveBeenCalledWith(
'yarn',
['install'],
expect.objectContaining({}),
);
});
});
36 changes: 26 additions & 10 deletions tools/link/src/utils/install.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import { getPackageManager } from '@lg-tools/meta';
import { SupportedPackageManager } from '@lg-tools/meta/src/getPackageManager';
import { spawn } from 'cross-spawn';
import fsx from 'fs-extra';

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`);
});
export async function installPackages(
path: string,
options?: {
packageManager?: SupportedPackageManager;
verbose?: boolean;
},
): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (fsx.existsSync(path)) {
const pkgMgr = options?.packageManager ?? getPackageManager(path);

spawn(pkgMgr, ['install'], {
cwd: path,
stdio: options?.verbose ? 'inherit' : 'ignore',
})
.on('close', resolve)
.on('error', err => {
throw new Error(`Error installing packages\n` + err);
});
} else {
console.error(`Path ${path} does not exist`);
reject();
}
});
}
47 changes: 47 additions & 0 deletions tools/link/src/utils/linkPackageTo.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ChildProcess } from 'child_process';
import xSpawn from 'cross-spawn';
import fsx from 'fs-extra';
import path from 'path';

import { linkPackageTo } from './linkPackageTo';
import { MockChildProcess } from './mocks.testutils';

describe('tools/link/linkPackageTo', () => {
let spawnSpy: jest.SpyInstance<ChildProcess>;

beforeAll(() => {
fsx.emptyDirSync('./tmp');
fsx.rmdirSync('./tmp/');
fsx.mkdirSync('./tmp/');
fsx.mkdirSync('./tmp/app');
});

beforeEach(() => {
spawnSpy = jest.spyOn(xSpawn, 'spawn');
spawnSpy.mockImplementation((..._args) => new MockChildProcess());
});

afterEach(() => {
spawnSpy.mockRestore();
fsx.emptyDirSync('./tmp');
});

afterAll(() => {
fsx.rmdirSync('./tmp/');
});

test('calls `npm link <package>` from the destination directory', () => {
linkPackageTo(path.resolve('./tmp/app'), {
scopeName: '@example',
packageName: 'test-package',
});

expect(spawnSpy).toHaveBeenCalledWith(
'npm',
['link', '@example/test-package'],
expect.objectContaining({
cwd: expect.stringContaining('tmp/app'),
}),
);
});
});
Loading

0 comments on commit 90bba7b

Please sign in to comment.