Skip to content

Commit b9ccd93

Browse files
authored
Core: Create DURATION settings type (#6942)
## What's the problem this PR addresses? There are a number of configurations that represent a duration of time (e.g. `httpTimeout`). Currently those use the `NUMBER` settings type with a predefined unit (e.g. milliseconds for `httpTimeout`). But: 1. Each setting uses a different unit so users need to context switch to the docs to read/write those config 2. Because YAML doesn't support arithmetic expressions, unit conversions need to be done beforehand in order to properly set those settings. ## How did you fix it? Create a new `DURATION` settings type that allows specifying the duration as a string in the following format: 1. An integer or decimal value 2. Optionally a unit among `ms`, `s`, `m`, `h`, `d`, `w` For backward compatibility, a `DURATION` is parsed to a number using the existing predefined unit for that setting. That unit is also used if one is not specified. I have only implemented units that the exactly convertible to each other* to avoid something like having to convert 1 month to an exact number of days. <sub>_(* not entirely true since 1 day is not always 24 hours)_</sub> In the future, maybe we can use a proper duration data structure to do proper date calculations. (So that, for example, setting a DURATION to 1 month actually means "1 calendar month" instead of an approximate number of seconds) ## Checklist <!--- Don't worry if you miss something, chores are automatically tested. --> <!--- This checklist exists to help you remember doing the chores when you submit a PR. --> <!--- Put an `x` in all the boxes that apply. --> - [x] I have read the [Contributing Guide](https://yarnpkg.com/advanced/contributing). <!-- See https://yarnpkg.com/advanced/contributing#preparing-your-pr-to-be-released for more details. --> <!-- Check with `yarn version check` and fix with `yarn version check -i` --> - [x] I have set the packages that need to be released for my changes to be effective. <!-- The "Testing chores" workflow validates that your PR follows our guidelines. --> <!-- If it doesn't pass, click on it to see details as to what your PR might be missing. --> - [x] I will check that all automated PR checks pass before the PR gets reviewed.
1 parent bafbef5 commit b9ccd93

File tree

7 files changed

+166
-68
lines changed

7 files changed

+166
-68
lines changed

.yarn/versions/0a4c10c5.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
releases:
2+
"@yarnpkg/cli": minor
3+
"@yarnpkg/core": minor
4+
"@yarnpkg/plugin-npm": minor
5+
6+
declined:
7+
- "@yarnpkg/plugin-catalog"
8+
- "@yarnpkg/plugin-compat"
9+
- "@yarnpkg/plugin-constraints"
10+
- "@yarnpkg/plugin-dlx"
11+
- "@yarnpkg/plugin-essentials"
12+
- "@yarnpkg/plugin-exec"
13+
- "@yarnpkg/plugin-file"
14+
- "@yarnpkg/plugin-git"
15+
- "@yarnpkg/plugin-github"
16+
- "@yarnpkg/plugin-http"
17+
- "@yarnpkg/plugin-init"
18+
- "@yarnpkg/plugin-interactive-tools"
19+
- "@yarnpkg/plugin-jsr"
20+
- "@yarnpkg/plugin-link"
21+
- "@yarnpkg/plugin-nm"
22+
- "@yarnpkg/plugin-npm-cli"
23+
- "@yarnpkg/plugin-pack"
24+
- "@yarnpkg/plugin-patch"
25+
- "@yarnpkg/plugin-pnp"
26+
- "@yarnpkg/plugin-pnpm"
27+
- "@yarnpkg/plugin-stage"
28+
- "@yarnpkg/plugin-typescript"
29+
- "@yarnpkg/plugin-version"
30+
- "@yarnpkg/plugin-workspace-tools"
31+
- "@yarnpkg/builder"
32+
- "@yarnpkg/doctor"
33+
- "@yarnpkg/extensions"
34+
- "@yarnpkg/nm"
35+
- "@yarnpkg/pnpify"
36+
- "@yarnpkg/sdks"

packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
const ONE_DAY_IN_MINUTES = 24 * 60;
2-
31
describe(`Features`, () => {
42
describe(`npmMinimalAgeGate and npmPreapprovedPackages`, () => {
53
describe(`add`, () => {
64
test(
75
`add should install the latest version allowed by the minimum release age`,
86
makeTemporaryEnv({}, {
9-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
7+
npmMinimalAgeGate: `1d`,
108
}, async ({run, source}) => {
119
await run(`add`, `release-date`);
1210
await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({
@@ -19,7 +17,7 @@ describe(`Features`, () => {
1917
test(
2018
`it should fail when trying to install exact version that is newer than the minimum release age`,
2119
makeTemporaryEnv({}, {
22-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
20+
npmMinimalAgeGate: `1d`,
2321
}, async ({run}) => {
2422
await expect(run(`add`, `release-date@1.1.1`)).rejects.toThrowError(`No candidates found`);
2523
}),
@@ -28,7 +26,7 @@ describe(`Features`, () => {
2826
test(
2927
`it should install older package versions when the minimum release age disallows the newest suitable version`,
3028
makeTemporaryEnv({}, {
31-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
29+
npmMinimalAgeGate: `1d`,
3230
}, async ({run, source}) => {
3331
await run(`add`, `release-date@^1.0.0`);
3432

@@ -42,7 +40,7 @@ describe(`Features`, () => {
4240
test(
4341
`it should install new version when excluded by a descriptor; while transitive dependencies are not excluded`,
4442
makeTemporaryEnv({}, {
45-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
43+
npmMinimalAgeGate: `1d`,
4644
npmPreapprovedPackages: [`release-date@^1.0.0`],
4745
}, async ({run, source}) => {
4846
await run(`add`, `release-date@^1.0.0`);
@@ -63,7 +61,7 @@ describe(`Features`, () => {
6361
test(
6462
`it should install new version when excluded by package ident`,
6563
makeTemporaryEnv({}, {
66-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
64+
npmMinimalAgeGate: `1d`,
6765
npmPreapprovedPackages: [`release-date`],
6866
}, async ({run, source}) => {
6967
await run(`add`, `release-date@^1.0.0`);
@@ -92,7 +90,7 @@ describe(`Features`, () => {
9290
test(
9391
`it should work with scoped packages`,
9492
makeTemporaryEnv({}, {
95-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
93+
npmMinimalAgeGate: `1d`,
9694
}, async ({run}) => {
9795
await expect(run(`add`, `@scoped/release-date@1.1.1`)).rejects.toThrowError(`No candidates found`);
9896
}),
@@ -101,7 +99,7 @@ describe(`Features`, () => {
10199
test(
102100
`it should install scoped package when excluded`,
103101
makeTemporaryEnv({}, {
104-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
102+
npmMinimalAgeGate: `1d`,
105103
npmPreapprovedPackages: [`@scoped/release-date`],
106104
}, async ({run, source}) => {
107105
await run(`add`, `@scoped/release-date@^1.0.0`);
@@ -116,7 +114,7 @@ describe(`Features`, () => {
116114
test(
117115
`it should install scoped package when excluded by scoped glob pattern`,
118116
makeTemporaryEnv({}, {
119-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
117+
npmMinimalAgeGate: `1d`,
120118
npmPreapprovedPackages: [`@scoped/*`],
121119
}, async ({run, source}) => {
122120
await run(`add`, `@scoped/release-date@^1.0.0`);
@@ -132,7 +130,7 @@ describe(`Features`, () => {
132130
`it should not install a version via add that is higher than the latest tag`,
133131
makeTemporaryEnv({
134132
}, {
135-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
133+
npmMinimalAgeGate: `1d`,
136134
}, async ({run, source}) => {
137135
await run(`add`, `@scoped/release-date`);
138136

@@ -149,7 +147,7 @@ describe(`Features`, () => {
149147
makeTemporaryEnv({
150148
dependencies: {[`release-date`]: `1.1.1`},
151149
}, {
152-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
150+
npmMinimalAgeGate: `1d`,
153151
}, async ({run}) => {
154152
await expect(run(`install`)).rejects.toThrowError(`No candidates found`);
155153
}),
@@ -160,7 +158,7 @@ describe(`Features`, () => {
160158
makeTemporaryEnv({
161159
dependencies: {[`release-date`]: `^1.0.0`},
162160
}, {
163-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
161+
npmMinimalAgeGate: `1d`,
164162
}, async ({run, source}) => {
165163
await run(`install`);
166164

@@ -176,7 +174,7 @@ describe(`Features`, () => {
176174
makeTemporaryEnv({
177175
dependencies: {[`release-date`]: `^1.0.0`},
178176
}, {
179-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
177+
npmMinimalAgeGate: `1d`,
180178
npmPreapprovedPackages: [`release-date@^1.0.0`],
181179
}, async ({run, source}) => {
182180
await run(`install`);
@@ -193,7 +191,7 @@ describe(`Features`, () => {
193191
makeTemporaryEnv({
194192
dependencies: {[`release-date`]: `^1.0.0`},
195193
}, {
196-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
194+
npmMinimalAgeGate: `1d`,
197195
npmPreapprovedPackages: [`release-*`],
198196
}, async ({run, source}) => {
199197
await run(`install`);
@@ -210,7 +208,7 @@ describe(`Features`, () => {
210208
makeTemporaryEnv({
211209
dependencies: {[`release-date`]: `^1.0.0`},
212210
}, {
213-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
211+
npmMinimalAgeGate: `1d`,
214212
npmPreapprovedPackages: [`release-date`],
215213
}, async ({run, source}) => {
216214
await run(`install`);
@@ -243,7 +241,7 @@ describe(`Features`, () => {
243241
makeTemporaryEnv({
244242
dependencies: {[`@scoped/release-date`]: `1.1.1`},
245243
}, {
246-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
244+
npmMinimalAgeGate: `1d`,
247245
}, async ({run}) => {
248246
await expect(run(`install`)).rejects.toThrowError(`No candidates found`);
249247
}),
@@ -254,7 +252,7 @@ describe(`Features`, () => {
254252
makeTemporaryEnv({
255253
dependencies: {[`@scoped/release-date`]: `^1.0.0`},
256254
}, {
257-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
255+
npmMinimalAgeGate: `1d`,
258256
npmPreapprovedPackages: [`@scoped/release-date`],
259257
}, async ({run, source}) => {
260258
await run(`install`);
@@ -271,7 +269,7 @@ describe(`Features`, () => {
271269
makeTemporaryEnv({
272270
dependencies: {[`@scoped/release-date`]: `^1.0.0`},
273271
}, {
274-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
272+
npmMinimalAgeGate: `1d`,
275273
npmPreapprovedPackages: [`@scoped/*`],
276274
}, async ({run, source}) => {
277275
await run(`install`);
@@ -288,7 +286,7 @@ describe(`Features`, () => {
288286
makeTemporaryEnv({
289287
dependencies: {[`@scoped/release-date`]: `latest`},
290288
}, {
291-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
289+
npmMinimalAgeGate: `1d`,
292290
}, async ({run, source}) => {
293291
await run(`install`);
294292

@@ -305,7 +303,7 @@ describe(`Features`, () => {
305303
makeTemporaryEnv({
306304
dependencies: {[`release-date`]: `^1.0.0`},
307305
}, {
308-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
306+
npmMinimalAgeGate: `1d`,
309307
}, async ({run, source}) => {
310308
await run(`install`);
311309
await run(`set`, `resolution`, `release-date@npm:^1.0.0`, `npm:1.0.0`);
@@ -330,7 +328,7 @@ describe(`Features`, () => {
330328
// disabling these checks for the purpose of this test
331329
pnpFallbackMode: `all`,
332330
pnpMode: `loose`,
333-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
331+
npmMinimalAgeGate: `1d`,
334332
}, async ({run, source}) => {
335333
await run(`install`);
336334
await run(`set`, `resolution`, `release-date@npm:^1.0.0`, `npm:1.0.0`);
@@ -358,7 +356,7 @@ describe(`Features`, () => {
358356
makeTemporaryEnv({
359357
dependencies: {[`@scoped/release-date`]: `^1.0.0`},
360358
}, {
361-
npmMinimalAgeGate: ONE_DAY_IN_MINUTES,
359+
npmMinimalAgeGate: `1d`,
362360
}, async ({run, source}) => {
363361
await run(`set`, `resolution`, `@scoped/release-date@npm:^1.0.0`, `npm:1.0.0`);
364362

packages/docusaurus/static/configuration/yarnrc.json

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,13 @@
272272
},
273273
"httpTimeout": {
274274
"_package": "@yarnpkg/core",
275-
"title": "Amount of time to wait in milliseconds before cancelling pending HTTP requests.",
276-
"type": "number",
277-
"default": 60000
275+
"title": "Amount of time to wait before cancelling pending HTTP requests.",
276+
"type": "mixed",
277+
"oneOf": [
278+
{ "type": "number" },
279+
{ "type": "string", "pattern": "^(\\d*\\.?\\d+)(ms|s|m|h|d|w)?$" }
280+
],
281+
"default": "1m"
278282
},
279283
"httpsCaFilePath": {
280284
"_package": "@yarnpkg/core",
@@ -481,10 +485,14 @@
481485
},
482486
"npmMinimalAgeGate": {
483487
"_package": "@yarnpkg/core",
484-
"title": "Minimum age of a package version according to the publish date on the npm registry in minutes to be considered for installation.",
488+
"title": "Minimum age of a package version according to the publish date on the npm registry to be considered for installation.",
485489
"description": "If a package version is newer than the minimal age gate, it will not be considered for installation. This can be used to reduce the likelihood of installing compromised packages.",
486-
"type": "number",
487-
"default": 0
490+
"type": "mixed",
491+
"oneOf": [
492+
{ "type": "number" },
493+
{ "type": "string", "pattern": "^(\\d*\\.?\\d+)(ms|s|m|h|d|w)?$" }
494+
],
495+
"default": "0m"
488496
},
489497
"npmPreapprovedPackages": {
490498
"_package": "@yarnpkg/core",
@@ -851,10 +859,14 @@
851859
},
852860
"telemetryInterval": {
853861
"_package": "@yarnpkg/core",
854-
"title": "Define the minimal amount of time between two telemetry events, in days.",
862+
"title": "Define the minimal amount of time between two telemetry events.",
855863
"description": "By default we only send one request per week, making it impossible for us to track your usage with a lower granularity.",
856-
"type": "number",
857-
"default": 7
864+
"type": "mixed",
865+
"oneOf": [
866+
{ "type": "number" },
867+
{ "type": "string", "pattern": "^(\\d*\\.?\\d+)(ms|s|m|h|d|w)?$" }
868+
],
869+
"default": "7d"
858870
},
859871
"telemetryUserId": {
860872
"_package": "@yarnpkg/core",

packages/plugin-npm/sources/index.ts

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import {Plugin, SettingsType, miscUtils, Configuration, Ident} from '@yarnpkg/core';
2-
3-
import {NpmHttpFetcher} from './NpmHttpFetcher';
4-
import {NpmRemapResolver} from './NpmRemapResolver';
5-
import {NpmSemverFetcher} from './NpmSemverFetcher';
6-
import {NpmSemverResolver} from './NpmSemverResolver';
7-
import {NpmTagResolver} from './NpmTagResolver';
8-
import * as npmConfigUtils from './npmConfigUtils';
9-
import * as npmHttpUtils from './npmHttpUtils';
10-
import * as npmPublishUtils from './npmPublishUtils';
1+
import {Plugin, SettingsType, DurationUnit, miscUtils, Configuration, Ident} from '@yarnpkg/core';
2+
import type {SettingsDefinition} from '@yarnpkg/core';
3+
4+
import {NpmHttpFetcher} from './NpmHttpFetcher';
5+
import {NpmRemapResolver} from './NpmRemapResolver';
6+
import {NpmSemverFetcher} from './NpmSemverFetcher';
7+
import {NpmSemverResolver} from './NpmSemverResolver';
8+
import {NpmTagResolver} from './NpmTagResolver';
9+
import * as npmConfigUtils from './npmConfigUtils';
10+
import * as npmHttpUtils from './npmHttpUtils';
11+
import * as npmPublishUtils from './npmPublishUtils';
1112

1213
export {npmConfigUtils};
1314
export {npmHttpUtils};
@@ -34,52 +35,53 @@ export interface Hooks {
3435
const authSettings = {
3536
npmAlwaysAuth: {
3637
description: `URL of the selected npm registry (note: npm enterprise isn't supported)`,
37-
type: SettingsType.BOOLEAN as const,
38+
type: SettingsType.BOOLEAN,
3839
default: false,
3940
},
4041
npmAuthIdent: {
4142
description: `Authentication identity for the npm registry (_auth in npm and yarn v1)`,
42-
type: SettingsType.SECRET as const,
43+
type: SettingsType.SECRET,
4344
default: null,
4445
},
4546
npmAuthToken: {
4647
description: `Authentication token for the npm registry (_authToken in npm and yarn v1)`,
47-
type: SettingsType.SECRET as const,
48+
type: SettingsType.SECRET,
4849
default: null,
4950
},
50-
};
51+
} satisfies Record<string, SettingsDefinition>;
5152

5253
const registrySettings = {
5354
npmAuditRegistry: {
5455
description: `Registry to query for audit reports`,
55-
type: SettingsType.STRING as const,
56+
type: SettingsType.STRING,
5657
default: null,
5758
},
5859
npmPublishRegistry: {
5960
description: `Registry to push packages to`,
60-
type: SettingsType.STRING as const,
61+
type: SettingsType.STRING,
6162
default: null,
6263
},
6364
npmRegistryServer: {
6465
description: `URL of the selected npm registry (note: npm enterprise isn't supported)`,
65-
type: SettingsType.STRING as const,
66+
type: SettingsType.STRING,
6667
default: `https://registry.yarnpkg.com`,
6768
},
68-
};
69+
} satisfies Record<string, SettingsDefinition>;
6970

7071
const packageGateSettings = {
7172
npmMinimalAgeGate: {
72-
description: `Minimum age of a package version according to the publish date on the npm registry in minutes to be considered for installation`,
73-
type: SettingsType.NUMBER as const,
74-
default: 0,
73+
description: `Minimum age of a package version according to the publish date on the npm registry to be considered for installation`,
74+
type: SettingsType.DURATION,
75+
unit: DurationUnit.MINUTES,
76+
default: `0m`,
7577
},
7678
npmPreapprovedPackages: {
7779
description: `Array of package descriptors or package name glob patterns to exclude from the minimum release age check`,
78-
type: SettingsType.STRING as const,
79-
isArray: true as const,
80+
type: SettingsType.STRING,
81+
isArray: true,
8082
default: [],
8183
},
82-
};
84+
} satisfies Record<string, SettingsDefinition>;
8385

8486
declare module '@yarnpkg/core' {
8587
interface ConfigurationValueMap {

0 commit comments

Comments
 (0)