1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
|
const Arborist = require('@npmcli/arborist')
const packlist = require('npm-packlist')
const { join, relative } = require('path')
const localeCompare = require('@isaacs/string-locale-compare')('en')
const PackageJson = require('@npmcli/package-json')
const { run, CWD, git, fs } = require('./util')
const npmGit = require('@npmcli/git')
const ALWAYS_IGNORE = `
.bin/
.cache/
package-lock.json
CHANGELOG*
changelog*
ChangeLog*
Changelog*
README*
readme*
ReadMe*
Readme*
__pycache__
.editorconfig
.idea/
.npmignore
.eslintrc*
.travis*
.github
.jscsrc
.nycrc
.istanbul*
.eslintignore
.jshintrc*
.prettierrc*
.jscs.json
.dir-locals*
.coveralls*
.babelrc*
.nyc_output
.gitkeep
`
const lsAndRmIgnored = async (dir) => {
const files = await git(
'ls-files',
'--cached',
'--ignored',
`--exclude-standard`,
dir,
{ lines: true }
)
for (const file of files) {
await git('rm', file)
}
// check if there are still ignored files left
// if so we will error in the next step
const notRemoved = await git(
'ls-files',
'--cached',
'--ignored',
`--exclude-standard`,
dir,
{ lines: true }
)
return notRemoved
}
const getAllowedPaths = (files) => {
// Get all files within node_modules and remove
// the node_modules/ portion of the path for processing
// since this list will go inside a gitignore at the
// root of the node_modules dir
const nmFiles = files
.filter(f => f.startsWith('node_modules/'))
.map(f => f.replace(/^node_modules\//, ''))
.sort(localeCompare)
class AllowSegments {
#segments
#usedSegments
constructor (pathSegments, rootSegments = []) {
// Copy strings with spread operator since we mutate these arrays
this.#segments = [...pathSegments]
this.#usedSegments = [...rootSegments]
}
get next () {
return this.#segments[0]
}
get remaining () {
return this.#segments
}
get used () {
return this.#usedSegments
}
use () {
const segment = this.#segments.shift()
this.#usedSegments.push(segment)
return segment
}
allowContents ({ use = true, isDirectory = true } = {}) {
if (use) {
this.use()
}
// Allow a previously ignored directy
// Important: this should NOT have a trailing
// slash if we are not sure it is a directory.
// Since a dep can be a directory or a symlink and
// a trailing slash in a .gitignore file
// tells git to treat it only as a directory
return [`!/${this.used.join('/')}${isDirectory ? '/' : ''}`]
}
allow ({ use = true } = {}) {
if (use) {
this.use()
}
// Allow a previously ignored directory but ignore everything inside
return [
...this.allowContents({ use: false, isDirectory: true }),
`/${this.used.join('/')}/*`,
]
}
}
const gatherAllows = (pathParts, usedParts) => {
const ignores = []
const segments = new AllowSegments(pathParts, usedParts)
if (segments.next) {
// 1) Process scope segment of the path, if it has one
if (segments.next.startsWith('@')) {
// For scoped deps we need to allow the entire scope dir
// due to how gitignore works. Without this the gitignore will
// never look to allow our bundled dep since the scope dir was ignored.
// It ends up looking like this for `@colors/colors`:
//
// # Allow @colors dir
// !/@colors/
// # Ignore everything inside. This is safe because there is
// # nothing inside a scope except other packages
// !/colors/*
//
// Then later we will allow the specific dep inside that scope.
// This way if a scope includes bundled AND unbundled deps,
// we only allow the bundled ones.
ignores.push(...segments.allow())
}
// 2) Now we process the name segment of the path
// and allow the dir and everything inside of it (like source code, etc)
ignores.push(...segments.allowContents({ isDirectory: false }))
// 3) If we still have remaining segments and the next segment
// is a nested node_modules directory...
if (segments.next && segments.use() === 'node_modules') {
ignores.push(
// Allow node_modules and ignore everything inside of it
// Set false here since we already "used" the node_modules path segment
...segments.allow({ use: false }),
// Repeat the process with the remaining path segments to include whatever is left
...gatherAllows(segments.remaining, segments.used)
)
}
}
return ignores
}
const allowPaths = new Set()
for (const file of nmFiles) {
for (const allow of gatherAllows(file.split('/'))) {
allowPaths.add(allow)
}
}
return [...allowPaths]
}
const setBundleDeps = async () => {
const pkg = await PackageJson.load(CWD)
pkg.update({
bundleDependencies: Object.keys(pkg.content.dependencies).sort(localeCompare),
})
await pkg.save()
return pkg.content.bundleDependencies
}
/*
This file sets what is checked in to node_modules. The root .gitignore file
includes node_modules and this file writes an ignore file to
node_modules/.gitignore. We ignore everything and then use a query to find all
the bundled deps and allow each one of those explicitly.
Since node_modules can be nested we have to process each portion of the path and
allow it while also ignoring everything inside of it, with the exception of a
deps source. We have to do this since everything is ignored by default, and git
will not allow a nested path if its parent has not also been allowed. BUT! We
also have to ignore other things in those directories.
*/
const main = async () => {
await setBundleDeps()
const arb = new Arborist({ path: CWD })
const files = await arb.loadActual().then(packlist)
const ignoreFile = [
'# Automatically generated to ignore everything except bundled deps',
'# Ignore everything by default except this file',
'/*',
'!/.gitignore',
'# Allow all bundled deps',
...getAllowedPaths(files),
'# Always ignore some specific patterns within any allowed package',
...ALWAYS_IGNORE.trim().split('\n'),
]
const NODE_MODULES = join(CWD, 'node_modules')
const res = await fs.writeFile(join(NODE_MODULES, '.gitignore'), ignoreFile.join('\n'))
if (!await npmGit.is({ cwd: CWD })) {
// if we are not running in a git repo then write the files but we do not
// need to run any git commands to check if we have unmatched files in source
return res
}
// After we write the file we have to check if any of the paths already checked in
// inside node_modules are now going to be ignored. If we find any then fail with
// a list of the paths remaining. We already attempted to `git rm` them so just
// explain what happened and leave the repo in a state to debug.
const trackedAndIgnored = await lsAndRmIgnored(NODE_MODULES)
if (trackedAndIgnored.length) {
const message = [
'The following files are checked in to git but will now be ignored.',
`They could not be removed automatically and will need to be removed manually.`,
...trackedAndIgnored.map(p => relative(NODE_MODULES, p)),
].join('\n')
throw new Error(message)
}
return res
}
run(main)
|