Skip to content

Commit

Permalink
RFC: feat(config): Use more conventional paths for config and data (#…
Browse files Browse the repository at this point in the history
…5336)

* feat(config): Use more conventional paths for config and data

This implements:

* Supporting user-defined environment variables adhering to the [XDG
Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-0.6.html)
to override yarn's defaults
* Supporting equivalent environment variables on Windows such as
%LOCALAPPDATA%
* More conventional defaults for these locations according to the
operating system.
* Support for the user defined config dir in the `.yarnrc` lookup path
* Storing global modules in a data-oriented location rather than a
config location (seeing this is actually what motivated this PR)

Concerns:

* Existing Windows config locations will break. This probably need to be
addressed with a migration path and/or a breaking change
* A few notes included in comments (will highlight these with inline GH
comments)
* Unclear test status as master builds appears to fail on my laptop as
well.

Really interested in your feedback. I know [this has been attempted
before](https://github.com/yarnpkg/yarn/pull/3674/files) -- cc @kelseasy
-- and I'd really like to get this in!

* Prettier

* it -> test

* Fall back to ~/.config/yarn instead of XDG/Windows paths
  • Loading branch information
wbinnssmith authored and bestander committed Feb 8, 2018
1 parent 18bed13 commit 2d454b5
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 20 deletions.
131 changes: 131 additions & 0 deletions __tests__/util/user-dirs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/* @flow */

import {getDataDir, getCacheDir, getConfigDir} from '../../src/util/user-dirs';
import userHome from '../../src/util/user-home-dir';

const path = require('path');

describe('getDataDir', () => {
describe('on windows', () => {
beforeEach(() => {
mockProcessPlatform('win32');
});

test('uses Yarn/data within LOCALAPPDATA if it exists', () => {
process.env.LOCALAPPDATA = 'foo';
expect(getDataDir()).toBe(path.join('foo', 'Yarn', 'Data'));
delete process.env.LOCALAPPDATA;
});

test('uses the config dir otherwise', () => {
expect(getDataDir()).toBe(path.join(userHome, '.config', 'yarn'));
});
});

describe('on linux/darwin', () => {
beforeEach(() => {
mockProcessPlatform('linux');
});

test('uses XDG_DATA_HOME if it is set', () => {
process.env.XDG_DATA_HOME = 'foo';
expect(getDataDir()).toBe(path.join('foo', 'yarn'));
delete process.env.XDG_DATA_HOME;
});

test('falls back to the config dir', () => {
expect(getDataDir()).toBe(path.join(userHome, '.config', 'yarn'));
});
});
});

describe('getCacheDir', () => {
describe('on windows', () => {
beforeEach(() => {
mockProcessPlatform('win32');
});

test('uses Yarn\\Cache within LOCALAPPDATA if it exists', () => {
process.env.LOCALAPPDATA = 'foo';
expect(getCacheDir()).toBe(path.join('foo', 'Yarn', 'Cache'));
delete process.env.LOCALAPPDATA;
});

test('uses AppData\\Local\\Cache otherwise', () => {
expect(getCacheDir()).toBe(path.join(userHome, 'AppData', 'Local', 'Yarn', 'Cache'));
});
});

describe('on darwin (macOS)', () => {
beforeEach(() => {
mockProcessPlatform('darwin');
});

test('uses XDG_CACHE_HOME if it is set', () => {
process.env.XDG_CACHE_HOME = 'foo';
expect(getCacheDir()).toBe(path.join('foo', 'yarn'));
delete process.env.XDG_CACHE_HOME;
});

test('falls back to Library/Caches/Yarn', () => {
expect(getCacheDir()).toBe(path.join(userHome, 'Library', 'Caches', 'Yarn'));
});
});

describe('on others (linux, etc)', () => {
beforeEach(() => {
mockProcessPlatform('linux');
});

test('uses XDG_CACHE_HOME if it is set', () => {
process.env.XDG_CACHE_HOME = 'foo';
expect(getCacheDir()).toBe(path.join('foo', 'yarn'));
delete process.env.XDG_CACHE_HOME;
});

test('falls back to .cache/yarn', () => {
expect(getCacheDir()).toBe(path.join(userHome, '.cache', 'yarn'));
});
});
});

describe('getConfigDir', () => {
describe('on windows', () => {
beforeEach(() => {
mockProcessPlatform('win32');
});

test('uses Yarn\\Config within LOCALAPPDATA if it exists', () => {
process.env.LOCALAPPDATA = 'foo';
expect(getConfigDir()).toBe(path.join('foo', 'Yarn', 'Config'));
delete process.env.LOCALAPPDATA;
});

test('uses the config dir otherwise', () => {
expect(getConfigDir()).toBe(path.join(userHome, '.config', 'yarn'));
});
});

describe('on linux/darwin', () => {
beforeEach(() => {
mockProcessPlatform('linux');
});

test('uses XDG_CONFIG_HOME if it is set', () => {
process.env.XDG_CONFIG_HOME = 'foo';
expect(getConfigDir()).toBe(path.join('foo', 'yarn'));
delete process.env.XDG_CONFIG_HOME;
});

test('falls back to .config/yarn', () => {
expect(getConfigDir()).toBe(path.join(userHome, '.config', 'yarn'));
});
});
});

function mockProcessPlatform(name: string) {
// $FlowFixMe this is valid
Object.defineProperty(process, 'platform', {
get: jest.fn(() => name),
});
}
26 changes: 6 additions & 20 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const os = require('os');
const path = require('path');
const userHome = require('./util/user-home-dir').default;
const {getCacheDir, getConfigDir, getDataDir} = require('./util/user-dirs');
const isWebpackBundle = require('is-webpack-bundle');

type Env = {
Expand Down Expand Up @@ -42,24 +43,8 @@ export const CHILD_CONCURRENCY = 5;

export const REQUIRED_PACKAGE_KEYS = ['name', 'version', '_uid'];

function getDirectory(category: string): string {
// use %LOCALAPPDATA%/Yarn on Windows
if (process.platform === 'win32' && process.env.LOCALAPPDATA) {
return path.join(process.env.LOCALAPPDATA, 'Yarn', category);
}

// otherwise use ~/.{category}/yarn
return path.join(userHome, `.${category}`, 'yarn');
}

function getPreferredCacheDirectories(): Array<string> {
const preferredCacheDirectories = [];

if (process.platform === 'darwin') {
preferredCacheDirectories.push(path.join(userHome, 'Library', 'Caches', 'Yarn'));
} else {
preferredCacheDirectories.push(getDirectory('cache'));
}
const preferredCacheDirectories = [getCacheDir()];

if (process.getuid) {
// $FlowFixMe: process.getuid exists, dammit
Expand All @@ -72,9 +57,10 @@ function getPreferredCacheDirectories(): Array<string> {
}

export const PREFERRED_MODULE_CACHE_DIRECTORIES = getPreferredCacheDirectories();
export const CONFIG_DIRECTORY = getDirectory('config');
export const LINK_REGISTRY_DIRECTORY = path.join(CONFIG_DIRECTORY, 'link');
export const GLOBAL_MODULE_DIRECTORY = path.join(CONFIG_DIRECTORY, 'global');
export const CONFIG_DIRECTORY = getConfigDir();
export const DATA_DIRECTORY = getDataDir();
export const LINK_REGISTRY_DIRECTORY = path.join(DATA_DIRECTORY, 'link');
export const GLOBAL_MODULE_DIRECTORY = path.join(DATA_DIRECTORY, 'global');

export const NODE_BIN_PATH = process.execPath;
export const YARN_BIN_PATH = getYarnBinPath();
Expand Down
2 changes: 2 additions & 0 deletions src/util/rc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {readFileSync} from 'fs';
import * as path from 'path';
import {CONFIG_DIRECTORY} from '../constants';

const etc = '/etc';
const isWin = process.platform === 'win32';
Expand All @@ -20,6 +21,7 @@ function getRcPaths(name: string, cwd: string): Array<string> {
}

if (home) {
addConfigPath(CONFIG_DIRECTORY);
addConfigPath(home, '.config', name, 'config');
addConfigPath(home, '.config', name);
addConfigPath(home, `.${name}`, 'config');
Expand Down
54 changes: 54 additions & 0 deletions src/util/user-dirs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* @flow */

const path = require('path');
const userHome = require('./user-home-dir').default;

const FALLBACK_CONFIG_DIR = path.join(userHome, '.config', 'yarn');
const FALLBACK_CACHE_DIR = path.join(userHome, '.cache', 'yarn');

export function getDataDir(): string {
if (process.platform === 'win32') {
const WIN32_APPDATA_DIR = getLocalAppDataDir();
return WIN32_APPDATA_DIR == null ? FALLBACK_CONFIG_DIR : path.join(WIN32_APPDATA_DIR, 'Data');
} else if (process.env.XDG_DATA_HOME) {
return path.join(process.env.XDG_DATA_HOME, 'yarn');
} else {
// This could arguably be ~/Library/Application Support/Yarn on Macs,
// but that feels unintuitive for a cli tool

// Instead, use our prior fallback. Some day this could be
// path.join(userHome, '.local', 'share', 'yarn')
// or return path.join(WIN32_APPDATA_DIR, 'Data') on win32
return FALLBACK_CONFIG_DIR;
}
}

export function getCacheDir(): string {
if (process.platform === 'win32') {
// process.env.TEMP also exists, but most apps put caches here
return path.join(getLocalAppDataDir() || path.join(userHome, 'AppData', 'Local', 'Yarn'), 'Cache');
} else if (process.env.XDG_CACHE_HOME) {
return path.join(process.env.XDG_CACHE_HOME, 'yarn');
} else if (process.platform === 'darwin') {
return path.join(userHome, 'Library', 'Caches', 'Yarn');
} else {
return FALLBACK_CACHE_DIR;
}
}

export function getConfigDir(): string {
if (process.platform === 'win32') {
// Use our prior fallback. Some day this could be
// return path.join(WIN32_APPDATA_DIR, 'Config')
const WIN32_APPDATA_DIR = getLocalAppDataDir();
return WIN32_APPDATA_DIR == null ? FALLBACK_CONFIG_DIR : path.join(WIN32_APPDATA_DIR, 'Config');
} else if (process.env.XDG_CONFIG_HOME) {
return path.join(process.env.XDG_CONFIG_HOME, 'yarn');
} else {
return FALLBACK_CONFIG_DIR;
}
}

function getLocalAppDataDir(): ?string {
return process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Yarn') : null;
}

0 comments on commit 2d454b5

Please sign in to comment.