Skip to content

Commit

Permalink
Add support for new link: dependency type
Browse files Browse the repository at this point in the history
  • Loading branch information
mgcrea committed May 30, 2017
1 parent 9477877 commit 0b79bc7
Show file tree
Hide file tree
Showing 21 changed files with 237 additions and 9 deletions.
16 changes: 16 additions & 0 deletions __tests__/commands/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ test.concurrent('install with --optional flag', (): Promise<void> => {
});
});

test.concurrent('install with link: specifier', (): Promise<void> => {
return runAdd(['link:../left-pad'], {dev: true}, 'add-with-flag', async config => {
const lockfile = explodeLockfile(await fs.readFile(path.join(config.cwd, 'yarn.lock')));
const pkg = await fs.readJson(path.join(config.cwd, 'package.json'));

const expectPath = path.join(config.cwd, 'node_modules', 'left-pad');

const stat = await fs.lstat(expectPath);
expect(stat.isSymbolicLink()).toEqual(true);

expect(lockfile.indexOf('left-pad@1.1.0:')).toEqual(-1);
expect(pkg.devDependencies).toEqual({'left-pad': 'link:../left-pad'});
expect(pkg.dependencies).toEqual({});
});
});

test.concurrent('install with arg that has binaries', (): Promise<void> => {
return runAdd(['react-native-cli'], {}, 'install-with-arg-and-bin');
});
Expand Down
64 changes: 64 additions & 0 deletions __tests__/commands/install/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,53 @@ test.concurrent('properly find and save build artifacts', async () => {
});
});

test.concurrent('creates a symlink to a directory when using the link: protocol', async () => {
await runInstall({}, 'install-link', async (config): Promise<void> => {
const expectPath = path.join(config.cwd, 'node_modules', 'test-absolute');

const stat = await fs.lstat(expectPath);
expect(stat.isSymbolicLink()).toEqual(true);

const target = await fs.readlink(expectPath);
expect(path.resolve(config.cwd, target)).toMatch(/[\\\/]bar$/);
});
});

test.concurrent('creates a symlink to a non-existing directory when using the link: protocol', async () => {
await runInstall({}, 'install-link', async (config): Promise<void> => {
const expectPath = path.join(config.cwd, 'node_modules', 'test-missing');

const stat = await fs.lstat(expectPath);
expect(stat.isSymbolicLink()).toEqual(true);

const target = await fs.readlink(expectPath);
if (process.platform !== 'win32') {
expect(target).toEqual('../baz');
} else {
expect(target).toMatch(/[\\\/]baz[\\\/]$/);
}
});
});

test.concurrent(
'resolves the symlinks relative to the package path when using the link: protocol; not the node_modules',
async () => {
await runInstall({}, 'install-link', async (config): Promise<void> => {
const expectPath = path.join(config.cwd, 'node_modules', 'test-relative');

const stat = await fs.lstat(expectPath);
expect(stat.isSymbolicLink()).toEqual(true);

const target = await fs.readlink(expectPath);
if (process.platform !== 'win32') {
expect(target).toEqual('../bar');
} else {
expect(target).toMatch(/[\\\/]bar[\\\/]$/);
}
});
},
);

