Skip to content

Commit d24cb95

Browse files
committed
Add --dry-run as an alias of --preview
It’s actually the other way around, but it’s easier to explain it as an alias since the change is backwards compatible. Fixes #783
1 parent 86a7f26 commit d24cb95

5 files changed

Lines changed: 100 additions & 35 deletions

File tree

readme.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<sub>(does not apply to external registries)</sub>
2424
- Opens a prefilled GitHub Releases draft after publish
2525
- Warns about the possibility of extraneous files being published
26-
- See exactly what will be executed with [preview mode](https://github.com/sindresorhus/np/issues/391), without pushing or publishing anything remotely
26+
- See exactly what will be executed with [dry-run mode](https://github.com/sindresorhus/np/issues/391), without pushing or publishing anything remotely
2727
- Supports [GitHub Packages](https://github.com/features/packages)
2828
- Supports npm 9+, Yarn (Classic and Berry), pnpm 8+, and Bun
2929

@@ -62,7 +62,7 @@ $ np --help
6262
--no-tests Skips tests
6363
--yolo Skips cleanup and testing
6464
--no-publish Skips publishing
65-
--preview Show tasks without actually executing them
65+
--dry-run Show tasks without actually executing them
6666
--tag Publish under a given dist-tag
6767
--contents Subdirectory to publish
6868
--no-release-draft Skips opening a GitHub release draft
@@ -101,7 +101,7 @@ Currently, these are the flags you can configure:
101101
- `tests` - Run `npm test` (`true` by default).
102102
- `yolo` - Skip cleanup and testing (`false` by default).
103103
- `publish` - Publish (`true` by default).
104-
- `preview` - Show tasks without actually executing them (`false` by default).
104+
- `dryRun` - Show tasks without actually executing them (`false` by default). The CLI also accepts `--preview` as an alias.
105105
- `tag` - Publish under a given dist-tag (`latest` by default).
106106
- `contents` - Subdirectory to publish (`.` by default).
107107
- `releaseDraft` - Open a GitHub release draft after releasing (`true` by default).

source/cli-implementation.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const cli = meow(`
3232
--no-tests Skips tests
3333
--yolo Skips cleanup and testing
3434
--no-publish Skips publishing
35-
--preview Show tasks without actually executing them
35+
--dry-run Show tasks without actually executing them
3636
--tag Publish under a given dist-tag
3737
--contents Subdirectory to publish
3838
--no-release-draft Skips opening a GitHub release draft
@@ -54,6 +54,7 @@ const cli = meow(`
5454
`, {
5555
importMeta: import.meta,
5656
booleanDefault: undefined,
57+
allowUnknownFlags: false,
5758
// Don't use `default` for flags - we apply defaults later so config can override them
5859
flags: {
5960
anyBranch: {
@@ -92,8 +93,9 @@ const cli = meow(`
9293
contents: {
9394
type: 'string',
9495
},
95-
preview: {
96+
dryRun: {
9697
type: 'boolean',
98+
aliases: ['preview'],
9799
},
98100
testScript: {
99101
type: 'string',
@@ -133,7 +135,7 @@ async function getOptions() {
133135
const explicitCliFlags = Object.fromEntries(Object.entries(cli.flags).filter(([, value]) => value !== undefined));
134136

135137
// Merge: local config → explicit CLI flags → defaults
136-
const flags = {
138+
const mergedFlags = {
137139
cleanup: true,
138140
tests: true,
139141
publish: true,
@@ -144,6 +146,9 @@ async function getOptions() {
144146
...explicitCliFlags,
145147
};
146148

149+
const {preview, ...flags} = mergedFlags;
150+
flags.dryRun ??= preview;
151+
147152
// Workaround for unintended auto-casing behavior from `meow`.
148153
if ('2Fa' in flags) {
149154
flags['2fa'] = flags['2Fa'];
@@ -223,7 +228,7 @@ try {
223228
console.log(); // Prints a newline for readability
224229
const newPackage = await np(options.version.toString(), options, {package_, projectDirectory, rootDirectory});
225230

226-
if (options.preview || options.releaseDraftOnly) {
231+
if (options.dryRun || options.releaseDraftOnly) {
227232
gracefulExit();
228233
}
229234

source/index.js

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@ const exec = (command, arguments_, options) => {
5454
@param {import('./cli-implementation.js').Options} options
5555
@param {{package_: import('read-pkg').NormalizedPackageJson; projectDirectory?: string; rootDirectory: string}} context
5656
*/
57-
const np = async (input = 'patch', {packageManager, ...options}, {package_, projectDirectory, rootDirectory}) => {
57+
const np = async (input = 'patch', {packageManager, ...rawOptions}, {package_, projectDirectory, rootDirectory}) => {
5858
projectDirectory ??= rootDirectory;
59+
60+
const {preview, ...options} = rawOptions;
61+
options.dryRun ??= preview;
62+
5963
// TODO: Remove sometime far in the future
6064
if (options.skipCleanup) {
6165
options.cleanup = false;
@@ -103,7 +107,7 @@ const np = async (input = 'patch', {packageManager, ...options}, {package_, proj
103107
});
104108

105109
asyncExitHook(async () => {
106-
if (options.preview || publishStatus === 'SUCCESS') {
110+
if (options.dryRun || publishStatus === 'SUCCESS') {
107111
return;
108112
}
109113

@@ -152,8 +156,8 @@ const np = async (input = 'patch', {packageManager, ...options}, {package_, proj
152156
title: 'Cleanup',
153157
enabled: () => runCleanup && !lockfile,
154158
skip() {
155-
if (options.preview) {
156-
return '[Preview] Command not executed: delete node_modules.';
159+
if (options.dryRun) {
160+
return '[Dry run] Command not executed: delete node_modules.';
157161
}
158162
},
159163
task: () => deleteAsync(path.join(projectDirectory, 'node_modules')),
@@ -162,8 +166,8 @@ const np = async (input = 'patch', {packageManager, ...options}, {package_, proj
162166
title: `Installing dependencies using ${packageManager.id}`,
163167
enabled: () => runInstall,
164168
skip() {
165-
if (options.preview) {
166-
return `[Preview] Command not executed: ${printCommand(getInstallCommand())}.`;
169+
if (options.dryRun) {
170+
return `[Dry run] Command not executed: ${printCommand(getInstallCommand())}.`;
167171
}
168172
},
169173
task: () => new Listr([
@@ -183,23 +187,23 @@ const np = async (input = 'patch', {packageManager, ...options}, {package_, proj
183187
title: 'Running tests',
184188
enabled: () => runTests,
185189
skip() {
186-
if (options.preview) {
187-
return `[Preview] Command not executed: ${packageManager.cli} run ${testScript}.`;
190+
if (options.dryRun) {
191+
return `[Dry run] Command not executed: ${packageManager.cli} run ${testScript}.`;
188192
}
189193
},
190194
task: () => exec(packageManager.cli, ['run', testScript], {...ciEnvOptions, cwd: projectDirectory}),
191195
},
192196
{
193197
title: 'Bumping version',
194198
skip() {
195-
if (options.preview) {
199+
if (options.dryRun) {
196200
const [cli, arguments_] = packageManager.versionCommand(input);
197201

198202
if (options.message) {
199203
arguments_.push('--message', options.message.replaceAll('%s', input));
200204
}
201205

202-
return `[Preview] Command not executed: ${printCommand([cli, arguments_])}`;
206+
return `[Dry run] Command not executed: ${printCommand([cli, arguments_])}`;
203207
}
204208
},
205209
task() {
@@ -218,9 +222,9 @@ const np = async (input = 'patch', {packageManager, ...options}, {package_, proj
218222
{
219223
title: 'Publishing package',
220224
skip() {
221-
if (options.preview) {
225+
if (options.dryRun) {
222226
const command = getPublishCommand(options);
223-
return `[Preview] Command not executed: ${printCommand(command)}.`;
227+
return `[Dry run] Command not executed: ${printCommand(command)}.`;
224228
}
225229
},
226230
/** @type {(context, task) => Listr.ListrTaskResult<any>} */
@@ -252,9 +256,9 @@ const np = async (input = 'patch', {packageManager, ...options}, {package_, proj
252256
? [{
253257
title: 'Enabling two-factor authentication',
254258
async skip() {
255-
if (options.preview) {
259+
if (options.dryRun) {
256260
const arguments_ = await getEnable2faArguments(package_.name, options);
257-
return `[Preview] Command not executed: npm ${arguments_.join(' ')}.`;
261+
return `[Dry run] Command not executed: npm ${arguments_.join(' ')}.`;
258262
}
259263
},
260264
task: (context, task) => enable2fa(task, package_.name, {otp: context.otp}),
@@ -269,9 +273,9 @@ const np = async (input = 'patch', {packageManager, ...options}, {package_, proj
269273
return 'Upstream branch not found; not pushing.';
270274
}
271275

272-
if (options.preview) {
276+
if (options.dryRun) {
273277
const remote = options.remote ? `${options.remote} ` : '';
274-
return `[Preview] Command not executed: git push ${remote}--follow-tags.`;
278+
return `[Dry run] Command not executed: git push ${remote}--follow-tags.`;
275279
}
276280

277281
if (publishStatus === 'FAILED' && options.runPublish) {
@@ -287,8 +291,8 @@ const np = async (input = 'patch', {packageManager, ...options}, {package_, proj
287291
title: 'Creating release draft on GitHub',
288292
enabled: () => isOnGitHub === true,
289293
skip() {
290-
if (options.preview) {
291-
return '[Preview] GitHub Releases draft will not be opened in preview mode.';
294+
if (options.dryRun) {
295+
return '[Dry run] GitHub Releases draft will not be opened in dry-run mode.';
292296
}
293297
},
294298
task: () => releaseTaskHelper(options, package_, packageManager),
@@ -297,7 +301,7 @@ const np = async (input = 'patch', {packageManager, ...options}, {package_, proj
297301
], {
298302
showSubtasks: false,
299303
renderer: options.renderer ?? 'default',
300-
clearOutput: !options.preview && !options.releaseDraftOnly,
304+
clearOutput: !options.dryRun && !options.releaseDraftOnly,
301305
});
302306

303307
if (!options.runPublish) {

test/cli.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import path from 'node:path';
22
import test from 'ava';
3+
import {execa} from 'execa';
34
import {npPackage, npRootDirectory as rootDirectory} from '../source/util.js';
45
import {cliPasses} from './_helpers/verify-cli.js';
56

@@ -22,7 +23,7 @@ test('flags: --help', cliPasses, cli, '--help', [
2223
'--no-tests Skips tests',
2324
'--yolo Skips cleanup and testing',
2425
'--no-publish Skips publishing',
25-
'--preview Show tasks without actually executing them',
26+
'--dry-run Show tasks without actually executing them',
2627
'--tag Publish under a given dist-tag',
2728
'--contents Subdirectory to publish',
2829
'--no-release-draft Skips opening a GitHub release draft',
@@ -45,3 +46,26 @@ test('flags: --help', cliPasses, cli, '--help', [
4546
]);
4647

4748
test('flags: --version', cliPasses, cli, '--version', [npPackage.version]);
49+
50+
test('flags: --dry-run is shown in help', async t => {
51+
const {stdout} = await execa(cli, ['--help']);
52+
53+
t.true(stdout.includes('--dry-run Show tasks without actually executing them'));
54+
});
55+
56+
test('flags: unknown flags fail', async t => {
57+
const {exitCode, stderr} = await execa(cli, ['--wat'], {reject: false});
58+
59+
t.is(exitCode, 2);
60+
t.true(stderr.includes('Unknown flag'));
61+
t.true(stderr.includes('--wat'));
62+
});
63+
64+
test('flags: --preview remains an alias for --dry-run', async t => {
65+
const {exitCode, stderr} = await execa(cli, ['--preview', '--wat'], {reject: false});
66+
67+
t.is(exitCode, 2);
68+
t.true(stderr.includes('Unknown flag'));
69+
t.true(stderr.includes('--wat'));
70+
t.false(stderr.includes('--preview'));
71+
});

test/index.js

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ test('install uses projectDirectory from context as cwd', async t => {
299299
tests: false,
300300
publish: false,
301301
runPublish: false,
302-
preview: false,
302+
dryRun: false,
303303
}, {package_: npPackageResult.package_, projectDirectory, rootDirectory: contentsDirectory});
304304

305305
t.deepEqual(execaStub.firstCall.args, ['npm', ['install', '--no-package-lock', '--no-production', '--engine-strict'], {cwd: projectDirectory}]);
@@ -332,7 +332,7 @@ test('cleanup uses projectDirectory from context', async t => {
332332
tests: false,
333333
publish: false,
334334
runPublish: false,
335-
preview: false,
335+
dryRun: false,
336336
}, {package_: npPackageResult.package_, projectDirectory, rootDirectory: contentsDirectory});
337337

338338
t.true(deleteAsyncStub.calledOnceWithExactly(path.join(projectDirectory, 'node_modules')));
@@ -361,7 +361,7 @@ test('tests use projectDirectory from context as cwd', async t => {
361361
cleanup: false,
362362
publish: false,
363363
runPublish: false,
364-
preview: false,
364+
dryRun: false,
365365
}, {package_: npPackageResult.package_, projectDirectory, rootDirectory: contentsDirectory});
366366

367367
t.deepEqual(execaStub.secondCall.args, ['npm', ['run', 'test'], {env: {CI: 'true'}, cwd: projectDirectory}]);
@@ -393,7 +393,7 @@ test('no-cleanup still uses lockfile-aware install command', async t => {
393393
tests: false,
394394
publish: false,
395395
runPublish: false,
396-
preview: false,
396+
dryRun: false,
397397
}, npPackageResult);
398398

399399
t.deepEqual(execaStub.firstCall.args, ['npm', ['ci', '--engine-strict'], {cwd: npPackageResult.rootDirectory}]);
@@ -427,7 +427,7 @@ test('contents mode looks up lockfile in projectDirectory and installs there', a
427427
tests: false,
428428
publish: false,
429429
runPublish: false,
430-
preview: false,
430+
dryRun: false,
431431
}, {package_: npPackageResult.package_, projectDirectory, rootDirectory});
432432

433433
t.true(findLockfileStub.calledOnceWithExactly(projectDirectory, packageManager));
@@ -474,7 +474,7 @@ test('contents mode keeps cleanup, install, and tests in projectDirectory while
474474
t.is(publishCwd, rootDirectory);
475475
});
476476

477-
test('preview with no-cleanup does not execute install command', async t => {
477+
test('dryRun with no-cleanup does not execute install command', async t => {
478478
const execaStub = sinon.stub().returns(fakeExecaReturn());
479479
const verifyWorkingTreeIsCleanStub = sinon.stub();
480480

@@ -501,14 +501,46 @@ test('preview with no-cleanup does not execute install command', async t => {
501501
tests: false,
502502
publish: false,
503503
runPublish: false,
504-
preview: true,
504+
dryRun: true,
505505
}, npPackageResult);
506506

507507
t.true(execaStub.notCalled);
508508
t.true(verifyWorkingTreeIsCleanStub.notCalled);
509509
});
510510

511-
test('preview without lockfile does not clean up or run tests', async t => {
511+
test('dryRun without lockfile does not clean up or run tests', async t => {
512+
const deleteAsyncStub = sinon.stub();
513+
const execaStub = sinon.stub().returns(fakeExecaReturn());
514+
515+
/** @type {typeof np} */
516+
const npMock = await esmock('../source/index.js', {
517+
del: {deleteAsync: deleteAsyncStub},
518+
execa: {execa: execaStub},
519+
'../source/prerequisite-tasks.js': sinon.stub(),
520+
'../source/git-tasks.js': sinon.stub(),
521+
'../source/git-util.js': {
522+
hasUpstream: sinon.stub().returns(true),
523+
pushGraceful: sinon.stub(),
524+
verifyWorkingTreeIsClean: sinon.stub(),
525+
},
526+
'../source/package-manager/index.js': {
527+
...await import('../source/package-manager/index.js'),
528+
findLockfile: sinon.stub().returns(undefined),
529+
},
530+
});
531+
532+
await npMock('1.0.0', {
533+
...defaultOptions,
534+
dryRun: true,
535+
publish: false,
536+
runPublish: false,
537+
}, npPackageResult);
538+
539+
t.true(deleteAsyncStub.notCalled);
540+
t.true(execaStub.notCalled);
541+
});
542+
543+
test('preview option remains a supported alias for dryRun', async t => {
512544
const deleteAsyncStub = sinon.stub();
513545
const execaStub = sinon.stub().returns(fakeExecaReturn());
514546

0 commit comments

Comments
 (0)