Skip to content

Commit 2cdff54

Browse files
Shared tooling: @lg-tools/link (#1821)
* Creates new link script * creates unlink script * Update unlink.ts * Create brave-schools-shop.md * lint * Fix logging
1 parent 199ee0d commit 2cdff54

13 files changed

+340
-4
lines changed

.changeset/brave-schools-shop.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@lg-tools/link': minor
3+
---
4+
5+
First pre-release of LeafyGreen link tools

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
"build": "turbo run build tsc",
1313
"build-storybook": "storybook build",
1414
"build:docs": "npx ts-node ./scripts/parse-tsdocs.ts",
15-
"lint": "lg-lint",
1615
"chromatic": "npx chromatic",
1716
"clean": "yarn clean:builds && yarn clean:modules",
1817
"clean:builds": "rm -rf packages/*/dist packages/*/tsconfig.tsbuildinfo ./tsconfig.tsbuildinfo packages/**/stories.js",
1918
"clean:modules": "rm -rf node_modules packages/*/node_modules ",
2019
"fix": "lg-lint --fix",
21-
"link": "./scripts/link.sh",
22-
"link:all": "lerna exec -- yarn link",
20+
"link": "lg-link",
21+
"lint": "lg-lint",
2322
"prepublishOnly": "yarn fix && yarn test && yarn build && yarn prepublish:ts-downlevel",
2423
"prepublish:ts-downlevel": "rm -rf packages/*/dist/ts3.4 && echo packages/*/dist | NODE_NO_WARNINGS=1 xargs -I{} -n1 -P8 downlevel-dts {} {}/ts3.4",
2524
"publish": "npm-run-all --serial publish:*",
@@ -28,7 +27,7 @@
2827
"start": "storybook dev -p 9001 --no-version-updates",
2928
"test": "lg-test",
3029
"test:ssr": "lg-test --ssr",
31-
"unlink": "./scripts/unlink.sh",
30+
"unlink": "lg-unlink",
3231
"update:proptypes": "npx ts-node ./scripts/proptypes.ts",
3332
"validate": "npm-run-all --parallel validate:*",
3433
"validate:dependencies": "npx ts-node ./scripts/depcheck.ts",
@@ -45,6 +44,7 @@
4544
"@leafygreen-ui/lib": "^10.1.0",
4645
"@leafygreen-ui/testing-lib": "*",
4746
"@lg-tools/build": "^0.0.1",
47+
"@lg-tools/link": "^0.0.1",
4848
"@lg-tools/lint": "^0.0.1",
4949
"@lg-tools/slackbot": "^0.0.1",
5050
"@lg-tools/storybook": "^0.0.1",

tools/link/bin/link.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#! /usr/bin/env node
2+
require('../dist/link.js');
File renamed without changes.

tools/link/bin/unlink.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#! /usr/bin/env node
2+
require('../dist/unlink.js');
File renamed without changes.

tools/link/package.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "@lg-tools/link",
3+
"version": "0.0.1",
4+
"description": "Tools for linking LeafyGreen packages",
5+
"license": "MIT",
6+
"bin": {
7+
"lg-link": "./bin/link.js",
8+
"lg-unlink": "./bin/unlink.js"
9+
},
10+
"scripts": {
11+
"build": "rollup -c rollup.config.mjs"
12+
},
13+
"dependencies": {
14+
"chalk": "^4.1.2",
15+
"commander": "^11.0.0"
16+
}
17+
}

tools/link/rollup.config.mjs

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { umdConfig } from '@lg-tools/build/config/rollup.config.mjs';
2+
3+
export default [
4+
{
5+
...umdConfig,
6+
input: 'src/link.ts',
7+
},
8+
{
9+
...umdConfig,
10+
input: 'src/unlink.ts',
11+
},
12+
];

