npx-import
Advanced tools
Comparing version 1.0.0 to 1.0.1
@@ -60,3 +60,3 @@ import semver from 'semver'; | ||
for (const p of Array.isArray(pkg) ? pkg : [pkg]) { | ||
const { name, version, path } = parseAndValidate(p); | ||
const { name, version, path, exact } = parseAndValidate(p); | ||
if (packages[name]) | ||
@@ -70,2 +70,3 @@ throw new Error(`npx-import cannot import the same package twice! Got: '${p}' but already saw '${name}' earlier!`); | ||
version, | ||
exact, | ||
path, | ||
@@ -90,3 +91,3 @@ imported, | ||
} | ||
return { name, version, path }; | ||
return { name, version, path, exact: semver.parse(version) !== null }; | ||
} | ||
@@ -112,3 +113,6 @@ async function tryImport(packageWithPath) { | ||
async function installAndReturnDir(packages, logger) { | ||
const installPackage = `npx -y ${packages.map((p) => `-p ${formatForCLI(p)}`).join(' ')}`; | ||
const offline = packages.every((p) => p.exact); | ||
const installPackage = `npx --prefer-${offline ? 'offline' : 'online'} -y ${packages | ||
.map((p) => `-p ${formatForCLI(p)}`) | ||
.join(' ')}`; | ||
logger(`Installing... (${installPackage})`); | ||
@@ -115,0 +119,0 @@ const emitPath = `node -e 'console.log(process.env.PATH)'`; |
{ | ||
"name": "npx-import", | ||
"version": "1.0.0", | ||
"version": "1.0.1", | ||
"description": "Runtime dependencies, installed as if by magic ✨", | ||
@@ -22,2 +22,3 @@ "main": "lib/index.js", | ||
"@types/validate-npm-package-name": "^4.0.0", | ||
"chalk": "^5.0.1", | ||
"prettier": "^2.7.1", | ||
@@ -24,0 +25,0 @@ "typescript": "^4.7.4", |
@@ -5,3 +5,3 @@ # 🧙♂️ `npx-import` 🧙♀️ | ||
[![twitter](https://img.shields.io/badge/author-@glenmaddern-blue.svg?style=flat)](https://twitter.com/glenmaddern) [![npm](https://img.shields.io/npm/v/npx-import)](https://www.npmjs.com/package/npx-import) [![GitHub last commit](https://img.shields.io/github/last-commit/geelen/npx-import)](https://github.com/geelen/npx-import) | ||
[![twitter](https://img.shields.io/badge/@glenmaddern-blue.svg?style=flat&logo=twitter&label=)](https://twitter.com/glenmaddern) [![GitHub last commit](https://img.shields.io/github/last-commit/geelen/npx-import?logo=github&style=flat&label=)](https://github.com/geelen/npx-import) [![npm](https://img.shields.io/npm/v/npx-import?label=&logo=npm)](https://www.npmjs.com/package/npx-import) | ||
@@ -97,5 +97,5 @@ `npx-import` can be used as a drop-in replacement for [dynamic `import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import): | ||
import { npxImport } from 'npx-import' | ||
import type BigDep from 'big-dep' | ||
type BigDepType = typeof import('big-dep') | ||
const bigDep = await npxImport<BigDep>('big-dep') | ||
const bigDep = await npxImport<BigDepType>('big-dep') | ||
``` | ||
@@ -111,4 +111,6 @@ | ||
Any package specifier that's valid in `package.json` will work here: e.g. `^1.0.0`, `~2.3.0`, `>4.0.0` | ||
Any package specifier that's valid in `package.json` will work here: e.g. `^1.0.0`, `~2.3.0`, `>4.0.0`, `@latest`, `@next`, etc. | ||
Note: there is a speed benefit from using exact versions. `npxImport(pkg-a@1.2.3)` will run `npx --prefer-offline` under the hood, making it faster after the first run (since it doesn't first check the NPM registry for newer versions). | ||
* You can also install multiple packages at once: | ||
@@ -171,4 +173,13 @@ | ||
### 😵💫 What about multiple projects? Won't you get conflicting versions? | ||
### 🤪 Doesn't this mean dependencies gets repeatedly downloaded & installed? | ||
No! `npx` maintains a cache in the user's home directory. If a cached package is found, `npx` will (by default) hit NPM to check if there's any new versions for that specifier, and if not, return the cache. `npxImport` adds a small optimisation—if you specify an exact package version (e.g. `@7.8.2`), it'll run `npx --prefer-offline` to skip the NPM check. | ||
So new packages are only downloaded & installed when: | ||
* It's the first time a particular package/version combo is seen (see next section) | ||
* No locked version was provided and there's a new version on NPM | ||
### 😵💫 What about multiple projects? Doesn't the cache mean projects can clobber/overwrite/conflict with each other? | ||
As it turns out, no! While I wasn't paying attention, `npx` got really smart! To understand why, we need to look at how `npx` works: | ||
@@ -259,3 +270,3 @@ | ||
So `npx` is doing exactly the same as an `npm install`, with a `package.json`, `package-lock.json`, `node_modules` etc. It's just dynamically creating directories based on some hash of its inputs. It's super clever! | ||
So `npx` is doing exactly the same as an `npm install`, with a `package.json`, `package-lock.json`, `node_modules` etc. It's just dynamically creating directories based on some hash of its inputs. So the only way two projects can use the same package in the cache is if they _both_ ask for _exactly_ the same packages & versions. It's super clever! | ||
@@ -262,0 +273,0 @@ ### 😐 But what about transitive deps? Won't you get duplication? |
@@ -14,2 +14,3 @@ import semver from 'semver' | ||
version: string | ||
exact: boolean | ||
path: string | ||
@@ -85,3 +86,3 @@ imported: typeof NOT_IMPORTABLE | any | ||
for (const p of Array.isArray(pkg) ? pkg : [pkg]) { | ||
const { name, version, path } = parseAndValidate(p) | ||
const { name, version, path, exact } = parseAndValidate(p) | ||
if (packages[name]) | ||
@@ -97,2 +98,3 @@ throw new Error( | ||
version, | ||
exact, | ||
path, | ||
@@ -122,3 +124,3 @@ imported, | ||
} | ||
return { name, version, path } | ||
return { name, version, path, exact: semver.parse(version) !== null } | ||
} | ||
@@ -147,3 +149,6 @@ | ||
async function installAndReturnDir(packages: Package[], logger: Logger) { | ||
const installPackage = `npx -y ${packages.map((p) => `-p ${formatForCLI(p)}`).join(' ')}` | ||
const offline = packages.every((p) => p.exact) | ||
const installPackage = `npx --prefer-${offline ? 'offline' : 'online'} -y ${packages | ||
.map((p) => `-p ${formatForCLI(p)}`) | ||
.join(' ')}` | ||
logger(`Installing... (${installPackage})`) | ||
@@ -150,0 +155,0 @@ const emitPath = `node -e 'console.log(process.env.PATH)'` |
import { npxImport, npxResolve } from '../lib/index.js' | ||
import chalk from 'chalk' | ||
console.log(`${chalk.green('❯')} node ./index.js --filename=image.png\n`) | ||
try { | ||
@@ -8,6 +11,5 @@ const leftPad = await import('left-pad') | ||
console.log(`This is a PNG! We'll have to compile imagemagick!`) | ||
const leftPad = await npxImport('left-pad@>1.0.0') | ||
await npxImport('left-pad@^1.1.0', log => console.log(' ' + chalk.gray(log.replace(/left-pad/,'imagemagick-utils')))) | ||
} | ||
console.log({ location: await npxResolve('left-pad@3.4.0') }) | ||
console.log(`Done!`) |
@@ -165,3 +165,3 @@ import { afterEach, describe, expect, test, vi } from 'vitest' | ||
expectExecaCommand( | ||
`npx -y -p broken-install@^2.0.0 node -e 'console.log(process.env.PATH)'`, | ||
`npx --prefer-online -y -p broken-install@^2.0.0 node -e 'console.log(process.env.PATH)'`, | ||
{ shell: true } | ||
@@ -183,3 +183,3 @@ ).returning(new Error('EXPLODED TRYING TO INSTALL')) | ||
expectExecaCommand( | ||
`npx -y -p left-pad@this-tag-no-exist node -e 'console.log(process.env.PATH)'`, | ||
`npx --prefer-online -y -p left-pad@this-tag-no-exist node -e 'console.log(process.env.PATH)'`, | ||
{ shell: true } | ||
@@ -203,3 +203,3 @@ ).returning(new Error('No matching version found for left-pad@this-tag-no-exist.')) | ||
expectExecaCommand('npx --version').returning({ stdout: '8.1.2' }) | ||
expectExecaCommand(`npx -y -p @org/pkg@my-tag node -e 'console.log(process.env.PATH)'`, { | ||
expectExecaCommand(`npx --prefer-online -y -p @org/pkg@my-tag node -e 'console.log(process.env.PATH)'`, { | ||
shell: true, | ||
@@ -228,3 +228,3 @@ }).returning({ stdout: getNpxPath(npxDirectoryHash) }) | ||
expectExecaCommand('npx --version').returning({ stdout: '8.1.2' }) | ||
expectExecaCommand(`npx -y -p @org/pkg@my-tag node -e 'console.log(process.env.PATH)'`, { | ||
expectExecaCommand(`npx --prefer-online -y -p @org/pkg@my-tag node -e 'console.log(process.env.PATH)'`, { | ||
shell: true, | ||
@@ -238,3 +238,3 @@ }).returning({ stdout: getNpxPath(npxDirectoryHash) }) | ||
`@org/pkg/lib/index.js not available locally. Attempting to use npx to install temporarily.`, | ||
`Installing... (npx -y -p @org/pkg@my-tag)`, | ||
`Installing... (npx --prefer-online -y -p @org/pkg@my-tag)`, | ||
`Installed into ${basePath}.`, | ||
@@ -247,2 +247,24 @@ `To skip this step in future, run: pnpm add -D @org/pkg@my-tag` | ||
test(`Should prefer offline for exact versions`, async () => { | ||
const npxDirectoryHash = randomString(12) | ||
const basePath = `/Users/glen/.npm/_npx/${npxDirectoryHash}/node_modules` | ||
expectExecaCommand('npx --version').returning({ stdout: '8.1.2' }) | ||
expectExecaCommand(`npx --prefer-offline -y -p @org/pkg@3.0.1 node -e 'console.log(process.env.PATH)'`, { | ||
shell: true, | ||
}).returning({ stdout: getNpxPath(npxDirectoryHash) }) | ||
expectRelativeImport(basePath, '@org/pkg/lib/index.js').returning({ foo: 1, bar: 2 }) | ||
const imported = await npxImportSucceeded( | ||
'@org/pkg@3.0.1/lib/index.js', | ||
matchesAllLines( | ||
`@org/pkg/lib/index.js not available locally. Attempting to use npx to install temporarily.`, | ||
`Installing... (npx --prefer-offline -y -p @org/pkg@3.0.1)`, | ||
`Installed into ${basePath}.`, | ||
`To skip this step in future, run: pnpm add -D @org/pkg@3.0.1` | ||
) | ||
) | ||
expect(imported).toStrictEqual({ foo: 1, bar: 2 }) | ||
}) | ||
test(`Should install two packages`, async () => { | ||
@@ -254,3 +276,3 @@ const npxDirectoryHash = randomString(12) | ||
expectExecaCommand( | ||
`npx -y -p pkg-a@latest -p pkg-b@latest node -e 'console.log(process.env.PATH)'`, | ||
`npx --prefer-online -y -p pkg-a@latest -p pkg-b@latest node -e 'console.log(process.env.PATH)'`, | ||
{ | ||
@@ -267,3 +289,3 @@ shell: true, | ||
'Packages pkg-a, pkg-b not available locally. Attempting to use npx to install temporarily.', | ||
'Installing... (npx -y -p pkg-a@latest -p pkg-b@latest)', | ||
'Installing... (npx --prefer-online -y -p pkg-a@latest -p pkg-b@latest)', | ||
`Installed into ${basePath}.`, | ||
@@ -287,3 +309,3 @@ 'To skip this step in future, run: pnpm add -D pkg-a@latest pkg-b@latest' | ||
expectExecaCommand('npx --version').returning({ stdout: '8.1.2' }) | ||
expectExecaCommand(`npx -y -p pkg-b@latest node -e 'console.log(process.env.PATH)'`, { | ||
expectExecaCommand(`npx --prefer-offline -y -p pkg-b@1.2.3 node -e 'console.log(process.env.PATH)'`, { | ||
shell: true, | ||
@@ -294,3 +316,3 @@ }).returning({ stdout: getNpxPath(npxDirectoryHash) }) | ||
const logs: string[] = [] | ||
const imported = await npxImport(['pkg-a', 'pkg-b'], (msg: string) => logs.push(msg)) | ||
const imported = await npxImport(['pkg-a', 'pkg-b@1.2.3'], (msg: string) => logs.push(msg)) | ||
expect(_import).toHaveBeenCalledTimes(2) | ||
@@ -301,5 +323,5 @@ | ||
'pkg-b not available locally. Attempting to use npx to install temporarily.', | ||
'Installing... (npx -y -p pkg-b@latest)', | ||
'Installing... (npx --prefer-offline -y -p pkg-b@1.2.3)', | ||
`Installed into ${basePath}.`, | ||
'To skip this step in future, run: pnpm add -D pkg-b@latest' | ||
'To skip this step in future, run: pnpm add -D pkg-b@1.2.3' | ||
) | ||
@@ -319,3 +341,3 @@ ) | ||
expectExecaCommand( | ||
`npx -y -p 'pkg-a@>1.0.0' -p 'pkg-b@*' node -e 'console.log(process.env.PATH)'`, | ||
`npx --prefer-online -y -p 'pkg-a@>1.0.0' -p 'pkg-b@*' node -e 'console.log(process.env.PATH)'`, | ||
{ | ||
@@ -332,3 +354,3 @@ shell: true, | ||
'Packages pkg-a, pkg-b not available locally. Attempting to use npx to install temporarily.', | ||
`Installing... (npx -y -p 'pkg-a@>1.0.0' -p 'pkg-b@*')`, | ||
`Installing... (npx --prefer-online -y -p 'pkg-a@>1.0.0' -p 'pkg-b@*')`, | ||
`Installed into ${basePath}.`, | ||
@@ -360,3 +382,3 @@ `To skip this step in future, run: pnpm add -D 'pkg-a@>1.0.0' 'pkg-b@*'` | ||
expectExecaCommand('npx --version').returning({ stdout: '8.1.2' }) | ||
expectExecaCommand(`npx -y -p pkg-b@latest node -e 'console.log(process.env.PATH)'`, { | ||
expectExecaCommand(`npx --prefer-online -y -p pkg-b@latest node -e 'console.log(process.env.PATH)'`, { | ||
shell: true, | ||
@@ -363,0 +385,0 @@ }).returning({ stdout: getNpxPath(npxDirectoryHash) }) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
58005
876
290
7