Skip to content

Commit

Permalink
Add parcel-link and parcel-unlink dev CLIs (#8618)
Browse files Browse the repository at this point in the history
* 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 <4586894+mischnic@users.noreply.github.com>
  • Loading branch information
lettertwo and mischnic authored Nov 3, 2023
1 parent a1391ed commit 506e762
Show file tree
Hide file tree
Showing 10 changed files with 1,339 additions and 0 deletions.
555 changes: 555 additions & 0 deletions packages/core/integration-tests/test/parcel-link.js

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions packages/dev/parcel-link/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# `parcel-link`

A CLI for linking a dev version of Parcel into a project.

## Installation

Clone and run `yarn`, then `cd packages/dev/parcel-link && yarn link`
to make the `parcel-link` binary globally available.

## Usage

In an Parcel project root:

```sh
$ parcel-link [options] [packageRoot]
```

### Specifying `packageRoot`

```sh
$ parcel-link /path/to/parcel/packages
```

By default, `parcel-link` will link to packages in the same
location where `parcel-link` is found. But it is common
to want to link other worktrees of Parcel, and it's not fun
to have to first re-link `parcel-link` to a new location.

For this reason, `parcel-link` accepts a `packageRoot` argument,
which specifies a path to a Parcel `packages` directory.
Links will then be made to packages in that location instead
of the default.

### Specifying a `namespace`

```sh
$ parcel-link --namespace @my-parcel-fork
```

When linking into a project that uses a fork of Parcel,
the published packages may have a different namespace from
Parcel, so `parcel-link` allows specifying a namespace.

If defined to someting other than `"@parcel"`,
`parcel-link` will do some extra work to adjust
namespaced packages to reference linked packages instead.

### Linking into a monorepo

```sh
$ parcel-link --node-modules-globs build-tools/*/node_modules build-tools/parcel/*/node_modules
```

In a monorepo, there may be multiple locations where
Parcel packages are installed. For this, `parcel-link`
allows specifying globs of locations where packages should be linked.

Note that specifying any value here will override the default of `node_modules`,
so if you want to preserve the default behavior, be sure to include `node_modules`
in the list of globs:

```sh
$ parcel-link -g build-tools/*/node_modules -g build-tools/parcel/*/node_modules -g node_modules
```

## Cleanup

To restore the project to its default Parcel install:

```sh
$ parcel-link unlink [options] [packageRoot]
```
20 changes: 20 additions & 0 deletions packages/dev/parcel-link/bin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#! /usr/bin/env node

// @flow strict-local
/* eslint-disable no-console */

'use strict';

// $FlowFixMe[untyped-import]
require('@parcel/babel-register');

let program = require('./src/cli').createProgram();

(async function main() {
try {
await program.parseAsync();
} catch (e) {
console.error(e);
process.exit(1);
}
})();
21 changes: 21 additions & 0 deletions packages/dev/parcel-link/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@parcel/link",
"description": "A CLI for linking a dev version of Parcel into a project",
"version": "2.10.2",
"private": true,
"bin": {
"parcel-link": "bin.js"
},
"scripts": {
"test": "cd ../../.. && yarn test:integration --grep @parcel/link"
},
"main": "src/index.js",
"dependencies": {
"@babel/core": "^7.22.11",
"@parcel/babel-register": "2.10.2",
"@parcel/fs": "2.10.2",
"@parcel/utils": "2.10.2",
"commander": "^7.0.0",
"nullthrows": "^1.1.1"
}
}
106 changes: 106 additions & 0 deletions packages/dev/parcel-link/src/ParcelLinkConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// @flow

import type {FileSystem} from '@parcel/fs';

import {globSync} from '@parcel/utils';

import assert from 'assert';
import nullthrows from 'nullthrows';
import path from 'path';

const LOCK_FILE_NAMES = ['yarn.lock', 'package-lock.json', 'pnpm-lock.yaml'];
const SCM_FILE_NAMES = ['.git', '.hg'];

