Skip to content

Commit fd45213

Browse files
committed
build: option to automatically retry postinstall patches
Sometimes we need to change existing postinstall patches. This requires a cleanup of the currently installed node modules, but there is no good warning/messaging. We improve this with this commit by detecting such stale patches and prompting for automatic retry with cleaned up node modulesbuild: option to automatically retry postinstall patches Sometimes we need to change existing postinstall patches. This requires a cleanup of the currently installed node modules, but there is no good warning/messaging. We improve this with this commit by detecting such stale patches and prompting for automatic retry with cleaned up node modules. Note: Please clean up your node modules after this change. This needs to still happen manually once, so that we can keep track of applied patch versions and report/prompt automatically in the future.
1 parent d15f19e commit fd45213

File tree

1 file changed

+207
-144
lines changed

1 file changed

+207
-144
lines changed

tools/postinstall/apply-patches.js

Lines changed: 207 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
const shelljs = require('shelljs');
88
const path = require('path');
99
const 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,169 @@ const projectDir = path.join(__dirname, '../..');
2325
*/
2426
const 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-
/(?!function\s+)createMetadataReexportFile\([^)]+\);/, '',
36-
'node_modules/@angular/bazel/src/ng_package/packager.js');
37-
searchAndReplace(
38-
/(?!function\s+)createTypingsReexportFile\([^)]+\);/, '',
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-
/if \((this\.options\.generateCodeForLibraries === false)/, `
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-
/(const NGC_ASSETS = \/[^(]+\()([^)]*)(\).*\/;)/, '$1$2|metadata.json$3',
69-
'node_modules/@angular/bazel/src/ngc-wrapped/index.js');
70-
searchAndReplace(
71-
/^((\s*)results = depset\(dep.angular.summaries, transitive = \[results]\))$/m,
72-
`$1#\n$2results = depset(dep.angular.metadata, transitive = [results])`,
73-
'node_modules/@angular/bazel/src/ng_module.bzl');
74-
searchAndReplace(
75-
/^((\s*)results = depset\(target.angular\.summaries if _has_target_angular_summaries\(target\) else \[]\))$/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-
/providers\["angular"]\["metadata"] = outs\.metadata/,
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(/angular_compiler_options = {/, `$&
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;
144-
}
28+
const PATCH_MARKER_FILE_PATH = path.join(
29+
projectDir, 'node_modules/_ng-comp-patch-marker.json');
30+
31+
/** Registry of applied patches. */
32+
let registry = null;
33+
34+
main();
35+
36+
async function main() {
37+
shelljs.set('-e');
38+
shelljs.cd(projectDir);
39+
40+
registry = await readAndValidatePatchMarker();
41+
42+
// Apply all patches synchronously.
43+
applyPatches();
44+
45+
// Write the patch marker file so that we don't accidentally re-apply patches
46+
// in subsequent Yarn installations.
47+
fs.writeFileSync(PATCH_MARKER_FILE_PATH, JSON.stringify(registry, null, 2));
48+
}
14549

146-
let content = fs.readFileSync(filePath, 'utf8');
147-
const patchFunctions = PATCHES_PER_FILE[filePath];
50+
function applyPatches() {
51+
// Workaround for https://github.com/angular/angular/issues/18810.
52+
shelljs.exec('ngc -p angular-tsconfig.json');
14853

149-
console.info(`Patching file ${filePath} with ${patchFunctions.length} edits..`);
150-
patchFunctions.forEach(patchFn => content = patchFn(content));
54+
// Workaround for: https://github.com/angular/angular/issues/32651. We just do not
55+
// generate re-exports for secondary entry-points. Similar to what "ng-packagr" does.
56+
searchAndReplace(
57+
/(?!function\s+)createMetadataReexportFile\([^)]+\);/, '',
58+
'node_modules/@angular/bazel/src/ng_package/packager.js');
59+
searchAndReplace(
60+
/(?!function\s+)createTypingsReexportFile\([^)]+\);/, '',
61+
'node_modules/@angular/bazel/src/ng_package/packager.js');
62+
63+
// Workaround for: https://github.com/angular/angular/pull/32650
64+
searchAndReplace(
65+
'var indexFile;', `
66+
var indexFile = files.find(f => f.endsWith('/public-api.ts'));
67+
`,
68+
'node_modules/@angular/compiler-cli/src/metadata/bundle_index_host.js');
69+
searchAndReplace(
70+
'var resolvedEntryPoint = null;', `
71+
var resolvedEntryPoint = tsFiles.find(f => f.endsWith('/public-api.ts')) || null;
72+
`,
73+
'node_modules/@angular/compiler-cli/src/ngtsc/entry_point/src/logic.js');
74+
75+
// Workaround for: https://hackmd.io/MlqFp-yrSx-0mw4rD7dnQQ?both. We only want to discard
76+
// the metadata of files in the bazel managed node modules. That way we keep the default
77+
// behavior of ngc-wrapped except for dependencies between sources of the library. This makes
78+
// the "generateCodeForLibraries" flag more accurate in the Bazel environment where previous
79+
// compilations should not be treated as external libraries. Read more about this in the document.
80+
searchAndReplace(
81+
/if \((this\.options\.generateCodeForLibraries === false)/, `
82+
const fs = require('fs');
83+
const hasFlatModuleBundle = fs.existsSync(filePath.replace('.d.ts', '.metadata.json'));
84+
if ((filePath.includes('node_modules/') || !hasFlatModuleBundle) && $1`,
85+
'node_modules/@angular/compiler-cli/src/transformers/compiler_host.js');
86+
applyPatch(path.join(__dirname, './flat_module_factory_resolution.patch'));
87+
// The three replacements below ensure that metadata files can be read by NGC and
88+
// that metadata files are collected as Bazel action inputs.
89+
searchAndReplace(
90+
/(const NGC_ASSETS = \/[^(]+\()([^)]*)(\).*\/;)/, '$1$2|metadata.json$3',
91+
'node_modules/@angular/bazel/src/ngc-wrapped/index.js');
92+
searchAndReplace(
93+
/^((\s*)results = depset\(dep.angular.summaries, transitive = \[results]\))$/m,
94+
`$1#\n$2results = depset(dep.angular.metadata, transitive = [results])`,
95+
'node_modules/@angular/bazel/src/ng_module.bzl');
96+
searchAndReplace(
97+
/^((\s*)results = depset\(target.angular\.summaries if _has_target_angular_summaries\(target\) else \[]\))$/m,
98+
`$1#\n$2results = depset(target.angular.metadata if _has_target_angular_summaries(target) else [], transitive = [results])`,
99+
'node_modules/@angular/bazel/src/ng_module.bzl');
100+
// Ensure that "metadata" of transitive dependencies can be collected.
101+
searchAndReplace(
102+
/providers\["angular"]\["metadata"] = outs\.metadata/,
103+
`$& + [m for dep in ctx.attr.deps if (hasattr(dep, "angular") and hasattr(dep.angular, "metadata")) for m in dep.angular.metadata]`,
104+
'node_modules/@angular/bazel/src/ng_module.bzl');
105+
106+
// Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1208.
107+
applyPatch(path.join(__dirname, './manifest_externs_hermeticity.patch'));
108+
109+
try {
110+
// Temporary patch pre-req for https://github.com/angular/angular/pull/36333.
111+
// Can be removed once @angular/bazel is updated here to include this patch.
112+
// try/catch needed for this the material CI tests to work in angular/repo
113+
applyPatch(path.join(__dirname, './@angular_bazel_ng_module.patch'));
114+
} catch {}
115+
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+
// Workaround for https://github.com/angular/angular/issues/33452:
125+
searchAndReplace(/angular_compiler_options = {/, `$&
126+
"strictTemplates": True,`, 'node_modules/@angular/bazel/src/ng_module.bzl');
127+
128+
// More info in https://github.com/angular/angular/pull/33786
129+
shelljs.rm('-rf', [
130+
'node_modules/rxjs/add/',
131+
'node_modules/rxjs/observable/',
132+
'node_modules/rxjs/operator/',
133+
// rxjs/operators is a public entry point that also contains files to support legacy deep import
134+
// paths, so we need to preserve index.* and package.json files that are required for module
135+
// resolution.
136+
'node_modules/rxjs/operators/!(index.*|package.json)',
137+
'node_modules/rxjs/scheduler/',
138+
'node_modules/rxjs/symbol/',
139+
'node_modules/rxjs/util/',
140+
'node_modules/rxjs/internal/Rx.d.ts',
141+
'node_modules/rxjs/AsyncSubject.*',
142+
'node_modules/rxjs/BehaviorSubject.*',
143+
'node_modules/rxjs/InnerSubscriber.*',
144+
'node_modules/rxjs/interfaces.*',
145+
'node_modules/rxjs/Notification.*',
146+
'node_modules/rxjs/Observable.*',
147+
'node_modules/rxjs/Observer.*',
148+
'node_modules/rxjs/Operator.*',
149+
'node_modules/rxjs/OuterSubscriber.*',
150+
'node_modules/rxjs/ReplaySubject.*',
151+
'node_modules/rxjs/Rx.*',
152+
'node_modules/rxjs/Scheduler.*',
153+
'node_modules/rxjs/Subject.*',
154+
'node_modules/rxjs/SubjectSubscription.*',
155+
'node_modules/rxjs/Subscriber.*',
156+
'node_modules/rxjs/Subscription.*',
157+
]);
158+
159+
// Apply all collected patches on a per-file basis. This is necessary because
160+
// multiple edits might apply to the same file, and we only want to mark a given
161+
// file as patched once all edits have been made.
162+
Object.keys(PATCHES_PER_FILE).forEach(filePath => {
163+
if (isFilePatched(filePath)) {
164+
console.info('File ' + filePath + ' is already patched. Skipping..');
165+
return;
166+
}
151167

152-
fs.writeFileSync(filePath, content, 'utf8');
153-
writePatchMarker(filePath);
154-
});
168+
let content = fs.readFileSync(filePath, 'utf8');
169+
const patchFunctions = PATCHES_PER_FILE[filePath];
170+
171+
console.info(`Patching file ${filePath} with ${patchFunctions.length} edits..`);
172+
patchFunctions.forEach(patchFn => content = patchFn(content));
173+
174+
fs.writeFileSync(filePath, content, 'utf8');
175+
captureFileAsPatched(filePath);
176+
});
177+
}
155178

156179
/**
157-
* Applies the given patch if not done already. Throws if the patch does
158-
* not apply cleanly.
180+
* Applies the given patch if not done already. Throws if the patch
181+
* does not apply cleanly.
159182
*/
160183
function 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)) {
184+
if (isFilePatched(patchFile)) {
185+
console.info('Patch: ' + patchFile + ' has been applied already. Skipping..');
169186
return;
170187
}
171188

172189
shelljs.cat(patchFile).exec('patch -p0');
173-
writePatchMarker(patchMarkerPath);
190+
captureFileAsPatched(patchFile);
174191
}
175192

