Skip to content

Commit e75cf74

Browse files
feat: update CLI options for improved linking process
1 parent 999febf commit e75cf74

File tree

7 files changed

+116
-38
lines changed

7 files changed

+116
-38
lines changed

DEVELOPER.md

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,27 @@ We use @testing-library/react for writing tests locally. This library helps mock
7272

7373
### Linking
7474

75-
We also have a link script, such that you can test components that are in development in environments beyond Storybook. To do so, run `yarn run link -- [path-to-application]`.
75+
We provide a `link` script to help you test in-development components within environments outside of Storybook. To do this, run:
7676

77-
Note: There are some known issues using `yarn link` from yarn workspaces. Using Verdaccio, while more involved, is the more reliable and recommended approach for testing in an external project.
77+
```
78+
yarn run link --to=[path-to-application]
79+
```
7880

79-
### Using a local registry (Verdaccio)
81+
If you encounter issues while linking, try the following troubleshooting flags:
8082

81-
Publishing test versions to a local registry can be helpful when you need to make changes and test in an external app (or other library). To do this, you can install and use [Verdaccio](https://verdaccio.org/)
83+
- When linking multiple packages with `--scope` or multiple `--packages` options, link processes run in parallel by default. If you experience failures, add the `--no-parallel` flag to run the linking tasks sequentially, which can help avoid race conditions.
8284

83-
#### 1. Install `verdaccio`
85+
- If you are using a Node version manager such as `asdf` or `nvm`, add the `--launch-env="$(env)"` flag. This ensures the link script spawns commands using your current shell’s environment, preventing environment pollutions that may happen through the tooling of the version manager.
8486

85-
```bash
86-
yarn install --global verdaccio
87-
```
87+
- In your external project, make sure your module resolver picks up `'react'` from your own `node_modules` (not from LeafyGreen’s). If using webpack, you can enforce this by adding an alias in your webpack configuration:
88+
89+
```js
90+
resolve: {
91+
alias: {
92+
react: path.dirname(require.resolve('react/package.json'))
93+
},
94+
},
95+
```
8896

8997
#### 2. Start `verdaccio`, and make note on the localhost port (should be `http://localhost:4873/` by default)
9098

@@ -115,12 +123,12 @@ You should expect to see the following line in that file. (if not you can add it
115123
Ensure all packages are built, then navigate to some package and manually publish:
116124

117125
```bash
118-
pnpm build;
126+
yarn build;
119127
cd packages/<package-name>;
120-
pnpm publish;
128+
yarn publish;
121129
```
122130

123-
To ensure you are pointing to the correct registry, you can add the `--dry-run` flag to the `pnpm publish` command. This command should echo:
131+
To ensure you are pointing to the correct registry, you can add the `--dry-run` flag to the `yarn publish` command. This command should echo:
124132

125133
```
126134
npm notice Publishing to http://localhost:4873

tools/cli/src/index.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,23 @@ cli
8383
'When running from a consuming application, defines the source of linked packages',
8484
)
8585
.option('-v --verbose', 'Prints additional information to the console', false)
86-
.option('--scope <name>', 'The NPM organization')
86+
.option(
87+
'--no-parallel',
88+
'Run the link command sequentially for each package. Useful for debugging or when the parallel approach fails',
89+
true,
90+
)
91+
.option(
92+
'--launch-env <launchEnv>',
93+
'A string of environment variable lines as `KEY=VALUE`, separated by a newline. ' +
94+
'Only the specified environment variables will be used during npm link commands in the source and destination directories. ' +
95+
'This is useful to workaround environment variable pollution by tools such as version managers (e.g., asdf) or script runners (e.g., pnpm) that interfere with `npm link`. ' +
96+
'We recommend using --launch-env="$(env)" to use your original shell environment.',
97+
undefined,
98+
)
99+
.option('--scope <name>', 'The NPM organization, e.g. @lg-charts')
87100
.option(
88101
'--packages <names...>',
89-
'Specific package names (requires `scope` option, or full package name)',
102+
'Specific package names (requires `scope` option, or full package name) e.g. `@lg-charts/core` or `core` if @lg-charts is the scope',
90103
)
91104
.action(linkPackages);
92105

tools/link/src/link.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,32 @@ interface LinkOptions {
1313
verbose: boolean;
1414
to?: string;
1515
from?: string;
16+
parallel?: boolean;
17+
launchEnv?: string;
1618
}
1719

1820
export async function linkPackages(
1921
dest: string | undefined,
2022
opts: LinkOptions,
2123
) {
22-
const { verbose, scope: scopeFlag, packages, to, from } = opts;
24+
const {
25+
verbose,
26+
scope: scopeFlag,
27+
packages,
28+
to,
29+
from,
30+
parallel,
31+
launchEnv: launchEnvString,
32+
} = opts;
33+
let launchEnv: NodeJS.ProcessEnv | undefined;
34+
35+
if (launchEnvString) {
36+
const keyValuePairs = launchEnvString
37+
.split('\n')
38+
.filter(line => line.trim() && line.includes('='))
39+
.map(line => line.trim().split(/=(.*)/));
40+
launchEnv = Object.fromEntries(keyValuePairs);
41+
}
2342

2443
const rootDir = process.cwd();
2544

@@ -81,6 +100,8 @@ export async function linkPackages(
81100
destination,
82101
packages,
83102
verbose,
103+
parallel,
104+
launchEnv,
84105
),
85106
);
86107
}

tools/link/src/utils/createLinkFrom.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { PackageDetails } from './types';
1010
interface CreateLinkOptions extends PackageDetails {
1111
verbose?: boolean;
1212
packageManager?: SupportedPackageManager;
13+
env?: NodeJS.ProcessEnv;
1314
}
1415

1516
/**
@@ -24,6 +25,7 @@ export async function createLinkFrom(
2425
packageName,
2526
packageManager,
2627
verbose,
28+
env,
2729
}: CreateLinkOptions,
2830
): Promise<void> {
2931
const scopeSrc = scopePath;
@@ -44,6 +46,7 @@ export async function createLinkFrom(
4446
name: `link_src:${packageName}`,
4547
cwd: path.join(packagesDirectory, packageName),
4648
verbose,
49+
env,
4750
});
4851
} catch (_) {
4952
throw new Error(`Couldn't create link for package: ${packageName}`);

tools/link/src/utils/install.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export async function installPackages(
88
options?: {
99
packageManager?: SupportedPackageManager;
1010
verbose?: boolean;
11+
env?: NodeJS.ProcessEnv;
1112
},
1213
): Promise<void> {
1314
if (fsx.existsSync(path)) {
@@ -18,6 +19,7 @@ export async function installPackages(
1819
name: 'install',
1920
cwd: path,
2021
verbose: options?.verbose,
22+
env: options?.env,
2123
});
2224
} catch (err) {
2325
throw new Error(`Error installing packages\n` + err);

tools/link/src/utils/linkPackageTo.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface LinkPackagesToOptions
88
extends Pick<PackageDetails, 'scopeName' | 'packageName'> {
99
verbose?: boolean;
1010
packageManager?: SupportedPackageManager;
11+
env?: NodeJS.ProcessEnv;
1112
}
1213

1314
/**
@@ -16,7 +17,13 @@ interface LinkPackagesToOptions
1617
*/
1718
export async function linkPackageTo(
1819
destination: string,
19-
{ scopeName, packageName, verbose, packageManager }: LinkPackagesToOptions,
20+
{
21+
scopeName,
22+
packageName,
23+
verbose,
24+
packageManager,
25+
env,
26+
}: LinkPackagesToOptions,
2027
): Promise<void> {
2128
const fullPackageName = `${scopeName}/${packageName}`;
2229
// eslint-disable-next-line no-console
@@ -30,6 +37,7 @@ export async function linkPackageTo(
3037
name: `link_dst:${packageName}`,
3138
cwd: destination,
3239
verbose,
40+
env,
3341
});
3442
} catch (_) {
3543
throw new Error(`Couldn't link package: ${fullPackageName}`);

tools/link/src/utils/linkPackagesForScope.ts

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export async function linkPackagesForScope(
1919
destination: string,
2020
packages?: Array<string>,
2121
verbose?: boolean,
22+
parallel?: boolean,
23+
launchEnv?: NodeJS.ProcessEnv,
2224
): Promise<void> {
2325
const node_modulesDir = path.join(destination, 'node_modules');
2426

@@ -39,7 +41,11 @@ export async function linkPackagesForScope(
3941
installedPkg =>
4042
!ignorePackages.includes(installedPkg) &&
4143
(!packages ||
42-
packages.some(pkgFlag => pkgFlag.includes(installedPkg))),
44+
packages.some(
45+
pkgFlag =>
46+
pkgFlag === `${scopeName}/${installedPkg}` ||
47+
pkgFlag === installedPkg,
48+
)),
4349
);
4450

4551
/** Create links */
@@ -48,17 +54,23 @@ export async function linkPackagesForScope(
4854
` Creating links to ${formatLog.scope(scopeName)} packages...`,
4955
),
5056
);
51-
await Promise.all(
52-
packagesToLink.map(pkg => {
53-
createLinkFrom(source, {
54-
scopeName,
55-
scopePath,
56-
packageName: pkg,
57-
verbose,
58-
packageManager: destinationPackageManager,
59-
});
60-
}),
61-
);
57+
const linkFromSource = (pkg: string) =>
58+
createLinkFrom(source, {
59+
scopeName,
60+
scopePath,
61+
packageName: pkg,
62+
verbose,
63+
packageManager: destinationPackageManager,
64+
env: launchEnv,
65+
});
66+
67+
if (parallel) {
68+
await Promise.all(packagesToLink.map(linkFromSource));
69+
} else {
70+
for (const pkg of packagesToLink) {
71+
await linkFromSource(pkg);
72+
}
73+
}
6274

6375
/** Connect link */
6476
console.log(
@@ -68,16 +80,22 @@ export async function linkPackagesForScope(
6880
)} packages to ${chalk.blue(formatLog.path(destination))}...`,
6981
),
7082
);
71-
await Promise.all(
72-
packagesToLink.map((pkg: string) =>
73-
linkPackageTo(destination, {
74-
scopeName,
75-
packageName: pkg,
76-
packageManager: destinationPackageManager,
77-
verbose,
78-
}),
79-
),
80-
);
83+
const linkToDestination = (pkg: string) =>
84+
linkPackageTo(destination, {
85+
scopeName,
86+
packageName: pkg,
87+
packageManager: destinationPackageManager,
88+
verbose,
89+
env: launchEnv,
90+
});
91+
92+
if (parallel) {
93+
await Promise.all(packagesToLink.map(linkToDestination));
94+
} else {
95+
for (const pkg of packagesToLink) {
96+
await linkToDestination(pkg);
97+
}
98+
}
8199
} else {
82100
console.error(
83101
chalk.gray(
@@ -93,14 +111,19 @@ export async function linkPackagesForScope(
93111
console.error(chalk.yellow(`${formatLog.path('node_modules')} not found.`));
94112
// TODO: Prompt user to install instead of just running it
95113

96-
await installPackages(destination);
114+
await installPackages(destination, {
115+
verbose,
116+
env: launchEnv,
117+
});
97118

98119
await linkPackagesForScope(
99120
{ scopeName, scopePath },
100121
destination,
101122
source,
102123
packages,
103124
verbose,
125+
parallel,
126+
launchEnv,
104127
);
105128
}
106129
}

0 commit comments

Comments
 (0)