Skip to content

Commit 08a2c06

Browse files
bunysaesindresorhus
andauthoredOct 29, 2020
Show files added since the last release and not part of the package (#456)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 0cff2b4 commit 08a2c06

22 files changed

+280
-5
lines changed
 

‎.gitmodules

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "integration-test"]
2+
path = integration-test
3+
url = https://github.com/bunysae/np_integration_test

‎integration-test

Submodule integration-test added at ad5e6e3

‎package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"github-url-from-git": "^1.5.0",
4343
"has-yarn": "^2.1.0",
4444
"hosted-git-info": "^3.0.0",
45+
"ignore-walk": "^3.0.3",
4546
"import-local": "^3.0.2",
4647
"inquirer": "^7.0.0",
4748
"is-installed-globally": "^0.3.1",
@@ -51,6 +52,7 @@
5152
"listr-input": "^0.2.1",
5253
"log-symbols": "^3.0.0",
5354
"meow": "^6.0.0",
55+
"minimatch": "^3.0.4",
5456
"new-github-release-url": "^1.0.0",
5557
"npm-name": "^6.0.0",
5658
"onetime": "^5.1.0",
@@ -77,7 +79,8 @@
7779
},
7880
"ava": {
7981
"files": [
80-
"!test/fixtures"
82+
"!test/fixtures",
83+
"!integration-test"
8184
]
8285
}
8386
}

‎readme.md

+4
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ Host *
280280

281281
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/).
282282

