From 0b79bc7d09f70b140866ec921fc249f16dc25b1d Mon Sep 17 00:00:00 2001 From: Olivier Louvignes Date: Tue, 30 May 2017 14:51:41 +0200 Subject: [PATCH] Add support for new link: dependency type --- __tests__/commands/add.js | 16 +++++ __tests__/commands/install/integration.js | 64 +++++++++++++++++++ .../install-file-link-dependencies/.npmrc | 1 + .../install-file-link-dependencies/a/index.js | 1 + .../a/package.json | 7 ++ .../install-file-link-dependencies/b/index.js | 1 + .../b/package.json | 4 ++ .../package.json | 7 ++ .../install/install-link/bar/index.js | 1 + .../install/install-link/bar/package.json | 5 ++ .../install/install-link/package.json | 7 ++ src/cli/commands/check.js | 23 ++++++- src/cli/commands/install.js | 6 +- src/config.js | 3 + src/fetchers/index.js | 2 +- src/package-fetcher.js | 7 ++ src/package-linker.js | 14 ++-- src/resolvers/exotics/file-resolver.js | 14 ++++ src/resolvers/exotics/link-resolver.js | 46 +++++++++++++ src/resolvers/index.js | 2 + src/util/fs.js | 15 ++++- 21 files changed, 237 insertions(+), 9 deletions(-) create mode 100644 __tests__/fixtures/install/install-file-link-dependencies/.npmrc create mode 100644 __tests__/fixtures/install/install-file-link-dependencies/a/index.js create mode 100644 __tests__/fixtures/install/install-file-link-dependencies/a/package.json create mode 100644 __tests__/fixtures/install/install-file-link-dependencies/b/index.js create mode 100644 __tests__/fixtures/install/install-file-link-dependencies/b/package.json create mode 100644 __tests__/fixtures/install/install-file-link-dependencies/package.json create mode 100644 __tests__/fixtures/install/install-link/bar/index.js create mode 100644 __tests__/fixtures/install/install-link/bar/package.json create mode 100644 __tests__/fixtures/install/install-link/package.json create mode 100644 src/resolvers/exotics/link-resolver.js diff --git a/__tests__/commands/add.js b/__tests__/commands/add.js index af48e5682f..9da59ffa4b 100644 --- a/__tests__/commands/add.js +++ b/__tests__/commands/add.js @@ -109,6 +109,22 @@ test.concurrent('install with --optional flag', (): Promise => { }); }); +test.concurrent('install with link: specifier', (): Promise => { + 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 => { return runAdd(['react-native-cli'], {}, 'install-with-arg-and-bin'); }); diff --git a/__tests__/commands/install/integration.js b/__tests__/commands/install/integration.js index 2099634222..7c43a0a424 100644 --- a/__tests__/commands/install/integration.js +++ b/__tests__/commands/install/integration.js @@ -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 => { + 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 => { + 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 => { + 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 => { const inOut = new stream.PassThrough(); @@ -263,6 +310,23 @@ test.concurrent('install file: local packages with local dependencies', async () }); }); +test.concurrent('install file: link file dependencies', async (): Promise => { + 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 => { return runInstall({noLockfile: true}, 'install-file', async config => { expect(await fs.readFile(path.join(config.cwd, 'node_modules', 'foo', 'index.js'))).toEqual('foobar;\n'); diff --git a/__tests__/fixtures/install/install-file-link-dependencies/.npmrc b/__tests__/fixtures/install/install-file-link-dependencies/.npmrc new file mode 100644 index 0000000000..a1bd8c7dec --- /dev/null +++ b/__tests__/fixtures/install/install-file-link-dependencies/.npmrc @@ -0,0 +1 @@ +yarn-link-file-dependencies=true diff --git a/__tests__/fixtures/install/install-file-link-dependencies/a/index.js b/__tests__/fixtures/install/install-file-link-dependencies/a/index.js new file mode 100644 index 0000000000..e901f01b48 --- /dev/null +++ b/__tests__/fixtures/install/install-file-link-dependencies/a/index.js @@ -0,0 +1 @@ +foo; diff --git a/__tests__/fixtures/install/install-file-link-dependencies/a/package.json b/__tests__/fixtures/install/install-file-link-dependencies/a/package.json new file mode 100644 index 0000000000..b283edff8b --- /dev/null +++ b/__tests__/fixtures/install/install-file-link-dependencies/a/package.json @@ -0,0 +1,7 @@ +{ + "name": "a", + "version": "1.0.2", + "dependencies": { + "b": "*" + } +} diff --git a/__tests__/fixtures/install/install-file-link-dependencies/b/index.js b/__tests__/fixtures/install/install-file-link-dependencies/b/index.js new file mode 100644 index 0000000000..e46160df1c --- /dev/null +++ b/__tests__/fixtures/install/install-file-link-dependencies/b/index.js @@ -0,0 +1 @@ +bar; diff --git a/__tests__/fixtures/install/install-file-link-dependencies/b/package.json b/__tests__/fixtures/install/install-file-link-dependencies/b/package.json new file mode 100644 index 0000000000..c2d84cc127 --- /dev/null +++ b/__tests__/fixtures/install/install-file-link-dependencies/b/package.json @@ -0,0 +1,4 @@ +{ + "name": "b", + "version": "1.0.0" +} diff --git a/__tests__/fixtures/install/install-file-link-dependencies/package.json b/__tests__/fixtures/install/install-file-link-dependencies/package.json new file mode 100644 index 0000000000..80a9b8a533 --- /dev/null +++ b/__tests__/fixtures/install/install-file-link-dependencies/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "a": "file:./a", + "b": "file:./b", + "c": "file:./c" + } +} diff --git a/__tests__/fixtures/install/install-link/bar/index.js b/__tests__/fixtures/install/install-link/bar/index.js new file mode 100644 index 0000000000..6e6da2546d --- /dev/null +++ b/__tests__/fixtures/install/install-link/bar/index.js @@ -0,0 +1 @@ +foobar; diff --git a/__tests__/fixtures/install/install-link/bar/package.json b/__tests__/fixtures/install/install-link/bar/package.json new file mode 100644 index 0000000000..f92edb96b8 --- /dev/null +++ b/__tests__/fixtures/install/install-link/bar/package.json @@ -0,0 +1,5 @@ +{ + "name": "bar", + "version": "0.0.0", + "main": "index.js" +} diff --git a/__tests__/fixtures/install/install-link/package.json b/__tests__/fixtures/install/install-link/package.json new file mode 100644 index 0000000000..59ced77fa9 --- /dev/null +++ b/__tests__/fixtures/install/install-link/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "test-absolute": "link:/tmp/bar", + "test-relative": "link:bar", + "test-missing": "link:baz" + } +} diff --git a/src/cli/commands/check.js b/src/cli/commands/check.js index a375441a7d..7a6c1c9f2e 100644 --- a/src/cli/commands/check.js +++ b/src/cli/commands/check.js @@ -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, }); } } @@ -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) { diff --git a/src/cli/commands/install.js b/src/cli/commands/install.js index d9019beeb7..d29f1980b6 100644 --- a/src/cli/commands/install.js +++ b/src/cli/commands/install.js @@ -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); diff --git a/src/config.js b/src/config.js index 11bdcfad81..4682921c6c 100644 --- a/src/config.js +++ b/src/config.js @@ -31,6 +31,7 @@ export type ConfigOptions = { preferOffline?: boolean, pruneOfflineMirror?: boolean, enableMetaFolder?: boolean, + linkFileDependencies?: boolean, captureHar?: boolean, ignoreScripts?: boolean, ignorePlatform?: boolean, @@ -92,6 +93,7 @@ export default class Config { pruneOfflineMirror: boolean; enableMetaFolder: boolean; enableLockfileVersions: boolean; + linkFileDependencies: boolean; ignorePlatform: boolean; binLinks: boolean; @@ -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)); diff --git a/src/fetchers/index.js b/src/fetchers/index.js index 43a213bfb6..50b0c18d84 100644 --- a/src/fetchers/index.js +++ b/src/fetchers/index.js @@ -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'; diff --git a/src/package-fetcher.js b/src/package-fetcher.js index 0aca179395..c250b293b2 100644 --- a/src/package-fetcher.js +++ b/src/package-fetcher.js @@ -24,6 +24,13 @@ async function fetchOne(ref: PackageReference, config: Config): Promise = 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}`]; @@ -166,6 +171,7 @@ export default class PackageLinker { copyQueue.set(dest, { src, dest, + type: remote.type, onFresh() { if (ref) { ref.setFresh(true); diff --git a/src/resolvers/exotics/file-resolver.js b/src/resolvers/exotics/file-resolver.js index 861b3242c6..cb4c00c858 100644 --- a/src/resolvers/exotics/file-resolver.js +++ b/src/resolvers/exotics/file-resolver.js @@ -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'; @@ -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)); } diff --git a/src/resolvers/exotics/link-resolver.js b/src/resolvers/exotics/link-resolver.js new file mode 100644 index 0000000000..524a4a603b --- /dev/null +++ b/src/resolvers/exotics/link-resolver.js @@ -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 { + 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; + } +} diff --git a/src/resolvers/index.js b/src/resolvers/index.js index 1d5912ee71..5b11b5b617 100644 --- a/src/resolvers/index.js +++ b/src/resolvers/index.js @@ -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'; @@ -23,6 +24,7 @@ export const exotics = { tarball: ExoticTarball, github: ExoticGitHub, file: ExoticFile, + link: ExoticLink, gitlab: ExoticGitLab, gist: ExoticGist, bitbucket: ExoticBitbucket, diff --git a/src/util/fs.js b/src/util/fs.js index 3810708d78..e5b552380c 100644 --- a/src/util/fs.js +++ b/src/util/fs.js @@ -42,6 +42,7 @@ const noop = () => {}; export type CopyQueueItem = { src: string, dest: string, + type?: string, onFresh?: ?() => void, onDone?: ?() => void, }; @@ -159,11 +160,23 @@ async function buildActionsForCopy( // async function build(data): Promise { - 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;