Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the local cache opt-in (rather than opt-out) #4252

Merged
merged 3 commits into from
Mar 24, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/gatsby/content/advanced/lexicon.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ See also: the [`Linker` interface](https://github.com/yarnpkg/berry/blob/master/

See also: the [`Installer` interface](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Installer.ts#L18)

### Local Cache

The local cache is a way to protect against registries going down by keeping a cache of your packages within your very project (often checked-in within the repository). While not always practical (it causes the repository size to grow, although we have ways to mitigate it significantly), it presents various interesting properties:

- It doesn't require additional infrastructure, such as a [Verdaccio proxy](https://verdaccio.org/)
- The install fetch step is as fast as it can be, with no data transfer at all
- It lets you reach [zero-installs](https://yarnpkg.com/features/zero-installs) if you also use the PnP linker

Copy link
Member

@merceyz merceyz Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another use case for the local cache, without committing it, is that you can use it to avoid dealing with credentials in your Dockerfile.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another use case unrelated to offline mode:

github actions cache: actions/setup-node#325.

As yarn is able to prune the cache, I don't have to invalidate on yarn.lock changes. That gives a little extra speed / reliability when deps changes often

### Locator

A locator is a combination of a package name (for example `lodash`) and a package <abbr>reference</abbr> (for example `1.2.3`). Locators are used to identify a single unique package (interestingly, all valid locators also are valid <abbr>descriptors</abbr>).
Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-npm/sources/NpmHttpFetcher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Fetcher, FetchOptions, MinimalFetchOptions} from '@yarnpkg/core';
import {Locator} from '@yarnpkg/core';
import {structUtils, tgzUtils} from '@yarnpkg/core';
import {formatUtils, structUtils, tgzUtils} from '@yarnpkg/core';
import semver from 'semver';

import {PROTOCOL} from './constants';
Expand Down Expand Up @@ -50,6 +50,7 @@ export class NpmHttpFetcher implements Fetcher {
throw new Error(`Assertion failed: The archiveUrl querystring parameter should have been available`);

const sourceBuffer = await npmHttpUtils.get(params.__archiveUrl, {
customErrorMessage: npmHttpUtils.customPackageError,
configuration: opts.project.configuration,
ident: locator,
});
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-npm/sources/NpmSemverFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class NpmSemverFetcher implements Fetcher {
let sourceBuffer;
try {
sourceBuffer = await npmHttpUtils.get(NpmSemverFetcher.getLocatorUrl(locator), {
customErrorMessage: npmHttpUtils.customPackageError,
configuration: opts.project.configuration,
ident: locator,
});
Expand All @@ -58,6 +59,7 @@ export class NpmSemverFetcher implements Fetcher {
// OK: https://registry.yarnpkg.com/@emotion%2fbabel-preset-css-prop/-/babel-preset-css-prop-10.0.7.tgz
// KO: https://registry.yarnpkg.com/@xtuc%2fieee754/-/ieee754-1.2.0.tgz
sourceBuffer = await npmHttpUtils.get(NpmSemverFetcher.getLocatorUrl(locator).replace(/%2f/g, `/`), {
customErrorMessage: npmHttpUtils.customPackageError,
configuration: opts.project.configuration,
ident: locator,
});
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-npm/sources/NpmSemverResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class NpmSemverResolver implements Resolver {
throw new Error(`Expected a valid range, got ${descriptor.range.slice(PROTOCOL.length)}`);

const registryData = await npmHttpUtils.get(npmHttpUtils.getIdentUrl(descriptor), {
customErrorMessage: npmHttpUtils.customPackageError,
configuration: opts.project.configuration,
ident: descriptor,
jsonResponse: true,
Expand Down Expand Up @@ -118,6 +119,7 @@ export class NpmSemverResolver implements Resolver {
throw new ReportError(MessageName.RESOLVER_NOT_FOUND, `The npm semver resolver got selected, but the version isn't semver`);

const registryData = await npmHttpUtils.get(npmHttpUtils.getIdentUrl(locator), {
customErrorMessage: npmHttpUtils.customPackageError,
configuration: opts.project.configuration,
ident: locator,
jsonResponse: true,
Expand Down
16 changes: 13 additions & 3 deletions packages/plugin-npm/sources/npmHttpUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Configuration, Ident, httpUtils} from '@yarnpkg/core';
import {Configuration, Ident, formatUtils, httpUtils} from '@yarnpkg/core';
import {MessageName, ReportError} from '@yarnpkg/core';
import {prompt} from 'enquirer';
import {URL} from 'url';
Expand Down Expand Up @@ -42,8 +42,18 @@ export async function handleInvalidAuthenticationError(error: any, {attemptedAs,
}
}

export function customPackageError(error: httpUtils.RequestError) {
return error.response?.statusCode === 404 ? `Package not found` : null;
export function customPackageError(error: httpUtils.RequestError, configuration: Configuration) {
const statusCode = error.response?.statusCode;
if (!statusCode)
return null;

if (statusCode === 404)
return `Package not found`;

if (statusCode >= 500 && statusCode < 600)
return `The registry appears to be down (using a ${formatUtils.applyHyperlink(configuration, `local cache`, `https://yarnpkg.com/advanced/lexicon#local-cache`)} might have protected you against such outages)`;

return null;
}

export function getIdentUrl(ident: Ident) {
Expand Down
2 changes: 1 addition & 1 deletion packages/yarnpkg-core/sources/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export const coreDefinitions: {[coreSettingName: string]: SettingsDefinition} =
enableGlobalCache: {
description: `If true, the system-wide cache folder will be used regardless of \`cache-folder\``,
type: SettingsType.BOOLEAN,
default: false,
default: true,
},

// Settings related to the output style
Expand Down
18 changes: 9 additions & 9 deletions packages/yarnpkg-core/sources/httpUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ function prettyResponseCode({statusCode, statusMessage}: Response, configuration
return formatUtils.applyHyperlink(configuration, `${prettyStatusCode}${statusMessage ? ` (${statusMessage})` : ``}`, href);
}

async function prettyNetworkError(response: Promise<Response<any>>, {configuration, customErrorMessage}: {configuration: Configuration, customErrorMessage?: (err: RequestError) => string | null}) {
async function prettyNetworkError(response: Promise<Response<any>>, {configuration, customErrorMessage}: {configuration: Configuration, customErrorMessage?: (err: RequestError, configuration: Configuration) => string | null}) {
try {
return await response;
} catch (err) {
if (err.name !== `HTTPError`)
throw err;

let message = customErrorMessage?.(err) ?? err.response.body?.error;
let message = customErrorMessage?.(err, configuration) ?? err.response.body?.error;

if (message == null) {
if (err.message.startsWith(`Response code`)) {
Expand All @@ -65,7 +65,7 @@ async function prettyNetworkError(response: Promise<Response<any>>, {configurati
}

if (err instanceof TimeoutError && err.event === `socket`)
message += `(can be increased via ${formatUtils.pretty(configuration, `httpTimeout`, formatUtils.Type.SETTING)})`;
message += ` (can be increased via ${formatUtils.pretty(configuration, `httpTimeout`, formatUtils.Type.SETTING)})`;

const networkError = new ReportError(MessageName.NETWORK_ERROR, message, report => {
if (err.response) {
Expand Down Expand Up @@ -166,7 +166,7 @@ export enum Method {

export type Options = {
configuration: Configuration;
customErrorMessage?: (err: RequestError) => string | null;
customErrorMessage?: (err: RequestError, configuration: Configuration) => string | null;
headers?: {[headerName: string]: string};
jsonRequest?: boolean;
jsonResponse?: boolean;
Expand All @@ -183,9 +183,9 @@ export async function request(target: string | URL, body: Body, {configuration,
return await executor();
}

export async function get(target: string, {configuration, jsonResponse, ...rest}: Options) {
export async function get(target: string, {configuration, jsonResponse, customErrorMessage, ...rest}: Options) {
let entry = miscUtils.getFactoryWithDefault(cache, target, () => {
return prettyNetworkError(request(target, null, {configuration, ...rest}), {configuration}).then(response => {
return prettyNetworkError(request(target, null, {configuration, ...rest}), {configuration, customErrorMessage}).then(response => {
cache.set(target, response.body);
return response.body;
});
Expand All @@ -202,19 +202,19 @@ export async function get(target: string, {configuration, jsonResponse, ...rest}
}

export async function put(target: string, body: Body, {customErrorMessage, ...options}: Options): Promise<Buffer> {
const response = await prettyNetworkError(request(target, body, {...options, method: Method.PUT}), options);
const response = await prettyNetworkError(request(target, body, {...options, method: Method.PUT}), {customErrorMessage, configuration: options.configuration});

return response.body;
}

export async function post(target: string, body: Body, {customErrorMessage, ...options}: Options): Promise<Buffer> {
const response = await prettyNetworkError(request(target, body, {...options, method: Method.POST}), options);
const response = await prettyNetworkError(request(target, body, {...options, method: Method.POST}), {customErrorMessage, configuration: options.configuration});

return response.body;
}

export async function del(target: string, {customErrorMessage, ...options}: Options): Promise<Buffer> {
const response = await prettyNetworkError(request(target, null, {...options, method: Method.DELETE}), options);
const response = await prettyNetworkError(request(target, null, {...options, method: Method.DELETE}), {customErrorMessage, configuration: options.configuration});

return response.body;
}
Expand Down