77const shelljs = require ( 'shelljs' ) ;
88const path = require ( 'path' ) ;
99const fs = require ( 'fs' ) ;
10+ const inquirer = require ( 'inquirer' ) ;
11+ const chalk = require ( 'chalk' ) ;
1012
1113/**
1214 * Version of the post install patch. Needs to be incremented when
@@ -23,154 +25,172 @@ const projectDir = path.join(__dirname, '../..');
2325 */
2426const PATCHES_PER_FILE = { } ;
2527
26- shelljs . set ( '-e' ) ;
27- shelljs . cd ( projectDir ) ;
28-
29- // Workaround for https://github.com/angular/angular/issues/18810.
30- shelljs . exec ( 'ngc -p angular-tsconfig.json' ) ;
31-
32- // Workaround for: https://github.com/angular/angular/issues/32651. We just do not
33- // generate re-exports for secondary entry-points. Similar to what "ng-packagr" does.
34- searchAndReplace (
35- / (? ! f u n c t i o n \s + ) c r e a t e M e t a d a t a R e e x p o r t F i l e \( [ ^ ) ] + \) ; / , '' ,
36- 'node_modules/@angular/bazel/src/ng_package/packager.js' ) ;
37- searchAndReplace (
38- / (? ! f u n c t i o n \s + ) c r e a t e T y p i n g s R e e x p o r t F i l e \( [ ^ ) ] + \) ; / , '' ,
39- 'node_modules/@angular/bazel/src/ng_package/packager.js' ) ;
40-
41- // Workaround for: https://github.com/angular/angular/pull/32650
42- searchAndReplace (
43- 'var indexFile;' , `
44- var indexFile = files.find(f => f.endsWith('/public-api.ts'));
45- ` ,
46- 'node_modules/@angular/compiler-cli/src/metadata/bundle_index_host.js' ) ;
47- searchAndReplace (
48- 'var resolvedEntryPoint = null;' , `
49- var resolvedEntryPoint = tsFiles.find(f => f.endsWith('/public-api.ts')) || null;
50- ` ,
51- 'node_modules/@angular/compiler-cli/src/ngtsc/entry_point/src/logic.js' ) ;
52-
53- // Workaround for: https://hackmd.io/MlqFp-yrSx-0mw4rD7dnQQ?both. We only want to discard
54- // the metadata of files in the bazel managed node modules. That way we keep the default
55- // behavior of ngc-wrapped except for dependencies between sources of the library. This makes
56- // the "generateCodeForLibraries" flag more accurate in the Bazel environment where previous
57- // compilations should not be treated as external libraries. Read more about this in the document.
58- searchAndReplace (
59- / i f \( ( t h i s \. o p t i o n s \. g e n e r a t e C o d e F o r L i b r a r i e s = = = f a l s e ) / , `
60- const fs = require('fs');
61- const hasFlatModuleBundle = fs.existsSync(filePath.replace('.d.ts', '.metadata.json'));
62- if ((filePath.includes('node_modules/') || !hasFlatModuleBundle) && $1` ,
63- 'node_modules/@angular/compiler-cli/src/transformers/compiler_host.js' ) ;
64- applyPatch ( path . join ( __dirname , './flat_module_factory_resolution.patch' ) ) ;
65- // The three replacements below ensure that metadata files can be read by NGC and
66- // that metadata files are collected as Bazel action inputs.
67- searchAndReplace (
68- / ( c o n s t N G C _ A S S E T S = \/ [ ^ ( ] + \( ) ( [ ^ ) ] * ) ( \) .* \/ ; ) / , '$1$2|metadata.json$3' ,
69- 'node_modules/@angular/bazel/src/ngc-wrapped/index.js' ) ;
70- searchAndReplace (
71- / ^ ( ( \s * ) r e s u l t s = d e p s e t \( d e p .a n g u l a r .s u m m a r i e s , t r a n s i t i v e = \[ r e s u l t s ] \) ) $ / m,
72- `$1#\n$2results = depset(dep.angular.metadata, transitive = [results])` ,
73- 'node_modules/@angular/bazel/src/ng_module.bzl' ) ;
74- searchAndReplace (
75- / ^ ( ( \s * ) r e s u l t s = d e p s e t \( t a r g e t .a n g u l a r \. s u m m a r i e s i f _ h a s _ t a r g e t _ a n g u l a r _ s u m m a r i e s \( t a r g e t \) e l s e \[ ] \) ) $ / m,
76- `$1#\n$2results = depset(target.angular.metadata if _has_target_angular_summaries(target) else [], transitive = [results])` ,
77- 'node_modules/@angular/bazel/src/ng_module.bzl' ) ;
78- // Ensure that "metadata" of transitive dependencies can be collected.
79- searchAndReplace (
80- / p r o v i d e r s \[ " a n g u l a r " ] \[ " m e t a d a t a " ] = o u t s \. m e t a d a t a / ,
81- `$& + [m for dep in ctx.attr.deps if (hasattr(dep, "angular") and hasattr(dep.angular, "metadata")) for m in dep.angular.metadata]` ,
82- 'node_modules/@angular/bazel/src/ng_module.bzl' ) ;
83-
84- // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1208.
85- applyPatch ( path . join ( __dirname , './manifest_externs_hermeticity.patch' ) ) ;
86-
87- try {
88- // Temporary patch pre-req for https://github.com/angular/angular/pull/36333.
89- // Can be removed once @angular /bazel is updated here to include this patch.
90- // try/catch needed for this the material CI tests to work in angular/repo
91- applyPatch ( path . join ( __dirname , './@angular_bazel_ng_module.patch' ) ) ;
92- } catch { }
93-
94- try {
95- // Temporary patch pre-req for https://github.com/angular/angular/pull/36971.
96- // Can be removed once @angular /bazel is updated here to include this patch.
97- // try/catch needed for this as the framework repo has this patch already applied,
98- // and re-applying again causes an error.
99- applyPatch ( path . join ( __dirname , './@angular_bazel_ivy_flat_module.patch' ) ) ;
100- } catch { }
101-
102- // Workaround for https://github.com/angular/angular/issues/33452:
103- searchAndReplace ( / a n g u l a r _ c o m p i l e r _ o p t i o n s = { / , `$&
104- "strictTemplates": True,` , 'node_modules/@angular/bazel/src/ng_module.bzl' ) ;
105-
106- // More info in https://github.com/angular/angular/pull/33786
107- shelljs . rm ( '-rf' , [
108- 'node_modules/rxjs/add/' ,
109- 'node_modules/rxjs/observable/' ,
110- 'node_modules/rxjs/operator/' ,
111- // rxjs/operators is a public entry point that also contains files to support legacy deep import
112- // paths, so we need to preserve index.* and package.json files that are required for module
113- // resolution.
114- 'node_modules/rxjs/operators/!(index.*|package.json)' ,
115- 'node_modules/rxjs/scheduler/' ,
116- 'node_modules/rxjs/symbol/' ,
117- 'node_modules/rxjs/util/' ,
118- 'node_modules/rxjs/internal/Rx.d.ts' ,
119- 'node_modules/rxjs/AsyncSubject.*' ,
120- 'node_modules/rxjs/BehaviorSubject.*' ,
121- 'node_modules/rxjs/InnerSubscriber.*' ,
122- 'node_modules/rxjs/interfaces.*' ,
123- 'node_modules/rxjs/Notification.*' ,
124- 'node_modules/rxjs/Observable.*' ,
125- 'node_modules/rxjs/Observer.*' ,
126- 'node_modules/rxjs/Operator.*' ,
127- 'node_modules/rxjs/OuterSubscriber.*' ,
128- 'node_modules/rxjs/ReplaySubject.*' ,
129- 'node_modules/rxjs/Rx.*' ,
130- 'node_modules/rxjs/Scheduler.*' ,
131- 'node_modules/rxjs/Subject.*' ,
132- 'node_modules/rxjs/SubjectSubscription.*' ,
133- 'node_modules/rxjs/Subscriber.*' ,
134- 'node_modules/rxjs/Subscription.*' ,
135- ] ) ;
136-
137- // Apply all collected patches on a per-file basis. This is necessary because
138- // multiple edits might apply to the same file, and we only want to mark a given
139- // file as patched once all edits have been made.
140- Object . keys ( PATCHES_PER_FILE ) . forEach ( filePath => {
141- if ( hasFileBeenPatched ( filePath ) ) {
142- console . info ( 'File ' + filePath + ' is already patched. Skipping..' ) ;
143- return ;
28+ const PATCH_MARKER_FILE_PATH = path . join ( projectDir , 'node_modules/_ng-comp-patch-marker.json' ) ;
29+
30+ /** Registry of applied patches. */
31+ let registry = null ;
32+
33+ main ( ) ;
34+
35+ async function main ( ) {
36+ shelljs . set ( '-e' ) ;
37+ shelljs . cd ( projectDir ) ;
38+
39+ registry = await readAndValidatePatchMarker ( ) ;
40+
41+ // Apply all patches synchronously.
42+ applyPatches ( ) ;
43+
44+ // Write the patch marker file so that we don't accidentally re-apply patches
45+ // in subsequent Yarn installations.
46+ fs . writeFileSync ( PATCH_MARKER_FILE_PATH , JSON . stringify ( registry , null , 2 ) ) ;
47+ }
48+
49+ function applyPatches ( ) {
50+ // Workaround for https://github.com/angular/angular/issues/18810.
51+ shelljs . exec ( 'ngc -p angular-tsconfig.json' ) ;
52+
53+ // Workaround for: https://github.com/angular/angular/issues/32651. We just do not
54+ // generate re-exports for secondary entry-points. Similar to what "ng-packagr" does.
55+ searchAndReplace (
56+ / (? ! f u n c t i o n \s + ) c r e a t e M e t a d a t a R e e x p o r t F i l e \( [ ^ ) ] + \) ; / , '' ,
57+ 'node_modules/@angular/bazel/src/ng_package/packager.js' ) ;
58+ searchAndReplace (
59+ / (? ! f u n c t i o n \s + ) c r e a t e T y p i n g s R e e x p o r t F i l e \( [ ^ ) ] + \) ; / , '' ,
60+ 'node_modules/@angular/bazel/src/ng_package/packager.js' ) ;
61+
62+ // Workaround for: https://github.com/angular/angular/pull/32650
63+ searchAndReplace (
64+ 'var indexFile;' , `
65+ var indexFile = files.find(f => f.endsWith('/public-api.ts'));
66+ ` ,
67+ 'node_modules/@angular/compiler-cli/src/metadata/bundle_index_host.js' ) ;
68+ searchAndReplace (
69+ 'var resolvedEntryPoint = null;' , `
70+ var resolvedEntryPoint = tsFiles.find(f => f.endsWith('/public-api.ts')) || null;
71+ ` ,
72+ 'node_modules/@angular/compiler-cli/src/ngtsc/entry_point/src/logic.js' ) ;
73+
74+ // Workaround for: https://hackmd.io/MlqFp-yrSx-0mw4rD7dnQQ?both. We only want to discard
75+ // the metadata of files in the bazel managed node modules. That way we keep the default
76+ // behavior of ngc-wrapped except for dependencies between sources of the library. This makes
77+ // the "generateCodeForLibraries" flag more accurate in the Bazel environment where previous
78+ // compilations should not be treated as external libraries. Read more about this in the document.
79+ searchAndReplace (
80+ / i f \( ( t h i s \. o p t i o n s \. g e n e r a t e C o d e F o r L i b r a r i e s = = = f a l s e ) / , `
81+ const fs = require('fs');
82+ const hasFlatModuleBundle = fs.existsSync(filePath.replace('.d.ts', '.metadata.json'));
83+ if ((filePath.includes('node_modules/') || !hasFlatModuleBundle) && $1` ,
84+ 'node_modules/@angular/compiler-cli/src/transformers/compiler_host.js' ) ;
85+ applyPatch ( path . join ( __dirname , './flat_module_factory_resolution.patch' ) ) ;
86+ // The three replacements below ensure that metadata files can be read by NGC and
87+ // that metadata files are collected as Bazel action inputs.
88+ searchAndReplace (
89+ / ( c o n s t N G C _ A S S E T S = \/ [ ^ ( ] + \( ) ( [ ^ ) ] * ) ( \) .* \/ ; ) / , '$1$2|metadata.json$3' ,
90+ 'node_modules/@angular/bazel/src/ngc-wrapped/index.js' ) ;
91+ searchAndReplace (
92+ / ^ ( ( \s * ) r e s u l t s = d e p s e t \( d e p .a n g u l a r .s u m m a r i e s , t r a n s i t i v e = \[ r e s u l t s ] \) ) $ / m,
93+ `$1#\n$2results = depset(dep.angular.metadata, transitive = [results])` ,
94+ 'node_modules/@angular/bazel/src/ng_module.bzl' ) ;
95+ searchAndReplace (
96+ / ^ ( ( \s * ) r e s u l t s = d e p s e t \( t a r g e t .a n g u l a r \. s u m m a r i e s i f _ h a s _ t a r g e t _ a n g u l a r _ s u m m a r i e s \( t a r g e t \) e l s e \[ ] \) ) $ / m,
97+ `$1#\n$2results = depset(target.angular.metadata if _has_target_angular_summaries(target) else [], transitive = [results])` ,
98+ 'node_modules/@angular/bazel/src/ng_module.bzl' ) ;
99+ // Ensure that "metadata" of transitive dependencies can be collected.
100+ searchAndReplace (
101+ / p r o v i d e r s \[ " a n g u l a r " ] \[ " m e t a d a t a " ] = o u t s \. m e t a d a t a / ,
102+ `$& + [m for dep in ctx.attr.deps if (hasattr(dep, "angular") and hasattr(dep.angular, "metadata")) for m in dep.angular.metadata]` ,
103+ 'node_modules/@angular/bazel/src/ng_module.bzl' ) ;
104+
105+ // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1208.
106+ applyPatch ( path . join ( __dirname , './manifest_externs_hermeticity.patch' ) ) ;
107+
108+ try {
109+ // Temporary patch pre-req for https://github.com/angular/angular/pull/36333.
110+ // Can be removed once @angular /bazel is updated here to include this patch.
111+ // try/catch needed for this the material CI tests to work in angular/repo
112+ applyPatch ( path . join ( __dirname , './@angular_bazel_ng_module.patch' ) ) ;
113+ } catch {
144114 }
145115
146- let content = fs . readFileSync ( filePath , 'utf8' ) ;
147- const patchFunctions = PATCHES_PER_FILE [ filePath ] ;
116+ try {
117+ // Temporary patch pre-req for https://github.com/angular/angular/pull/36971.
118+ // Can be removed once @angular /bazel is updated here to include this patch.
119+ // try/catch needed for this as the framework repo has this patch already applied,
120+ // and re-applying again causes an error.
121+ applyPatch ( path . join ( __dirname , './@angular_bazel_ivy_flat_module.patch' ) ) ;
122+ } catch {
123+ }
124+
125+ // Workaround for https://github.com/angular/angular/issues/33452:
126+ searchAndReplace (
127+ / a n g u l a r _ c o m p i l e r _ o p t i o n s = { / , `$&
128+ "strictTemplates": True,` ,
129+ 'node_modules/@angular/bazel/src/ng_module.bzl' ) ;
148130
149- console . info ( `Patching file ${ filePath } with ${ patchFunctions . length } edits..` ) ;
150- patchFunctions . forEach ( patchFn => content = patchFn ( content ) ) ;
131+ // More info in https://github.com/angular/angular/pull/33786
132+ shelljs . rm ( '-rf' , [
133+ 'node_modules/rxjs/add/' ,
134+ 'node_modules/rxjs/observable/' ,
135+ 'node_modules/rxjs/operator/' ,
136+ // rxjs/operators is a public entry point that also contains files to support legacy deep import
137+ // paths, so we need to preserve index.* and package.json files that are required for module
138+ // resolution.
139+ 'node_modules/rxjs/operators/!(index.*|package.json)' ,
140+ 'node_modules/rxjs/scheduler/' ,
141+ 'node_modules/rxjs/symbol/' ,
142+ 'node_modules/rxjs/util/' ,
143+ 'node_modules/rxjs/internal/Rx.d.ts' ,
144+ 'node_modules/rxjs/AsyncSubject.*' ,
145+ 'node_modules/rxjs/BehaviorSubject.*' ,
146+ 'node_modules/rxjs/InnerSubscriber.*' ,
147+ 'node_modules/rxjs/interfaces.*' ,
148+ 'node_modules/rxjs/Notification.*' ,
149+ 'node_modules/rxjs/Observable.*' ,
150+ 'node_modules/rxjs/Observer.*' ,
151+ 'node_modules/rxjs/Operator.*' ,
152+ 'node_modules/rxjs/OuterSubscriber.*' ,
153+ 'node_modules/rxjs/ReplaySubject.*' ,
154+ 'node_modules/rxjs/Rx.*' ,
155+ 'node_modules/rxjs/Scheduler.*' ,
156+ 'node_modules/rxjs/Subject.*' ,
157+ 'node_modules/rxjs/SubjectSubscription.*' ,
158+ 'node_modules/rxjs/Subscriber.*' ,
159+ 'node_modules/rxjs/Subscription.*' ,
160+ ] ) ;
161+
162+ // Apply all collected patches on a per-file basis. This is necessary because
163+ // multiple edits might apply to the same file, and we only want to mark a given
164+ // file as patched once all edits have been made.
165+ Object . keys ( PATCHES_PER_FILE ) . forEach ( filePath => {
166+ if ( isFilePatched ( filePath ) ) {
167+ console . info ( 'File ' + filePath + ' is already patched. Skipping..' ) ;
168+ return ;
169+ }
151170
152- fs . writeFileSync ( filePath , content , 'utf8' ) ;
153- writePatchMarker ( filePath ) ;
154- } ) ;
171+ let content = fs . readFileSync ( filePath , 'utf8' ) ;
172+ const patchFunctions = PATCHES_PER_FILE [ filePath ] ;
173+
174+ console . info ( `Patching file ${ filePath } with ${ patchFunctions . length } edits..` ) ;
175+ patchFunctions . forEach ( patchFn => content = patchFn ( content ) ) ;
176+
177+ fs . writeFileSync ( filePath , content , 'utf8' ) ;
178+ captureFileAsPatched ( filePath ) ;
179+ } ) ;
180+ }
155181
156182/**
157- * Applies the given patch if not done already. Throws if the patch does
158- * not apply cleanly.
183+ * Applies the given patch if not done already. Throws if the patch
184+ * does not apply cleanly.
159185 */
160186function applyPatch ( patchFile ) {
161- // Note: We replace non-word characters from the patch marker file name.
162- // This is necessary because Yarn throws if cached node modules are restored
163- // which contain files with special characters. Below is an example error:
164- // ENOTDIR: not a directory, scandir '/<...>/node_modules/@angular_bazel_ng_module.<..>'".
165- const patchMarkerBasename = `${ path . basename ( patchFile ) . replace ( / [ ^ \w ] / , '_' ) } ` ;
166- const patchMarkerPath = path . join ( projectDir , 'node_modules/' , patchMarkerBasename ) ;
167-
168- if ( hasFileBeenPatched ( patchMarkerPath ) ) {
187+ if ( isFilePatched ( patchFile ) ) {
188+ console . info ( 'Patch: ' + patchFile + ' has been applied already. Skipping..' ) ;
169189 return ;
170190 }
171191
172192 shelljs . cat ( patchFile ) . exec ( 'patch -p0' ) ;
173- writePatchMarker ( patchMarkerPath ) ;
193+ captureFileAsPatched ( patchFile ) ;
174194}
175195
176196/**
@@ -185,21 +205,70 @@ function searchAndReplace(search, replacement, relativeFilePath) {
185205 fileEdits . push ( originalContent => {
186206 const newFileContent = originalContent . replace ( search , replacement ) ;
187207 if ( originalContent === newFileContent ) {
188- throw Error ( `Could not perform replacement in: ${ filePath } .\n` +
208+ throw Error (
209+ `Could not perform replacement in: ${ filePath } .\n` +
189210 `Searched for pattern: ${ search } ` ) ;
190211 }
191212 return newFileContent ;
192213 } ) ;
193214}
194215
216+ /** Gets a project unique id for a given file path. */
217+ function getIdForFile ( filePath ) {
218+ return path . relative ( projectDir , filePath ) . replace ( / \\ / g, '/' ) ;
219+ }
220+
195221/** Marks the specified file as patched. */
196- function writePatchMarker ( filePath ) {
197- new shelljs . ShellString ( PATCH_VERSION ) . to ( `${ filePath } .patch_marker` ) ;
222+ function captureFileAsPatched ( filePath ) {
223+ registry . patched [ getIdForFile ( filePath ) ] = true ;
224+ }
225+
226+ /** Checks whether the given file is patched. */
227+ function isFilePatched ( filePath ) {
228+ return registry . patched [ getIdForFile ( filePath ) ] === true ;
198229}
199230
200- /** Checks if the given file has been patched. */
201- function hasFileBeenPatched ( filePath ) {
202- const markerFilePath = `${ filePath } .patch_marker` ;
203- return shelljs . test ( '-e' , markerFilePath ) &&
204- shelljs . cat ( markerFilePath ) . toString ( ) . trim ( ) === `${ PATCH_VERSION } ` ;
231+ /**
232+ * Reads the patch marker from the node modules if present. Validates that applied
233+ * patches are up-to-date. If not, an error will be reported with a prompt that
234+ * allows convenient clean up of node modules in case those need to be cleaned up.
235+ */
236+ async function readAndValidatePatchMarker ( ) {
237+ if ( ! shelljs . test ( '-e' , PATCH_MARKER_FILE_PATH ) ) {
238+ return { version : PATCH_VERSION , patched : { } } ;
239+ }
240+ const registry = JSON . parse ( shelljs . cat ( PATCH_MARKER_FILE_PATH ) ) ;
241+ // If the node modules are up-to-date, return the parsed patch registry.
242+ if ( registry . version === PATCH_VERSION ) {
243+ return registry ;
244+ }
245+ // Print errors that explain the current situation where patches from another
246+ // postinstall patch revision are applied in the current node modules.
247+ if ( registry . version < PATCH_VERSION ) {
248+ console . error ( chalk . red ( 'Your node modules have been patched by a previous Yarn install.' ) ) ;
249+ console . error ( chalk . red ( 'The postinstall patches have changed since then, and in order to' ) ) ;
250+ console . error ( chalk . red ( 'apply the most recent patches, your node modules need to be cleaned' ) ) ;
251+ console . error ( chalk . red ( 'up from past changes.' ) ) ;
252+ } else {
253+ console . error ( chalk . red ( 'Your node modules already have patches applied from a more recent.' ) ) ;
254+ console . error ( chalk . red ( 'revision of the components repository. In order to be able to apply' ) ) ;
255+ console . error ( chalk . red ( 'patches for the current revision, your node modules need to be' ) ) ;
256+ console . error ( chalk . red ( 'cleaned up.' ) ) ;
257+ }
258+
259+ const { cleanupModules} = await inquirer . prompt ( {
260+ name : 'cleanupModules' ,
261+ type : 'confirm' ,
262+ message : 'Clean up node modules automatically?' ,
263+ default : false
264+ } ) ;
265+
266+ if ( cleanupModules ) {
267+ // This re-runs Yarn with `--check-files` mode. The postinstall will rerun afterwards,
268+ // so we can exit with a zero exit-code here.
269+ shelljs . exec ( 'yarn --check-files --frozen-lockfile' , { cwd : projectDir } ) ;
270+ process . exit ( 0 ) ;
271+ } else {
272+ process . exit ( 1 ) ;
273+ }
205274}
0 commit comments