From d031c5667881d5d7207c24814b81e8c1f6ec929d Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Sun, 24 Mar 2024 13:46:00 +0100 Subject: [PATCH] feat: Rely on local cache instead of npm global dependencies folder BREAKING CHANGE: - `npm-cross-link` no longer interferes with npm global `node_modules` folder. For any reuse it relies on user local cache directory. - No longer `npm link` is used internally. Any links are configured directly via symlinking --- README.md | 34 +++----- install-packages-globally.js | 27 ++---- lib/cache-package/index.js | 2 +- lib/cache-package/sem-ver.js | 2 +- lib/install-maintained-package/index.js | 6 -- lib/install-maintained-package/npm-link.js | 46 ---------- lib/install-package-globally.js | 87 +++++++++---------- lib/non-overridable-externals.js | 3 - lib/resolve-external-context.js | 16 +--- lib/setup-dependency/index.js | 16 ++-- .../install-external/index.js | 73 ++++++++++------ .../resolve-is-to-be-linked.js | 25 ++++++ lib/setup-dependency/setup-external/index.js | 53 ----------- .../setup-external/resolve-is-to-be-linked.js | 62 ------------- .../setup-local/generate-link.js | 15 ---- lib/setup-dependency/setup-local/index.js | 77 +++++++--------- lib/utils/tokenize-package-specs.js | 7 +- 17 files changed, 179 insertions(+), 372 deletions(-) delete mode 100644 lib/install-maintained-package/npm-link.js delete mode 100644 lib/non-overridable-externals.js create mode 100644 lib/setup-dependency/install-external/resolve-is-to-be-linked.js delete mode 100644 lib/setup-dependency/setup-external/index.js delete mode 100644 lib/setup-dependency/setup-external/resolve-is-to-be-linked.js delete mode 100644 lib/setup-dependency/setup-local/generate-link.js diff --git a/README.md b/README.md index bd4ae96..1994e6c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ [![Build status][build-image]][build-url] [![npm version][npm-image]][npm-url] -_Note: Due to quirky way of how `npm link ` works in npm v7 (it manipulates also other not related dependencies in `node_modules`). this package at this point doesn't work well with npm v7. This issue will be addressed with next major release_ - # npm-cross-link ## Automate `npm link` across maintained packages and projects @@ -13,12 +11,14 @@ npm install -g npm-cross-link ### Use case -You maintain many npm packages and prefer to cross install them via `npm link`, handling such setup manually can be time taking and error-prone. +You maintain many distinct npm packages and prefer to cross link them across each other, handling such setup manually can be time taking and error-prone. -`npm-cross-link` is the npm installer that ensures all latest versions of dependencies are linked to global folder -and non latest are installed on spot (but with its deep dependencies located in its own `node_modules`) +`npm-cross-link` is packages installer which installs dependencies into `node_modules` the following way: -For maintained packages, it ensures its local installation is linked into global folder +- Dependencies which expect to rely on some peer dependencies, are placed directly in `node_modules` +- All other dependencies are linked: + - Maintained dependencies referenced by _latest_ versions are linked to its corresponding repository folders + - All others are installed once in dedicated cache folder, and are linked into that folder ### How it works? @@ -29,10 +29,8 @@ When running `npm-cross-link -g ` for maintained package, or when 1. If package repository is not setup, it is cloned into corresponding folder (`~/npm-packages/` by default). Otherwise optionally new changes from remote can be pulled (`--pull`) and committed changes pushed (`--push`) 2. All maintained project dependencies (also `devDependencies` and eventual `optionalDependencies`) are installed/updated according to same flow. - - Not maintained dependencies (not found in `packagesMeta`) if at latest version are ensured to be installed globally and npm linked to global npm folder. Otherwise they're installed on spot but with its dependencies contained in dependency folder (not top level node_modules). - - Maintained project dependencies (those found in `packagesMeta`) if referenced version matches local, are simply cross linked, otherwise they're istalled on spot (with its dependencies contained in dependency folder, not top level node_modules). - -3. Package is ensured to be linked to global npm folder + - Not maintained dependencies (not found in `packagesMeta`) are ensured to be installed in cache and linked (unless they depend on some peer dependencies, then they're copied directly into `node_modules`) + - Maintained project dependencies (those found in `packagesMeta`) if referenced version matches local, are simply cross linked, otherwise they're linked from cache folder All important events and findings are communicated via logs (level of output can be fine tuned via [LOG_LEVEL](https://github.com/medikoo/log/#log_level) env setting). @@ -40,17 +38,11 @@ As each dependency is installed individually (and maintained packages are being #### npm resolution -When relying on npm, it relies on version as accessible via command line. - -If you rely on global Node.js installation, then Node.js update doesn't change location of global npm folder, so updates to Node.js are free from side effects when package links are concerned. - -However when relying on [nvm](https://github.com/creationix/nvm), different npm is used with every different Node.js version, which means each Node.js/npm version points to other npm global folder. That's not harmful per se, but on reinstallation all links would be updated to reflect new path. - -To avoid confusion it's better to rely on global installation. Still [nvm](https://github.com/creationix/nvm) is great for checking this project out (as then globally installed packages are not affected). +Internally `npm-cross-link` relies on `npm` being accesible to prepare cached versions of installed packages. #### Limitations -All subdependencies of project dependencies are installed within dependencies `node_modules` folders. It means that if e.g. dependency `A` and dependency `B`, depend on same version of dependency `C`, (and they're not maintained packages, so they're either linked to global installation or installed on spot) they will use different installations of `C`. +All subdependencies of project dependencies are installed within dependencies `node_modules` folders. It means that if e.g. dependency `A` and dependency `B`, depend on same version of dependency `C`, (and they're not maintained packages, so they're either linked to cache or installed on spot) they will use different installations of `C`. npm since early days ensured that in such scenarios `C` is installed top level (so it's shared among `A` and `B`), npm-cross-link doesn't ensure that. @@ -68,7 +60,7 @@ Installs or updates given project dependencies. If dependency version is not spe #### `npm-cross-link -g [...options] ...[<@scope>/]` -Installs or updates given packages globally. Due to `npm-cross-link` installation rules it's only latest versions of packages that are globally linked. +Installs or updates given packages on its own. If it's maintained package, then it's ensured in resolved maintained folder, in all other cases packages is simply ensured to be installed in cache #### `npm-cross-link-update-all [...options]` @@ -78,7 +70,7 @@ Updates all are already installed maintained packages - `--pull` - Pull eventual new updates from remote - `--push` - For all updated packages push eventually committed changes to remote -- `--bump-deps` - (only non global installations) Bump version ranges of dependencies in `package.json` +- `--bump-deps` - (only non-global installations) Bump version ranges of dependencies in `package.json` - `--no-save` - (only for dependencies install) Do not save dependency to `package.json` (effective only if its not there yet) - `--dev` - (only for dependencies install) Force to store updated version in `devDependencies` section - `--optional` - (only for dependencies install) Force to store updated version in `optionalDependencies` section @@ -174,7 +166,7 @@ Installer by default removes all dependencies not referenced in package `package #### `toBeCopiedDependencies` -Optional. Eventual list of non maintained dependencies that in all cases should be copied into `node_modules` and not linked to global installation. +Optional. Eventual list of non maintained dependencies that in all cases should be copied into `node_modules` and not linked to cache [build-image]: https://github.com/medikoo/npm-cross-link/workflows/Integrate/badge.svg [build-url]: https://github.com/medikoo/npm-cross-link/actions?query=workflow%3AIntegrate diff --git a/install-packages-globally.js b/install-packages-globally.js index a891d0c..47b7100 100644 --- a/install-packages-globally.js +++ b/install-packages-globally.js @@ -1,30 +1,21 @@ "use strict"; -const ensureObject = require("es5-ext/object/valid-object") - , toPlainObject = require("es5-ext/object/normalize-options") - , ensurePackageName = require("./lib/ensure-package-name") - , ensureConfiguration = require("./lib/ensure-user-configuration") - , createProgressData = require("./lib/create-progress-data") - , installPackageGlobally = require("./lib/install-package-globally") - , installMaintainedPackage = require("./lib/install-maintained-package"); +const toPlainObject = require("es5-ext/object/normalize-options") + , ensureConfiguration = require("./lib/ensure-user-configuration") + , createProgressData = require("./lib/create-progress-data") + , installPackageGlobally = require("./lib/install-package-globally") + , tokenizePackageSpecs = require("./lib/utils/tokenize-package-specs"); module.exports = (packageNames, userConfiguration, inputOptions = {}) => { - packageNames = Array.from(ensureObject(packageNames), ensurePackageName); + const packageSpecsData = tokenizePackageSpecs(packageNames); userConfiguration = ensureConfiguration(userConfiguration); inputOptions = toPlainObject(inputOptions); const progressData = createProgressData(); - const promise = packageNames.reduce(async (previousPromise, name) => { + const promise = packageSpecsData.reduce(async (previousPromise, packageContext) => { await previousPromise; - progressData.topPackageName = name; - const isExternal = !userConfiguration.packagesMeta[name]; - const packageContext = { name }; - if (isExternal) { - return installPackageGlobally( - packageContext, userConfiguration, inputOptions, progressData - ); - } - return installMaintainedPackage( + progressData.topPackageName = packageContext.name; + return installPackageGlobally( packageContext, userConfiguration, inputOptions, progressData ); }, Promise.resolve()); diff --git a/lib/cache-package/index.js b/lib/cache-package/index.js index fa28ca5..94817cb 100644 --- a/lib/cache-package/index.js +++ b/lib/cache-package/index.js @@ -23,7 +23,6 @@ module.exports = memoizee( potentialMethod => (methodData = potentialMethod.isApplicable(name, version, externalContext)) ); - const versionCacheName = await method.resolveCacheName(version, methodData); log.info("%s cache name for %s is %s", name, version, versionCacheName); const versionCachePath = versionCacheName && resolve(cachePath, name, versionCacheName); @@ -51,6 +50,7 @@ module.exports = memoizee( ); } } + log.notice("prepared %s", `${ name }@${ version }`, versionCachePath); if (!versionCachePath) return packageTmpDir; await rename(packageTmpDir, versionCachePath, { intermediate: true }); diff --git a/lib/cache-package/sem-ver.js b/lib/cache-package/sem-ver.js index f9ba398..0107d48 100644 --- a/lib/cache-package/sem-ver.js +++ b/lib/cache-package/sem-ver.js @@ -35,7 +35,7 @@ module.exports = { delete pkgJson.devDependencies; if (pkgJson.scripts) delete pkgJson.scripts.prepare; await writeFile(pkgJsonPath, JSON.stringify(pkgJson)); - await runProgram("npm", ["install", "--production"], { + await runProgram("npm", ["install", "--production", "--ignore-scripts"], { cwd: tmpDir, logger: log.levelRoot.get("npm:install") }); diff --git a/lib/install-maintained-package/index.js b/lib/install-maintained-package/index.js index af61bda..1b51377 100644 --- a/lib/install-maintained-package/index.js +++ b/lib/install-maintained-package/index.js @@ -7,13 +7,11 @@ const optionalChaining = require("es5-ext/optional-chaining") , rm = require("fs2/rm") , NpmCrossLinkError = require("../npm-cross-link-error") , getPackageJson = require("../get-package-json") - , getNpmModulesPath = require("../get-npm-modules-path") , cleanupNpmInstall = require("../cleanup-npm-install") , setupRepository = require("../setup-repository") , resolveExternalContext = require("../resolve-external-context") , removeNonDirectDependencies = require("../remove-non-direct-dependencies") , resolveMaintainedPackagePath = require("../resolve-maintained-package-path") - , npmLink = require("./npm-link") , finalize = require("./finalize"); module.exports = async (packageContext, userConfiguration, inputOptions, progressData) => { @@ -56,7 +54,6 @@ module.exports = async (packageContext, userConfiguration, inputOptions, progres packageContext.packageJson = getPackageJson(path); } - packageContext.linkedPath = resolve(await getNpmModulesPath(), name); await resolveExternalContext(packageContext, progressData); if ( packageContext.externalContext.latestVersion && @@ -71,9 +68,6 @@ module.exports = async (packageContext, userConfiguration, inputOptions, progres // Cleanup outcome of eventual previous npm crashes await cleanupNpmInstall(packageContext); - // Link package - await npmLink(packageContext); - // Setup dependencies // (cyclic module dependency, hence required on spot) await require("../setup-dependencies")( diff --git a/lib/install-maintained-package/npm-link.js b/lib/install-maintained-package/npm-link.js deleted file mode 100644 index c443cb8..0000000 --- a/lib/install-maintained-package/npm-link.js +++ /dev/null @@ -1,46 +0,0 @@ -"use strict"; - -const { resolve } = require("path") - , log = require("log").get("npm-cross-link") - , isSymlink = require("fs2/is-symlink") - , rm = require("fs2/rm") - , cleanupNpmInstall = require("../cleanup-npm-install") - , getNpmModulesPath = require("../get-npm-modules-path") - , runProgram = require("../run-program"); - -module.exports = async packageContext => { - const { path, name, installationJobs, isNameMismatch } = packageContext; - const symlinkPath = resolve(await getNpmModulesPath(), name); - if (await isSymlink(symlinkPath, { linkPath: path })) { - if (isNameMismatch) await rm(symlinkPath, { loose: true, recursive: true, force: true }); - return; - } - if (isNameMismatch) return; - - installationJobs.add("link"); - - await Promise.all([ - rm(symlinkPath, { loose: true, recursive: true, force: true }), - // If there are linked packages in node_modules `npm link` tends to = - // mess with its dependencies. To avoid that node_modules is removed prior linking - rm(resolve(path, "node_modules"), { loose: true, recursive: true, force: true }) - ]); - - log.info("link %s", name); - try { - await runProgram("npm", ["link", "--force"], { - cwd: path, - logger: log.levelRoot.get("npm:link") - }); - } catch (error) { - await cleanupNpmInstall(packageContext); - if (await isSymlink(symlinkPath, { linkPath: path })) { - log.warn("npm crashed, still link was created so that's ignored"); - } else { - throw error; - } - } - - // Remove node_modules so it doesn't give false hints to future installation steps - await rm(resolve(path, "node_modules"), { loose: true, recursive: true, force: true }); -}; diff --git a/lib/install-package-globally.js b/lib/install-package-globally.js index e47d06a..4a01561 100644 --- a/lib/install-package-globally.js +++ b/lib/install-package-globally.js @@ -1,67 +1,58 @@ "use strict"; -const { resolve } = require("path") - , log = require("log").get("npm-cross-link") - , wait = require("timers-ext/promise/sleep") - , rm = require("fs2/rm") - , NpmCrossLinkError = require("./npm-cross-link-error") - , getNpmModulesPath = require("./get-npm-modules-path") - , runProgram = require("./run-program") - , nonOverridableExternals = require("./non-overridable-externals") - , resolveExternalContext = require("./resolve-external-context"); +const log = require("log").get("npm-cross-link") + , wait = require("timers-ext/promise/sleep") + , semver = require("semver") + , installMaintainedPackage = require("./install-maintained-package") + , NpmCrossLinkError = require("./npm-cross-link-error") + , resolveExternalContext = require("./resolve-external-context") + , resolveLocalContext = require("./resolve-local-context") + , cachePackage = require("./cache-package"); module.exports = async (packageContext, userConfiguration, inputOptions, progressData) => { - const { name } = packageContext; + const { name, versionRange } = packageContext; const { packagesMeta } = userConfiguration; - if (packagesMeta[name]) { - throw new NpmCrossLinkError( - `Cannot install "${ name }" globally. It's not recognized as a maintained package` - ); - } - if (nonOverridableExternals.has(name)) { - throw new NpmCrossLinkError( - `Cannot install "${ name }" globally. It should not be installed with npm-cross-link` - ); - } + const isExternal = !packagesMeta[name]; + packageContext.installationJobs = new Set(); // Ensure to emit "start" event in next event loop await wait(); progressData.emit("start", packageContext); - const linkedPath = (packageContext.linkedPath = resolve(await getNpmModulesPath(), name)); + if (!isExternal) { + const { ongoing, done } = progressData; + // Esure we have it installed locally + if (!ongoing.has(name) && !done.has(name)) { + await installMaintainedPackage({ name }, userConfiguration, inputOptions, progressData); + } - const externalContext = await resolveExternalContext(packageContext, progressData); - if (!externalContext) { - throw new NpmCrossLinkError( - `Cannot install "${ name }" globally. It's doesn't seem to be published` + if (!versionRange || versionRange === "latest") { + progressData.emit("end", packageContext); + return; + } + + const { localVersion } = resolveLocalContext( + packageContext, userConfiguration, progressData ); - } - const { globallyInstalledVersion, latestVersion } = externalContext; - if (!latestVersion) { - throw new NpmCrossLinkError( - `Cannot install "${ name }" globally. There's no latest version tagged` + + if (localVersion && semver.satisfies(localVersion, versionRange)) { + progressData.emit("end", packageContext); + return; + } + log.error( + "%s will have %s version installed externally as non latest version is referenced", + name, versionRange ); } - // Lastest version supported, ensure it's linked - if (globallyInstalledVersion === latestVersion) return; - if (globallyInstalledVersion) { - packageContext.installationType = "update"; - log.notice( - "%s outdated at global folder (got %s expected %s), upgrading", name, - globallyInstalledVersion, latestVersion + const externalContext = await resolveExternalContext(packageContext, progressData); + if (!externalContext) { + throw new NpmCrossLinkError( + `Cannot install "${ name }" globally. It's doesn't seem to be published` ); - } else { - packageContext.installationType = "install"; - log.notice("%s not installed at global folder, linking", name); } - // Global node_modules hosts outdated version, cleanup - await rm(linkedPath, { loose: true, recursive: true, force: true }); - - await runProgram("npm", ["install", "-g", `${ name }@${ latestVersion }`], { - logger: log.levelRoot.get("npm:link") - }); - - progressData.emit("end", packageContext); + await cachePackage( + name, packageContext.latestSupportedPublishedVersion || versionRange, externalContext + ); }; diff --git a/lib/non-overridable-externals.js b/lib/non-overridable-externals.js deleted file mode 100644 index b5e69a3..0000000 --- a/lib/non-overridable-externals.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -module.exports = new Set(["npm"]); diff --git a/lib/resolve-external-context.js b/lib/resolve-external-context.js index ca20093..fff2033 100644 --- a/lib/resolve-external-context.js +++ b/lib/resolve-external-context.js @@ -1,11 +1,8 @@ "use strict"; -const optionalChaining = require("es5-ext/optional-chaining") - , log = require("log").get("npm-cross-link") - , isDirectory = require("fs2/is-directory") - , semver = require("semver") - , getPackageJson = require("./get-package-json") - , getMetadata = require("./get-metadata"); +const log = require("log").get("npm-cross-link") + , semver = require("semver") + , getMetadata = require("./get-metadata"); const getVersions = ({ externalContext: { metadata } }) => Object.keys(metadata.versions); @@ -18,12 +15,6 @@ const resolveStableVersions = ({ externalContext: { metadata } }) => }) .map(([version]) => version); -const getGloballyInstalledVersion = async ({ linkedPath }) => { - // Accept installation only if in directory (not symlink) - if (!(await isDirectory(linkedPath))) return null; - return optionalChaining(getPackageJson(linkedPath), "version") || null; -}; - const resolveLatestSupportedVersion = packageContext => { const { dependentContext, name, versionRange, isSemVerVersionRange } = packageContext; if (!isSemVerVersionRange) return null; @@ -46,7 +37,6 @@ module.exports = async (packageContext, progressData) => { const metadata = await getMetadata(name); if (metadata) { externals.set(name, { - globallyInstalledVersion: await getGloballyInstalledVersion(packageContext), latestVersion: metadata["dist-tags"].latest, latestHasPeers: Boolean( metadata.versions[metadata["dist-tags"].latest].peerDependencies diff --git a/lib/setup-dependency/index.js b/lib/setup-dependency/index.js index 55d7eca..8bc5042 100644 --- a/lib/setup-dependency/index.js +++ b/lib/setup-dependency/index.js @@ -1,12 +1,11 @@ "use strict"; -const { resolve } = require("path") - , log = require("log").get("npm-cross-link") - , getNpmModulesPath = require("../get-npm-modules-path") - , setupExternal = require("./setup-external") - , setupLocal = require("./setup-local") - , resolveLogLevel = require("./resolve-log-level") - , isSemVerRange = require("../utils/is-sem-ver-range"); +const { resolve } = require("path") + , log = require("log").get("npm-cross-link") + , installExternal = require("./install-external") + , setupLocal = require("./setup-local") + , resolveLogLevel = require("./resolve-log-level") + , isSemVerRange = require("../utils/is-sem-ver-range"); const getVersionRange = ({ dependentContext, name }) => { if ( @@ -34,7 +33,6 @@ module.exports = async (dependencyContext, userConfiguration, inputOptions, prog const { dependentContext, name, isExternal } = dependencyContext; dependencyContext.path = resolve(dependentContext.path, "node_modules", name); - dependencyContext.linkedPath = resolve(await getNpmModulesPath(), name); const packageJsonVersionRange = getVersionRange(dependencyContext); dependencyContext.packageJsonVersionRange = packageJsonVersionRange; @@ -52,6 +50,6 @@ module.exports = async (dependencyContext, userConfiguration, inputOptions, prog log.warn("%s doesn't reference %s as dependency", dependentContext.name, name); } - if (isExternal) return setupExternal(dependencyContext, userConfiguration, progressData); + if (isExternal) return installExternal(dependencyContext, userConfiguration, progressData); return setupLocal(dependencyContext, userConfiguration, inputOptions, progressData); }; diff --git a/lib/setup-dependency/install-external/index.js b/lib/setup-dependency/install-external/index.js index e19166a..a6a172e 100644 --- a/lib/setup-dependency/install-external/index.js +++ b/lib/setup-dependency/install-external/index.js @@ -1,17 +1,22 @@ "use strict"; -const optionalChaining = require("es5-ext/optional-chaining") - , { join, resolve } = require("path") - , copyDir = require("fs2/copy-dir") - , isDirectory = require("fs2/is-directory") - , rm = require("fs2/rm") - , log = require("log").get("npm-cross-link") - , getPackageJson = require("../../get-package-json") - , muteErrorIfOptional = require("../mute-error-if-optional") - , cachePackage = require("../../cache-package") - , resolveBinariesDict = require("../../resolve-package-binaries-dict") - , binaryHandler = require("./binary-handler") - , isCoherent = require("./is-coherent"); +const optionalChaining = require("es5-ext/optional-chaining") + , { join, resolve, relative, dirname } = require("path") + , copyDir = require("fs2/copy-dir") + , isDirectory = require("fs2/is-directory") + , isSymlink = require("fs2/is-symlink") + , symlink = require("fs2/symlink") + , rm = require("fs2/rm") + , lstat = require("fs2/lstat") + , log = require("log").get("npm-cross-link") + , getPackageJson = require("../../get-package-json") + , muteErrorIfOptional = require("../mute-error-if-optional") + , cachePackage = require("../../cache-package") + , resolveBinariesDict = require("../../resolve-package-binaries-dict") + , resolveExternalContext = require("../../resolve-external-context") + , binaryHandler = require("./binary-handler") + , isCoherent = require("./is-coherent") + , resolveIsToBeLinked = require("./resolve-is-to-be-linked"); const mapBinaries = async ({ name, path, dependentContext }) => { const binDict = resolveBinariesDict(path); @@ -26,15 +31,35 @@ const mapBinaries = async ({ name, path, dependentContext }) => { ); }; -module.exports = async dependencyContext => { - const { - name, - path, - dependentContext, - externalContext, - latestSupportedPublishedVersion, - versionRange - } = dependencyContext; +module.exports = async (dependencyContext, userConfiguration, progressData) => { + const externalContext = await resolveExternalContext(dependencyContext, progressData); + + const { name, path, dependentContext, latestSupportedPublishedVersion, versionRange } = + dependencyContext; + + const targetVersion = latestSupportedPublishedVersion || versionRange; + const sourceDirname = await muteErrorIfOptional(dependencyContext, () => + cachePackage(name, targetVersion, externalContext) + ); + if (!sourceDirname) return; + const isToBeLinked = await resolveIsToBeLinked( + dependencyContext, userConfiguration, progressData + ); + + if (isToBeLinked) { + const linkedPath = relative(dirname(path), sourceDirname); + if (await isSymlink(path, { linkPath: linkedPath })) return; + const isInstalled = await lstat(path, { loose: true }); + if (isInstalled) await rm(path, { loose: true, recursive: true, force: true }); + log.notice("%s linking %s @ %s", dependentContext.name, name, targetVersion); + await symlink(linkedPath, path, { intermediate: true }); + dependentContext.installationJobs.add( + `${ isInstalled ? "update" : "install" }-dependency:${ name }` + ); + + return; + } + const isInstalled = await isDirectory(path); const dependencyPackageJson = isInstalled ? getPackageJson(path) : null; if (latestSupportedPublishedVersion && isInstalled) { @@ -47,11 +72,7 @@ module.exports = async dependencyContext => { log.notice("%s not coherent %s, reinstalling", dependentContext.name, name); } } - const targetVersion = latestSupportedPublishedVersion || versionRange; - const sourceDirname = await muteErrorIfOptional(dependencyContext, () => - cachePackage(name, targetVersion, externalContext) - ); - if (!sourceDirname) return; + if ( !latestSupportedPublishedVersion && isInstalled && diff --git a/lib/setup-dependency/install-external/resolve-is-to-be-linked.js b/lib/setup-dependency/install-external/resolve-is-to-be-linked.js new file mode 100644 index 0000000..bb7379d --- /dev/null +++ b/lib/setup-dependency/install-external/resolve-is-to-be-linked.js @@ -0,0 +1,25 @@ +"use strict"; + +const log = require("log").get("npm-cross-link"); + +module.exports = async (dependencyContext, { toBeCopiedDependencies }) => { + const { name, dependentContext, externalContext } = dependencyContext; + const { latestHasPeers } = externalContext; + if (toBeCopiedDependencies.has(name)) { + log.info( + "%s will have %s installed on spot, " + + "as it's marked as one of \"to be copied\" dependencies in user config", + dependentContext.name, name + ); + return false; + } + + if (latestHasPeers) { + log.info( + "%s will have %s installed on spot, as it lists peer dependencies", + dependentContext.name, name + ); + return false; + } + return true; +}; diff --git a/lib/setup-dependency/setup-external/index.js b/lib/setup-dependency/setup-external/index.js deleted file mode 100644 index bfc6e9a..0000000 --- a/lib/setup-dependency/setup-external/index.js +++ /dev/null @@ -1,53 +0,0 @@ -"use strict"; - -const log = require("log").get("npm-cross-link") - , isSymlink = require("fs2/is-symlink") - , rm = require("fs2/rm") - , NpmCrossLinkError = require("../../npm-cross-link-error") - , runProgram = require("../../run-program") - , installExternal = require("../install-external") - , muteErrorIfOptional = require("../mute-error-if-optional") - , resolveIsToBeLinked = require("./resolve-is-to-be-linked"); - -module.exports = async (dependencyContext, userConfiguration, progressData) => { - const { linkedPath, name, path, dependentContext } = dependencyContext; - const isToBeLinked = await resolveIsToBeLinked( - dependencyContext, userConfiguration, progressData - ); - - if (!isToBeLinked) { - await installExternal(dependencyContext); - return; - } - const { externalContext } = dependencyContext - , { globallyInstalledVersion, latestVersion } = externalContext; - - if (globallyInstalledVersion !== latestVersion) { - if (globallyInstalledVersion) { - log.notice( - "external dependency %s outdated at global folder (got %s expected %s), upgrading", - name, globallyInstalledVersion, latestVersion - ); - } else { - log.notice("external dependency %s not linked at global folder, linking", name); - } - // Global node_modules hosts outdated version, cleanup - await rm(linkedPath, { loose: true, recursive: true, force: true }); - } else if (await isSymlink(path, { linkPath: linkedPath })) { - return; - } - if (!latestVersion) { - throw new NpmCrossLinkError(`Cannot install non published ${ name }`); - } - - dependentContext.installationJobs.add(`install-dependency:${ name }`); - log.info("%s link external dependency %s", dependentContext.name, name); - await rm(path, { loose: true, recursive: true, force: true }); - await muteErrorIfOptional(dependencyContext, async () => { - await runProgram("npm", ["link", "--force", `${ name }@${ latestVersion }`], { - cwd: dependentContext.path, - logger: log.levelRoot.get("npm:link") - }); - externalContext.globallyInstalledVersion = latestVersion; - }); -}; diff --git a/lib/setup-dependency/setup-external/resolve-is-to-be-linked.js b/lib/setup-dependency/setup-external/resolve-is-to-be-linked.js deleted file mode 100644 index 0ec7792..0000000 --- a/lib/setup-dependency/setup-external/resolve-is-to-be-linked.js +++ /dev/null @@ -1,62 +0,0 @@ -"use strict"; - -const log = require("log").get("npm-cross-link") - , resolveExternalContext = require("../../resolve-external-context") - , nonOverridableExternals = require("../../non-overridable-externals") - , resolveLogLevel = require("../resolve-log-level"); - -module.exports = async (dependencyContext, { toBeCopiedDependencies }, progressData) => { - const { name, dependentContext, versionRange, isSemVerVersionRange } = dependencyContext; - if (versionRange && !isSemVerVersionRange) { - log.info( - "%s will have %s installed on spot, " + - "as it's referenced with non-semver version range", - dependentContext.name, name - ); - return false; - } - const externalContext = await resolveExternalContext(dependencyContext, progressData); - const { latestVersion, latestHasPeers } = externalContext; - if (nonOverridableExternals.has(name)) { - log.info( - "%s will have %s installed on spot, " + - "as it's marked as non-overridable for global install", - dependentContext.name, name - ); - return false; - } - if (toBeCopiedDependencies.has(name)) { - log.info( - "%s will have %s installed on spot, " + - "as it's marked as one of \"to be copied\" dependencies in user config", - dependentContext.name, name - ); - return false; - } - - if (latestHasPeers) { - log.info( - "%s will have %s installed on spot, as it lists peer dependencies", - dependentContext.name, name - ); - return false; - } - const { latestSupportedPublishedVersion } = dependencyContext; - if (!latestSupportedPublishedVersion) { - if (versionRange) { - log.error( - "%s references %s by %s version range, which doesn't mach any published one", - dependentContext.name, name, versionRange - ); - } - } else if (latestSupportedPublishedVersion !== latestVersion) { - // Latest version not supported, therefore dependency is installed directly (not linked) - log[resolveLogLevel(dependentContext, progressData)]( - "%s references %s by %s version range, which doesn't match the latest", - dependentContext.name, name, versionRange - ); - // Expects outdated version, therefore do not link but install in place (if needed) - return false; - } - return true; -}; diff --git a/lib/setup-dependency/setup-local/generate-link.js b/lib/setup-dependency/setup-local/generate-link.js deleted file mode 100644 index 878cd26..0000000 --- a/lib/setup-dependency/setup-local/generate-link.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict"; - -const rm = require("fs2/rm") - , log = require("log").get("npm-cross-link") - , runProgram = require("../../run-program"); - -module.exports = async dependencyContext => { - const { dependentContext, name, path } = dependencyContext; - log.info("%s link dependency %s", dependentContext.name, name); - await rm(path, { loose: true, recursive: true, force: true }); - await runProgram("npm", ["link", "--force", name], { - cwd: dependentContext.path, - logger: log.levelRoot.get("npm:link") - }); -}; diff --git a/lib/setup-dependency/setup-local/index.js b/lib/setup-dependency/setup-local/index.js index c79e574..63d0815 100644 --- a/lib/setup-dependency/setup-local/index.js +++ b/lib/setup-dependency/setup-local/index.js @@ -1,26 +1,27 @@ "use strict"; -const isSymlink = require("fs2/is-symlink") - , realpath = require("fs2/realpath") - , stat = require("fs2/stat") - , semver = require("semver") - , log = require("log").get("npm-cross-link") - , resolveExternalContext = require("../../resolve-external-context") - , installExternal = require("../install-external") - , resolveLogLevel = require("../resolve-log-level") - , resolveLocalContext = require("../../resolve-local-context") - , generateLink = require("./generate-link"); +const { dirname, relative } = require("path") + , fsp = require("fs").promises + , isSymlink = require("fs2/is-symlink") + , lstat = require("fs2/lstat") + , rm = require("fs2/rm") + , semver = require("semver") + , log = require("log").get("npm-cross-link") + , resolveExternalContext = require("../../resolve-external-context") + , resolveMaintainedPackagePath = require("../../resolve-maintained-package-path") + , installExternal = require("../install-external") + , resolveLogLevel = require("../resolve-log-level") + , resolveLocalContext = require("../../resolve-local-context"); module.exports = async (dependencyContext, userConfiguration, inputOptions, progressData) => { - const { dependentContext, linkedPath, name, path, versionRange, isSemVerVersionRange } = - dependencyContext; + const { dependentContext, name, path, versionRange, isSemVerVersionRange } = dependencyContext; if (versionRange && !isSemVerVersionRange) { log.info( "%s will have %s installed externally, as it's not referenced by semver range", dependentContext.name, name ); - await installExternal(dependencyContext); + await installExternal(dependencyContext, userConfiguration, progressData); return; } const { ongoing, done } = progressData; @@ -53,7 +54,7 @@ module.exports = async (dependencyContext, userConfiguration, inputOptions, prog ); } // Expects outdated version, therefore do not link but install in place (if needed) - await installExternal(dependencyContext); + await installExternal(dependencyContext, userConfiguration, progressData); return; } log.error( @@ -62,38 +63,18 @@ module.exports = async (dependencyContext, userConfiguration, inputOptions, prog ); } - // In valid scenarios `linkedPath` exists. - // It may not, if maintained package was referenced in its repository - // (in currently checkout state) under different name than one provided in config - const stats = await stat(linkedPath, { loose: true }); - if (stats) { - if (await isSymlink(path, { linkPath: await realpath(linkedPath), recursive: true })) { - const dependencyInstallationJobs = (done.get(name) || ongoing.get(name)) - .installationJobs; - if (dependencyInstallationJobs.size) { - dependentContext.installationJobs.add( - `${ - dependencyInstallationJobs.has("clone") ? "install" : "update" - }-dependency:${ name }` - ); - } - return; - } - log.info( - "%s dependency %s at %s doesn't resemble %s", dependentContext.name, name, path, - linkedPath - ); - } else { - log.error( - "%s references %s which exposed itself under different name", dependentContext.name, - name - ); - } - - dependentContext.installationJobs.add(`install-dependency:${ name }`); - if (ongoing.has(name)) { - ongoing.get(name).installationHooks.after.push(() => generateLink(dependencyContext)); - return; - } - await generateLink(dependencyContext); + const linkedPath = relative( + dirname(path), resolveMaintainedPackagePath(name, userConfiguration) + ); + if (await isSymlink(path, { linkPath: linkedPath })) return; + log.info( + "%s dependency %s at %s doesn't resemble %s", dependentContext.name, name, path, linkedPath + ); + const isInstalled = await lstat(path, { loose: true }); + await rm(path, { loose: true, recursive: true, force: true }); + await fsp.symlink(linkedPath, path); + dependentContext.installationJobs.add( + `${ isInstalled ? "update" : "install" }-dependency:${ name }` + ); + log.notice("%s linking %s (as maintained package)", dependentContext.name, name); }; diff --git a/lib/utils/tokenize-package-specs.js b/lib/utils/tokenize-package-specs.js index 395cc43..0f9b645 100644 --- a/lib/utils/tokenize-package-specs.js +++ b/lib/utils/tokenize-package-specs.js @@ -2,15 +2,18 @@ const ensureObject = require("es5-ext/object/valid-object") , ensureString = require("es5-ext/object/validate-stringifiable-value") - , ensurePackageName = require("../ensure-package-name"); + , ensurePackageName = require("../ensure-package-name") + , isSemVerRange = require("./is-sem-ver-range"); module.exports = packageSpecs => Array.from(ensureObject(packageSpecs), packageSpec => { packageSpec = ensureString(packageSpec); if (packageSpec.slice(1).includes("@")) { + const versionRange = packageSpec.slice(packageSpec.lastIndexOf("@") + 1); return { name: ensurePackageName(packageSpec.slice(0, packageSpec.lastIndexOf("@"))), - versionRange: packageSpec.slice(packageSpec.lastIndexOf("@") + 1) + versionRange, + isSemVerVersionRange: isSemVerRange(versionRange) }; } return { name: ensurePackageName(packageSpec) };