Skip to content

Commit

Permalink
Add commands for working with locally linked packages (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
iansu authored May 16, 2020
1 parent f6fac42 commit a440b0d
Show file tree
Hide file tree
Showing 16 changed files with 354 additions and 105 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# CHANGELOG

## 1.1.0 (May 16, 2020)

Add `linked`, `linkable`, `unlink` and `unlink-all` commands for working with locally linked packages

## 1.0.0 (April 4, 2020)

Initial release! :tada:
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ A Yarn wrapper with extra functionality
## Features

- Automatically add and remove TypeScript `@types` packages when adding or removing packages in a TypeScript project
- List linked and linkable packages, unlink all packages and automatically rerun Yarn after unlinking packages.

## Usage

Expand Down Expand Up @@ -50,6 +51,18 @@ If you run this command in a TypeScript project `yargs` will be installed as a d

If you run this command in a TypeScript project and `@types/yargs` exists in `package.json` both `yargs` and `@types/yargs` will be removed. If you run this command in a JavaScript project only `yargs` will be removed.

### Showing linked packages in a project

`blarn linked`

### Showing all packages available to be linked

`blarn linkable`

### Unlinking all linked packages in a project

`blarn unlink-all`

## Contributing

### Running locally
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "blarn",
"description": "A Yarn wrapper with extra functionality",
"version": "1.0.0",
"version": "1.1.0",
"author": "Ian Sutherland <[email protected]>",
"license": "MIT",
"repository": {
Expand Down Expand Up @@ -49,7 +49,8 @@
]
},
"dependencies": {
"update-notifier": "4.1.0"
"update-notifier": "4.1.0",
"yarn-config-directory": "1.0.2"
},
"devDependencies": {
"@types/find-package-json": "1.1.1",
Expand Down
141 changes: 38 additions & 103 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,26 @@
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import execa from 'execa';
import pacote from 'pacote';
import findPackageJson from 'find-package-json';
import updateNotifier from 'update-notifier';

interface PackageJson {
name: string;
version: string;
devDependencies?: {
[key: string]: string;
};
}

const getPackageJson = (startDirectory: string): PackageJson => {
const finder = findPackageJson(startDirectory);
const results = finder.next();

if (results.filename) {
return { name: '', version: '1.0.0', ...results.value };
} else {
throw new Error('Unable to find package.json');
}
};

const printVersion = async (): Promise<void> => {
const packageJson = getPackageJson(__dirname);
const { stdout: yarnVersion } = await execa('yarn', ['--version']);

console.log(`Blarn version: ${packageJson.version}`);
console.log(`Yarn version: ${yarnVersion}`);
};

const isYarn1 = async (): Promise<boolean> => {
const { stdout } = await execa('yarn', ['--version']);
import { getPackageJson, PackageJson } from './lib/package';
import { printVersion, isYarn1, runYarn } from './lib/yarn';
import { isTypeScriptProject } from './lib/typescript';

return stdout.startsWith('1.');
};
import { add } from './commands/add';
import { linkable } from './commands/linkable';
import { linked } from './commands/linked';
import { remove } from './commands/remove';
import { unlink } from './commands/unlink';
import { unlinkAll } from './commands/unlink-all';

const isTypeScriptProject = (packageJson: PackageJson): boolean => {
if (
packageJson.devDependencies &&
Object.keys(packageJson.devDependencies).includes('typescript')
) {
return true;
} else {
return fs.existsSync(path.join(process.cwd(), 'tsconfig.json'));
}
};
const app = async (): Promise<void> => {
const command = process.argv[2];
const args = process.argv.slice(3);

const runYarn = async (...args: string[]): Promise<void> => {
try {
await execa('yarn', args, { stdio: 'inherit' });
} catch (error) {
process.exit(error.exitCode);
}
};
let packageJson: PackageJson;

const app = async (): Promise<void> => {
updateNotifier({ pkg: getPackageJson(__dirname) }).notify();

if (process.argv[2] === '--version' || process.argv[2] === '-v') {
if (command === '--version' || command === '-v') {
await printVersion();

return;
Expand All @@ -73,66 +32,42 @@ const app = async (): Promise<void> => {
return;
}

const packageJson = getPackageJson(process.cwd());

if (process.argv[2] === 'remove' && isTypeScriptProject(packageJson)) {
const typePackages = [];

if (packageJson.devDependencies) {
for (const pkg of process.argv.slice(3)) {
if (!pkg.startsWith('-') && !pkg.startsWith('--') && !pkg.startsWith('@types')) {
let typePackage = `@types/${pkg}`;

if (pkg.startsWith('@')) {
const [org, packageName] = pkg.slice(1).split('/');

typePackage = `@types/${org}__${packageName}`;
}

if (Object.keys(packageJson.devDependencies).includes(typePackage)) {
typePackages.push(typePackage);
}
}
}
}

await runYarn('remove', ...process.argv.slice(3), ...typePackages);
if (command === 'linkable') {
await linkable();

return;
}

await runYarn(...process.argv.slice(2));
try {
packageJson = getPackageJson(process.cwd());
} catch (error) {
console.error(`${chalk.red('error')} not a node package`);

if (process.argv[2] === 'add' && isTypeScriptProject(packageJson)) {
const typePackages = [];
return;
}

for (const pkg of process.argv.slice(3)) {
if (!pkg.startsWith('-') && !pkg.startsWith('--') && !pkg.startsWith('@types')) {
let typePackage = `@types/${pkg}`;
if (command === 'remove' && isTypeScriptProject(packageJson)) {
await remove(packageJson, args);

if (pkg.startsWith('@')) {
const [org, packageName] = pkg.slice(1).split('/');
return;
} else if (command === 'linked') {
await linked();

typePackage = `@types/${org}__${packageName}`;
}
return;
} else if (command === 'unlink') {
await unlink(args);

try {
await pacote.manifest(typePackage);
return;
} else if (command === 'unlink-all') {
await unlinkAll();

typePackages.push(typePackage);
} catch (error) {
if (error.code !== 'E404') {
console.error(`Error finding package: ${typePackage}`);
}
}
}
}
return;
}

if (typePackages.length > 0) {
console.log(`\n${chalk.blue('info')} Installing types: ${typePackages.join(' ')}`);
await runYarn(...process.argv.slice(2));

await runYarn('add', '--dev', ...typePackages);
}
if (command === 'add' && isTypeScriptProject(packageJson)) {
await add(args);
}
};

Expand Down
38 changes: 38 additions & 0 deletions src/commands/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import chalk from 'chalk';
import pacote from 'pacote';

import { runYarn } from '../lib/yarn';

const add = async (packages: string[]): Promise<void> => {
const typePackages = [];

for (const pkg of packages) {
if (!pkg.startsWith('-') && !pkg.startsWith('--') && !pkg.startsWith('@types')) {
let typePackage = `@types/${pkg}`;

if (pkg.startsWith('@')) {
const [org, packageName] = pkg.slice(1).split('/');

typePackage = `@types/${org}__${packageName}`;
}

try {
await pacote.manifest(typePackage);

typePackages.push(typePackage);
} catch (error) {
if (error.code !== 'E404') {
console.error(`Error finding package: ${typePackage}`);
}
}
}

if (typePackages.length > 0) {
console.log(`\n${chalk.blue('info')} Installing types: ${typePackages.join(' ')}`);

await runYarn('add', '--dev', ...typePackages);
}
}
};

export { add };
32 changes: 32 additions & 0 deletions src/commands/linkable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import yarnConfig from 'yarn-config-directory';

import { getSymlinks } from '../lib/symlink';

const linkable = async (): Promise<void> => {
const linkPath = path.join(yarnConfig(), 'link');

if (!fs.existsSync(linkPath)) {
console.log('No linkable packages');
}

const symlinks = getSymlinks(linkPath);
const linkablePackages = symlinks.map(symlink => ({
name: symlink.name.replace(`${linkPath}/`, ''),
target: symlink.target
}));

if (linkablePackages.length > 0) {
linkablePackages.sort((a, b) => (a.name > b.name ? 1 : -1));

for (const pkg of linkablePackages) {
console.log(chalk.magenta(pkg.name), '->', pkg.target);
}
} else {
console.log('No linkable packages');
}
};

export { linkable };
33 changes: 33 additions & 0 deletions src/commands/linked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';

import { getSymlinks } from '../lib/symlink';

const linked = async (): Promise<void> => {
const searchPath = path.join(process.cwd(), 'node_modules');

if (!fs.existsSync(searchPath)) {
console.error(`${chalk.red('error')} not a node package`);
}

const symlinks = getSymlinks(searchPath);
const linkedPackages = symlinks
.filter(symlink => !symlink.name.includes('.bin'))
.map(symlink => ({
name: symlink.name.replace(`${searchPath}/`, ''),
target: symlink.target
}));

if (linkedPackages.length > 0) {
linkedPackages.sort((a, b) => (a.name > b.name ? 1 : -1));

for (const pkg of linkedPackages) {
console.log(chalk.magenta(pkg.name), '->', pkg.target);
}
} else {
console.log('No linked packages');
}
};

export { linked };
28 changes: 28 additions & 0 deletions src/commands/remove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PackageJson } from '../lib/package';
import { runYarn } from '../lib/yarn';

const remove = async (packageJson: PackageJson, packages: string[]): Promise<void> => {
const typePackages = [];

if (packageJson.devDependencies) {
for (const pkg of packages) {
if (!pkg.startsWith('-') && !pkg.startsWith('--') && !pkg.startsWith('@types')) {
let typePackage = `@types/${pkg}`;

if (pkg.startsWith('@')) {
const [org, packageName] = pkg.slice(1).split('/');

typePackage = `@types/${org}__${packageName}`;
}

if (Object.keys(packageJson.devDependencies).includes(typePackage)) {
typePackages.push(typePackage);
}
}
}
}

await runYarn('remove', ...process.argv.slice(3), ...typePackages);
};

export { remove };
Loading

0 comments on commit a440b0d

Please sign in to comment.