-
Notifications
You must be signed in to change notification settings - Fork 117
Expand file tree
/
Copy pathpreset.ts
More file actions
220 lines (195 loc) · 6.8 KB
/
preset.ts
File metadata and controls
220 lines (195 loc) · 6.8 KB
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
import { info } from "@rnx-kit/console";
import { readPackage } from "@rnx-kit/tools-node/package";
import type { Capability } from "@rnx-kit/types-kit-config";
import { spawn } from "node:child_process";
import * as nodefs from "node:fs";
import { findPackageJSON } from "node:module";
import { pathToFileURL } from "node:url";
import semverCoerce from "semver/functions/coerce.js";
import semverGreater from "semver/functions/gt.js";
import semverSatisfies from "semver/functions/satisfies.js";
import semverValidRange from "semver/ranges/valid.js";
import { gatherRequirements } from "./dependencies.ts";
import { preset as reactNativePreset } from "./presets/microsoft/react-native.ts";
import type { AlignDepsOptions, Options, Preset } from "./types.ts";
type Resolution = {
devPreset: Preset;
prodPreset: Preset;
capabilities: Capability[];
};
const notifyLatestVersion = (() => {
const cache = new Set<string>();
return (preset: string, fs = nodefs): void => {
if (cache.has(preset)) {
return;
}
cache.add(preset);
const manifestPath = findPackageJSON(pathToFileURL(preset));
if (!manifestPath) {
return;
}
const { name, version } = readPackage(manifestPath, fs);
const shell = process.platform === "win32";
const opts = { shell, windowsVerbatimArguments: true };
spawn("npm", ["view", name, "version"], opts).stdout.on("data", (data) => {
const latestVersion = data.toString().trim();
if (semverGreater(latestVersion, version)) {
info(
`A newer version of '${name}' was found: ${latestVersion} (current: ${version})`
);
}
});
};
})();
function loadPreset(
preset: string,
projectRoot: string,
resolve = require.resolve
): Preset {
switch (preset) {
case "microsoft/react-native":
return reactNativePreset;
default: {
const spec = resolve(preset, { paths: [projectRoot] });
notifyLatestVersion(spec);
return require(spec);
}
}
}
export function ensurePreset(preset: Preset, requirements: string[]): void {
if (Object.keys(preset).length === 0) {
throw new Error(
`No profiles could satisfy requirements: ${requirements.join(", ")}`
);
}
}
export function parseRequirements(requirements: string[]): [string, string][] {
return requirements.map((req) => {
const index = req.lastIndexOf("@");
if (index <= 0) {
throw new Error(`Invalid requirement: ${req}`);
}
const name = req.substring(0, index);
const version = req.substring(index + 1);
if (!version || !semverValidRange(version)) {
throw new Error(`Invalid version range in requirement: ${req}`);
}
return [name, version];
});
}
/**
* Filters out any profiles that do not satisfy the specified requirements.
* @param preset The preset to filter
* @param requirements The requirements that a profile must satisfy
* @returns Preset with only profiles that satisfy the requirements
*/
export function filterPreset(preset: Preset, requirements: string[]): Preset {
const filteredPreset: Preset = {};
const includePrerelease = { includePrerelease: true };
const reqs = parseRequirements(requirements);
for (const [profileName, profile] of Object.entries(preset)) {
const packages = Object.values(profile);
const satisfiesRequirements = reqs.every(([pkgName, pkgVersion]) => {
// User provided capabilities can resolve to the same package (e.g. core
// vs core-microsoft). We will only look at the first capability to avoid
// unexpected behaviour, e.g. due to extensions declaring an older version
// of a package that is also declared in the built-in preset.
const pkg = packages.find((pkg) => pkg.name === pkgName);
if (!pkg || !("version" in pkg)) {
return false;
}
const coercedVersion = semverCoerce(pkg.version);
if (!coercedVersion) {
throw new Error(
`Invalid version number in '${profileName}': ${pkg.name}@${pkg.version}`
);
}
return semverSatisfies(coercedVersion, pkgVersion, includePrerelease);
});
if (satisfiesRequirements) {
filteredPreset[profileName] = profile;
}
}
return filteredPreset;
}
/**
* Loads and merges specified presets.
*
* The order of presets is significant. The profiles from each preset are merged
* when the names overlap. If there are overlaps within the profiles, i.e. when
* multiple profiles declare the same capability, the last profile wins. This
* allows users to both extend and override profiles as needed.
*
* @param presets The presets to load and merge
* @param projectRoot The project root from which presets should be resolved
* @returns Merged preset
*/
export function mergePresets(
presets: string[],
projectRoot: string,
resolve = require.resolve
): Preset {
const mergedPreset: Preset = {};
for (const presetName of presets) {
const preset = loadPreset(presetName, projectRoot, resolve);
for (const [profileName, profile] of Object.entries(preset)) {
mergedPreset[profileName] = {
...mergedPreset[profileName],
...profile,
};
}
}
return mergedPreset;
}
/**
* Loads specified presets and filters them according to the requirements. The
* list of capabilities are also gathered from transitive dependencies if
* `kitType` is `app`.
* @param config User input config
* @param projectRoot Root of the project we're currently scanniing
* @param options
* @returns The resolved presets and capabilities
*/
export function resolve(
{ kitType, alignDeps, manifest }: AlignDepsOptions,
projectRoot: string,
options: Options
): Resolution {
const { capabilities, presets, requirements } = alignDeps;
const prodRequirements = Array.isArray(requirements)
? requirements
: requirements.production;
const mergedPreset = mergePresets(presets, projectRoot);
const initialProdPreset = filterPreset(mergedPreset, prodRequirements);
ensurePreset(initialProdPreset, prodRequirements);
const devPreset = (() => {
if (kitType === "app") {
// Preset for development is unused when the package is an app.
return {};
} else if (Array.isArray(requirements)) {
return initialProdPreset;
} else {
const devRequirements = requirements.development;
const devPreset = filterPreset(mergedPreset, devRequirements);
ensurePreset(devPreset, devRequirements);
return devPreset;
}
})();
if (kitType === "app") {
const { preset: prodMergedPreset, capabilities: mergedCapabilities } =
gatherRequirements(
projectRoot,
manifest,
initialProdPreset,
prodRequirements,
capabilities,
options
);
return {
devPreset,
prodPreset: prodMergedPreset,
capabilities: mergedCapabilities,
};
}
return { devPreset, prodPreset: initialProdPreset, capabilities };
}