tools/link/src/link.ts

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/* eslint-disable no-console */
2+
import chalk from 'chalk';
3+
import { spawn } from 'child_process';
4+
import { Command } from 'commander';
5+
import fs from 'fs';
6+
import { homedir } from 'os';
7+
import path from 'path';
8+
9+
import { Scope } from './scopes';
10+
import { formatLog } from './utils';
11+
12+
const program = new Command();
13+
14+
program
15+
.name('link')
16+
.description(
17+
"Link local LeafyGreen packages to a destination app. This is useful for testing changes to a package's source code in another project.",
18+
)
19+
.arguments('destination')
20+
.option('-v --verbose', 'Prints additional information to the console', false)
21+
// TODO: Add `scope` option using `.addOption` method
22+
.action(linkPackages)
23+
.parse(process.argv);
24+
25+
async function linkPackages(
26+
destination: string,
27+
opts: { scope: string; verbose: boolean },
28+
) {
29+
const { verbose } = opts;
30+
const relativeDestination = path.relative(process.cwd(), destination);
31+
32+
// Check if the destination exists
33+
if (fs.existsSync(destination) && fs.lstatSync(destination).isDirectory()) {
34+
console.log(
35+
chalk.green(
36+
`Linking packages to ${formatLog.path(relativeDestination)} ...`,
37+
),
38+
);
39+
await linkPackageForScope('@leafygreen-ui');
40+
await linkPackageForScope('@lg-tools');
41+
console.log(chalk.green('Finished linking packages.'));
42+
} else {
43+
throw new Error(
44+
`Can't find the directory ${formatLog.path(relativeDestination)}.`,
45+
);
46+
}
47+
48+
async function linkPackageForScope(scope: keyof typeof Scope) {
49+
// The directory where the leafygreen-ui packages are installed
50+
const installedModulesDir = path.join(destination, 'node_modules', scope);
51+
52+
// Check that the destination has leafygreen-ui packages installed
53+
if (fs.existsSync(installedModulesDir)) {
54+
// Get a list of all the packages in the destination
55+
// Run yarn link on each package
56+
// Run yarn link <packageName> on the destination
57+
const installedLGPackages = fs.readdirSync(installedModulesDir);
58+
console.log(
59+
chalk.gray(` Creating links to ${formatLog.scope(scope)} packages...`),
60+
);
61+
await Promise.all(
62+
installedLGPackages.map(pkg => {
63+
createYarnLinkForPackage(scope, pkg);
64+
}),
65+
);
66+
console.log(
67+
chalk.gray(
68+
` Connecting links for ${formatLog.scope(
69+
scope,
70+
)} packages to ${chalk.blue(formatLog.path(relativeDestination))}...`,
71+
),
72+
);
73+
await Promise.all(
74+
installedLGPackages.map((pkg: string) =>
75+
linkPackageToDestination(scope, pkg),
76+
),
77+
);
78+
} else {
79+
console.error(
80+
chalk.gray(
81+
` Couldn't find ${formatLog.scope(
82+
scope,
83+
)} scoped packages installed at ${chalk.blue(
84+
formatLog.path(relativeDestination),
85+
)}. Skipping.`,
86+
),
87+
);
88+
}
89+
}
90+
91+
/**
92+
* Runs the yarn link command in a leafygreen-ui package directory
93+
* @returns Promise that resolves when the yarn link command has finished
94+
*/
95+
function createYarnLinkForPackage(
96+
scope: keyof typeof Scope,
97+
packageName: string,
98+
): Promise<void> {
99+
const scopeSrc = Scope[scope];
100+
return new Promise<void>(resolve => {
101+
const packagesDirectory = findDirectory(process.cwd(), scopeSrc);
102+
103+
if (packagesDirectory) {
104+
verbose &&
105+
console.log(
106+
'Creating link for:',
107+
chalk.green(`${scope}/${packageName}`),
108+
);
109+
spawn('yarn', ['link'], {
110+
cwd: path.join(packagesDirectory, packageName),
111+
stdio: verbose ? 'inherit' : 'ignore',
112+
})
113+
.on('close', resolve)
114+
.on('error', () => {
115+
throw new Error(`Couldn't create link for package: ${packageName}`);
116+
});
117+
} else {
118+
throw new Error(
119+
`Can't find a ${scopeSrc} directory in ${process.cwd()} or any of its parent directories.`,
120+
);
121+
}
122+
});
123+
}
124+
125+
/**
126+
* Runs the yarn link <packageName> command in the destination directory
127+
* @returns Promise that resolves when the yarn link <packageName> command has finished
128+
*/
129+
function linkPackageToDestination(
130+
scope: keyof typeof Scope,
131+
packageName: string,
132+
): Promise<void> {
133+
const fullPackageName = `${scope}/${packageName}`;
134+
return new Promise<void>(resolve => {
135+
verbose && console.log('Linking package:', chalk.blue(fullPackageName));
136+
spawn('yarn', ['link', fullPackageName], {
137+
cwd: destination,
138+
stdio: verbose ? 'inherit' : 'ignore',
139+
})
140+
.on('close', resolve)
141+
.on('error', () => {
142+
throw new Error(`Couldn't link package: ${fullPackageName}`);
143+
});
144+
});
145+
}
146+
}
147+
148+
function findDirectory(
149+
startDir: string,
150+
targetDir: string,
151+
): string | undefined {
152+
const testDir = path.join(startDir, targetDir);
153+
154+
if (fs.existsSync(testDir) && fs.lstatSync(testDir).isDirectory()) {
155+
return testDir;
156+
} else {
157+
const parentDir = path.join(startDir, '..');
158+
159+
// If we haven't reached the users home directory, recursively look for the packages directory
160+
if (parentDir !== homedir()) {
161+
return findDirectory(path.join(startDir, '..'), targetDir);
162+
}
163+
}
164+
}