283+
### Ignore strategy
284+
285+
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`.
286+
283287
## FAQ
284288

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

‎source/git-util.js

+16
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
'use strict';
22
const execa = require('execa');
33
const escapeStringRegexp = require('escape-string-regexp');
4+
const ignoreWalker = require('ignore-walk');
5+
const pkgDir = require('pkg-dir');
46
const {verifyRequirementSatisfied} = require('./version');
57

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

13+
exports.newFilesSinceLastRelease = async () => {
14+
try {
15+
const {stdout} = await execa('git', ['diff', '--stat', '--diff-filter=A', await this.latestTag(), 'HEAD']);
16+
const result = stdout.trim().split('\n').slice(0, -1).map(row => row.slice(0, row.indexOf('|')).trim());
17+
return result;
18+
} catch (_) {
19+
// Get all files under version control
20+
return ignoreWalker({
21+
path: pkgDir.sync(),
22+
ignoreFiles: ['.gitignore']
23+
});
24+
}
25+
};
26+
1127
const firstCommit = async () => {
1228
const {stdout} = await execa('git', ['rev-list', '--max-parents=0', 'HEAD']);
1329
return stdout;

‎source/npm/util.js

+100-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const ow = require('ow');
77
const npmName = require('npm-name');
88
const chalk = require('chalk');
99
const pkgDir = require('pkg-dir');
10+
const ignoreWalker = require('ignore-walk');
11+
const minimatch = require('minimatch');
1012
const {verifyRequirementSatisfied} = require('../version');
1113

1214
exports.checkConnection = () => pTimeout(
@@ -117,16 +119,110 @@ exports.verifyRecentNpmVersion = async () => {
117119
};
118120

119121
exports.checkIgnoreStrategy = ({files}) => {
120-
const rootDir = pkgDir.sync();
121-
const npmignoreExists = fs.existsSync(path.resolve(rootDir, '.npmignore'));
122-
123-
if (!files && !npmignoreExists) {
122+
if (!files && !npmignoreExistsInPackageRootDir()) {
124123
console.log(`
125124
\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.
126125
`);
127126
}
128127
};
129128

129+
function npmignoreExistsInPackageRootDir() {
130+
const rootDir = pkgDir.sync();
131+
return fs.existsSync(path.resolve(rootDir, '.npmignore'));
132+
}
133+
134+
async function getFilesIgnoredByDotnpmignore(pkg, fileList) {
135+
const whiteList = await ignoreWalker({
136+
path: pkgDir.sync(),
137+
ignoreFiles: ['.npmignore']
138+
});
139+
return fileList.filter(minimatch.filter(getIgnoredFilesGlob(whiteList, pkg.directories), {matchBase: true, dot: true}));
140+
}
141+
142+
function getFilesNotIncludedInFilesProperty(pkg, fileList) {
143+
const globArrayForFilesAndDirectories = [...pkg.files];
144+
const rootDir = pkgDir.sync();
145+
for (const glob of pkg.files) {
146+
try {
147+
if (fs.statSync(path.resolve(rootDir, glob)).isDirectory()) {
148+
globArrayForFilesAndDirectories.push(`${glob}/**/*`);
149+
}
150+
} catch (_) {}
151+
}
152+
153+
const result = fileList.filter(minimatch.filter(getIgnoredFilesGlob(globArrayForFilesAndDirectories, pkg.directories), {matchBase: true, dot: true}));
154+
return result.filter(minimatch.filter(getDefaultIncludedFilesGlob(pkg.main), {nocase: true, matchBase: true}));
155+
}
156+
157+
function getDefaultIncludedFilesGlob(mainFile) {
158+
// According to https://docs.npmjs.com/files/package.json#files
159+
// npm's default behavior is to always include these files.
160+
const filesAlwaysIncluded = [
161+
'package.json',
162+
'README*',
163+
'CHANGES*',
164+
'CHANGELOG*',
165+
'HISTORY*',
166+
'LICENSE*',
167+
'LICENCE*',
168+
'NOTICE*'
169+
];
170+
if (mainFile) {
171+
filesAlwaysIncluded.push(mainFile);
172+
}
173+
174+
return `!{${filesAlwaysIncluded}}`;
175+
}
176+
177+
function getIgnoredFilesGlob(globArrayFromFilesProperty, packageDirectories) {
178+
// According to https://docs.npmjs.com/files/package.json#files
179+
// npm's default behavior is to ignore these files.
180+
const filesIgnoredByDefault = [
181+
'.*.swp',
182+
'.npmignore',
183+
'.gitignore',
184+
'._*',
185+
'.DS_Store',
186+
'.hg',
187+
'.npmrc',
188+
'.lock-wscript',
189+
'.svn',
190+
'.wafpickle-N',
191+
'*.orig',
192+
'config.gypi',
193+
'CVS',
194+
'node_modules/**/*',
195+
'npm-debug.log',
196+
'package-lock.json',
197+
'.git/**/*',
198+
'.git'
199+
];
200+
201+
// Test files are assumed not to be part of the package
202+
let testDirectoriesGlob = '';
203+
if (packageDirectories && Array.isArray(packageDirectories.test)) {
204+
testDirectoriesGlob = packageDirectories.test.join(',');
205+
} else if (packageDirectories && typeof packageDirectories.test === 'string') {
206+
testDirectoriesGlob = packageDirectories.test;
207+
} else {
208+
// Fallback to `test` directory
209+
testDirectoriesGlob = 'test/**/*';
210+
}
211+
212+
return `!{${globArrayFromFilesProperty.join(',')},${filesIgnoredByDefault.join(',')},${testDirectoriesGlob}}`;
213+
}
214+
215+
// Get all files which will be ignored by either `.npmignore` or the `files` property in `package.json` (if defined).
216+
exports.getNewAndUnpublishedFiles = async (pkg, newFiles = []) => {
217+
if (pkg.files) {
218+
return getFilesNotIncludedInFilesProperty(pkg, newFiles);
219+
}
220+
221+
if (npmignoreExistsInPackageRootDir()) {
222+
return getFilesIgnoredByDotnpmignore(pkg, newFiles);
223+
}
224+
};
225+
130226
exports.getRegistryUrl = async (pkgManager, pkg) => {
131227
const args = ['config', 'get', 'registry'];
132228
if (exports.isExternalRegistry(pkg)) {

‎source/ui.js

+24
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,22 @@ const printCommitLog = async (repoUrl, registryUrl) => {
5050
};
5151
};
5252

53+
const checkIgnoredFiles = async pkg => {
54+
const ignoredFiles = await util.getNewAndUnpublishedFiles(pkg);
55+
if (!ignoredFiles || ignoredFiles.length === 0) {
56+
return true;
57+
}
58+
59+
const answers = await inquirer.prompt([{
60+
type: 'confirm',
61+
name: 'confirm',
62+
message: `The following new files are not already part of your published package:\n${chalk.reset(ignoredFiles.map(path => `- ${path}`).join('\n'))}\nContinue?`,
63+
default: false
64+
}]);
65+
66+
return answers.confirm;
67+
};
68+
5369
module.exports = async (options, pkg) => {
5470
const oldVersion = pkg.version;
5571
const extraBaseUrls = ['gitlab.com'];
@@ -59,6 +75,14 @@ module.exports = async (options, pkg) => {
5975

6076
if (options.runPublish) {
6177
checkIgnoreStrategy(pkg);
78+
79+
const answerIgnoredFiles = await checkIgnoredFiles(pkg);
80+
if (!answerIgnoredFiles) {
81+
return {
82+
...options,
83+
confirm: answerIgnoredFiles
84+
};
85+
}
6286
}
6387

6488
console.log(`\nPublish a new version of ${chalk.bold.magenta(pkg.name)} ${chalk.dim(`(current: ${oldVersion})`)}\n`);

‎source/util.js

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const execa = require('execa');
66
const pMemoize = require('p-memoize');
77
const ow = require('ow');
88
const pkgDir = require('pkg-dir');
9+
const gitUtil = require('./git-util');
10+
const npmUtil = require('./npm/util');
911

1012
exports.readPkg = packagePath => {
1113
packagePath = packagePath ? pkgDir.sync(packagePath) : pkgDir.sync();
@@ -69,6 +71,11 @@ exports.getTagVersionPrefix = pMemoize(async options => {
6971
}
7072
});
7173

74+
exports.getNewAndUnpublishedFiles = async pkg => {
75+
const listNewFiles = await gitUtil.newFilesSinceLastRelease();
76+
return npmUtil.getNewAndUnpublishedFiles(pkg, listNewFiles);
77+
};
78+
7279
exports.getPreReleasePrefix = pMemoize(async options => {
7380
ow(options, ow.object.hasKeys('yarn'));
7481

‎test/fixtures/npmignore/.hg

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
should be ignored by default

‎test/fixtures/npmignore/.npmignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ignore.txt
2+
test

‎test/fixtures/npmignore/README.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
File is always included in package.

‎test/fixtures/npmignore/readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
File is always included in package.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Ignore this file
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
File is excluded from .npmignore

‎test/fixtures/npmignore/test/file.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ignore this file

‎test/fixtures/package/.hg

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
should be ignored by default

‎test/fixtures/package/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"files": ["pay_attention.txt"]
3+
}
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
File is excluded from package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
File in included in package.json

‎test/fixtures/readme.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The directory is for the resources
2+
in the script npmignore.js

‎test/integration.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const test = require('ava');
2+
const execa = require('execa');
3+
4+
test.after.always(async () => {
5+
await execa('git', ['submodule', 'update', '--remote']);
6+
});
7+
8+
test('Integration tests', async t => {
9+
await execa('ava', {cwd: 'integration-test'});
10+
t.pass();
11+
});

‎test/npmignore.js

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import path from 'path';
2+
import test from 'ava';
3+
import proxyquire from 'proxyquire';
4+
5+
const newFiles = [
6+
'source/ignore.txt',
7+
'source/pay_attention.txt',
8+
'.hg',
9+
'test/file.txt',
10+
'readme.md',
11+
'README.txt'
12+
];
13+
14+
test('ignored files using file-attribute in package.json with one file', async t => {
15+
const testedModule = proxyquire('../source/npm/util', {
16+
'pkg-dir':
17+
{
18+
sync: () => path.resolve('test', 'fixtures', 'package')
19+
}
20+
});
21+
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['pay_attention.txt']}, newFiles), ['source/ignore.txt']);
22+
});
23+
24+
test('ignored file using file-attribute in package.json with directory', async t => {
25+
const testedModule = proxyquire('../source/npm/util', {
26+
'pkg-dir':
27+
{
28+
sync: () => path.resolve('test', 'fixtures', 'package')
29+
}
30+
});
31+
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['source']}, newFiles), []);
32+
});
33+
34+
test('ignored test files using files attribute and directory structure in package.json', async t => {
35+
const testedModule = proxyquire('../source/npm/util', {
36+
'pkg-dir':
37+
{
38+
sync: () => path.resolve('test', 'fixtures', 'package')
39+
}
40+
});
41+
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['source'], directories: {test: 'test-tap'}}, newFiles), ['test/file.txt']);
42+
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['source'], directories: {test: ['test-tap']}}, newFiles), ['test/file.txt']);
43+
});
44+
45+
test('ignored files using .npmignore', async t => {
46+
const testedModule = proxyquire('../source/npm/util', {
47+
'pkg-dir':
48+
{
49+
sync: () => path.resolve('test', 'fixtures', 'npmignore')
50+
}
51+
});
52+
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({name: 'npmignore'}, newFiles), ['source/ignore.txt']);
53+
});
54+
55+
test('ignored test files using files attribute and .npmignore', async t => {
56+
const testedModule = proxyquire('../source/npm/util', {
57+
'pkg-dir':
58+
{
59+
sync: () => path.resolve('test', 'fixtures', 'npmignore')
60+
}
61+
});
62+
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({directories: {test: 'test-tap'}}, newFiles), ['source/ignore.txt', 'test/file.txt']);
63+
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({directories: {test: ['test-tap']}}, newFiles), ['source/ignore.txt', 'test/file.txt']);
64+
});
65+
66+
test('dot files using files attribute', async t => {
67+
const testedModule = proxyquire('../source/npm/util', {
68+
'pkg-dir':
69+
{
70+
sync: () => path.resolve('test', 'fixtures', 'package')
71+
}
72+
});
73+
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({files: ['source']}, ['test/.dotfile']), []);
74+
});
75+
76+
test('dot files using .npmignore', async t => {
77+
const testedModule = proxyquire('../source/npm/util', {
78+
'pkg-dir':
79+
{
80+
sync: () => path.resolve('test', 'fixtures', 'npmignore')
81+
}
82+
});
83+
t.deepEqual(await testedModule.getNewAndUnpublishedFiles({}, ['test/.dot']), []);
84+
});
85+
86+
test('ignore strategy is not used', async t => {
87+
const testedModule = proxyquire('../source/npm/util', {
88+
'pkg-dir':
89+
{
90+
sync: () => path.resolve('test', 'fixtures')
91+
}
92+
});
93+
t.is(await testedModule.getNewAndUnpublishedFiles({name: 'no ignore strategy'}, newFiles), undefined);
94+
});

0 commit comments

Comments
 (0)
Please sign in to comment.