Skip to content

Commit 506e762

Browse files
lettertwomischnic
andauthored
Add parcel-link and parcel-unlink dev CLIs (#8618)
* Link script * Lint * Scaffold atlassian-parcel-link package * Make link and unlink executable * Move binaries from src/ to bin/ * Parse args and scaffold logging * Factor in original link implementation * Extract mapAtlassianPackageAliases util * Use log, not console.log * Implement unlink * Export link and unlink from module I guess in case they would be useful in another script... * Allow configuration of the packageRoot for linking * Force install after unlink * Parametrize the namespace and node_modules globs This gets us a step closer to a more generic solution that can be published publicly by replacing all explicit references to the "@atlassian" namespace. * Improve namespaced config rewrites This should both expand to capture any entries in the root package.json that configure a namespaced package while also avoiding rewriting dependencies. * Add namespace and nodeModulesGlobs options This makes it so that the default behavior of link/unlink works for @parcel packages in any standard Parcel project, but allow configuring custom package namespace (e.g., for forks of Parcel) and custom node_modules locations for more complex setups. * Remove references to atlassian from parcel-link * Update README * Fix multi option parsing * Fix unlink arguments * Unify CLI and create submcommands `link` is the default subcommand and can be omitted, so `parcel-link [packageRoot]` still works. Now, unlinking is done via subcommand: `parcel-link unlink` * Lint/nits * Extract ParcelLinkConfig * Extract command to factory * Interface with @parcel/fs * Fix default command * Make command configurable * [WIP] tests * Throw instead of exit * Improve app root detection * toJSON not toJson * Validate fs operations before performing them This is really meant to avoid logging actions that actually sliently fail, like trying to remove a file that doesn't exist. * Add createFS test util * Improve logged messages * Naming nit * Add descriptive error messages * Rename parcel-link util to utils utils is the convention in the monorepo * Lint * Use globSync from @parcel/utils More testable * Use `withFileTypes` readdir option * Use CopyOnWriteToMemoryFS in tests * Use OverlayFS in tests * Reverse direction of symlink message * Add tests for link with default and common options * Add tests, fixes for linking with a custom namespace * Add tests, fixes for custom node_modules globs * Remove old unlink options * Fix link --dry-run * Add unlink tests * Use fsFixture in parcel-link tests * Fix missing bin link for namespaced links * Update version * lint * Fix package versions * Fix parcel-link tests * Extract link and unlink commands * Update @babel/core dep * Update readme * Fix parcel-link tests * skip tests failing on windows these tests are for the '--namespace' feature, which is is only useful if you're testing a fork of Parcel, so seems safe enough to skip. * Fix package versions --------- Co-authored-by: Niklas Mischkulnig <[email protected]>
1 parent a1391ed commit 506e762

File tree

10 files changed

+1339
-0
lines changed

10 files changed

+1339
-0
lines changed

packages/core/integration-tests/test/parcel-link.js

Lines changed: 555 additions & 0 deletions
Large diffs are not rendered by default.

packages/dev/parcel-link/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# `parcel-link`
2+
3+
A CLI for linking a dev version of Parcel into a project.
4+
5+
## Installation
6+
7+
Clone and run `yarn`, then `cd packages/dev/parcel-link && yarn link`
8+
to make the `parcel-link` binary globally available.
9+
10+
## Usage
11+
12+
In an Parcel project root:
13+
14+
```sh
15+
$ parcel-link [options] [packageRoot]
16+
```
17+
18+
### Specifying `packageRoot`
19+
20+
```sh
21+
$ parcel-link /path/to/parcel/packages
22+
```
23+
24+
By default, `parcel-link` will link to packages in the same
25+
location where `parcel-link` is found. But it is common
26+
to want to link other worktrees of Parcel, and it's not fun
27+
to have to first re-link `parcel-link` to a new location.
28+
29+
For this reason, `parcel-link` accepts a `packageRoot` argument,
30+
which specifies a path to a Parcel `packages` directory.
31+
Links will then be made to packages in that location instead
32+
of the default.
33+
34+
### Specifying a `namespace`
35+
36+
```sh
37+
$ parcel-link --namespace @my-parcel-fork
38+
```
39+
40+
When linking into a project that uses a fork of Parcel,
41+
the published packages may have a different namespace from
42+
Parcel, so `parcel-link` allows specifying a namespace.
43+
44+
If defined to someting other than `"@parcel"`,
45+
`parcel-link` will do some extra work to adjust
46+
namespaced packages to reference linked packages instead.
47+
48+
### Linking into a monorepo
49+
50+
```sh
51+
$ parcel-link --node-modules-globs build-tools/*/node_modules build-tools/parcel/*/node_modules
52+
```
53+
54+
In a monorepo, there may be multiple locations where
55+
Parcel packages are installed. For this, `parcel-link`
56+
allows specifying globs of locations where packages should be linked.
57+
58+
Note that specifying any value here will override the default of `node_modules`,
59+
so if you want to preserve the default behavior, be sure to include `node_modules`
60+
in the list of globs:
61+
62+
```sh
63+
$ parcel-link -g build-tools/*/node_modules -g build-tools/parcel/*/node_modules -g node_modules
64+
```
65+
66+
## Cleanup
67+
68+
To restore the project to its default Parcel install:
69+
70+
```sh
71+
$ parcel-link unlink [options] [packageRoot]
72+
```

packages/dev/parcel-link/bin.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#! /usr/bin/env node
2+
3+
// @flow strict-local
4+
/* eslint-disable no-console */
5+
6+
'use strict';
7+
8+
// $FlowFixMe[untyped-import]
9+
require('@parcel/babel-register');
10+
11+
let program = require('./src/cli').createProgram();
12+
13+
(async function main() {
14+
try {
15+
await program.parseAsync();
16+
} catch (e) {
17+
console.error(e);
18+
process.exit(1);
19+
}
20+
})();

packages/dev/parcel-link/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@parcel/link",
3+
"description": "A CLI for linking a dev version of Parcel into a project",
4+
"version": "2.10.2",
5+
"private": true,
6+
"bin": {
7+
"parcel-link": "bin.js"
8+
},
9+
"scripts": {
10+
"test": "cd ../../.. && yarn test:integration --grep @parcel/link"
11+
},
12+
"main": "src/index.js",
13+
"dependencies": {
14+
"@babel/core": "^7.22.11",
15+
"@parcel/babel-register": "2.10.2",
16+
"@parcel/fs": "2.10.2",
17+
"@parcel/utils": "2.10.2",
18+
"commander": "^7.0.0",
19+
"nullthrows": "^1.1.1"
20+
}
21+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// @flow
2+
3+
import type {FileSystem} from '@parcel/fs';
4+
5+
import {globSync} from '@parcel/utils';
6+
7+
import assert from 'assert';
8+
import nullthrows from 'nullthrows';
9+
import path from 'path';
10+
11+
const LOCK_FILE_NAMES = ['yarn.lock', 'package-lock.json', 'pnpm-lock.yaml'];
12+
const SCM_FILE_NAMES = ['.git', '.hg'];
13+
14+
export class ParcelLinkConfig {
15+
fs: FileSystem;
16+
appRoot: string;
17+
packageRoot: string;
18+
namespace: string = '@parcel';
19+
nodeModulesGlobs: string[] = ['node_modules'];
20+
filename: string = '.parcel-link';
21+
22+
static load(
23+
appRoot: string,
24+
{fs, filename = '.parcel-link'}: {|fs: FileSystem, filename?: string|},
25+
): ParcelLinkConfig {
26+
let manifest = JSON.parse(
27+
fs.readFileSync(path.join(appRoot, filename), 'utf8'),
28+
);
29+
return new ParcelLinkConfig({...manifest, fs});
30+
}
31+
32+
constructor(options: {|
33+
fs: FileSystem,
34+
appRoot: string,
35+
packageRoot: string,
36+
namespace?: string,
37+
nodeModulesGlobs?: string[],
38+
filename?: string,
39+
|}) {
40+
this.fs = nullthrows(options.fs, 'fs is required');
41+
this.appRoot = nullthrows(options.appRoot, 'appRoot is required');
42+
this.packageRoot = nullthrows(
43+
options.packageRoot,
44+
'packageRoot is required',
45+
);
46+
this.namespace = options.namespace ?? this.namespace;
47+
this.nodeModulesGlobs = options.nodeModulesGlobs ?? this.nodeModulesGlobs;
48+
this.filename = options.filename ?? this.filename;
49+
}
50+
51+
save(): Promise<void> {
52+
return this.fs.writeFile(
53+
path.join(this.appRoot, this.filename),
54+
JSON.stringify(this, null, 2),
55+
);
56+
}
57+
58+
delete(): Promise<void> {
59+
return this.fs.rimraf(path.join(this.appRoot, this.filename));
60+
}
61+
62+
validateAppRoot() {
63+
assert(
64+
[...LOCK_FILE_NAMES, ...SCM_FILE_NAMES].some(filename =>
65+
this.fs.existsSync(path.join(this.appRoot, filename)),
66+
),
67+
`Not a project root: '${this.appRoot}'`,
68+
);
69+
}
70+
71+
validatePackageRoot() {
72+
assert(
73+
this.fs.existsSync(path.join(this.packageRoot, 'core/core')),
74+
`Not a package root: '${this.packageRoot}'`,
75+
);
76+
}
77+
78+
validate(): void {
79+
this.validateAppRoot();
80+
this.validatePackageRoot();
81+
}
82+
83+
getNodeModulesPaths(): string[] {
84+
return this.nodeModulesGlobs.reduce(
85+
(matches, pattern) => [
86+
...matches,
87+
...globSync(pattern, this.fs, {cwd: this.appRoot, onlyFiles: false}),
88+
],
89+
[],
90+
);
91+
}
92+
93+
toJSON(): {|
94+
appRoot: string,
95+
packageRoot: string,
96+
namespace: string,
97+
nodeModulesGlobs: string[],
98+
|} {
99+
return {
100+
appRoot: this.appRoot,
101+
packageRoot: this.packageRoot,
102+
namespace: this.namespace,
103+
nodeModulesGlobs: this.nodeModulesGlobs,
104+
};
105+
}
106+
}

packages/dev/parcel-link/src/cli.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// @flow strict-local
2+
/* eslint-disable no-console */
3+
4+
import type {LinkCommandOptions} from './link';
5+
import type {UnlinkCommandOptions} from './unlink';
6+
7+
// $FlowFixMe[untyped-import]
8+
import {version} from '../package.json';
9+
import {createLinkCommand} from './link';
10+
import {createUnlinkCommand} from './unlink';
11+
12+
import commander from 'commander';
13+
14+
export type ProgramOptions = {|...LinkCommandOptions, ...UnlinkCommandOptions|};
15+
16+
// $FlowFixMe[invalid-exported-annotation]
17+
export function createProgram(opts?: ProgramOptions): commander.Command {
18+
let {fs, log = console.log, link, unlink} = opts ?? {};
19+
return new commander.Command()
20+
.version(version, '-V, --version')
21+
.description('A tool for linking a dev copy of Parcel into an app')
22+
.addHelpText('after', `\nThe link command is the default command.`)
23+
.addCommand(createLinkCommand({fs, log, link}), {isDefault: true})
24+
.addCommand(createUnlinkCommand({fs, log, unlink}));
25+
}

packages/dev/parcel-link/src/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// @flow strict-local
2+
3+
export type {ProgramOptions} from './cli';
4+
export type {LinkOptions} from './link';
5+
export type {UnlinkOptions} from './unlink';
6+
7+
export {createProgram} from './cli';
8+
export {link} from './link';
9+
export {unlink} from './unlink';
10+
export {ParcelLinkConfig} from './ParcelLinkConfig';

0 commit comments

Comments
 (0)