Skip to content

Implements dlx #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .pnp.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ exports.setPackageWhitelist = async function whitelistPackages(
fn: () => Promise<void>,
) {
whitelist = packages;
await fn();
whitelist = new Map();
try {
await fn();
} finally {
whitelist = new Map();
}
};

let packageRegistryPromise = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
module.exports = 42;
module.exports = require('./package.json').version;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env node

console.log(process.cwd());
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

const secret = require('./secret');

console.log(secret);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env node

const noDeps = require('no-deps');

console.log(noDeps.name);
console.log(noDeps.version);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

for (let t = 2; t < process.argv.length; ++t) {
console.log(process.argv[t]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* @flow */

module.exports = require(`./package.json`);

for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) {
for (const dep of Object.keys(module.exports[key] || {})) {
// $FlowFixMe The whole point of this file is to be dynamic
module.exports[key][dep] = require(dep);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "has-bin-entries",
"version": "2.0.0",
"bin": {
"has-bin-entries": "./bin.js",
"has-bin-entries-with-require": "./bin-with-require.js",
"has-bin-entries-with-relative-require": "./bin-with-relative-require.js",
"has-bin-entries-get-pwd": "./bin-get-pwd.js"
},
"dependencies": {
"no-deps": "1.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./package.json').version;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const {
tests: {setPackageWhitelist},
} = require('pkg-tests-core');

describe(`Commands`, () => {
describe(`dlx`, () => {
test(
`it should run the specified binary`,
makeTemporaryEnv({}, async ({path, run, source}) => {
await expect(run(`dlx`, `-q`, `has-bin-entries`)).resolves.toMatchObject({
stdout: ``,
});
}),
);

test(
`it should forward the arguments to the binary`,
makeTemporaryEnv({}, async ({path, run, source}) => {
await expect(run(`dlx`, `-q`, `has-bin-entries`, `--foo`, `hello`, `world`)).resolves.toMatchObject({
stdout: `--foo\nhello\nworld\n`,
});
}),
);

test(
`it should support running different binaries than the default one`,
makeTemporaryEnv({}, async ({path, run, source}) => {
await expect(run(`dlx`, `-q`, `-p`, `has-bin-entries`, `has-bin-entries-with-relative-require`)).resolves.toMatchObject({
// Note: must be updated if you add further versions of "has-bin-entries", since it will always use the latest unless specified otherwise
stdout: `2.0.0\n`,
});
}),
);

test(
`it should support running arbitrary versions`,
makeTemporaryEnv({}, async ({path, run, source}) => {
await expect(run(`dlx`, `-q`, `-p`, `has-bin-entries@1.0.0`, `has-bin-entries-with-relative-require`)).resolves.toMatchObject({
stdout: `1.0.0\n`,
});
}),
);

test(
`it should always update the binary between two calls`,
makeTemporaryEnv({}, async ({path, run, source}) => {
await setPackageWhitelist(new Map([[`has-bin-entries`, new Set([`1.0.0`])]]), async () => {
await expect(run(`dlx`, `-q`, `-p`, `has-bin-entries`, `has-bin-entries-with-relative-require`)).resolves.toMatchObject({
stdout: `1.0.0\n`,
});
});
await setPackageWhitelist(new Map([[`has-bin-entries`, new Set([`1.0.0`, `2.0.0`])]]), async () => {
await expect(run(`dlx`, `-q`, `-p`, `has-bin-entries`, `has-bin-entries-with-relative-require`)).resolves.toMatchObject({
stdout: `2.0.0\n`,
});
});
}),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ describe(`Scripts tests`, () => {
await run(`install`);

await expect(run(`run`, `has-bin-entries-with-relative-require`)).resolves.toMatchObject({
stdout: `42\n`,
stdout: `1.0.0\n`,
});
}),
);
Expand Down
2 changes: 2 additions & 0 deletions packages/berry-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"devDependencies": {
"@berry/builder": "workspace:*",
"@berry/plugin-constraints": "workspace:*",
"@berry/plugin-dlx": "workspace:*",
"@berry/plugin-essentials": "workspace:*",
"@berry/plugin-file": "workspace:*",
"@berry/plugin-github": "workspace:*",
Expand All @@ -39,6 +40,7 @@
"@berry/builder": {
"bundles": {
"standard": [
"@berry/plugin-dlx",
"@berry/plugin-essentials",
"@berry/plugin-init",
"@berry/plugin-constraints",
Expand Down
2 changes: 2 additions & 0 deletions packages/berry-core/sources/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,8 @@ export class Project {
}

const bstateHeader = `# Warning: This file is automatically generated. Removing it is fine, but will\n# cause all your builds to become invalidated.\n\n`;

await xfs.mkdirpPromise(posix.dirname(bstatePath));
await xfs.changeFilePromise(bstatePath, bstateHeader + stringifySyml(bstate));
}

Expand Down
11 changes: 7 additions & 4 deletions packages/berry-core/sources/scriptUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,14 @@ export async function hasPackageScript(locator: Locator, scriptName: string, {pr
}

type ExecutePackageScriptOptions = {
cwd?: string | undefined,
project: Project,
stdin: Readable,
stdout: Writable,
stderr: Writable,
};

export async function executePackageScript(locator: Locator, scriptName: string, args: Array<string>, {project, stdin, stdout, stderr}: ExecutePackageScriptOptions) {
export async function executePackageScript(locator: Locator, scriptName: string, args: Array<string>, {cwd, project, stdin, stdout, stderr}: ExecutePackageScriptOptions) {
const pkg = project.storedPackages.get(locator.locatorHash);
if (!pkg)
throw new Error(`Package for ${structUtils.prettyLocator(project.configuration, locator)} not found in the project`);
Expand All @@ -115,7 +116,8 @@ export async function executePackageScript(locator: Locator, scriptName: string,
const packageFs = new CwdFS(packageLocation, {baseFs: zipOpenFs});
const manifest = await Manifest.find(`.`, {baseFs: packageFs});

const cwd = packageLocation;
if (typeof cwd === `undefined`)
cwd = packageLocation;

const script = manifest.scripts.get(scriptName);
if (!script)
Expand All @@ -130,13 +132,14 @@ export async function executePackageScript(locator: Locator, scriptName: string,
}

type ExecuteWorkspaceScriptOptions = {
cwd?: string | undefined,
stdin: Readable,
stdout: Writable,
stderr: Writable,
};

export async function executeWorkspaceScript(workspace: Workspace, scriptName: string, args: Array<string>, {stdin, stdout, stderr}: ExecuteWorkspaceScriptOptions) {
return await executePackageScript(workspace.anchoredLocator, scriptName, args, {project: workspace.project, stdin, stdout, stderr});
export async function executeWorkspaceScript(workspace: Workspace, scriptName: string, args: Array<string>, {cwd, stdin, stdout, stderr}: ExecuteWorkspaceScriptOptions) {
return await executePackageScript(workspace.anchoredLocator, scriptName, args, {cwd, project: workspace.project, stdin, stdout, stderr});
}

type GetPackageAccessibleBinariesOptions = {
Expand Down
14 changes: 14 additions & 0 deletions packages/plugin-dlx/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@berry/plugin-dlx",
"private": true,
"main": "./sources/index.ts",
"dependencies": {
"@berry/cli": "workspace:*",
"@berry/core": "workspace:*",
"@berry/fslib": "workspace:*",
"@berry/json-proxy": "workspace:*",
"@berry/plugin-essentials": "workspace:*",
"@manaflair/concierge": "^0.12.3",
"tmp": "^0.0.33"
}
}
85 changes: 85 additions & 0 deletions packages/plugin-dlx/sources/commands/dlx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {WorkspaceRequiredError} from '@berry/cli';
import {Cache, Configuration, PluginConfiguration, Project} from '@berry/core';
import {LightReport} from '@berry/core';
import {scriptUtils, structUtils} from '@berry/core';
import {xfs} from '@berry/fslib';
import {suggestUtils} from '@berry/plugin-essentials';
import {UsageError} from '@manaflair/concierge';
import {posix} from 'path';
import {Readable, Writable} from 'stream';
import tmp from 'tmp';

export default (concierge: any, pluginConfiguration: PluginConfiguration) => concierge

.command(`dlx <command> [... args] [-p,--package NAME ...] [-q,--quiet]`)
.describe(`run a package in a temporary environment`)
.flags({proxyArguments: true})

.detail(`
This command will install a package within a temporary environment, and run its binary script if it contains any. The binary will run within the current cwd.

By default Yarn will print the full logs for the given package install process. This behavior can be silenced by using the \`-q,--quiet\` flag which will instruct Yarn to only report critical errors.

Using \`yarn dlx\` as a replacement of \`yarn add\` isn't recommended, as it makes your project non-deterministic (since Yarn doesn't register that your project depends on packages installed via \`dlx\`).
`)

.example(
`Use create-react-app to create a new React app`,
`yarn dlx create-react-app ./my-app`,
)

.action(async ({cwd, stdin, stdout, stderr, command, package: packages, args, quiet, ... rest}: {cwd: string, stdin: Readable, stdout: Writable, stderr: Writable, command: string, package: Array<string>, args: Array<string>, quiet: boolean}) => {
const tmpDir = await createTemporaryDirectory(`dlx-${process.pid}`);
await xfs.writeFilePromise(`${tmpDir}/package.json`, `{}\n`);
await xfs.writeFilePromise(`${tmpDir}/.yarnrc`, `enable-global-cache true\n`);

if (packages.length === 0) {
packages = [command];
command = structUtils.parseDescriptor(command).name;
}

const addOptions = [];
if (quiet)
addOptions.push(`--quiet`);

const addExitCode = await concierge.run(null, [`add`, ... addOptions, `--`, ... packages], {cwd: tmpDir, stdin, stdout, stderr, ... rest});
if (addExitCode !== 0)
return addExitCode;

const configuration = await Configuration.find(tmpDir, pluginConfiguration);
const {project, workspace} = await Project.find(configuration, tmpDir);
const cache = await Cache.find(configuration);

if (workspace === null)
throw new WorkspaceRequiredError(cwd);

const report = await LightReport.start({configuration, stdout}, async (report: LightReport) => {
await project.resolveEverything({lockfileOnly: true, cache, report});
});

if (report.hasErrors())
return report.exitCode();

return await scriptUtils.executeWorkspaceAccessibleBinary(workspace, command, args, {cwd, stdin, stdout, stderr});
});

function createTemporaryDirectory(name?: string) {
return new Promise<string>((resolve, reject) => {
tmp.dir({unsafeCleanup: true}, (error, dirPath) => {
if (error) {
reject(error);
} else {
resolve(dirPath);
}
});
}).then(async dirPath => {
dirPath = await xfs.realpathPromise(dirPath);

if (name) {
dirPath = posix.join(dirPath, name);
await xfs.mkdirpPromise(dirPath);
}

return dirPath;
});
}
11 changes: 11 additions & 0 deletions packages/plugin-dlx/sources/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {Plugin, SettingsType} from '@berry/core';

import dlx from './commands/dlx';

const plugin: Plugin = {
commands: [
dlx,
],
};

export default plugin;
Loading