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