tools/link/src/scopes.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const Scope = {
2+
'@leafygreen-ui': 'packages',
3+
'@lg-tools': 'tools',
4+
} as const;
5+
export type scope = keyof typeof Scope;

tools/link/src/unlink.ts

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/* eslint-disable no-console */
2+
import chalk from 'chalk';
3+
import { spawn } from 'child_process';
4+
import { Command } from 'commander';
5+
import fs from 'fs';
6+
import path from 'path';
7+
8+
import { Scope } from './scopes';
9+
import { formatLog } from './utils';
10+
11+
const program = new Command();
12+
13+
program
14+
.name('unlink')
15+
.description(
16+
"Link local LeafyGreen packages to a destination app. This is useful for testing changes to a package's source code in another project.",
17+
)
18+
.arguments('destination')
19+
.option('-v --verbose', 'Prints additional information to the console', false)
20+
.option('--noInstall', 'Skip the yarn install step', false)
21+
.action(unlinkPackages)
22+
.parse(process.argv);
23+
24+
interface Opts {
25+
verbose: boolean;
26+
noInstall: boolean;
27+
}
28+
29+
async function unlinkPackages(destination: string, opts: Opts) {
30+
const { verbose, noInstall } = opts;
31+
const relativeDestination = path.relative(process.cwd(), destination);
32+
33+
// Check if the destination exists
34+
if (fs.existsSync(destination) && fs.lstatSync(destination).isDirectory()) {
35+
console.log(
36+
chalk.yellow(
37+
`Unlinking packages from ${formatLog.path(relativeDestination)} ...`,
38+
),
39+
);
40+
41+
await unlinkPackageForScope('@leafygreen-ui');
42+
await unlinkPackageForScope('@lg-tools');
43+
if (noInstall) {
44+
console.log(
45+
` Skipping yarn install. \nYou will need to run ${formatLog.cmd(
46+
'yarn install --force',
47+
)} in ${formatLog.path(destination)} to restore dependencies.`,
48+
);
49+
} else {
50+
await forceInstall();
51+
}
52+
console.log(chalk.yellow('Finished unlinking packages.'));
53+
} else {
54+
throw new Error(
55+
`Can't find the directory ${formatLog.path(relativeDestination)}.`,
56+
);
57+
}
58+
59+
async function unlinkPackageForScope(scope: keyof typeof Scope) {
60+
const installedModulesDir = path.join(destination, 'node_modules', scope);
61+
62+
if (fs.existsSync(installedModulesDir)) {
63+
const installedLGPackages = fs.readdirSync(installedModulesDir);
64+
chalk.gray(
65+
` Removing links to ${formatLog.scope(scope)} scoped packages...`,
66+
),
67+
await Promise.all(
68+
installedLGPackages.map(pkg =>
69+
unlinkPackageFromDestination(scope, pkg),
70+
),
71+
);
72+
} else {
73+
console.error(
74+
chalk.gray(
75+
` Couldn't find any ${formatLog.scope(
76+
scope,
77+
)} packages installed at ${formatLog.path(
78+
relativeDestination,
79+
)}. Skipping.`,
80+
),
81+
);
82+
}
83+
}
84+
85+
function unlinkPackageFromDestination(
86+
scope: keyof typeof Scope,
87+
packageName: string,
88+
): Promise<void> {
89+
const fullPackageName = `${scope}/${packageName}`;
90+
91+
return new Promise(resolve => {
92+
verbose && console.log('Linking package:', chalk.blue(fullPackageName));
93+
94+
spawn('yarn', ['unlink', fullPackageName], {
95+
cwd: destination,
96+
stdio: verbose ? 'inherit' : 'ignore',
97+
})
98+
.on('close', resolve)
99+
.on('error', () => {
100+
throw new Error(`Couldn't unlink package: ${fullPackageName}`);
101+
});
102+
});
103+
}
104+
105+
function forceInstall() {
106+
return new Promise(resolve => {
107+
console.log(
108+
chalk.gray(
109+
` Reinstalling packages in ${formatLog.path(destination)}...`,
110+
),
111+
);
112+
spawn('yarn', ['install', '--force'], {
113+
cwd: destination,
114+
stdio: 'inherit',
115+
}).on('close', resolve);
116+
});
117+
}
118+
}

tools/link/src/utils.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import chalk from 'chalk';
2+
import { Scope } from './scopes';
3+
4+
export const formatLog = {
5+
scope: (scope: keyof typeof Scope) => chalk.blue.bold(scope),
6+
path: (path: string) => chalk.bold(path),
7+
cmd: (cmd: string) => chalk.bgGray.black(cmd),
8+
};

tools/link/tsconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "@lg-tools/build/config/package.tsconfig.json",
3+
}

0 commit comments

Comments
 (0)