Skip to content

Commit 7c4e326

Browse files
committed
docs: Create README and doc comments for patch generator
1 parent 343ec14 commit 7c4e326

File tree

2 files changed

+91
-2
lines changed

2 files changed

+91
-2
lines changed

packages/plugin-compat/extra/PatchGenerator.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,19 @@ export abstract class PatchGenerator<S extends {id: string, range: string}> {
2525
this.patches = ppath.join(npath.toPortablePath(__dirname), this.name as Filename, `patches`);
2626
}
2727

28+
/**
29+
* Given the path to the build cache directory, populate it by saving the
30+
* "before" state of the slice into `<path>/base` and the "after" state into
31+
* `<path>/patched`.
32+
*
33+
* The build cache directory is guaranteed to exists, but the `base` and
34+
* `patched` subdirectories are not.
35+
*/
2836
protected abstract build(slice: S, path: PortablePath): Promise<void>;
37+
/**
38+
* Given a slice, return the list of package versions the generated patch
39+
* should be validated against.
40+
*/
2941
protected abstract getValidateVersions(slice: S): Promise<Array<string>>;
3042

3143
private async fetchTarball(version: string): Promise<Buffer> {
@@ -38,6 +50,10 @@ export abstract class PatchGenerator<S extends {id: string, range: string}> {
3850

3951
return Buffer.from(await response.arrayBuffer());
4052
}
53+
/**
54+
* Return the tarball for the given version of the package with caching. If
55+
* not already cached, it will be fetched from the npm registry.
56+
*/
4157
protected async getTarball(version: string): Promise<Buffer> {
4258
const path = ppath.join(this.tmp, `tarballs`, `${version}.tgz` as Filename);
4359
if (await xfs.existsPromise(path))
@@ -52,6 +68,10 @@ export abstract class PatchGenerator<S extends {id: string, range: string}> {
5268
return tarball;
5369
}
5470

71+
/**
72+
* Generate a unified diff between the `base` and `patched` sub-directories of
73+
* the given directory, with the custom semver exclusivity header.
74+
*/
5575
protected async diff(range: string, dir: PortablePath): Promise<string> {
5676
const patch = await spawn(`git`, [
5777
`diff`,
@@ -89,6 +109,10 @@ export abstract class PatchGenerator<S extends {id: string, range: string}> {
89109
});
90110
}
91111

112+
/**
113+
* Create the patch for the given slice, reusing any existing cached patch or
114+
* build if available, and write it to disk if not already cached.
115+
*/
92116
protected createPatch(slice: S): Promise<{path: PortablePath, content: string}> {
93117
return logger.section(`Create patch`, async () => {
94118
const path = ppath.join(this.patches, `patch-${slice.id}.diff` as Filename);
@@ -128,8 +152,8 @@ export abstract class PatchGenerator<S extends {id: string, range: string}> {
128152
});
129153
}
130154

131-
protected readonly envs = new Map<string, Promise<PortablePath>>();
132-
protected async prepareValidationEnv(version: string): Promise<PortablePath> {
155+
private readonly envs = new Map<string, Promise<PortablePath>>();
156+
private async prepareValidationEnv(version: string): Promise<PortablePath> {
133157
const path = ppath.join(this.tmp, `validate`, version as Filename);
134158
const [tarball] = await Promise.all([
135159
this.getTarball(version),
@@ -138,6 +162,10 @@ export abstract class PatchGenerator<S extends {id: string, range: string}> {
138162
await tgzUtils.extractArchiveTo(tarball, new CwdFS(path), {stripComponents: 1});
139163
return path;
140164
}
165+
/**
166+
* Prepare the validation environment for the specified version by extracting
167+
* the package tarball into a temporary directory.
168+
*/
141169
protected async getValidationEnv(version: string): Promise<PortablePath> {
142170
return miscUtils.getFactoryWithDefault(this.envs, version, () => this.prepareValidationEnv(version));
143171
}
@@ -158,6 +186,10 @@ export abstract class PatchGenerator<S extends {id: string, range: string}> {
158186
.catch(() => {}); // Prevent unhandled rejection - errors will be handled during validation
159187
}
160188
}
189+
/**
190+
* Validate that the given patch can be cleanly applied to the versions of the
191+
* package as selected by {@link getValidateVersions}.
192+
*/
161193
protected validatePatch(slice: S, patch: string): Promise<void> {
162194
return logger.section(`Validate patch`, async () => {
163195
const versions = await this.getValidateVersions(slice);
@@ -175,6 +207,9 @@ export abstract class PatchGenerator<S extends {id: string, range: string}> {
175207
});
176208
}
177209

210+
/**
211+
* Create, write to disk, and validate the patch for the given slice.
212+
*/
178213
protected async generatePatch(slice: S): Promise<string> {
179214
const clearBuildCache = () => xfs.removeSync(ppath.join(this.tmp, `builds`, slice.id as Filename));
180215

@@ -198,6 +233,11 @@ export abstract class PatchGenerator<S extends {id: string, range: string}> {
198233
});
199234
}
200235

236+
/**
237+
* Generate all patches and write the compressed TS bundle to the specified
238+
* path. If one or more ranges are specified, caches are not used for slices
239+
* whose range overlaps with any of the specified ranges.
240+
*/
201241
public async generateBundle(ranges: Array<string>, path: PortablePath): Promise<void> {
202242
// Start preparing validation environments immediately
203243
const controller = new AbortController();
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Compat patches generator
2+
3+
## Overview
4+
5+
This directory contains scripts and files to generate the builtin patches. Each patched package has its own sub-directory.
6+
7+
For each package, the `patches` sub-directory contains the set of builtin patches that are applied to it. Each of those patch files is also known as a patch *slice*, and is marked with a semver range of package versions that the patch applies to.
8+
9+
The `gen-<package>-patch.ts` script is used to (re-)generate those patches and update the files in `../source/patches`, which are then bundled with Yarn.
10+
11+
## Running
12+
13+
The generator scripts can be run via the package script `build:patch:<package>`.
14+
15+
As a high-level overview, the script generates each patch slice by:
16+
1. Running a package-specific build process to create the "before" and "after" states of the package
17+
2. Running `git diff` to calculate the diff between the two
18+
3. Validating the diff by trying to apply it as a patch to some versions of the package
19+
4. Saving it to `patches`
20+
21+
After all slices are generated, they are aggregated, compressed, and encoded into the `../source/patches` bundles.
22+
23+
## Caching
24+
25+
There are two layers of caching applied. First, if a patch already exists in the `patches` directory, then the diff is used instead of building and calculating a fresh diff. Second, the "before" and "after" states created during builds are saved and reused to skip future builds.
26+
27+
The cache and other temporary files used by the generator are store in `<tmp>/yarn-compat-gen-patches/`, where `<tmp>` is the system temporary directory. This directory can be changed by setting `GEN_PATCHES_BASE_DIR` environment variable to an absolute path. The caches for each package are isolated under sub-directories.
28+
29+
When running the generator scripts, one or more semver ranges can be given as additional arguments. In that case, when generating any slice whose range overlaps with the given ranges, the cached patches and builds are not reused and a fresh patch is generated.
30+
31+
On Windows, it is recommended to exclude the cache from Windows Defender as its real-time protection has a huge negative impact to performance during I/O intensive builds.
32+
33+
## Package-specific notes
34+
35+
### `fsevents`
36+
37+
The build process for `fsevents` is simply downloading and extracting the package from npm and copying patched and/or new files into the package.
38+
39+
### `resolve`
40+
41+
The build process for `resolve` is similar to that for `fsevents` -- downloading the package from npm and copying patched and/or new files into the package.
42+
43+
### `typescript`
44+
45+
For `typescript`, the build process is much heavier. We clone `yarnpkg/TypeScript` (our own fork of the TypeScript repo) from GitHub, cherry-pick commits that implement PnP, and build the TypeScript distributables.
46+
47+
As the TypeScript repo uses Volta to pin the node and npm versions used to build it, installing Volta is recommended to ensure consistent builds. If Volta is not installed, the generator script only switches the npm version by running npm via Corepack. A warning is printed in this case.
48+
49+
If you already have a local clone of `yarnpkg/TypeScript`, you can use git's alternates mechanism to allow git to find objects in the local clone, via the `GIT_ALTERNATE_OBJECT_DIRECTORIES` environment variable. This way, you can generate patches using commits in the local clone. The local clone is also used to speed up the script's cloning and fetching operations. However, do note that PRs that update the patch should only reference public commits in `yarnpkg/Typescript` or our CI checks will fail on the PR.

0 commit comments

Comments
 (0)