diff --git a/__tests__/fetchers.js b/__tests__/fetchers.js index ef1551ce99..08db8199ec 100644 --- a/__tests__/fetchers.js +++ b/__tests__/fetchers.js @@ -5,6 +5,7 @@ import TarballFetcher, {LocalTarballFetcher} from '../src/fetchers/tarball-fetch import BaseFetcher from '../src/fetchers/base-fetcher.js'; import CopyFetcher from '../src/fetchers/copy-fetcher.js'; import GitFetcher from '../src/fetchers/git-fetcher.js'; +import LinkFetcher from '../src/fetchers/link-fetcher.js'; import {NoopReporter} from '../src/reporters/index.js'; import Config from '../src/config.js'; import mkdir from './_temp.js'; @@ -55,6 +56,22 @@ test('CopyFetcher.fetch', async () => { expect(contentFoo).toBe('bar'); }); +test('LinkFetcher.fetch', async () => { + const a = await mkdir('copy-fetcher-a'); + await fs.writeFile(path.join(a, 'package.json'), '{}'); + await fs.writeFile(path.join(a, 'foo'), 'bar'); + + const b = await mkdir('copy-fetcher-b'); + const fetcher = new LinkFetcher(b, { + type: 'link', + reference: a, + registry: 'npm', + }, await createConfig()); + await fetcher.fetch(); + const stat = await fs.lstat(b); + expect(stat.isDirectory()).toEqual(true); +}); + test('GitFetcher.fetch', async () => { const dir = await mkdir('git-fetcher'); const fetcher = new GitFetcher(dir, { diff --git a/__tests__/package-resolver.js b/__tests__/package-resolver.js index f82dc95eda..b59e445f8d 100644 --- a/__tests__/package-resolver.js +++ b/__tests__/package-resolver.js @@ -52,3 +52,6 @@ addTest('react-native'); // npm addTest('ember-cli'); // npm addTest('npm:gulp'); // npm addTest('@polymer/iron-icon'); // npm scoped package +addTest(`file:${__dirname}/../node_modules/babel-core`); // file +addTest(`link:${__dirname}/../node_modules/eslint`); // existing link +addTest(`link:./../not-created-yet`); // not existing link diff --git a/src/fetchers/base-fetcher.js b/src/fetchers/base-fetcher.js index ddee7fab8b..7b3f7d7736 100644 --- a/src/fetchers/base-fetcher.js +++ b/src/fetchers/base-fetcher.js @@ -1,7 +1,7 @@ /* @flow */ /* eslint no-unused-vars: 0 */ -import type {PackageRemote, FetchedMetadata, FetchedOverride} from '../types.js'; +import type {PackageRemote, FetchedMetadata, FetchedOverride, Manifest} from '../types.js'; import type {RegistryNames} from '../registries/index.js'; import type Config from '../config.js'; import * as constants from '../constants.js'; @@ -45,6 +45,12 @@ export default class BaseFetcher { // fetch package and get the hash const {hash, resolved} = await this._fetch(); + // skip any readManifest operation for link type as dest might not exist yet + if (this.remote.type === 'link') { + const mockPkg: Manifest = {_uid: '', name: '', version: 'link', _registry: this.registry}; + return Promise.resolve({resolved, hash, dest, package: mockPkg}); + } + // load the new normalized manifest const pkg = await this.config.readManifest(dest, this.registry); diff --git a/src/fetchers/index.js b/src/fetchers/index.js index a366320b15..8ad090fb9d 100644 --- a/src/fetchers/index.js +++ b/src/fetchers/index.js @@ -3,21 +3,25 @@ import BaseFetcher from './base-fetcher.js'; import CopyFetcher from './copy-fetcher.js'; import GitFetcher from './git-fetcher.js'; +import LinkFetcher from './link-fetcher.js'; import TarballFetcher from './tarball-fetcher.js'; export {BaseFetcher as base}; export {CopyFetcher as copy}; export {GitFetcher as git}; +export {LinkFetcher as link}; export {TarballFetcher as tarball}; export type Fetchers = | BaseFetcher | CopyFetcher | GitFetcher + | LinkFetcher | TarballFetcher; export type FetcherNames = | 'base' | 'copy' | 'git' + | 'link' | 'tarball'; diff --git a/src/fetchers/link-fetcher.js b/src/fetchers/link-fetcher.js new file mode 100644 index 0000000000..69f503a000 --- /dev/null +++ b/src/fetchers/link-fetcher.js @@ -0,0 +1,14 @@ +/* @flow */ + +import type {FetchedOverride} from '../types.js'; +import BaseFetcher from './base-fetcher.js'; + +export default class LinkFetcher extends BaseFetcher { + _fetch(): Promise { + // nothing to fetch + return Promise.resolve({ + hash: this.hash || '', + resolved: null, + }); + } +} diff --git a/src/package-linker.js b/src/package-linker.js index 30d7d2712c..a4958f2f19 100644 --- a/src/package-linker.js +++ b/src/package-linker.js @@ -116,7 +116,7 @@ export default class PackageLinker { async copyModules(patterns: Array): Promise { let flatTree = await this.getFlatHoistedTree(patterns); - + // sorted tree makes file creation and copying not to interfere with each other flatTree = flatTree.sort(function(dep1, dep2): number { return dep1[0].localeCompare(dep2[0]); @@ -124,14 +124,17 @@ export default class PackageLinker { // const queue: Map = 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); queue.set(dest, { src, dest, + type: remote.type, onFresh() { if (ref) { ref.setFresh(true); @@ -148,7 +151,15 @@ export default class PackageLinker { if (await fs.exists(loc)) { const files = await fs.readdir(loc); for (const file of files) { - possibleExtraneous.add(path.join(loc, file)); + // scoped packages + if (file.startsWith('@')) { + const scopedFiles = await fs.readdir(path.join(loc, file)); + for (const scopedFile of scopedFiles) { + possibleExtraneous.add(path.join(loc, file, scopedFile)); + } + } else { + possibleExtraneous.add(path.join(loc, file)); + } } } } diff --git a/src/resolvers/exotics/link-resolver.js b/src/resolvers/exotics/link-resolver.js new file mode 100644 index 0000000000..9025967510 --- /dev/null +++ b/src/resolvers/exotics/link-resolver.js @@ -0,0 +1,40 @@ +/* @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'; + +const path = require('path'); + +export default class FileResolver extends ExoticResolver { + constructor(request: PackageRequest, fragment: string) { + super(request, fragment); + this.loc = util.removePrefix(fragment, 'link:'); + } + + loc: string; + + static protocol = 'link'; + + resolve(): Promise { + let loc = this.loc; + if (!path.isAbsolute(loc)) { + loc = path.join(this.config.cwd, loc); + } + + const registry: RegistryNames = 'npm'; + const manifest: Manifest = {_uid: '', name: '', version: 'link', _registry: registry}; + + manifest._remote = { + type: 'link', + registry, + reference: loc, + }; + + manifest._uid = manifest.version; + + return Promise.resolve(manifest); + } +} diff --git a/src/resolvers/index.js b/src/resolvers/index.js index e6f010cb7a..4fe960aad1 100644 --- a/src/resolvers/index.js +++ b/src/resolvers/index.js @@ -16,6 +16,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'; @@ -25,6 +26,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 0943b8500e..69d642a9b4 100644 --- a/src/util/fs.js +++ b/src/util/fs.js @@ -37,6 +37,7 @@ const noop = () => {}; export type CopyQueueItem = { src: string, dest: string, + type?: string, onFresh?: ?() => void, onDone?: ?() => void, }; @@ -113,11 +114,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;