PageRenderTime 72ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/packages/yarnpkg-core/sources/Configuration.ts

https://github.com/yarnpkg/berry
TypeScript | 1482 lines | 1103 code | 266 blank | 113 comment | 324 complexity | f71e25ac379086c84eeca4403d4204bc MD5 | raw file
Possible License(s): BSD-3-Clause
  1. import {DEFAULT_COMPRESSION_LEVEL} from '@yarnpkg/fslib';
  2. import {Filename, PortablePath, npath, ppath, xfs} from '@yarnpkg/fslib';
  3. import {parseSyml, stringifySyml} from '@yarnpkg/parsers';
  4. import camelcase from 'camelcase';
  5. import {isCI} from 'ci-info';
  6. import {UsageError} from 'clipanion';
  7. import pLimit, {Limit} from 'p-limit';
  8. import semver from 'semver';
  9. import {PassThrough, Writable} from 'stream';
  10. import {CorePlugin} from './CorePlugin';
  11. import {Manifest} from './Manifest';
  12. import {MultiFetcher} from './MultiFetcher';
  13. import {MultiResolver} from './MultiResolver';
  14. import {Plugin, Hooks} from './Plugin';
  15. import {ProtocolResolver} from './ProtocolResolver';
  16. import {Report} from './Report';
  17. import {TelemetryManager} from './TelemetryManager';
  18. import {VirtualFetcher} from './VirtualFetcher';
  19. import {VirtualResolver} from './VirtualResolver';
  20. import {WorkspaceFetcher} from './WorkspaceFetcher';
  21. import {WorkspaceResolver} from './WorkspaceResolver';
  22. import * as folderUtils from './folderUtils';
  23. import * as formatUtils from './formatUtils';
  24. import * as miscUtils from './miscUtils';
  25. import * as nodeUtils from './nodeUtils';
  26. import * as semverUtils from './semverUtils';
  27. import * as structUtils from './structUtils';
  28. import {IdentHash, Package, Descriptor} from './types';
  29. const IGNORED_ENV_VARIABLES = new Set([
  30. // "binFolder" is the magic location where the parent process stored the
  31. // current binaries; not an actual configuration settings
  32. `binFolder`,
  33. // "version" is set by Docker:
  34. // https://github.com/nodejs/docker-node/blob/5a6a5e91999358c5b04fddd6c22a9a4eb0bf3fbf/10/alpine/Dockerfile#L51
  35. `version`,
  36. // "flags" is set by Netlify; they use it to specify the flags to send to the
  37. // CLI when running the automatic `yarn install`
  38. `flags`,
  39. // "gpg" and "profile" are used by the install.sh script:
  40. // https://classic.yarnpkg.com/install.sh
  41. `profile`,
  42. `gpg`,
  43. // "ignoreNode" is used to disable the Node version check
  44. `ignoreNode`,
  45. // "wrapOutput" was a variable used to indicate nested "yarn run" processes
  46. // back in Yarn 1.
  47. `wrapOutput`,
  48. ]);
  49. export const ENVIRONMENT_PREFIX = `yarn_`;
  50. export const DEFAULT_RC_FILENAME = `.yarnrc.yml` as Filename;
  51. export const DEFAULT_LOCK_FILENAME = `yarn.lock` as Filename;
  52. export const SECRET = `********`;
  53. export enum SettingsType {
  54. ANY = `ANY`,
  55. BOOLEAN = `BOOLEAN`,
  56. ABSOLUTE_PATH = `ABSOLUTE_PATH`,
  57. LOCATOR = `LOCATOR`,
  58. LOCATOR_LOOSE = `LOCATOR_LOOSE`,
  59. NUMBER = `NUMBER`,
  60. STRING = `STRING`,
  61. SECRET = `SECRET`,
  62. SHAPE = `SHAPE`,
  63. MAP = `MAP`,
  64. }
  65. export type FormatType = formatUtils.Type;
  66. export const FormatType = formatUtils.Type;
  67. export type BaseSettingsDefinition<T extends SettingsType = SettingsType> = {
  68. description: string,
  69. type: T,
  70. isArray?: boolean,
  71. };
  72. export type ShapeSettingsDefinition = BaseSettingsDefinition<SettingsType.SHAPE> & {
  73. properties: {[propertyName: string]: SettingsDefinition},
  74. };
  75. export type MapSettingsDefinition = BaseSettingsDefinition<SettingsType.MAP> & {
  76. valueDefinition: SettingsDefinitionNoDefault,
  77. normalizeKeys?: (key: string) => string,
  78. };
  79. export type SimpleSettingsDefinition = BaseSettingsDefinition<Exclude<SettingsType, SettingsType.SHAPE | SettingsType.MAP>> & {
  80. default: any,
  81. defaultText?: any,
  82. isNullable?: boolean,
  83. values?: Array<any>,
  84. };
  85. export type SettingsDefinitionNoDefault =
  86. | MapSettingsDefinition
  87. | ShapeSettingsDefinition
  88. | Omit<SimpleSettingsDefinition, 'default'>;
  89. export type SettingsDefinition =
  90. | MapSettingsDefinition
  91. | ShapeSettingsDefinition
  92. | SimpleSettingsDefinition;
  93. export type PluginConfiguration = {
  94. modules: Map<string, any>,
  95. plugins: Set<string>,
  96. };
  97. // General rules:
  98. //
  99. // - filenames that don't accept actual paths must end with the "Filename" suffix
  100. // prefer to use absolute paths instead, since they are automatically resolved
  101. // ex: lockfileFilename
  102. //
  103. // - folders must end with the "Folder" suffix
  104. // ex: cacheFolder, pnpVirtualFolder
  105. //
  106. // - actual paths to a file must end with the "Path" suffix
  107. // ex: pnpPath
  108. //
  109. // - options that tweaks the strictness must begin with the "allow" prefix
  110. // ex: allowInvalidChecksums
  111. //
  112. // - options that enable a feature must begin with the "enable" prefix
  113. // ex: enableEmojis, enableColors
  114. export const coreDefinitions: {[coreSettingName: string]: SettingsDefinition} = {
  115. // Not implemented for now, but since it's part of all Yarn installs we want to declare it in order to improve drop-in compatibility
  116. lastUpdateCheck: {
  117. description: `Last timestamp we checked whether new Yarn versions were available`,
  118. type: SettingsType.STRING,
  119. default: null,
  120. },
  121. // Settings related to proxying all Yarn calls to a specific executable
  122. yarnPath: {
  123. description: `Path to the local executable that must be used over the global one`,
  124. type: SettingsType.ABSOLUTE_PATH,
  125. default: null,
  126. },
  127. ignorePath: {
  128. description: `If true, the local executable will be ignored when using the global one`,
  129. type: SettingsType.BOOLEAN,
  130. default: false,
  131. },
  132. ignoreCwd: {
  133. description: `If true, the \`--cwd\` flag will be ignored`,
  134. type: SettingsType.BOOLEAN,
  135. default: false,
  136. },
  137. // Settings related to the package manager internal names
  138. cacheKeyOverride: {
  139. description: `A global cache key override; used only for test purposes`,
  140. type: SettingsType.STRING,
  141. default: null,
  142. },
  143. globalFolder: {
  144. description: `Folder where are stored the system-wide settings`,
  145. type: SettingsType.ABSOLUTE_PATH,
  146. default: folderUtils.getDefaultGlobalFolder(),
  147. },
  148. cacheFolder: {
  149. description: `Folder where the cache files must be written`,
  150. type: SettingsType.ABSOLUTE_PATH,
  151. default: `./.yarn/cache`,
  152. },
  153. compressionLevel: {
  154. description: `Zip files compression level, from 0 to 9 or mixed (a variant of 9, which stores some files uncompressed, when compression doesn't yield good results)`,
  155. type: SettingsType.NUMBER,
  156. values: [`mixed`, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
  157. default: DEFAULT_COMPRESSION_LEVEL,
  158. },
  159. virtualFolder: {
  160. description: `Folder where the virtual packages (cf doc) will be mapped on the disk (must be named $$virtual)`,
  161. type: SettingsType.ABSOLUTE_PATH,
  162. default: `./.yarn/$$virtual`,
  163. },
  164. bstatePath: {
  165. description: `Path of the file where the current state of the built packages must be stored`,
  166. type: SettingsType.ABSOLUTE_PATH,
  167. default: `./.yarn/build-state.yml`,
  168. },
  169. lockfileFilename: {
  170. description: `Name of the files where the Yarn dependency tree entries must be stored`,
  171. type: SettingsType.STRING,
  172. default: DEFAULT_LOCK_FILENAME,
  173. },
  174. installStatePath: {
  175. description: `Path of the file where the install state will be persisted`,
  176. type: SettingsType.ABSOLUTE_PATH,
  177. default: `./.yarn/install-state.gz`,
  178. },
  179. immutablePatterns: {
  180. description: `Array of glob patterns; files matching them won't be allowed to change during immutable installs`,
  181. type: SettingsType.STRING,
  182. default: [],
  183. isArray: true,
  184. },
  185. rcFilename: {
  186. description: `Name of the files where the configuration can be found`,
  187. type: SettingsType.STRING,
  188. default: getRcFilename(),
  189. },
  190. enableGlobalCache: {
  191. description: `If true, the system-wide cache folder will be used regardless of \`cache-folder\``,
  192. type: SettingsType.BOOLEAN,
  193. default: false,
  194. },
  195. enableAbsoluteVirtuals: {
  196. description: `If true, the virtual symlinks will use absolute paths if required [non portable!!]`,
  197. type: SettingsType.BOOLEAN,
  198. default: false,
  199. },
  200. // Settings related to the output style
  201. enableColors: {
  202. description: `If true, the CLI is allowed to use colors in its output`,
  203. type: SettingsType.BOOLEAN,
  204. default: formatUtils.supportsColor,
  205. defaultText: `<dynamic>`,
  206. },
  207. enableHyperlinks: {
  208. description: `If true, the CLI is allowed to use hyperlinks in its output`,
  209. type: SettingsType.BOOLEAN,
  210. default: formatUtils.supportsHyperlinks,
  211. defaultText: `<dynamic>`,
  212. },
  213. enableInlineBuilds: {
  214. description: `If true, the CLI will print the build output on the command line`,
  215. type: SettingsType.BOOLEAN,
  216. default: isCI,
  217. defaultText: `<dynamic>`,
  218. },
  219. enableProgressBars: {
  220. description: `If true, the CLI is allowed to show a progress bar for long-running events`,
  221. type: SettingsType.BOOLEAN,
  222. default: !isCI && process.stdout.isTTY && process.stdout.columns > 22,
  223. defaultText: `<dynamic>`,
  224. },
  225. enableTimers: {
  226. description: `If true, the CLI is allowed to print the time spent executing commands`,
  227. type: SettingsType.BOOLEAN,
  228. default: true,
  229. },
  230. preferAggregateCacheInfo: {
  231. description: `If true, the CLI will only print a one-line report of any cache changes`,
  232. type: SettingsType.BOOLEAN,
  233. default: isCI,
  234. },
  235. preferInteractive: {
  236. description: `If true, the CLI will automatically use the interactive mode when called from a TTY`,
  237. type: SettingsType.BOOLEAN,
  238. default: false,
  239. },
  240. preferTruncatedLines: {
  241. description: `If true, the CLI will truncate lines that would go beyond the size of the terminal`,
  242. type: SettingsType.BOOLEAN,
  243. default: false,
  244. },
  245. progressBarStyle: {
  246. description: `Which style of progress bar should be used (only when progress bars are enabled)`,
  247. type: SettingsType.STRING,
  248. default: undefined,
  249. defaultText: `<dynamic>`,
  250. },
  251. // Settings related to how packages are interpreted by default
  252. defaultLanguageName: {
  253. description: `Default language mode that should be used when a package doesn't offer any insight`,
  254. type: SettingsType.STRING,
  255. default: `node`,
  256. },
  257. defaultProtocol: {
  258. description: `Default resolution protocol used when resolving pure semver and tag ranges`,
  259. type: SettingsType.STRING,
  260. default: `npm:`,
  261. },
  262. enableTransparentWorkspaces: {
  263. description: `If false, Yarn won't automatically resolve workspace dependencies unless they use the \`workspace:\` protocol`,
  264. type: SettingsType.BOOLEAN,
  265. default: true,
  266. },
  267. // Settings related to network access
  268. enableMirror: {
  269. description: `If true, the downloaded packages will be retrieved and stored in both the local and global folders`,
  270. type: SettingsType.BOOLEAN,
  271. default: true,
  272. },
  273. enableNetwork: {
  274. description: `If false, the package manager will refuse to use the network if required to`,
  275. type: SettingsType.BOOLEAN,
  276. default: true,
  277. },
  278. httpProxy: {
  279. description: `URL of the http proxy that must be used for outgoing http requests`,
  280. type: SettingsType.STRING,
  281. default: null,
  282. },
  283. httpsProxy: {
  284. description: `URL of the http proxy that must be used for outgoing https requests`,
  285. type: SettingsType.STRING,
  286. default: null,
  287. },
  288. unsafeHttpWhitelist: {
  289. description: `List of the hostnames for which http queries are allowed (glob patterns are supported)`,
  290. type: SettingsType.STRING,
  291. default: [],
  292. isArray: true,
  293. },
  294. httpTimeout: {
  295. description: `Timeout of each http request in milliseconds`,
  296. type: SettingsType.NUMBER,
  297. default: 60000,
  298. },
  299. httpRetry: {
  300. description: `Retry times on http failure`,
  301. type: SettingsType.NUMBER,
  302. default: 3,
  303. },
  304. networkConcurrency: {
  305. description: `Maximal number of concurrent requests`,
  306. type: SettingsType.NUMBER,
  307. default: Infinity,
  308. },
  309. // Settings related to telemetry
  310. enableTelemetry: {
  311. description: `If true, telemetry will be periodically sent, following the rules in https://yarnpkg.com/advanced/telemetry`,
  312. type: SettingsType.BOOLEAN,
  313. default: !isCI,
  314. },
  315. telemetryInterval: {
  316. description: `Minimal amount of time between two telemetry uploads, in days`,
  317. type: SettingsType.NUMBER,
  318. default: 7,
  319. },
  320. telemetryUserId: {
  321. description: `If you desire to tell us which project you are, you can set this field. Completely optional and opt-in.`,
  322. type: SettingsType.STRING,
  323. default: null,
  324. },
  325. // Settings related to security
  326. enableScripts: {
  327. description: `If true, packages are allowed to have install scripts by default`,
  328. type: SettingsType.BOOLEAN,
  329. default: true,
  330. },
  331. enableImmutableCache: {
  332. description: `If true, the cache is reputed immutable and actions that would modify it will throw`,
  333. type: SettingsType.BOOLEAN,
  334. default: false,
  335. },
  336. checksumBehavior: {
  337. description: `Enumeration defining what to do when a checksum doesn't match expectations`,
  338. type: SettingsType.STRING,
  339. default: `throw`,
  340. },
  341. // Package patching - to fix incorrect definitions
  342. packageExtensions: {
  343. description: `Map of package corrections to apply on the dependency tree`,
  344. type: SettingsType.MAP,
  345. valueDefinition: {
  346. description: ``,
  347. type: SettingsType.ANY,
  348. },
  349. },
  350. };
  351. export interface MapConfigurationValue<T extends object> {
  352. get<K extends keyof T>(key: K): T[K];
  353. }
  354. export interface ConfigurationValueMap {
  355. lastUpdateCheck: string | null;
  356. yarnPath: PortablePath;
  357. ignorePath: boolean;
  358. ignoreCwd: boolean;
  359. cacheKeyOverride: string | null;
  360. globalFolder: PortablePath;
  361. cacheFolder: PortablePath;
  362. compressionLevel: `mixed` | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
  363. virtualFolder: PortablePath;
  364. bstatePath: PortablePath;
  365. lockfileFilename: Filename;
  366. installStatePath: PortablePath;
  367. immutablePatterns: Array<string>;
  368. rcFilename: Filename;
  369. enableGlobalCache: boolean;
  370. enableAbsoluteVirtuals: boolean;
  371. enableColors: boolean;
  372. enableHyperlinks: boolean;
  373. enableInlineBuilds: boolean;
  374. enableProgressBars: boolean;
  375. enableTimers: boolean;
  376. preferAggregateCacheInfo: boolean;
  377. preferInteractive: boolean;
  378. preferTruncatedLines: boolean;
  379. progressBarStyle: string | undefined;
  380. defaultLanguageName: string;
  381. defaultProtocol: string;
  382. enableTransparentWorkspaces: boolean;
  383. enableMirror: boolean;
  384. enableNetwork: boolean;
  385. httpProxy: string;
  386. httpsProxy: string;
  387. unsafeHttpWhitelist: Array<string>;
  388. httpTimeout: number;
  389. httpRetry: number;
  390. networkConcurrency: number;
  391. // Settings related to telemetry
  392. enableTelemetry: boolean;
  393. telemetryInterval: number;
  394. telemetryUserId: string | null;
  395. // Settings related to security
  396. enableScripts: boolean;
  397. enableImmutableCache: boolean;
  398. checksumBehavior: string;
  399. // Package patching - to fix incorrect definitions
  400. packageExtensions: Map<string, any>;
  401. }
  402. type SimpleDefinitionForType<T> = SimpleSettingsDefinition & {
  403. type:
  404. | (T extends boolean ? SettingsType.BOOLEAN : never)
  405. | (T extends number ? SettingsType.NUMBER : never)
  406. | (T extends PortablePath ? SettingsType.ABSOLUTE_PATH : never)
  407. | (T extends string ? SettingsType.LOCATOR | SettingsType.LOCATOR_LOOSE | SettingsType.SECRET | SettingsType.STRING : never)
  408. | SettingsType.ANY
  409. ;
  410. };
  411. type DefinitionForTypeHelper<T> = T extends Map<string, infer U>
  412. ? (MapSettingsDefinition & {valueDefinition: Omit<DefinitionForType<U>, 'default'>})
  413. : T extends MapConfigurationValue<infer U>
  414. ? (ShapeSettingsDefinition & {properties: ConfigurationDefinitionMap<U>})
  415. : SimpleDefinitionForType<T>;
  416. type DefinitionForType<T> = T extends Array<infer U>
  417. ? (DefinitionForTypeHelper<U> & {isArray: true})
  418. : (DefinitionForTypeHelper<T> & {isArray?: false});
  419. // We use this type to enforce that the types defined in the
  420. // `ConfigurationValueMap` interface match what's listed in
  421. // the `configuration` field from plugin definitions
  422. //
  423. // Note: it doesn't currently support checking enumerated types
  424. // against what's actually put in the `values` field.
  425. export type ConfigurationDefinitionMap<V = ConfigurationValueMap> = {
  426. [K in keyof V]: DefinitionForType<V[K]>;
  427. }
  428. function parseBoolean(value: unknown) {
  429. switch (value) {
  430. case `true`:
  431. case `1`:
  432. case 1:
  433. case true: {
  434. return true;
  435. } break;
  436. case `false`:
  437. case `0`:
  438. case 0:
  439. case false: {
  440. return false;
  441. } break;
  442. default: {
  443. throw new Error(`Couldn't parse "${value}" as a boolean`);
  444. } break;
  445. }
  446. }
  447. function parseValue(configuration: Configuration, path: string, value: unknown, definition: SettingsDefinition, folder: PortablePath) {
  448. if (definition.isArray) {
  449. if (!Array.isArray(value)) {
  450. return String(value).split(/,/).map(segment => {
  451. return parseSingleValue(configuration, path, segment, definition, folder);
  452. });
  453. } else {
  454. return value.map((sub, i) => parseSingleValue(configuration, `${path}[${i}]`, sub, definition, folder));
  455. }
  456. } else {
  457. if (Array.isArray(value)) {
  458. throw new Error(`Non-array configuration settings "${path}" cannot be an array`);
  459. } else {
  460. return parseSingleValue(configuration, path, value, definition, folder);
  461. }
  462. }
  463. }
  464. function parseSingleValue(configuration: Configuration, path: string, value: unknown, definition: SettingsDefinition, folder: PortablePath) {
  465. switch (definition.type) {
  466. case SettingsType.ANY:
  467. return value;
  468. case SettingsType.SHAPE:
  469. return parseShape(configuration, path, value, definition, folder);
  470. case SettingsType.MAP:
  471. return parseMap(configuration, path, value, definition, folder);
  472. }
  473. if (value === null && !definition.isNullable && definition.default !== null)
  474. throw new Error(`Non-nullable configuration settings "${path}" cannot be set to null`);
  475. if (definition.values?.includes(value))
  476. return value;
  477. const interpretValue = () => {
  478. if (definition.type === SettingsType.BOOLEAN)
  479. return parseBoolean(value);
  480. if (typeof value !== `string`)
  481. throw new Error(`Expected value (${value}) to be a string`);
  482. const valueWithReplacedVariables = miscUtils.replaceEnvVariables(value, {
  483. env: process.env,
  484. });
  485. switch (definition.type) {
  486. case SettingsType.ABSOLUTE_PATH:
  487. return ppath.resolve(folder, npath.toPortablePath(valueWithReplacedVariables));
  488. case SettingsType.LOCATOR_LOOSE:
  489. return structUtils.parseLocator(valueWithReplacedVariables, false);
  490. case SettingsType.NUMBER:
  491. return parseInt(valueWithReplacedVariables);
  492. case SettingsType.LOCATOR:
  493. return structUtils.parseLocator(valueWithReplacedVariables);
  494. default:
  495. return valueWithReplacedVariables;
  496. }
  497. };
  498. const interpreted = interpretValue();
  499. if (definition.values && !definition.values.includes(interpreted))
  500. throw new Error(`Invalid value, expected one of ${definition.values.join(`, `)}`);
  501. return interpreted;
  502. }
  503. function parseShape(configuration: Configuration, path: string, value: unknown, definition: ShapeSettingsDefinition, folder: PortablePath) {
  504. if (typeof value !== `object` || Array.isArray(value))
  505. throw new UsageError(`Object configuration settings "${path}" must be an object`);
  506. const result: Map<string, any> = getDefaultValue(configuration, definition);
  507. if (value === null)
  508. return result;
  509. for (const [propKey, propValue] of Object.entries(value)) {
  510. const subPath = `${path}.${propKey}`;
  511. const subDefinition = definition.properties[propKey];
  512. if (!subDefinition)
  513. throw new UsageError(`Unrecognized configuration settings found: ${path}.${propKey} - run "yarn config -v" to see the list of settings supported in Yarn`);
  514. result.set(propKey, parseValue(configuration, subPath, propValue, definition.properties[propKey], folder));
  515. }
  516. return result;
  517. }
  518. function parseMap(configuration: Configuration, path: string, value: unknown, definition: MapSettingsDefinition, folder: PortablePath) {
  519. const result = new Map<string, any>();
  520. if (typeof value !== `object` || Array.isArray(value))
  521. throw new UsageError(`Map configuration settings "${path}" must be an object`);
  522. if (value === null)
  523. return result;
  524. for (const [propKey, propValue] of Object.entries(value)) {
  525. const normalizedKey = definition.normalizeKeys? definition.normalizeKeys(propKey) : propKey;
  526. const subPath = `${path}['${normalizedKey}']`;
  527. // @ts-expect-error: SettingsDefinitionNoDefault has ... no default ... but
  528. // that's fine because we're guaranteed it's not undefined.
  529. const valueDefinition: SettingsDefinition = definition.valueDefinition;
  530. result.set(normalizedKey, parseValue(configuration, subPath, propValue, valueDefinition, folder));
  531. }
  532. return result;
  533. }
  534. function getDefaultValue(configuration: Configuration, definition: SettingsDefinition) {
  535. switch (definition.type) {
  536. case SettingsType.SHAPE: {
  537. const result = new Map<string, any>();
  538. for (const [propKey, propDefinition] of Object.entries(definition.properties))
  539. result.set(propKey, getDefaultValue(configuration, propDefinition));
  540. return result;
  541. } break;
  542. case SettingsType.MAP: {
  543. return new Map<string, any>();
  544. } break;
  545. case SettingsType.ABSOLUTE_PATH: {
  546. if (definition.default === null)
  547. return null;
  548. if (configuration.projectCwd === null) {
  549. if (ppath.isAbsolute(definition.default)) {
  550. return ppath.normalize(definition.default);
  551. } else if (definition.isNullable) {
  552. return null;
  553. } else {
  554. // Reached when a relative path is the default but the current
  555. // context is evaluated outside of a Yarn project
  556. return undefined;
  557. }
  558. } else {
  559. if (Array.isArray(definition.default)) {
  560. return definition.default.map((entry: string) => ppath.resolve(configuration.projectCwd!, entry as PortablePath));
  561. } else {
  562. return ppath.resolve(configuration.projectCwd, definition.default);
  563. }
  564. }
  565. } break;
  566. default: {
  567. return definition.default;
  568. } break;
  569. }
  570. }
  571. type SettingTransforms = {
  572. hideSecrets: boolean;
  573. getNativePaths: boolean;
  574. };
  575. function transformConfiguration(rawValue: unknown, definition: SettingsDefinitionNoDefault, transforms: SettingTransforms) {
  576. if (definition.type === SettingsType.SECRET && typeof rawValue === `string` && transforms.hideSecrets)
  577. return SECRET;
  578. if (definition.type === SettingsType.ABSOLUTE_PATH && typeof rawValue === `string` && transforms.getNativePaths)
  579. return npath.fromPortablePath(rawValue);
  580. if (definition.isArray && Array.isArray(rawValue)) {
  581. const newValue: Array<unknown> = [];
  582. for (const value of rawValue)
  583. newValue.push(transformConfiguration(value, definition, transforms));
  584. return newValue;
  585. }
  586. if (definition.type === SettingsType.MAP && rawValue instanceof Map) {
  587. const newValue: Map<string, unknown> = new Map();
  588. for (const [key, value] of rawValue.entries())
  589. newValue.set(key, transformConfiguration(value, definition.valueDefinition, transforms));
  590. return newValue;
  591. }
  592. if (definition.type === SettingsType.SHAPE && rawValue instanceof Map) {
  593. const newValue: Map<string, unknown> = new Map();
  594. for (const [key, value] of rawValue.entries()) {
  595. const propertyDefinition = definition.properties[key];
  596. newValue.set(key, transformConfiguration(value, propertyDefinition, transforms));
  597. }
  598. return newValue;
  599. }
  600. return rawValue;
  601. }
  602. function getEnvironmentSettings() {
  603. const environmentSettings: {[key: string]: any} = {};
  604. for (let [key, value] of Object.entries(process.env)) {
  605. key = key.toLowerCase();
  606. if (!key.startsWith(ENVIRONMENT_PREFIX))
  607. continue;
  608. key = camelcase(key.slice(ENVIRONMENT_PREFIX.length));
  609. environmentSettings[key] = value;
  610. }
  611. return environmentSettings;
  612. }
  613. function getRcFilename() {
  614. const rcKey = `${ENVIRONMENT_PREFIX}rc_filename`;
  615. for (const [key, value] of Object.entries(process.env))
  616. if (key.toLowerCase() === rcKey && typeof value === `string`)
  617. return value as Filename;
  618. return DEFAULT_RC_FILENAME as Filename;
  619. }
  620. export enum ProjectLookup {
  621. LOCKFILE,
  622. MANIFEST,
  623. NONE,
  624. }
  625. export type FindProjectOptions = {
  626. lookup?: ProjectLookup,
  627. strict?: boolean,
  628. usePath?: boolean,
  629. useRc?: boolean,
  630. };
  631. export class Configuration {
  632. public static telemetry: TelemetryManager | null = null;
  633. public startingCwd: PortablePath;
  634. public projectCwd: PortablePath | null = null;
  635. public plugins: Map<string, Plugin> = new Map();
  636. public settings: Map<string, SettingsDefinition> = new Map();
  637. public values: Map<string, any> = new Map();
  638. public sources: Map<string, string> = new Map();
  639. public invalid: Map<string, string> = new Map();
  640. public packageExtensions: Map<IdentHash, Array<{
  641. descriptor: Descriptor,
  642. changes: Set<string>,
  643. patch: (pkg: Package) => void,
  644. }>> = new Map();
  645. public limits: Map<string, Limit> = new Map();
  646. /**
  647. * Instantiate a new configuration object with the default values from the
  648. * core. You typically don't want to use this, as it will ignore the values
  649. * configured in the rc files. Instead, prefer to use `Configuration#find`.
  650. */
  651. static create(startingCwd: PortablePath, plugins?: Map<string, Plugin>): Configuration;
  652. static create(startingCwd: PortablePath, projectCwd: PortablePath | null, plugins?: Map<string, Plugin>): Configuration;
  653. static create(startingCwd: PortablePath, projectCwdOrPlugins?: Map<string, Plugin> | PortablePath | null, maybePlugins?: Map<string, Plugin>) {
  654. const configuration = new Configuration(startingCwd);
  655. if (typeof projectCwdOrPlugins !== `undefined` && !(projectCwdOrPlugins instanceof Map))
  656. configuration.projectCwd = projectCwdOrPlugins;
  657. configuration.importSettings(coreDefinitions);
  658. const plugins = typeof maybePlugins !== `undefined`
  659. ? maybePlugins
  660. : projectCwdOrPlugins instanceof Map
  661. ? projectCwdOrPlugins
  662. : new Map();
  663. for (const [name, plugin] of plugins)
  664. configuration.activatePlugin(name, plugin);
  665. return configuration;
  666. }
  667. /**
  668. * Instantiate a new configuration object exposing the configuration obtained
  669. * from reading the various rc files and the environment settings.
  670. *
  671. * The `pluginConfiguration` parameter is expected to indicate:
  672. *
  673. * 1. which modules should be made available to plugins when they require a
  674. * package (this is the dynamic linking part - for example we want all the
  675. * plugins to use the exact same version of @yarnpkg/core, which also is the
  676. * version used by the running Yarn instance).
  677. *
  678. * 2. which of those modules are actually plugins that need to be injected
  679. * within the configuration.
  680. *
  681. * Note that some extra plugins will be automatically added based on the
  682. * content of the rc files - with the rc plugins taking precedence over
  683. * the other ones.
  684. *
  685. * One particularity: the plugin initialization order is quite strict, with
  686. * plugins listed in /foo/bar/.yarnrc.yml taking precedence over plugins
  687. * listed in /foo/.yarnrc.yml and /.yarnrc.yml. Additionally, while plugins
  688. * can depend on one another, they can only depend on plugins that have been
  689. * instantiated before them (so a plugin listed in /foo/.yarnrc.yml can
  690. * depend on another one listed on /foo/bar/.yarnrc.yml, but not the other
  691. * way around).
  692. */
  693. static async find(startingCwd: PortablePath, pluginConfiguration: PluginConfiguration | null, {lookup = ProjectLookup.LOCKFILE, strict = true, usePath = false, useRc = true}: FindProjectOptions = {}) {
  694. const environmentSettings = getEnvironmentSettings();
  695. delete environmentSettings.rcFilename;
  696. const rcFiles = await Configuration.findRcFiles(startingCwd);
  697. const homeRcFile = await Configuration.findHomeRcFile();
  698. // First we will parse the `yarn-path` settings. Doing this now allows us
  699. // to not have to load the plugins if there's a `yarn-path` configured.
  700. type CoreKeys = keyof typeof coreDefinitions;
  701. type CoreFields = {[key in CoreKeys]: any};
  702. const pickCoreFields = ({ignoreCwd, yarnPath, ignorePath, lockfileFilename}: CoreFields) => ({ignoreCwd, yarnPath, ignorePath, lockfileFilename});
  703. const excludeCoreFields = ({ignoreCwd, yarnPath, ignorePath, lockfileFilename, ...rest}: CoreFields) => rest;
  704. const configuration = new Configuration(startingCwd);
  705. configuration.importSettings(pickCoreFields(coreDefinitions));
  706. configuration.useWithSource(`<environment>`, pickCoreFields(environmentSettings), startingCwd, {strict: false});
  707. for (const {path, cwd, data} of rcFiles)
  708. configuration.useWithSource(path, pickCoreFields(data), cwd, {strict: false});
  709. if (homeRcFile)
  710. configuration.useWithSource(homeRcFile.path, pickCoreFields(homeRcFile.data), homeRcFile.cwd, {strict: false});
  711. if (usePath) {
  712. const yarnPath = configuration.get(`yarnPath`);
  713. const ignorePath = configuration.get(`ignorePath`);
  714. if (yarnPath !== null && !ignorePath) {
  715. return configuration;
  716. }
  717. }
  718. // We need to know the project root before being able to truly instantiate
  719. // our configuration, and to know that we need to know the lockfile name
  720. const lockfileFilename = configuration.get(`lockfileFilename`);
  721. let projectCwd: PortablePath | null;
  722. switch (lookup) {
  723. case ProjectLookup.LOCKFILE: {
  724. projectCwd = await Configuration.findProjectCwd(startingCwd, lockfileFilename);
  725. } break;
  726. case ProjectLookup.MANIFEST: {
  727. projectCwd = await Configuration.findProjectCwd(startingCwd, null);
  728. } break;
  729. case ProjectLookup.NONE: {
  730. if (xfs.existsSync(ppath.join(startingCwd, `package.json` as Filename))) {
  731. projectCwd = ppath.resolve(startingCwd);
  732. } else {
  733. projectCwd = null;
  734. }
  735. } break;
  736. }
  737. // Great! We now have enough information to really start to setup the
  738. // core configuration object.
  739. configuration.startingCwd = startingCwd;
  740. configuration.projectCwd = projectCwd;
  741. configuration.importSettings(excludeCoreFields(coreDefinitions));
  742. // Now that the configuration object is almost ready, we need to load all
  743. // the configured plugins
  744. const plugins = new Map<string, Plugin>([
  745. [`@@core`, CorePlugin],
  746. ]);
  747. const interop =
  748. (obj: any) => obj.__esModule
  749. ? obj.default
  750. : obj;
  751. if (pluginConfiguration !== null) {
  752. for (const request of pluginConfiguration.plugins.keys())
  753. plugins.set(request, interop(pluginConfiguration.modules.get(request)));
  754. const requireEntries = new Map();
  755. for (const request of nodeUtils.builtinModules())
  756. requireEntries.set(request, () => nodeUtils.dynamicRequire(request));
  757. for (const [request, embedModule] of pluginConfiguration.modules)
  758. requireEntries.set(request, () => embedModule);
  759. const dynamicPlugins = new Set();
  760. const getDefault = (object: any) => {
  761. return object.default || object;
  762. };
  763. const importPlugin = (pluginPath: PortablePath, source: string) => {
  764. const {factory, name} = nodeUtils.dynamicRequire(npath.fromPortablePath(pluginPath));
  765. // Prevent plugin redefinition so that the ones declared deeper in the
  766. // filesystem always have precedence over the ones below.
  767. if (dynamicPlugins.has(name))
  768. return;
  769. const pluginRequireEntries = new Map(requireEntries);
  770. const pluginRequire = (request: string) => {
  771. if (pluginRequireEntries.has(request)) {
  772. return pluginRequireEntries.get(request)();
  773. } else {
  774. throw new UsageError(`This plugin cannot access the package referenced via ${request} which is neither a builtin, nor an exposed entry`);
  775. }
  776. };
  777. const plugin = miscUtils.prettifySyncErrors(() => {
  778. return getDefault(factory(pluginRequire));
  779. }, message => {
  780. return `${message} (when initializing ${name}, defined in ${source})`;
  781. });
  782. requireEntries.set(name, () => plugin);
  783. dynamicPlugins.add(name);
  784. plugins.set(name, plugin);
  785. };
  786. if (environmentSettings.plugins) {
  787. for (const userProvidedPath of environmentSettings.plugins.split(`;`)) {
  788. const pluginPath = ppath.resolve(startingCwd, npath.toPortablePath(userProvidedPath));
  789. importPlugin(pluginPath, `<environment>`);
  790. }
  791. }
  792. for (const {path, cwd, data} of rcFiles) {
  793. if (!useRc)
  794. continue;
  795. if (!Array.isArray(data.plugins))
  796. continue;
  797. for (const userPluginEntry of data.plugins) {
  798. const userProvidedPath = typeof userPluginEntry !== `string`
  799. ? userPluginEntry.path
  800. : userPluginEntry;
  801. const pluginPath = ppath.resolve(cwd, npath.toPortablePath(userProvidedPath));
  802. importPlugin(pluginPath, path);
  803. }
  804. }
  805. }
  806. for (const [name, plugin] of plugins)
  807. configuration.activatePlugin(name, plugin);
  808. configuration.useWithSource(`<environment>`, excludeCoreFields(environmentSettings), startingCwd, {strict});
  809. for (const {path, cwd, data} of rcFiles)
  810. configuration.useWithSource(path, excludeCoreFields(data), cwd, {strict});
  811. // The home configuration is never strict because it improves support for
  812. // multiple projects using different Yarn versions on the same machine
  813. if (homeRcFile)
  814. configuration.useWithSource(homeRcFile.path, excludeCoreFields(homeRcFile.data), homeRcFile.cwd, {strict: false});
  815. if (configuration.get(`enableGlobalCache`)) {
  816. configuration.values.set(`cacheFolder`, `${configuration.get(`globalFolder`)}/cache`);
  817. configuration.sources.set(`cacheFolder`, `<internal>`);
  818. }
  819. await configuration.refreshPackageExtensions();
  820. return configuration;
  821. }
  822. static async findRcFiles(startingCwd: PortablePath) {
  823. const rcFilename = getRcFilename();
  824. const rcFiles = [];
  825. let nextCwd = startingCwd;
  826. let currentCwd = null;
  827. while (nextCwd !== currentCwd) {
  828. currentCwd = nextCwd;
  829. const rcPath = ppath.join(currentCwd, rcFilename as PortablePath);
  830. if (xfs.existsSync(rcPath)) {
  831. const content = await xfs.readFilePromise(rcPath, `utf8`);
  832. let data;
  833. try {
  834. data = parseSyml(content) as any;
  835. } catch (error) {
  836. let tip = ``;
  837. if (content.match(/^\s+(?!-)[^:]+\s+\S+/m))
  838. tip = ` (in particular, make sure you list the colons after each key name)`;
  839. throw new UsageError(`Parse error when loading ${rcPath}; please check it's proper Yaml${tip}`);
  840. }
  841. rcFiles.push({path: rcPath, cwd: currentCwd, data});
  842. }
  843. nextCwd = ppath.dirname(currentCwd);
  844. }
  845. return rcFiles;
  846. }
  847. static async findHomeRcFile() {
  848. const rcFilename = getRcFilename();
  849. const homeFolder = folderUtils.getHomeFolder();
  850. const homeRcFilePath = ppath.join(homeFolder, rcFilename);
  851. if (xfs.existsSync(homeRcFilePath)) {
  852. const content = await xfs.readFilePromise(homeRcFilePath, `utf8`);
  853. const data = parseSyml(content) as any;
  854. return {path: homeRcFilePath, cwd: homeFolder, data};
  855. }
  856. return null;
  857. }
  858. static async findProjectCwd(startingCwd: PortablePath, lockfileFilename: Filename | null) {
  859. let projectCwd = null;
  860. let nextCwd = startingCwd;
  861. let currentCwd = null;
  862. while (nextCwd !== currentCwd) {
  863. currentCwd = nextCwd;
  864. if (xfs.existsSync(ppath.join(currentCwd, `package.json` as Filename)))
  865. projectCwd = currentCwd;
  866. if (lockfileFilename !== null) {
  867. if (xfs.existsSync(ppath.join(currentCwd, lockfileFilename))) {
  868. projectCwd = currentCwd;
  869. break;
  870. }
  871. } else {
  872. if (projectCwd !== null) {
  873. break;
  874. }
  875. }
  876. nextCwd = ppath.dirname(currentCwd);
  877. }
  878. return projectCwd;
  879. }
  880. static async updateConfiguration(cwd: PortablePath, patch: {[key: string]: ((current: unknown) => unknown) | {} | undefined} | ((current: {[key: string]: unknown}) => {[key: string]: unknown})) {
  881. const rcFilename = getRcFilename();
  882. const configurationPath = ppath.join(cwd, rcFilename as PortablePath);
  883. const current = xfs.existsSync(configurationPath)
  884. ? parseSyml(await xfs.readFilePromise(configurationPath, `utf8`)) as any
  885. : {};
  886. let patched = false;
  887. let replacement: {[key: string]: unknown};
  888. if (typeof patch === `function`) {
  889. try {
  890. replacement = patch(current);
  891. } catch {
  892. replacement = patch({});
  893. }
  894. if (replacement === current) {
  895. return;
  896. }
  897. } else {
  898. replacement = current;
  899. for (const key of Object.keys(patch)) {
  900. const currentValue = current[key];
  901. const patchField = patch[key];
  902. let nextValue: unknown;
  903. if (typeof patchField === `function`) {
  904. try {
  905. nextValue = patchField(currentValue);
  906. } catch {
  907. nextValue = patchField(undefined);
  908. }
  909. } else {
  910. nextValue = patchField;
  911. }
  912. if (currentValue === nextValue)
  913. continue;
  914. replacement[key] = nextValue;
  915. patched = true;
  916. }
  917. if (!patched) {
  918. return;
  919. }
  920. }
  921. await xfs.changeFilePromise(configurationPath, stringifySyml(replacement), {
  922. automaticNewlines: true,
  923. });
  924. }
  925. static async updateHomeConfiguration(patch: {[key: string]: ((current: unknown) => unknown) | {} | undefined} | ((current: {[key: string]: unknown}) => {[key: string]: unknown})) {
  926. const homeFolder = folderUtils.getHomeFolder();
  927. return await Configuration.updateConfiguration(homeFolder, patch);
  928. }
  929. private constructor(startingCwd: PortablePath) {
  930. this.startingCwd = startingCwd;
  931. }
  932. activatePlugin(name: string, plugin: Plugin) {
  933. this.plugins.set(name, plugin);
  934. if (typeof plugin.configuration !== `undefined`) {
  935. this.importSettings(plugin.configuration);
  936. }
  937. }
  938. private importSettings(definitions: {[name: string]: SettingsDefinition | undefined}) {
  939. for (const [name, definition] of Object.entries(definitions)) {
  940. if (definition == null)
  941. continue;
  942. if (this.settings.has(name))
  943. throw new Error(`Cannot redefine settings "${name}"`);
  944. this.settings.set(name, definition);
  945. this.values.set(name, getDefaultValue(this, definition));
  946. }
  947. }
  948. useWithSource(source: string, data: {[key: string]: unknown}, folder: PortablePath, opts?: {strict?: boolean, overwrite?: boolean}) {
  949. try {
  950. this.use(source, data, folder, opts);
  951. } catch (error) {
  952. error.message += ` (in ${source})`;
  953. throw error;
  954. }
  955. }
  956. use(source: string, data: {[key: string]: unknown}, folder: PortablePath, {strict = true, overwrite = false}: {strict?: boolean, overwrite?: boolean} = {}) {
  957. for (const key of Object.keys(data)) {
  958. const value = data[key];
  959. if (typeof value === `undefined`)
  960. continue;
  961. // The plugins have already been loaded at this point
  962. if (key === `plugins`)
  963. continue;
  964. // Some environment variables should be ignored when applying the configuration
  965. if (source === `<environment>` && IGNORED_ENV_VARIABLES.has(key))
  966. continue;
  967. // It wouldn't make much sense, would it?
  968. if (key === `rcFilename`)
  969. throw new UsageError(`The rcFilename settings can only be set via ${`${ENVIRONMENT_PREFIX}RC_FILENAME`.toUpperCase()}, not via a rc file`);
  970. const definition = this.settings.get(key);
  971. if (!definition) {
  972. if (strict) {
  973. throw new UsageError(`Unrecognized or legacy configuration settings found: ${key} - run "yarn config -v" to see the list of settings supported in Yarn`);
  974. } else {
  975. this.invalid.set(key, source);
  976. continue;
  977. }
  978. }
  979. if (this.sources.has(key) && !(overwrite || definition.type === SettingsType.MAP))
  980. continue;
  981. let parsed;
  982. try {
  983. parsed = parseValue(this, key, data[key], definition, folder);
  984. } catch (error) {
  985. error.message += ` in ${source}`;
  986. throw error;
  987. }
  988. if (definition.type === SettingsType.MAP) {
  989. const previousValue = this.values.get(key) as Map<string, any>;
  990. this.values.set(key, new Map(overwrite
  991. ? [...previousValue, ...parsed as Map<string, any>]
  992. : [...parsed as Map<string, any>, ...previousValue]
  993. ));
  994. this.sources.set(key, `${this.sources.get(key)}, ${source}`);
  995. } else {
  996. this.values.set(key, parsed);
  997. this.sources.set(key, source);
  998. }
  999. }
  1000. }
  1001. get<K extends keyof ConfigurationValueMap>(key: K): ConfigurationValueMap[K];
  1002. /** @deprecated pass in a known configuration key instead */
  1003. get<T>(key: string): T;
  1004. /** @note Type will change to unknown in a future major version */
  1005. get(key: string): any;
  1006. get(key: string) {
  1007. if (!this.values.has(key))
  1008. throw new Error(`Invalid configuration key "${key}"`);
  1009. return this.values.get(key);
  1010. }
  1011. getSpecial<T = any>(key: string, {hideSecrets = false, getNativePaths = false}: Partial<SettingTransforms>) {
  1012. const rawValue = this.get(key);
  1013. const definition = this.settings.get(key);
  1014. if (typeof definition === `undefined`)
  1015. throw new UsageError(`Couldn't find a configuration settings named "${key}"`);
  1016. return transformConfiguration(rawValue, definition, {
  1017. hideSecrets,
  1018. getNativePaths,
  1019. }) as T;
  1020. }
  1021. getSubprocessStreams(logFile: PortablePath, {header, prefix, report}: {header?: string, prefix: string, report: Report}) {
  1022. let stdout: Writable;
  1023. let stderr: Writable;
  1024. const logStream = xfs.createWriteStream(logFile);
  1025. if (this.get(`enableInlineBuilds`)) {
  1026. const stdoutLineReporter = report.createStreamReporter(`${prefix} ${formatUtils.pretty(this, `STDOUT`, `green`)}`);
  1027. const stderrLineReporter = report.createStreamReporter(`${prefix} ${formatUtils.pretty(this, `STDERR`, `red`)}`);
  1028. stdout = new PassThrough();
  1029. stdout.pipe(stdoutLineReporter);
  1030. stdout.pipe(logStream);
  1031. stderr = new PassThrough();
  1032. stderr.pipe(stderrLineReporter);
  1033. stderr.pipe(logStream);
  1034. } else {
  1035. stdout = logStream;
  1036. stderr = logStream;
  1037. if (typeof header !== `undefined`) {
  1038. stdout.write(`${header}\n`);
  1039. }
  1040. }
  1041. return {stdout, stderr};
  1042. }
  1043. makeResolver() {
  1044. const pluginResolvers = [];
  1045. for (const plugin of this.plugins.values())
  1046. for (const resolver of plugin.resolvers || [])
  1047. pluginResolvers.push(new resolver());
  1048. return new MultiResolver([
  1049. new VirtualResolver(),
  1050. new WorkspaceResolver(),
  1051. new ProtocolResolver(),
  1052. ...pluginResolvers,
  1053. ]);
  1054. }
  1055. makeFetcher() {
  1056. const pluginFetchers = [];
  1057. for (const plugin of this.plugins.values())
  1058. for (const fetcher of plugin.fetchers || [])
  1059. pluginFetchers.push(new fetcher());
  1060. return new MultiFetcher([
  1061. new VirtualFetcher(),
  1062. new WorkspaceFetcher(),
  1063. ...pluginFetchers,
  1064. ]);
  1065. }
  1066. getLinkers() {
  1067. const linkers = [];
  1068. for (const plugin of this.plugins.values())
  1069. for (const linker of plugin.linkers || [])
  1070. linkers.push(new linker());
  1071. return linkers;
  1072. }
  1073. async refreshPackageExtensions() {
  1074. this.packageExtensions = new Map();
  1075. const packageExtensions = this.packageExtensions;
  1076. const registerPackageExtension = (descriptor: Descriptor, extensionData: any) => {
  1077. if (!semver.validRange(descriptor.range))
  1078. throw new Error(`Only semver ranges are allowed as keys for the lockfileExtensions setting`);
  1079. const extension = new Manifest();
  1080. extension.load(extensionData);
  1081. miscUtils.getArrayWithDefault(packageExtensions, descriptor.identHash).push({
  1082. descriptor,
  1083. changes: new Set([
  1084. ...[
  1085. ...extension.dependencies.values(),
  1086. ...extension.peerDependencies.values(),
  1087. ].map(descriptor => {
  1088. return structUtils.stringifyIdent(descriptor);
  1089. }),
  1090. ...extension.dependenciesMeta.keys(),
  1091. ...extension.peerDependenciesMeta.keys(),
  1092. ]),
  1093. patch: pkg => {
  1094. pkg.dependencies = new Map([...pkg.dependencies, ...extension.dependencies]);
  1095. pkg.peerDependencies = new Map([...pkg.peerDependencies, ...extension.peerDependencies]);
  1096. pkg.dependenciesMeta = new Map([...pkg.dependenciesMeta, ...extension.dependenciesMeta]);
  1097. pkg.peerDependenciesMeta = new Map([...pkg.peerDependenciesMeta, ...extension.peerDependenciesMeta]);
  1098. },
  1099. });
  1100. };
  1101. for (const [descriptorString, extensionData] of this.get(`packageExtensions`))
  1102. registerPackageExtension(structUtils.parseDescriptor(descriptorString, true), extensionData);
  1103. await this.triggerHook(hooks => {
  1104. return hooks.registerPackageExtensions;
  1105. }, this, registerPackageExtension);
  1106. }
  1107. normalizePackage(original: Package) {
  1108. const pkg = structUtils.copyPackage(original);
  1109. // We use the extensions to define additional dependencies that weren't
  1110. // properly listed in the original package definition
  1111. if (this.packageExtensions == null)
  1112. throw new Error(`refreshPackageExtensions has to be called before normalizing packages`);
  1113. const extensionList = this.packageExtensions.get(original.identHash);
  1114. if (typeof extensionList !== `undefined`) {
  1115. const version = original.version;
  1116. if (version !== null) {
  1117. const extensionEntry = extensionList.find(({descriptor}) => {
  1118. return semverUtils.satisfiesWithPrereleases(version, descriptor.range);
  1119. });
  1120. if (typeof extensionEntry !== `undefined`) {
  1121. extensionEntry.patch(pkg);
  1122. }
  1123. }
  1124. }
  1125. // We also add implicit optional @types peer dependencies for each peer
  1126. // dependency. This is for compatibility reason, as many existing packages
  1127. // forget to define their @types/react optional peer dependency when they
  1128. // peer-depend on react.
  1129. const getTypesName = (descriptor: Descriptor) => {
  1130. return descriptor.scope
  1131. ? `${descriptor.scope}__${descriptor.name}`
  1132. : `${descriptor.name}`;
  1133. };
  1134. for (const descriptor of pkg.peerDependencies.values()) {
  1135. if (descriptor.scope === `@types`)
  1136. continue;
  1137. const typesName = getTypesName(descriptor);
  1138. const typesIdent = structUtils.makeIdent(`types`, typesName);
  1139. if (pkg.peerDependencies.has(typesIdent.identHash) || pkg.peerDependenciesMeta.has(typesIdent.identHash))
  1140. continue;
  1141. pkg.peerDependenciesMeta.set(structUtils.stringifyIdent(typesIdent), {
  1142. optional: true,
  1143. });
  1144. }
  1145. // I don't like implicit dependencies, but package authors are reluctant to
  1146. // use optional peer dependencies because they would print warnings in older
  1147. // npm releases.
  1148. for (const identString of pkg.peerDependenciesMeta.keys()) {
  1149. const ident = structUtils.parseIdent(identString);
  1150. if (!pkg.peerDependencies.has(ident.identHash)) {
  1151. pkg.peerDependencies.set(ident.identHash, structUtils.makeDescriptor(ident, `*`));
  1152. }
  1153. }
  1154. // We sort the dependencies so that further iterations always occur in the
  1155. // same order, regardless how the various registries formatted their output
  1156. pkg.dependencies = new Map(miscUtils.sortMap(pkg.dependencies, ([, descriptor]) => structUtils.stringifyDescriptor(descriptor)));
  1157. pkg.peerDependencies = new Map(miscUtils.sortMap(pkg.peerDependencies, ([, descriptor]) => structUtils.stringifyDescriptor(descriptor)));
  1158. return pkg;
  1159. }
  1160. getLimit(key: string) {
  1161. return miscUtils.getFactoryWithDefault(this.limits, key, () => {
  1162. return pLimit(this.get<number>(key));
  1163. });
  1164. }
  1165. async triggerHook<U extends Array<any>, V, HooksDefinition = Hooks>(get: (hooks: HooksDefinition) => ((...args: U) => V) | undefined, ...args: U): Promise<void> {
  1166. for (const plugin of this.plugins.values()) {
  1167. const hooks = plugin.hooks as HooksDefinition;
  1168. if (!hooks)
  1169. continue;
  1170. const hook = get(hooks);
  1171. if (!hook)
  1172. continue;
  1173. await hook(...args);
  1174. }
  1175. }
  1176. async triggerMultipleHooks<U extends Array<any>, V, HooksDefinition = Hooks>(get: (hooks: HooksDefinition) => ((...args: U) => V) | undefined, argsList: Array<U>): Promise<void> {
  1177. for (const args of argsList) {
  1178. await this.triggerHook(get, ...args);
  1179. }
  1180. }
  1181. async reduceHook<U extends Array<any>, V, HooksDefinition = Hooks>(get: (hooks: HooksDefinition) => ((reduced: V, ...args: U) => Promise<V>) | undefined, initialValue: V, ...args: U): Promise<V> {
  1182. let value = initialValue;
  1183. for (const plugin of this.plugins.values()) {
  1184. const hooks = plugin.hooks as HooksDefinition;
  1185. if (!hooks)
  1186. continue;
  1187. const hook = get(hooks);
  1188. if (!hook)
  1189. continue;
  1190. value = await hook(value, ...args);
  1191. }
  1192. return value;
  1193. }
  1194. async firstHook<U extends Array<any>, V, HooksDefinition = Hooks>(get: (hooks: HooksDefinition) => ((...args: U) => Promise<V>) | undefined, ...args: U): Promise<Exclude<V, void> | null> {
  1195. for (const plugin of this.plugins.values()) {
  1196. const hooks = plugin.hooks as HooksDefinition;
  1197. if (!hooks)
  1198. continue;
  1199. const hook = get(hooks);
  1200. if (!hook)
  1201. continue;
  1202. const ret = await hook(...args);
  1203. if (typeof ret !== `undefined`) {
  1204. // @ts-expect-error
  1205. return ret;
  1206. }
  1207. }
  1208. return null;
  1209. }
  1210. /**
  1211. * @deprecated Prefer using formatUtils.pretty instead, which is type-safe
  1212. */
  1213. format(value: string, formatType: formatUtils.Type | string): string {
  1214. return formatUtils.pretty(this, value, formatType);
  1215. }
  1216. }