Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: sindresorhus/np
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v6.5.0
Choose a base ref
...
head repository: sindresorhus/np
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v7.0.0
Choose a head ref
  • 8 commits
  • 28 files changed
  • 6 contributors

Commits on Sep 23, 2020

  1. Fix grammar (#564)

    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    G-Rath and sindresorhus authored Sep 23, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    127311a View commit details

Commits on Oct 3, 2020

  1. Fix readme typo (#565)

    ferdicus authored Oct 3, 2020
    Copy the full SHA
    5689d51 View commit details
  2. Copy the full SHA
    c77e191 View commit details

Commits on Oct 4, 2020

  1. Copy the full SHA
    b519201 View commit details

Commits on Oct 29, 2020

  1. Copy the full SHA
    0cff2b4 View commit details
  2. Show files added since the last release and not part of the package (#…

    …456)
    
    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    bunysae and sindresorhus authored Oct 29, 2020
    Copy the full SHA
    08a2c06 View commit details
  3. Upgrade dependencies

    sindresorhus committed Oct 29, 2020
    Copy the full SHA
    c43c6cf View commit details
  4. 7.0.0

    sindresorhus committed Oct 29, 2020
    Copy the full SHA
    fee5ab8 View commit details
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "integration-test"]
path = integration-test
url = https://github.com/bunysae/np_integration_test
1 change: 1 addition & 0 deletions integration-test
Submodule integration-test added at ad5e6e
68 changes: 36 additions & 32 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "np",
"version": "6.5.0",
"version": "7.0.0",
"description": "A better `npm publish`",
"license": "MIT",
"repository": "sindresorhus/np",
@@ -30,53 +30,57 @@
"commit"
],
"dependencies": {
"@samverschueren/stream-to-observable": "^0.3.0",
"any-observable": "^0.5.0",
"@samverschueren/stream-to-observable": "^0.3.1",
"any-observable": "^0.5.1",
"async-exit-hook": "^2.0.1",
"chalk": "^3.0.0",
"cosmiconfig": "^6.0.0",
"del": "^4.1.0",
"chalk": "^4.1.0",
"cosmiconfig": "^7.0.0",
"del": "^6.0.0",
"escape-goat": "^3.0.0",
"escape-string-regexp": "^2.0.0",
"execa": "^4.0.0",
"escape-string-regexp": "^4.0.0",
"execa": "^4.1.0",
"github-url-from-git": "^1.5.0",
"has-yarn": "^2.1.0",
"hosted-git-info": "^3.0.0",
"inquirer": "^7.0.0",
"is-installed-globally": "^0.3.1",
"hosted-git-info": "^3.0.7",
"ignore-walk": "^3.0.3",
"import-local": "^3.0.2",
"inquirer": "^7.3.3",
"is-installed-globally": "^0.3.2",
"is-scoped": "^2.1.0",
"issue-regex": "^3.1.0",
"listr": "^0.14.3",
"listr-input": "^0.2.1",
"log-symbols": "^3.0.0",
"meow": "^6.0.0",
"log-symbols": "^4.0.0",
"meow": "^8.0.0",
"minimatch": "^3.0.4",
"new-github-release-url": "^1.0.0",
"npm-name": "^6.0.0",
"onetime": "^5.1.0",
"open": "^7.0.0",
"ow": "^0.15.0",
"p-memoize": "^3.1.0",
"p-timeout": "^3.1.0",
"pkg-dir": "^4.1.0",
"read-pkg-up": "^7.0.0",
"rxjs": "^6.5.4",
"semver": "^7.1.1",
"split": "^1.0.0",
"symbol-observable": "^1.2.0",
"terminal-link": "^2.0.0",
"update-notifier": "^4.0.0"
"npm-name": "^6.0.1",
"onetime": "^5.1.2",
"open": "^7.3.0",
"ow": "^0.18.0",
"p-memoize": "^4.0.1",
"p-timeout": "^3.2.0",
"pkg-dir": "^5.0.0",
"read-pkg-up": "^7.0.1",
"rxjs": "^6.6.3",
"semver": "^7.3.2",
"split": "^1.0.1",
"symbol-observable": "^2.0.3",
"terminal-link": "^2.1.1",
"update-notifier": "^5.0.0"
},
"devDependencies": {
"ava": "^2.3.0",
"execa_test_double": "^4.0.0",
"execa_test_double": "^4.0.1",
"mockery": "^2.1.0",
"proxyquire": "^2.1.0",
"sinon": "^8.0.1",
"xo": "^0.25.3"
"proxyquire": "^2.1.3",
"sinon": "^9.2.1",
"xo": "^0.34.1"
},
"ava": {
"files": [
"!test/fixtures"
"!test/fixtures",
"!integration-test"
]
}
}
16 changes: 10 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
@@ -80,7 +80,7 @@ Run `np` without arguments to launch the interactive UI that guides you through

