Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/auto-install/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# @rollup/plugin-auto-install ChangeLog

## v4.0.0

_2026-02-16_

### Breaking Changes

- feat!: ESM only. Update Node and Rollup minimum versions ([#1935](https://github.com/rollup/plugins/issues/1935))

## v3.0.5

_2023-10-05_
Expand Down
2 changes: 1 addition & 1 deletion packages/auto-install/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

## Requirements

This plugin requires an [LTS](https://github.com/nodejs/Release) Node version (v14.0.0+) and Rollup v1.20.0+.
This plugin requires an [LTS](https://github.com/nodejs/Release) Node version (v20.19.0+) and Rollup v4.0.0+.

## Install

Expand Down
45 changes: 16 additions & 29 deletions packages/auto-install/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rollup/plugin-auto-install",
"version": "3.0.5",
"version": "4.0.0",
"publishConfig": {
"access": "public"
},
Expand All @@ -13,34 +13,32 @@
"author": "Rich Harris",
"homepage": "https://github.com/rollup/plugins/tree/master/packages/auto-install/#readme",
"bugs": "https://github.com/rollup/plugins/issues",
"main": "./dist/cjs/index.js",
"module": "./dist/es/index.js",
"type": "module",
"exports": {
"types": "./types/index.d.ts",
"import": "./dist/es/index.js",
"default": "./dist/cjs/index.js"
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"engines": {
"node": ">=14.0.0"
"node": ">=20.19.0"
},
"scripts": {
"build": "rollup -c",
"build": "tsc --project tsconfig.json",
"ci:coverage": "nyc pnpm test && nyc report --reporter=text-lcov > coverage.lcov",
"ci:lint": "pnpm build && pnpm lint",
"ci:lint:commits": "commitlint --from=${CIRCLE_BRANCH} --to=${CIRCLE_SHA1}",
"ci:test": "pnpm test -- --verbose",
"ci:test": "pnpm test -- --reporter=verbose",
"prebuild": "del-cli dist",
"prepare": "if [ ! -d 'dist' ]; then pnpm build; fi",
"prerelease": "pnpm build",
"pretest": "pnpm build",
"release": "pnpm --workspace-root package:release $(pwd)",
"test": "ava",
"test:ts": "tsc --noEmit"
"test": "vitest --config ../../.config/vitest.config.mts run"
},
"files": [
"dist",
"!dist/**/*.map",
"types",
"README.md",
"LICENSE"
],
Expand All @@ -53,7 +51,7 @@
"modules"
],
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
"rollup": ">=4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
Expand All @@ -62,21 +60,10 @@
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-typescript": "^9.0.1",
"del": "^6.1.1",
"del-cli": "^5.0.0",
"node-noop": "^1.0.0",
"rollup": "^4.0.0-24",
"typescript": "^4.8.3"
"rollup": "^4.0.0",
"typescript": "catalog:"
},
"types": "./types/index.d.ts",
"ava": {
"workerThreads": false,
"files": [
"!**/fixtures/**",
"!**/output/**",
"!**/helpers/**",
"!**/recipes/**",
"!**/types.ts"
]
}
"types": "./dist/index.d.ts"
}
7 changes: 0 additions & 7 deletions packages/auto-install/rollup.config.mjs

This file was deleted.

70 changes: 48 additions & 22 deletions packages/auto-install/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,49 @@
import * as fs from 'fs';
import * as path from 'path';
import mod from 'module';
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'node:fs';
import path from 'node:path';
import { builtinModules } from 'node:module';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';

import type { Plugin } from 'rollup';

import type { RollupAutoInstallOptions } from '../types';
type PackageManager = 'npm' | 'pnpm' | 'yarn';

type Commands = Record<PackageManager, string>;

export interface RollupAutoInstallOptions {
/**
* Specifies the location on disk of the target `package.json` file.
* If the file doesn't exist, it will be created by the plugin,
* as package managers need to populate the `dependencies` property.
* @default '{cwd}/package.json'
*/
pkgFile?: string;

/**
* Specifies the package manager to use.
* If not specified, the plugin will default to:
* - `'yarn'` if `yarn.lock` exists
* - `'pnpm'` if `pnpm-lock.yaml` exists
* - `'npm'` otherwise
*/
manager?: PackageManager;

/**
* Intentionally undocumented options. Used for tests.
*/
commands?: Partial<Commands>;
}

const execAsync = promisify(exec);

export default function autoInstall(opts: RollupAutoInstallOptions = {}): Plugin {
const defaults = {
// intentionally undocumented options. used for tests
commands: {
npm: 'npm install',
pnpm: 'pnpm install',
yarn: 'yarn add'
},
manager: fs.existsSync('yarn.lock') ? 'yarn' : fs.existsSync('pnpm-lock.yaml') ? 'pnpm' : 'npm',
pkgFile: path.resolve(opts.pkgFile || 'package.json')
};
const manager =
opts.manager ??
(fs.existsSync('yarn.lock') ? 'yarn' : fs.existsSync('pnpm-lock.yaml') ? 'pnpm' : 'npm');

const pkgFile = path.resolve(opts.pkgFile ?? 'package.json');

const options = Object.assign({}, defaults, opts);
const { manager, pkgFile } = options;
const validManagers = ['npm', 'yarn', 'pnpm'];
const validManagers: readonly PackageManager[] = ['npm', 'yarn', 'pnpm'];

if (!validManagers.includes(manager)) {
throw new RangeError(
Expand All @@ -33,6 +52,13 @@ export default function autoInstall(opts: RollupAutoInstallOptions = {}): Plugin
);
}

const commands: Commands = {
npm: 'npm install',
pnpm: 'pnpm install',
yarn: 'yarn add',
...opts.commands
};

let pkg: any;
if (fs.existsSync(pkgFile)) {
pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
Expand All @@ -41,8 +67,8 @@ export default function autoInstall(opts: RollupAutoInstallOptions = {}): Plugin
pkg = {};
}

const installed = new Set(Object.keys(pkg.dependencies || {}).concat(mod.builtinModules));
const cmd = options.commands[manager];
const installed = new Set([...Object.keys(pkg.dependencies || {}), ...builtinModules]);
const cmd = commands[manager];

return {
name: 'auto-install',
Expand All @@ -54,7 +80,7 @@ export default function autoInstall(opts: RollupAutoInstallOptions = {}): Plugin
// this function doesn't actually resolve anything, but it provides us with a hook to discover uninstalled deps

const isExternalPackage =
importee[0] !== '.' && importee[0] !== '\0' && !path.isAbsolute(importee);
importee[0] !== '.' && importee[0] !== '\\0' && !path.isAbsolute(importee);

if (isExternalPackage) {
// we have a bare import — check it's installed
Expand Down
146 changes: 146 additions & 0 deletions packages/auto-install/test/auto-install.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

import { describe, it, expect } from 'vitest';
import { rollup, type Plugin } from 'rollup';
import nodeResolve from '@rollup/plugin-node-resolve';

import autoInstall from '~package';

const PACKAGE_ROOT = process.cwd();

async function withFixture(
fixtureName: string,
fn: (ctx: { cwd: string; input: string; outputFile: string }) => Promise<void>
) {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rollup-plugin-auto-install-'));
const cwd = path.join(tmpDir, 'cwd');
await fs.mkdir(cwd, { recursive: true });

const fixturesDir = path.join(PACKAGE_ROOT, 'test', 'fixtures');
await fs.cp(path.join(fixturesDir, fixtureName), cwd, { recursive: true });

const input = path.join(cwd, 'input.js');
await fs.copyFile(path.join(fixturesDir, 'input.js'), input);

const outputFile = path.join(cwd, 'output', 'bundle.js');
await fs.mkdir(path.dirname(outputFile), { recursive: true });

const previousCwd = process.cwd();
process.chdir(cwd);

try {
await fn({ cwd, input, outputFile });
} finally {
process.chdir(previousCwd);
await fs.rm(tmpDir, { recursive: true, force: true });
}
}

async function bundleWithPlugins(input: string, outputFile: string, plugins: Plugin[]) {
const bundle = await rollup({
input,
plugins
});

try {
await bundle.write({
file: outputFile,
format: 'cjs'
});
} finally {
await bundle.close();
}
}

const noopResolvePlugin: Plugin = {
name: 'noop-resolver',
resolveId(id) {
if (id === 'node-noop') return id;
return null;
},
load(id) {
if (id === 'node-noop') {
return 'export default {}';
}
return null;
}
};

describe('@rollup/plugin-auto-install', () => {
it('throws on invalid manager', () => {
expect(() => autoInstall({ manager: 'foo' as any })).toThrowError(RangeError);
expect(() => autoInstall({ manager: 'foo' as any })).toThrowError(
/is not a valid package manager/
);
});

it('npm', async () => {
await withFixture('npm', async ({ cwd, input, outputFile }) => {
await bundleWithPlugins(input, outputFile, [
autoInstall({ pkgFile: path.join(cwd, 'package.json'), manager: 'npm' }),
nodeResolve()
]);

const json = JSON.parse(await fs.readFile(path.join(cwd, 'package.json'), 'utf-8'));
expect(json.dependencies?.['node-noop']).toBeDefined();
});
}, 50_000);

it('npm, bare', async () => {
await withFixture('npm-bare', async ({ cwd, input, outputFile }) => {
await bundleWithPlugins(input, outputFile, [autoInstall(), nodeResolve()]);

const json = JSON.parse(await fs.readFile(path.join(cwd, 'package.json'), 'utf-8'));
expect(json.dependencies?.['node-noop']).toBeDefined();

const lockFile = await fs.readFile(path.join(cwd, 'package-lock.json'), 'utf-8');
expect(lockFile).toContain('"node-noop"');
});
}, 50_000);

it('pnpm', async () => {
await withFixture('pnpm', async ({ cwd, input, outputFile }) => {
await bundleWithPlugins(input, outputFile, [autoInstall(), nodeResolve()]);

const json = JSON.parse(await fs.readFile(path.join(cwd, 'package.json'), 'utf-8'));
expect(json.dependencies?.['node-noop']).toBeDefined();
});
}, 50_000);

it('pnpm, bare', async () => {
await withFixture('pnpm-bare', async ({ cwd, input, outputFile }) => {
await bundleWithPlugins(input, outputFile, [autoInstall({ manager: 'pnpm' }), nodeResolve()]);

const json = JSON.parse(await fs.readFile(path.join(cwd, 'package.json'), 'utf-8'));
expect(json.dependencies?.['node-noop']).toBeDefined();
});
}, 50_000);

it('yarn', async () => {
await withFixture('yarn', async ({ cwd, input, outputFile }) => {
await bundleWithPlugins(input, outputFile, [
autoInstall({ commands: { yarn: 'echo yarn > yarn.lock' } }),
noopResolvePlugin,
nodeResolve()
]);

const lockFile = await fs.readFile(path.join(cwd, 'yarn.lock'), 'utf-8');
expect(lockFile).toMatch(/yarn\s+node-noop/);
});
}, 50_000);

it('yarn, bare', async () => {
await withFixture('yarn-bare', async ({ cwd, input, outputFile }) => {
await bundleWithPlugins(input, outputFile, [
autoInstall({ manager: 'yarn', commands: { yarn: 'echo yarn.bare > yarn.lock' } }),
noopResolvePlugin,
nodeResolve()
]);

const lockFile = await fs.readFile(path.join(cwd, 'yarn.lock'), 'utf-8');
expect(lockFile).toMatch(/yarn\.bare\s+node-noop/);
});
}, 50_000);
});
Loading
Loading