Skip to content

Commit

Permalink
feat(type): add support for new link: dependency type
Browse files Browse the repository at this point in the history
  • Loading branch information
mgcrea committed May 27, 2017
1 parent 9acdd68 commit 7ad1bec
Show file tree
Hide file tree
Showing 21 changed files with 229 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
56 changes: 56 additions & 0 deletions __tests__/commands/install/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,45 @@ 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);
expect(target).toEqual('../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);
expect(target).toEqual('../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 +302,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 @@ -91,6 +92,7 @@ export default class Config {
preferOffline: boolean;
pruneOfflineMirror: boolean;
enableMetaFolder: boolean;
linkFileDependencies: boolean;
disableLockfileVersions: boolean;
ignorePlatform: boolean;
binLinks: boolean;
Expand Down Expand Up @@ -268,6 +270,7 @@ export default class Config {

this.pruneOfflineMirror = Boolean(this.getOption('yarn-offline-mirror-pruning'));
this.enableMetaFolder = Boolean(this.getOption('enable-meta-folder'));
this.linkFileDependencies = Boolean(this.getOption('yarn-link-file-dependencies'));
this.disableLockfileVersions = Boolean(this.getOption('yarn-disable-lockfile-versions'));

//init & create cacheFolder, tempFolder
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
15 changes: 14 additions & 1 deletion src/util/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const noop = () => {};
export type CopyQueueItem = {
src: string,
dest: string,
type?: string,
onFresh?: ?() => void,
onDone?: ?() => void,
};
Expand Down Expand Up @@ -159,11 +160,23 @@ async function buildActionsForCopy(

//
async function build(data): Promise<void> {
const {src, dest} = data;
const {src, dest, type} = data;
const onFresh = data.onFresh || noop;
const onDone = data.onDone || noop;
files.add(dest);

if (type === 'link') {
await mkdirp(path.dirname(dest));
onFresh();
actions.push({
type: 'symlink',
dest,
linkname: src,
});
onDone();
return;
}

if (events.ignoreBasenames.indexOf(path.basename(src)) >= 0) {
// ignored file
return;
Expand Down

0 comments on commit 7ad1bec

Please sign in to comment.