## Config

`np` can be configured both locally and globally. When using the global `np` binary, you can configure any of the CLI flags in either a `.np-config.js` or `.np-config.json` file in the home directory. When using the local `np` binary, for example, in a `npm run` script, you can configure `np` by setting the flags in either a top-level `np` field in `package.json` or in a `.np-config.js` or `.np-config.json` file in the project directory.
`np` can be configured both locally and globally. When using the global `np` binary, you can configure any of the CLI flags in either a `.np-config.js` or `.np-config.json` file in the home directory. When using the local `np` binary, for example, in a `npm run` script, you can configure `np` by setting the flags in either a top-level `np` field in `package.json` or in a `.np-config.js` or `.np-config.json` file in the project directory. If it exists, the local installation will always take precedence. This ensures any local config matches the version of `np` it was designed for.

Currently, these are the flags you can configure:

@@ -96,7 +96,7 @@ Currently, these are the flags you can configure:
- `contents` - Subdirectory to publish (`.` by default).
- `releaseDraft` - Open a GitHub release draft after releasing (`true` by default).
- `testScript` - Name of npm run script to run tests before publishing (`test` by default).
- `2fa` - Enable 2FA on new packages (`true` by default) (it's not recommended to set this to `false`).
- `2fa` - Enable 2FA on new packages (`true` by default) (setting this to `false` is not recommended).

For example, this configures `np` to never use Yarn and to use `dist` as the subdirectory to publish:

@@ -164,7 +164,7 @@ You can also add `np` to a custom script in `package.json`. This can be useful i

### User-defined tests

If you want to run a user-defined test script before publishing instead of the normal `npm test` or `yarn test`, you can use `--test-script` flag or the `testScript` config. This is can be useful when your normal test script is running with a `--watch` mode or in case you want to run some specific tests (maybe on the packaged files) before publishing.
If you want to run a user-defined test script before publishing instead of the normal `npm test` or `yarn test`, you can use `--test-script` flag or the `testScript` config. This can be useful when your normal test script is running with a `--watch` flag or in case you want to run some specific tests (maybe on the packaged files) before publishing.

For example, `np --test-script=publish-test` would run the `publish-test` script instead of the default `test`.

@@ -201,7 +201,7 @@ $ yarn config set version-sign-git-tag true

You can use `np` for packages that aren't publicly published to npm (perhaps installed from a private git repo).

Set `"private": true` in your `package.json` and the publish step will be skipped. All other steps
Set `"private": true` in your `package.json` and the publishing step will be skipped. All other steps
including versioning and pushing tags will still be completed.

### Public scoped packages
@@ -268,7 +268,7 @@ $ git push --set-upstream origin HEAD
$ np patch --any-branch --tag=v1
```

### Prerequisite step runs forever on macOS
### The prerequisite step runs forever on macOS

If you're using macOS Sierra 10.12.2 or later, your SSH key passphrase is no longer stored into the keychain by default. This may cause the `prerequisite` step to run forever because it prompts for your passphrase in the background. To fix this, add the following lines to your `~/.ssh/config` and run a simple Git command like `git fetch`.

@@ -280,11 +280,15 @@ Host *

If you're running into other issues when using SSH, please consult [GitHub's support article](https://help.github.com/articles/connecting-to-github-with-ssh/).

### Ignore strategy

The [ignore strategy](https://docs.npmjs.com/files/package.json#files), either maintained in the `files`-property in `package.json` or in `.npmignore`, is meant to help reduce the package size. To avoid broken packages caused by essential files being accidentally ignored, `np` prints out all the new and unpublished files added to Git. Test files and other [common files](https://docs.npmjs.com/files/package.json#files) that are never published are not considered. `np` assumes either a standard directory layout or a customized layout represented in the `directories` property in `package.json`.

## FAQ

### I get an error when publishing my package through Yarn

If you an error like this…
If you get an error like this…

```shell
❯ Prerequisite check
147 changes: 147 additions & 0 deletions source/cli-implementation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env node
'use strict';
// eslint-disable-next-line import/no-unassigned-import
require('symbol-observable'); // Important: This needs to be first to prevent weird Observable incompatibilities
const logSymbols = require('log-symbols');
const meow = require('meow');
const updateNotifier = require('update-notifier');
const hasYarn = require('has-yarn');
const config = require('./config');
const {isPackageNameAvailable} = require('./npm/util');
const version = require('./version');
const util = require('./util');
const ui = require('./ui');
const np = require('.');

const cli = meow(`
Usage
$ np <version>
Version can be:
${version.SEMVER_INCREMENTS.join(' | ')} | 1.2.3
Options
--any-branch Allow publishing from any branch
--branch Name of the release branch (default: master)
--no-cleanup Skips cleanup of node_modules
--no-tests Skips tests
--yolo Skips cleanup and testing
--no-publish Skips publishing
--preview Show tasks without actually executing them
--tag Publish under a given dist-tag
--no-yarn Don't use Yarn
--contents Subdirectory to publish
--no-release-draft Skips opening a GitHub release draft
--test-script Name of npm run script to run tests before publishing (default: test)
--no-2fa Don't enable 2FA on new packages (not recommended)
Examples
$ np
$ np patch
$ np 1.0.2
$ np 1.0.2-beta.3 --tag=beta
$ np 1.0.2-beta.3 --tag=beta --contents=dist
`, {
booleanDefault: undefined,
flags: {
anyBranch: {
type: 'boolean'
},
branch: {
type: 'string'
},
cleanup: {
type: 'boolean'
},
tests: {
type: 'boolean'
},
yolo: {
type: 'boolean'
},
publish: {
type: 'boolean'
},
releaseDraft: {
type: 'boolean'
},
tag: {
type: 'string'
},
yarn: {
type: 'boolean'
},
contents: {
type: 'string'
},
preview: {
type: 'boolean'
},
testScript: {
type: 'string'
},
'2fa': {
type: 'boolean'
}
}
});

updateNotifier({pkg: cli.pkg}).notify();

(async () => {
const pkg = util.readPkg();

const defaultFlags = {
cleanup: true,
tests: true,
publish: true,
releaseDraft: true,
yarn: hasYarn(),
'2fa': true
};

const localConfig = await config();

const flags = {
...defaultFlags,
...localConfig,
...cli.flags
};

// Workaround for unintended auto-casing behavior from `meow`.
if ('2Fa' in flags) {
flags['2fa'] = flags['2Fa'];
}

const runPublish = flags.publish && !pkg.private;

const availability = flags.publish ? await isPackageNameAvailable(pkg) : {
isAvailable: false,
isUnknown: false
};

const version = cli.input.length > 0 ? cli.input[0] : false;

const options = await ui({
...flags,
availability,
version,
runPublish
}, pkg);

if (!options.confirm) {
process.exit(0);
}

console.log(); // Prints a newline for readability
const newPkg = await np(options.version, options);

if (options.preview) {
return;
}

console.log(`\n ${newPkg.name} ${newPkg.version} published 🎉`);
})().catch(error => {
console.error(`\n${logSymbols.error} ${error.message}`);
process.exit(1);
});
152 changes: 11 additions & 141 deletions source/cli.js
Original file line number Diff line number Diff line change
@@ -1,147 +1,17 @@
#!/usr/bin/env node
'use strict';
// eslint-disable-next-line import/no-unassigned-import
require('symbol-observable'); // Important: This needs to be first to prevent weird Observable incompatibilities
const logSymbols = require('log-symbols');
const meow = require('meow');
const updateNotifier = require('update-notifier');
const hasYarn = require('has-yarn');
const config = require('./config');
const {isPackageNameAvailable} = require('./npm/util');
const version = require('./version');
const util = require('./util');
const ui = require('./ui');
const np = require('.');
const {debuglog} = require('util');
const importLocal = require('import-local');
const isInstalledGlobally = require('is-installed-globally');

const cli = meow(`
Usage
$ np <version>
const log = debuglog('np');

Version can be:
${version.SEMVER_INCREMENTS.join(' | ')} | 1.2.3
Options
--any-branch Allow publishing from any branch
--branch Name of the release branch (default: master)
--no-cleanup Skips cleanup of node_modules
--no-tests Skips tests
--yolo Skips cleanup and testing
--no-publish Skips publishing
--preview Show tasks without actually executing them
--tag Publish under a given dist-tag
--no-yarn Don't use Yarn
--contents Subdirectory to publish
--no-release-draft Skips opening a GitHub release draft
--test-script Name of npm run script to run tests before publishing (default: test)
--no-2fa Don't enable 2FA on new packages (not recommended)
Examples
$ np
$ np patch
$ np 1.0.2
$ np 1.0.2-beta.3 --tag=beta
$ np 1.0.2-beta.3 --tag=beta --contents=dist
`, {
booleanDefault: undefined,
flags: {
anyBranch: {
type: 'boolean'
},
branch: {
type: 'string'
},
cleanup: {
type: 'boolean'
},
tests: {
type: 'boolean'
},
yolo: {
type: 'boolean'
},
publish: {
type: 'boolean'
},
releaseDraft: {
type: 'boolean'
},
tag: {
type: 'string'
},
yarn: {
type: 'boolean'
},
contents: {
type: 'string'
},
preview: {
type: 'boolean'
},
testScript: {
type: 'string'
},
'2fa': {
type: 'boolean'
}
}
});

updateNotifier({pkg: cli.pkg}).notify();

(async () => {
const pkg = util.readPkg();

const defaultFlags = {
cleanup: true,
tests: true,
publish: true,
releaseDraft: true,
yarn: hasYarn(),
'2fa': true
};

const localConfig = await config();

const flags = {
...defaultFlags,
...localConfig,
...cli.flags
};

// Workaround for unintended auto-casing behavior from `meow`.
if ('2Fa' in flags) {
flags['2fa'] = flags['2Fa'];
}

const runPublish = flags.publish && !pkg.private;

const availability = flags.publish ? await isPackageNameAvailable(pkg) : {
isAvailable: false,
isUnknown: false
};

const version = cli.input.length > 0 ? cli.input[0] : false;

const options = await ui({
...flags,
availability,
version,
runPublish
}, pkg);

if (!options.confirm) {
process.exit(0);
}

console.log(); // Prints a newline for readability
const newPkg = await np(options.version, options);

if (options.preview) {
return;
// Prefer the local installation
if (!importLocal(__filename)) {
if (isInstalledGlobally) {
log('Using global install of np.');
}

console.log(`\n ${newPkg.name} ${newPkg.version} published 🎉`);
})().catch(error => {
console.error(`\n${logSymbols.error} ${error.message}`);
process.exit(1);
});
// eslint-disable-next-line import/no-unassigned-import
require('./cli-implementation');
}
22 changes: 19 additions & 3 deletions source/git-util.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
'use strict';
const execa = require('execa');
const escapeStringRegexp = require('escape-string-regexp');
const ignoreWalker = require('ignore-walk');
const pkgDir = require('pkg-dir');
const {verifyRequirementSatisfied} = require('./version');

exports.latestTag = async () => {
const {stdout} = await execa('git', ['describe', '--abbrev=0', '--tags']);
return stdout;
};

exports.newFilesSinceLastRelease = async () => {
try {
const {stdout} = await execa('git', ['diff', '--stat', '--diff-filter=A', await this.latestTag(), 'HEAD']);
const result = stdout.trim().split('\n').slice(0, -1).map(row => row.slice(0, row.indexOf('|')).trim());
return result;
} catch {
// Get all files under version control
return ignoreWalker({
path: pkgDir.sync(),
ignoreFiles: ['.gitignore']
});
}
};

const firstCommit = async () => {
const {stdout} = await execa('git', ['rev-list', '--max-parents=0', 'HEAD']);
return stdout;
@@ -18,7 +34,7 @@ exports.latestTagOrFirstCommit = async () => {
try {
// In case a previous tag exists, we use it to compare the current repo status to.
latest = await exports.latestTag();
} catch (_) {
} catch {
// Otherwise, we fallback to using the first commit for comparison.
latest = await firstCommit();
}
@@ -54,7 +70,7 @@ exports.isWorkingTreeClean = async () => {
}

return true;
} catch (_) {
} catch {
return false;
}
};
@@ -70,7 +86,7 @@ exports.isRemoteHistoryClean = async () => {
try { // Gracefully handle no remote set up.
const {stdout} = await execa('git', ['rev-list', '--count', '--left-only', '@{u}...HEAD']);
history = stdout;
} catch (_) {}
} catch {}

if (history && history !== '0') {
return false;
30 changes: 15 additions & 15 deletions source/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict';
require('any-observable/register/rxjs-all'); // eslint-disable-line import/no-unassigned-import
require('any-observable/register/rxjs-all');
const fs = require('fs');
const path = require('path');
const execa = require('execa');
@@ -117,7 +117,7 @@ module.exports = async (input = 'patch', options) => {
tasks.add([
{
title: 'Cleanup',
skip: () => hasLockFile,
enabled: () => !hasLockFile,
task: () => del('node_modules')
},
{
@@ -156,7 +156,7 @@ module.exports = async (input = 'patch', options) => {
enabled: () => options.yarn === true,
task: () => exec('yarn', testCommand).pipe(
catchError(error => {
if (error.message.includes(`Command ${testScript} not found`)) {
if (error.message.includes(`Command "${testScript}" not found`)) {
return [];
}

@@ -257,18 +257,18 @@ module.exports = async (input = 'patch', options) => {
}
});

tasks.add({
title: 'Creating release draft on GitHub',
enabled: () => isOnGitHub === true,
skip: () => {
if (options.preview) {
return '[Preview] GitHub Releases draft will not be opened in preview mode.';
}

return !options.releaseDraft;
},
task: () => releaseTaskHelper(options, pkg)
});
if (options.releaseDraft) {
tasks.add({
title: 'Creating release draft on GitHub',
enabled: () => isOnGitHub === true,
skip: () => {
if (options.preview) {
return '[Preview] GitHub Releases draft will not be opened in preview mode.';
}
},
task: () => releaseTaskHelper(options, pkg)
});
}

await tasks.run();

106 changes: 101 additions & 5 deletions source/npm/util.js
Original file line number Diff line number Diff line change
@@ -7,14 +7,16 @@ const ow = require('ow');
const npmName = require('npm-name');
const chalk = require('chalk');
const pkgDir = require('pkg-dir');
const ignoreWalker = require('ignore-walk');
const minimatch = require('minimatch');
const {verifyRequirementSatisfied} = require('../version');

exports.checkConnection = () => pTimeout(
(async () => {
try {
await execa('npm', ['ping']);
return true;
} catch (_) {
} catch {
throw new Error('Connection to npm registry failed');
}
})(),
@@ -117,16 +119,110 @@ exports.verifyRecentNpmVersion = async () => {
};

exports.checkIgnoreStrategy = ({files}) => {
const rootDir = pkgDir.sync();
const npmignoreExists = fs.existsSync(path.resolve(rootDir, '.npmignore'));

if (!files && !npmignoreExists) {
if (!files && !npmignoreExistsInPackageRootDir()) {
console.log(`
\n${chalk.bold.yellow('Warning:')} No ${chalk.bold.cyan('files')} field specified in ${chalk.bold.magenta('package.json')} nor is a ${chalk.bold.magenta('.npmignore')} file present. Having one of those will prevent you from accidentally publishing development-specific files along with your package's source code to npm.
`);
}
};

function npmignoreExistsInPackageRootDir() {
const rootDir = pkgDir.sync();
return fs.existsSync(path.resolve(rootDir, '.npmignore'));
}

async function getFilesIgnoredByDotnpmignore(pkg, fileList) {
const whiteList = await ignoreWalker({
path: pkgDir.sync(),
ignoreFiles: ['.npmignore']
});
return fileList.filter(minimatch.filter(getIgnoredFilesGlob(whiteList, pkg.directories), {matchBase: true, dot: true}));
}

function getFilesNotIncludedInFilesProperty(pkg, fileList) {
const globArrayForFilesAndDirectories = [...pkg.files];
const rootDir = pkgDir.sync();
for (const glob of pkg.files) {
try {
if (fs.statSync(path.resolve(rootDir, glob)).isDirectory()) {
globArrayForFilesAndDirectories.push(`${glob}/**/*`);
}
} catch {}
}

const result = fileList.filter(minimatch.filter(getIgnoredFilesGlob(globArrayForFilesAndDirectories, pkg.directories), {matchBase: true, dot: true}));
return result.filter(minimatch.filter(getDefaultIncludedFilesGlob(pkg.main), {nocase: true, matchBase: true}));
}

function getDefaultIncludedFilesGlob(mainFile) {
// According to https://docs.npmjs.com/files/package.json#files
// npm's default behavior is to always include these files.
const filesAlwaysIncluded = [
'package.json',
'README*',
'CHANGES*',
'CHANGELOG*',
'HISTORY*',
'LICENSE*',
'LICENCE*',
'NOTICE*'
];
if (mainFile) {
filesAlwaysIncluded.push(mainFile);
}

return `!{${filesAlwaysIncluded}}`;
}

function getIgnoredFilesGlob(globArrayFromFilesProperty, packageDirectories) {
// According to https://docs.npmjs.com/files/package.json#files
// npm's default behavior is to ignore these files.
const filesIgnoredByDefault = [
'.*.swp',
'.npmignore',
'.gitignore',
'._*',
'.DS_Store',
'.hg',
'.npmrc',
'.lock-wscript',
'.svn',
'.wafpickle-N',
'*.orig',
'config.gypi',
'CVS',
'node_modules/**/*',
'npm-debug.log',
'package-lock.json',
'.git/**/*',
'.git'
];

// Test files are assumed not to be part of the package
let testDirectoriesGlob = '';
if (packageDirectories && Array.isArray(packageDirectories.test)) {
testDirectoriesGlob = packageDirectories.test.join(',');
} else if (packageDirectories && typeof packageDirectories.test === 'string') {
testDirectoriesGlob = packageDirectories.test;
} else {
// Fallback to `test` directory
testDirectoriesGlob = 'test/**/*';
}

return `!{${globArrayFromFilesProperty.join(',')},${filesIgnoredByDefault.join(',')},${testDirectoriesGlob}}`;
}

// Get all files which will be ignored by either `.npmignore` or the `files` property in `package.json` (if defined).
exports.getNewAndUnpublishedFiles = async (pkg, newFiles = []) => {
if (pkg.files) {
return getFilesNotIncludedInFilesProperty(pkg, newFiles);
}

if (npmignoreExistsInPackageRootDir()) {
return getFilesIgnoredByDotnpmignore(pkg, newFiles);
}
};

exports.getRegistryUrl = async (pkgManager, pkg) => {
const args = ['config', 'get', 'registry'];
if (exports.isExternalRegistry(pkg)) {
4 changes: 2 additions & 2 deletions source/prerequisite-tasks.js
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ module.exports = (input, pkg, options) => {
const tasks = [
{
title: 'Ping npm registry',
skip: () => pkg.private || isExternalRegistry,
enabled: () => !pkg.private && !isExternalRegistry,
task: async () => npm.checkConnection()
},
{
@@ -30,7 +30,7 @@ module.exports = (input, pkg, options) => {
},
{
title: 'Verify user is authenticated',
skip: () => process.env.NODE_ENV === 'test' || pkg.private,
enabled: () => process.env.NODE_ENV !== 'test' && !pkg.private,
task: async () => {
const username = await npm.username({
externalRegistry: isExternalRegistry ? pkg.publishConfig.registry : false
24 changes: 24 additions & 0 deletions source/ui.js
Original file line number Diff line number Diff line change
@@ -50,6 +50,22 @@ const printCommitLog = async (repoUrl, registryUrl) => {
};
};

const checkIgnoredFiles = async pkg => {
const ignoredFiles = await util.getNewAndUnpublishedFiles(pkg);
if (!ignoredFiles || ignoredFiles.length === 0) {
return true;
}

const answers = await inquirer.prompt([{
type: 'confirm',
name: 'confirm',
message: `The following new files are not already part of your published package:\n${chalk.reset(ignoredFiles.map(path => `- ${path}`).join('\n'))}\nContinue?`,
default: false
}]);

return answers.confirm;
};

module.exports = async (options, pkg) => {
const oldVersion = pkg.version;
const extraBaseUrls = ['gitlab.com'];
@@ -59,6 +75,14 @@ module.exports = async (options, pkg) => {

if (options.runPublish) {
checkIgnoreStrategy(pkg);

const answerIgnoredFiles = await checkIgnoredFiles(pkg);
if (!answerIgnoredFiles) {
return {
...options,
confirm: answerIgnoredFiles
};
}
}

console.log(`\nPublish a new version of ${chalk.bold.magenta(pkg.name)} ${chalk.dim(`(current: ${oldVersion})`)}\n`);
11 changes: 9 additions & 2 deletions source/util.js
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@ const execa = require('execa');
const pMemoize = require('p-memoize');
const ow = require('ow');
const pkgDir = require('pkg-dir');
const gitUtil = require('./git-util');
const npmUtil = require('./npm/util');

exports.readPkg = packagePath => {
packagePath = packagePath ? pkgDir.sync(packagePath) : pkgDir.sync();
@@ -64,11 +66,16 @@ exports.getTagVersionPrefix = pMemoize(async options => {

const {stdout} = await execa('npm', ['config', 'get', 'tag-version-prefix']);
return stdout;
} catch (_) {
} catch {
return 'v';
}
});

exports.getNewAndUnpublishedFiles = async pkg => {
const listNewFiles = await gitUtil.newFilesSinceLastRelease();
return npmUtil.getNewAndUnpublishedFiles(pkg, listNewFiles);
};

exports.getPreReleasePrefix = pMemoize(async options => {
ow(options, ow.object.hasKeys('yarn'));

@@ -88,7 +95,7 @@ exports.getPreReleasePrefix = pMemoize(async options => {
}

return '';
} catch (_) {
} catch {
return '';
}
});
1 change: 1 addition & 0 deletions test/fixtures/npmignore/.hg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
should be ignored by default
2 changes: 2 additions & 0 deletions test/fixtures/npmignore/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ignore.txt
test
1 change: 1 addition & 0 deletions test/fixtures/npmignore/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
File is always included in package.
1 change: 1 addition & 0 deletions test/fixtures/npmignore/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
File is always included in package.
1 change: 1 addition & 0 deletions test/fixtures/npmignore/source/ignore.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ignore this file
1 change: 1 addition & 0 deletions test/fixtures/npmignore/source/pay_attention.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
File is excluded from .npmignore
1 change: 1 addition & 0 deletions test/fixtures/npmignore/test/file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ignore this file
1 change: 1 addition & 0 deletions test/fixtures/package/.hg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
should be ignored by default
3 changes: 3 additions & 0 deletions test/fixtures/package/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"files": ["pay_attention.txt"]
}
1 change: 1 addition & 0 deletions test/fixtures/package/source/ignore.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
File is excluded from package.json
1 change: 1 addition & 0 deletions test/fixtures/package/source/pay_attention.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
File in included in package.json
2 changes: 2 additions & 0 deletions test/fixtures/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The directory is for the resources
in the script npmignore.js
8 changes: 4 additions & 4 deletions test/hyperlinks.js
Original file line number Diff line number Diff line change
@@ -23,14 +23,14 @@ test('linkifyIssues correctly links issues', t => {
});

test('linkifyIssues returns raw message if url is not provided', t => {
const msg = 'Commit message - fixes #5';
t.is(linkifyIssues(undefined, msg), msg);
const message = 'Commit message - fixes #5';
t.is(linkifyIssues(undefined, message), message);
});

test.serial('linkifyIssues returns raw message if terminalLink is not supported', t => {
mockTerminalLinkUnsupported();
const msg = 'Commit message - fixes #6';
t.is(linkifyIssues(MOCK_REPO_URL, msg), msg);
const message = 'Commit message - fixes #6';
t.is(linkifyIssues(MOCK_REPO_URL, message), message);
});

test('linkifyCommit correctly links commits', t => {
11 changes: 11 additions & 0 deletions test/integration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const test = require('ava');
const execa = require('execa');

test.after.always(async () => {
await execa('git', ['submodule', 'update', '--remote']);
});

test('Integration tests', async t => {
await execa('ava', {cwd: 'integration-test'});
t.pass();
});
94 changes: 94 additions & 0 deletions test/npmignore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import path from 'path';
import test from 'ava';
import proxyquire from 'proxyquire';

const newFiles = [
'source/ignore.txt',
'source/pay_attention.txt',
'.hg',
'test/file.txt',
'readme.md',
'README.txt'
];

test('ignored files using file-attribute in package.json with one file', async t => {
const testedModule = proxyquire('../source/npm/util', {
'pkg-dir':
{
sync: () => path.resolve('test', 'fixtures', 'package')
}
});
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['pay_attention.txt']}, newFiles), ['source/ignore.txt']);
});

test('ignored file using file-attribute in package.json with directory', async t => {
const testedModule = proxyquire('../source/npm/util', {
'pkg-dir':
{
sync: () => path.resolve('test', 'fixtures', 'package')
}
});
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['source']}, newFiles), []);
});

test('ignored test files using files attribute and directory structure in package.json', async t => {
const testedModule = proxyquire('../source/npm/util', {
'pkg-dir':
{
sync: () => path.resolve('test', 'fixtures', 'package')
}
});
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['source'], directories: {test: 'test-tap'}}, newFiles), ['test/file.txt']);
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['source'], directories: {test: ['test-tap']}}, newFiles), ['test/file.txt']);
});

test('ignored files using .npmignore', async t => {
const testedModule = proxyquire('../source/npm/util', {
'pkg-dir':
{
sync: () => path.resolve('test', 'fixtures', 'npmignore')
}
});
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({name: 'npmignore'}, newFiles), ['source/ignore.txt']);
});

test('ignored test files using files attribute and .npmignore', async t => {
const testedModule = proxyquire('../source/npm/util', {
'pkg-dir':
{
sync: () => path.resolve('test', 'fixtures', 'npmignore')
}
});
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({directories: {test: 'test-tap'}}, newFiles), ['source/ignore.txt', 'test/file.txt']);
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({directories: {test: ['test-tap']}}, newFiles), ['source/ignore.txt', 'test/file.txt']);
});

test('dot files using files attribute', async t => {
const testedModule = proxyquire('../source/npm/util', {
'pkg-dir':
{
sync: () => path.resolve('test', 'fixtures', 'package')
}
});
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['source']}, ['test/.dotfile']), []);
});

test('dot files using .npmignore', async t => {
const testedModule = proxyquire('../source/npm/util', {
'pkg-dir':
{
sync: () => path.resolve('test', 'fixtures', 'npmignore')
}
});
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({}, ['test/.dot']), []);
});

test('ignore strategy is not used', async t => {
const testedModule = proxyquire('../source/npm/util', {
'pkg-dir':
{
sync: () => path.resolve('test', 'fixtures')
}
});
t.is(await testedModule.getNewAndUnpublishedFiles({name: 'no ignore strategy'}, newFiles), undefined);
});
12 changes: 6 additions & 6 deletions test/prerequisite-tasks.js
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@ test.serial('public-package published on npm registry: should fail when npm regi
t.true(SilentRenderer.tasks.some(task => task.title === 'Ping npm registry' && task.hasFailed()));
});

test.serial('private package: should skip task pinging npm registry', async t => {
test.serial('private package: should disable task pinging npm registry', async t => {
execaStub.createStub([
{
command: 'git rev-parse --quiet --verify refs/tags/v2.0.0',
@@ -47,10 +47,10 @@ test.serial('private package: should skip task pinging npm registry', async t =>
}
]);
await run(testedModule('2.0.0', {name: 'test', version: '1.0.0', private: true}, {yarn: false}));
t.true(SilentRenderer.tasks.some(task => task.title === 'Ping npm registry' && task.isSkipped()));
t.true(SilentRenderer.tasks.some(task => task.title === 'Ping npm registry' && !task.isEnabled()));
});

test.serial('external registry: should skip task pinging npm registry', async t => {
test.serial('external registry: should disable task pinging npm registry', async t => {
execaStub.createStub([
{
command: 'git rev-parse --quiet --verify refs/tags/v2.0.0',
@@ -60,7 +60,7 @@ test.serial('external registry: should skip task pinging npm registry', async t
]);
await run(testedModule('2.0.0', {name: 'test', version: '1.0.0', publishConfig: {registry: 'http://my.io'}},
{yarn: false}));
t.true(SilentRenderer.tasks.some(task => task.title === 'Ping npm registry' && task.isSkipped()));
t.true(SilentRenderer.tasks.some(task => task.title === 'Ping npm registry' && !task.isEnabled()));
});

test.serial('should fail when npm version does not match range in `package.json`', async t => {
@@ -141,7 +141,7 @@ test.serial('should fail when user is not authenticated at external registry', a
t.true(SilentRenderer.tasks.some(task => task.title === 'Verify user is authenticated' && task.hasFailed()));
});

test.serial('private package: should skip task `verify user is authenticated`', async t => {
test.serial('private package: should disable task `verify user is authenticated`', async t => {
execaStub.createStub([
{
command: 'git rev-parse --quiet --verify refs/tags/v2.0.0',
@@ -152,7 +152,7 @@ test.serial('private package: should skip task `verify user is authenticated`',
process.env.NODE_ENV = 'P';
await run(testedModule('2.0.0', {name: 'test', version: '1.0.0', private: true}, {yarn: false}));
process.env.NODE_ENV = 'test';
t.true(SilentRenderer.tasks.some(task => task.title === 'Verify user is authenticated' && task.isSkipped()));
t.true(SilentRenderer.tasks.some(task => task.title === 'Verify user is authenticated' && !task.isEnabled()));
});

test.serial('should fail when git version does not match range in `package.json`', async t => {