176193
/**
@@ -192,14 +209,60 @@ function searchAndReplace(search, replacement, relativeFilePath) {
192209
});
193210
}
194211

212+
/** Gets a project unique id for a given file path. */
213+
function getIdForFile(filePath) {
214+
return path.relative(projectDir, filePath).replace(/\\/g, '/');
215+
}
216+
195217
/** Marks the specified file as patched. */
196-
function writePatchMarker(filePath) {
197-
new shelljs.ShellString(PATCH_VERSION).to(`${filePath}.patch_marker`);
218+
function captureFileAsPatched(filePath) {
219+
registry.patched[getIdForFile(filePath)] = true;
220+
}
221+
222+
/** Checks whether the given file is patched. */
223+
function isFilePatched(filePath) {
224+
return registry.patched[getIdForFile(filePath)] === true;
198225
}
199226

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}`;
227+
/**
228+
* Checks if the given file has been patched.
229+
*/
230+
async function readAndValidatePatchMarker() {
231+
if (!shelljs.test('-e', PATCH_MARKER_FILE_PATH)) {
232+
return {version: PATCH_VERSION, patched: {}};
233+
}
234+
const registry = JSON.parse(shelljs.cat(PATCH_MARKER_FILE_PATH));
235+
// If the node modules are up-to-date, return the parsed patch registry.
236+
if (registry.version === PATCH_VERSION) {
237+
return registry;
238+
}
239+
// Print errors that explain the current situation where patches from another
240+
// postinstall patch revision are applied in the current node modules.
241+
if (registry.version < PATCH_VERSION) {
242+
console.error(chalk.red('Your node modules have been patched by a previous Yarn install.'));
243+
console.error(chalk.red('The postinstall patches have changed since then, and in order to'));
244+
console.error(chalk.red('apply the most recent patches, your node modules need to be cleaned'));
245+
console.error(chalk.red('up from past changes.'));
246+
} else {
247+
console.error(chalk.red('Your node modules already have patches applied from a more recent.'));
248+
console.error(chalk.red('revision of the components repository. In order to be able to apply'));
249+
console.error(chalk.red('patches for the current revision, your node modules need to be'));
250+
console.error(chalk.red('cleaned up.'));
251+
}
252+
253+
const {cleanupModules} = await inquirer.prompt({
254+
name: 'cleanupModules',
255+
type: 'confirm',
256+
message: 'Clean up node modules automatically?',
257+
default: false
258+
});
259+
260+
if (cleanupModules) {
261+
// This re-runs Yarn with `--check-files` mode. The postinstall will rerun afterwards,
262+
// so we can exit with a zero exit-code here.
263+
shelljs.exec('yarn --check-files --frozen-lockfile', {cwd: projectDir});
264+
process.exit(0);
265+
} else {
266+
process.exit(1);
267+
}
205268
}

0 commit comments

Comments
 (0)