export class ParcelLinkConfig {
fs: FileSystem;
appRoot: string;
packageRoot: string;
namespace: string = '@parcel';
nodeModulesGlobs: string[] = ['node_modules'];
filename: string = '.parcel-link';

static load(
appRoot: string,
{fs, filename = '.parcel-link'}: {|fs: FileSystem, filename?: string|},
): ParcelLinkConfig {
let manifest = JSON.parse(
fs.readFileSync(path.join(appRoot, filename), 'utf8'),
);
return new ParcelLinkConfig({...manifest, fs});
}

constructor(options: {|
fs: FileSystem,
appRoot: string,
packageRoot: string,
namespace?: string,
nodeModulesGlobs?: string[],
filename?: string,
|}) {
this.fs = nullthrows(options.fs, 'fs is required');
this.appRoot = nullthrows(options.appRoot, 'appRoot is required');
this.packageRoot = nullthrows(
options.packageRoot,
'packageRoot is required',
);
this.namespace = options.namespace ?? this.namespace;
this.nodeModulesGlobs = options.nodeModulesGlobs ?? this.nodeModulesGlobs;
this.filename = options.filename ?? this.filename;
}

save(): Promise<void> {
return this.fs.writeFile(
path.join(this.appRoot, this.filename),
JSON.stringify(this, null, 2),
);
}

delete(): Promise<void> {
return this.fs.rimraf(path.join(this.appRoot, this.filename));
}

validateAppRoot() {
assert(
[...LOCK_FILE_NAMES, ...SCM_FILE_NAMES].some(filename =>
this.fs.existsSync(path.join(this.appRoot, filename)),
),
`Not a project root: '${this.appRoot}'`,
);
}

validatePackageRoot() {
assert(
this.fs.existsSync(path.join(this.packageRoot, 'core/core')),
`Not a package root: '${this.packageRoot}'`,
);
}

validate(): void {
this.validateAppRoot();
this.validatePackageRoot();
}

getNodeModulesPaths(): string[] {
return this.nodeModulesGlobs.reduce(
(matches, pattern) => [
...matches,
...globSync(pattern, this.fs, {cwd: this.appRoot, onlyFiles: false}),
],
[],
);
}

toJSON(): {|
appRoot: string,
packageRoot: string,
namespace: string,
nodeModulesGlobs: string[],
|} {
return {
appRoot: this.appRoot,
packageRoot: this.packageRoot,
namespace: this.namespace,
nodeModulesGlobs: this.nodeModulesGlobs,
};
}
}
25 changes: 25 additions & 0 deletions packages/dev/parcel-link/src/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// @flow strict-local
/* eslint-disable no-console */

import type {LinkCommandOptions} from './link';
import type {UnlinkCommandOptions} from './unlink';

// $FlowFixMe[untyped-import]
import {version} from '../package.json';
import {createLinkCommand} from './link';
import {createUnlinkCommand} from './unlink';

import commander from 'commander';

export type ProgramOptions = {|...LinkCommandOptions, ...UnlinkCommandOptions|};

// $FlowFixMe[invalid-exported-annotation]
export function createProgram(opts?: ProgramOptions): commander.Command {
let {fs, log = console.log, link, unlink} = opts ?? {};
return new commander.Command()
.version(version, '-V, --version')
.description('A tool for linking a dev copy of Parcel into an app')
.addHelpText('after', `\nThe link command is the default command.`)
.addCommand(createLinkCommand({fs, log, link}), {isDefault: true})
.addCommand(createUnlinkCommand({fs, log, unlink}));
}
10 changes: 10 additions & 0 deletions packages/dev/parcel-link/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @flow strict-local

export type {ProgramOptions} from './cli';
export type {LinkOptions} from './link';
export type {UnlinkOptions} from './unlink';

export {createProgram} from './cli';
export {link} from './link';
export {unlink} from './unlink';
export {ParcelLinkConfig} from './ParcelLinkConfig';
Loading

0 comments on commit 506e762

Please sign in to comment.