test('changes the cache path when bumping the cache version', async () => {
await runInstall({}, 'install-github', async (config): Promise<void> => {
const inOut = new stream.PassThrough();
Expand Down Expand Up @@ -263,6 +310,23 @@ test.concurrent('install file: local packages with local dependencies', async ()
});
});

test.concurrent('install file: link file dependencies', async (): Promise<void> => {
await runInstall({}, 'install-file-link-dependencies', async (config, reporter) => {
const statA = await fs.lstat(path.join(config.cwd, 'node_modules', 'a'));
expect(statA.isSymbolicLink()).toEqual(true);

const statB = await fs.lstat(path.join(config.cwd, 'node_modules', 'b'));
expect(statB.isSymbolicLink()).toEqual(true);

const statC = await fs.lstat(path.join(config.cwd, 'node_modules', 'c'));
expect(statC.isSymbolicLink()).toEqual(true);

expect(await fs.readFile(path.join(config.cwd, 'node_modules', 'a', 'index.js'))).toEqual('foo;\n');

expect(await fs.readFile(path.join(config.cwd, 'node_modules', 'b', 'index.js'))).toEqual('bar;\n');
});
});

test.concurrent('install file: protocol', (): Promise<void> => {
return runInstall({noLockfile: true}, 'install-file', async config => {
expect(await fs.readFile(path.join(config.cwd, 'node_modules', 'foo', 'index.js'))).toEqual('foobar;\n');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
yarn-link-file-dependencies=true
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "a",
"version": "1.0.2",
"dependencies": {
"b": "*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "b",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"dependencies": {
"a": "file:./a",
"b": "file:./b",
"c": "file:./c"
}
}
1 change: 1 addition & 0 deletions __tests__/fixtures/install/install-link/bar/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foobar;
5 changes: 5 additions & 0 deletions __tests__/fixtures/install/install-link/bar/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "bar",
"version": "0.0.0",
"main": "index.js"
}
7 changes: 7 additions & 0 deletions __tests__/fixtures/install/install-link/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"dependencies": {
"test-absolute": "link:/tmp/bar",
"test-relative": "link:bar",
"test-missing": "link:baz"
}
}
23 changes: 21 additions & 2 deletions src/cli/commands/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,33 @@ export async function verifyTreeCheck(
const dependenciesToCheckVersion: PackageToVerify[] = [];
if (rootManifest.dependencies) {
for (const name in rootManifest.dependencies) {
const version = rootManifest.dependencies[name];
// skip linked dependencies
const isLinkedDepencency = /^link:/i.test(version) || (/^file:/i.test(version) && config.linkFileDependencies);
if (isLinkedDepencency) {
continue;
}
dependenciesToCheckVersion.push({
name,
originalKey: name,
parentCwd: registry.cwd,
version: rootManifest.dependencies[name],
version,
});
}
}
if (rootManifest.devDependencies && !config.production) {
for (const name in rootManifest.devDependencies) {
const version = rootManifest.devDependencies[name];
// skip linked dependencies
const isLinkedDepencency = /^link:/i.test(version) || (/^file:/i.test(version) && config.linkFileDependencies);
if (isLinkedDepencency) {
continue;
}
dependenciesToCheckVersion.push({
name,
originalKey: name,
parentCwd: registry.cwd,
version: rootManifest.devDependencies[name],
version,
});
}
}
Expand Down Expand Up @@ -252,6 +264,13 @@ export async function run(config: Config, reporter: Reporter, flags: Object, arg
human = humanParts.join('');
}

// skip unnecessary checks for linked dependencies
const remoteType = pkg._reference.remote.type;
const isLinkedDepencency = remoteType === 'link' || (remoteType === 'file' && config.linkFileDependencies);
if (isLinkedDepencency) {
continue;
}

const pkgLoc = path.join(loc, 'package.json');
if (!await fs.exists(loc) || !await fs.exists(pkgLoc)) {
if (pkg._reference.optional) {
Expand Down
6 changes: 5 additions & 1 deletion src/cli/commands/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,11 @@ export class Install {
for (const manifest of this.resolver.getManifests()) {
const ref = manifest._reference;
invariant(ref, 'expected reference');

const {type} = ref.remote;
// link specifier won't ever hit cache
if (type === 'link') {
continue;
}
const loc = this.config.generateHardModulePath(ref);
const newPkg = await this.config.readManifest(loc);
await this.resolver.updateManifest(ref, newPkg);
Expand Down
3 changes: 3 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type ConfigOptions = {
preferOffline?: boolean,
pruneOfflineMirror?: boolean,
enableMetaFolder?: boolean,
linkFileDependencies?: boolean,
captureHar?: boolean,
ignoreScripts?: boolean,
ignorePlatform?: boolean,
Expand Down Expand Up @@ -92,6 +93,7 @@ export default class Config {
pruneOfflineMirror: boolean;
enableMetaFolder: boolean;
enableLockfileVersions: boolean;
linkFileDependencies: boolean;
ignorePlatform: boolean;
binLinks: boolean;

Expand Down Expand Up @@ -269,6 +271,7 @@ export default class Config {
this.pruneOfflineMirror = Boolean(this.getOption('yarn-offline-mirror-pruning'));
this.enableMetaFolder = Boolean(this.getOption('enable-meta-folder'));
this.enableLockfileVersions = Boolean(this.getOption('yarn-enable-lockfile-versions'));
this.linkFileDependencies = Boolean(this.getOption('yarn-link-file-dependencies'));

//init & create cacheFolder, tempFolder
this.cacheFolder = path.join(this._cacheRootFolder, 'v' + String(constants.CACHE_VERSION));
Expand Down
2 changes: 1 addition & 1 deletion src/fetchers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ export {TarballFetcher as tarball};

export type Fetchers = BaseFetcher | CopyFetcher | GitFetcher | TarballFetcher;

export type FetcherNames = 'base' | 'copy' | 'git' | 'tarball';
export type FetcherNames = 'base' | 'copy' | 'git' | 'link' | 'tarball';
7 changes: 7 additions & 0 deletions src/package-fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ async function fetchOne(ref: PackageReference, config: Config): Promise<FetchedM
const dest = config.generateHardModulePath(ref);

const remote = ref.remote;

// Mock metedata for linked dependencies
if (remote.type === 'link') {
const mockPkg: Manifest = {_uid: '', name: '', version: '0.0.0'};
return Promise.resolve({resolved: null, hash: '', dest, package: mockPkg, cached: false});
}

const Fetcher = fetchers[remote.type];
if (!Fetcher) {
throw new MessageError(config.reporter.lang('unknownFetcherFor', remote.type));
Expand Down
14 changes: 10 additions & 4 deletions src/package-linker.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,20 @@ export default class PackageLinker {
const hardlinksEnabled = linkDuplicates && (await fs.hardlinksWork(this.config.cwd));

const copiedSrcs: Map<string, string> = new Map();
for (const [dest, {pkg, loc: src}] of flatTree) {
for (const [dest, {pkg, loc}] of flatTree) {
const remote = pkg._remote || {type: ''};
const ref = pkg._reference;
const src = remote.type === 'link' ? remote.reference : loc;
invariant(ref, 'expected package reference');
ref.setLocation(dest);

// backwards compatibility: get build artifacts from metadata
const metadata = await this.config.readPackageMetadata(src);
for (const file of metadata.artifacts) {
artifactFiles.push(path.join(dest, file));
// does not apply to linked dependencies
if (remote.type !== 'link') {
const metadata = await this.config.readPackageMetadata(src);
for (const file of metadata.artifacts) {
artifactFiles.push(path.join(dest, file));
}
}

const integrityArtifacts = this.artifacts[`${pkg.name}@${pkg.version}`];
Expand All @@ -166,6 +171,7 @@ export default class PackageLinker {
copyQueue.set(dest, {
src,
dest,
type: remote.type,
onFresh() {
if (ref) {
ref.setFresh(true);
Expand Down
14 changes: 14 additions & 0 deletions src/resolvers/exotics/file-resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import type {Manifest} from '../../types.js';
import type PackageRequest from '../../package-request.js';
import type {RegistryNames} from '../../registries/index.js';
import {MessageError} from '../../errors.js';
import ExoticResolver from './exotic-resolver.js';
import * as util from '../../util/misc.js';
Expand Down Expand Up @@ -30,6 +31,19 @@ export default class FileResolver extends ExoticResolver {
if (!path.isAbsolute(loc)) {
loc = path.join(this.config.cwd, loc);
}

if (this.config.linkFileDependencies) {
const registry: RegistryNames = 'npm';
const manifest: Manifest = {_uid: '', name: '', version: '0.0.0', _registry: registry};
manifest._remote = {
type: 'link',
registry,
hash: null,
reference: loc,
};
manifest._uid = manifest.version;
return manifest;
}
if (!await fs.exists(loc)) {
throw new MessageError(this.reporter.lang('doesntExist', loc));
}
Expand Down
46 changes: 46 additions & 0 deletions src/resolvers/exotics/link-resolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* @flow */

import type {Manifest} from '../../types.js';
import type {RegistryNames} from '../../registries/index.js';
import type PackageRequest from '../../package-request.js';
import ExoticResolver from './exotic-resolver.js';
import * as util from '../../util/misc.js';
import * as fs from '../../util/fs.js';

const path = require('path');

export default class LinkResolver extends ExoticResolver {
constructor(request: PackageRequest, fragment: string) {
super(request, fragment);
this.loc = util.removePrefix(fragment, 'link:');
}

loc: string;

static protocol = 'link';

async resolve(): Promise<Manifest> {
let loc = this.loc;
if (!path.isAbsolute(loc)) {
loc = path.join(this.config.cwd, loc);
}

const name = path.basename(loc);
const registry: RegistryNames = 'npm';

const manifest: Manifest = !await fs.exists(loc)
? {_uid: '', name, version: '0.0.0', _registry: registry}
: await this.config.readManifest(loc, this.registry);

manifest._remote = {
type: 'link',
registry,
hash: null,
reference: loc,
};

manifest._uid = manifest.version;

return manifest;
}
}
2 changes: 2 additions & 0 deletions src/resolvers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ExoticGit from './exotics/git-resolver.js';
import ExoticTarball from './exotics/tarball-resolver.js';
import ExoticGitHub from './exotics/github-resolver.js';
import ExoticFile from './exotics/file-resolver.js';
import ExoticLink from './exotics/link-resolver.js';
import ExoticGitLab from './exotics/gitlab-resolver.js';
import ExoticGist from './exotics/gist-resolver.js';
import ExoticBitbucket from './exotics/bitbucket-resolver.js';
Expand All @@ -23,6 +24,7 @@ export const exotics = {
tarball: ExoticTarball,
github: ExoticGitHub,
file: ExoticFile,
link: ExoticLink,
gitlab: ExoticGitLab,
gist: ExoticGist,
bitbucket: ExoticBitbucket,
Expand Down
Loading

0 comments on commit 0b79bc7

Please sign in to comment.