// @flow strict-local import {MemoryFS, NodeFS, OverlayFS} from '@parcel/fs'; import assert from 'assert'; import invariant from 'assert'; import path from 'path'; import sinon from 'sinon'; import ThrowableDiagnostic from '@parcel/diagnostic'; import {loadConfig} from '@parcel/utils'; import WorkerFarm from '@parcel/workers'; import {MockPackageInstaller, NodePackageManager} from '../src'; const FIXTURES_DIR = path.join(__dirname, 'fixtures'); function normalize(res) { return { ...res, invalidateOnFileCreate: res?.invalidateOnFileCreate?.sort((a, b) => { let ax = a.filePath ?? a.glob ?? (a.aboveFilePath != null && a.fileName != null ? a.aboveFilePath + a.fileName : ''); let bx = b.filePath ?? b.glob ?? (b.aboveFilePath != null && b.fileName != null ? b.aboveFilePath + b.fileName : ''); return ax < bx ? -1 : 1; }) ?? [], }; } function check(resolved, expected) { assert.deepEqual(normalize(resolved), normalize(expected)); } describe('NodePackageManager', function () { let fs; let packageManager; let packageInstaller; let workerFarm; // These can sometimes take a lil while this.timeout(20000); beforeEach(() => { workerFarm = new WorkerFarm({ workerPath: require.resolve('@parcel/core/src/worker.js'), }); fs = new OverlayFS(new MemoryFS(workerFarm), new NodeFS()); packageInstaller = new MockPackageInstaller(); packageManager = new NodePackageManager(fs, '/', packageInstaller); }); afterEach(async () => { loadConfig.clear(); await workerFarm.end(); }); it('resolves packages that exist', async () => { check( await packageManager.resolve( 'foo', path.join(FIXTURES_DIR, 'has-foo/index.js'), ), { pkg: { version: '1.1.0', }, resolved: path.join(FIXTURES_DIR, 'has-foo/node_modules/foo/index.js'), type: 1, invalidateOnFileChange: new Set([ path.join(FIXTURES_DIR, 'has-foo/node_modules/foo/package.json'), ]), invalidateOnFileCreate: [ { filePath: path.join( FIXTURES_DIR, 'has-foo/node_modules/foo/index.ts', ), }, { filePath: path.join( FIXTURES_DIR, 'has-foo/node_modules/foo/index.tsx', ), }, { fileName: 'node_modules/foo', aboveFilePath: path.join(FIXTURES_DIR, 'has-foo'), }, { fileName: 'tsconfig.json', aboveFilePath: path.join(FIXTURES_DIR, 'has-foo'), }, ], }, ); }); it('requires packages that exist', async () => { assert.deepEqual( await packageManager.require( 'foo', path.join(FIXTURES_DIR, 'has-foo/index.js'), ), 'foobar', ); }); it("autoinstalls packages that don't exist", async () => { packageInstaller.register('a', fs, path.join(FIXTURES_DIR, 'packages/a')); check( await packageManager.resolve( 'a', path.join(FIXTURES_DIR, 'has-foo/index.js'), {shouldAutoInstall: true}, ), { pkg: { name: 'a', }, resolved: path.join(FIXTURES_DIR, 'has-foo/node_modules/a/index.js'), type: 1, invalidateOnFileChange: new Set([ path.join(FIXTURES_DIR, 'has-foo/node_modules/a/package.json'), ]), invalidateOnFileCreate: [ { filePath: path.join( FIXTURES_DIR, 'has-foo/node_modules/a/index.ts', ), }, { filePath: path.join( FIXTURES_DIR, 'has-foo/node_modules/a/index.tsx', ), }, { fileName: 'node_modules/a', aboveFilePath: path.join(FIXTURES_DIR, 'has-foo'), }, { fileName: 'tsconfig.json', aboveFilePath: path.join(FIXTURES_DIR, 'has-foo'), }, ], }, ); }); it('does not autoinstall packages that are already listed in package.json', async () => { packageInstaller.register('a', fs, path.join(FIXTURES_DIR, 'packages/a')); // $FlowFixMe assert.rejects is Node 10+ await assert.rejects( () => packageManager.resolve( 'a', path.join(FIXTURES_DIR, 'has-a-not-yet-installed/index.js'), {shouldAutoInstall: true}, ), err => { invariant(err instanceof ThrowableDiagnostic); assert(err.message.includes('Run your package manager')); return true; }, ); }); it('does not autoinstall peer dependencies that are already listed in package.json', async () => { packageInstaller.register( 'peers', fs, path.join(FIXTURES_DIR, 'packages/peers'), ); let spy = sinon.spy(packageInstaller, 'install'); await packageManager.resolve( 'peers', path.join(FIXTURES_DIR, 'has-foo/index.js'), {shouldAutoInstall: true}, ); assert.deepEqual(spy.args, [ [ { cwd: path.join(FIXTURES_DIR, 'has-foo'), packagePath: path.join(FIXTURES_DIR, 'has-foo/package.json'), fs, saveDev: true, modules: [{name: 'peers', range: undefined}], }, ], ]); }); it('autoinstalls peer dependencies that are not listed in package.json', async () => { packageInstaller.register( 'foo', fs, path.join(FIXTURES_DIR, 'packages/foo-2.0'), ); packageInstaller.register( 'peers', fs, path.join(FIXTURES_DIR, 'packages/peers-2.0'), ); let spy = sinon.spy(packageInstaller, 'install'); await packageManager.resolve( 'peers', path.join(FIXTURES_DIR, 'empty/index.js'), {shouldAutoInstall: true}, ); assert.deepEqual(spy.args, [ [ { cwd: path.join(FIXTURES_DIR, 'empty'), packagePath: path.join(FIXTURES_DIR, 'empty/package.json'), fs, saveDev: true, modules: [{name: 'peers', range: undefined}], }, ], [ { cwd: path.join(FIXTURES_DIR, 'empty'), packagePath: path.join(FIXTURES_DIR, 'empty/package.json'), fs, saveDev: true, modules: [{name: 'foo', range: '^2.0.0'}], }, ], ]); }); describe('range mismatch', () => { it("cannot autoinstall if there's a local requirement", async () => { packageManager.invalidate( 'foo', path.join(FIXTURES_DIR, 'has-foo/index.js'), ); // $FlowFixMe assert.rejects is Node 10+ await assert.rejects( () => packageManager.resolve( 'foo', path.join(FIXTURES_DIR, 'has-foo/index.js'), { range: '^2.0.0', }, ), err => { invariant(err instanceof ThrowableDiagnostic); assert.equal( err.message, 'Could not find module "foo" satisfying ^2.0.0.', ); return true; }, ); }); it("can autoinstall into local package if there isn't a local requirement", async () => { packageInstaller.register( 'foo', fs, path.join(FIXTURES_DIR, 'packages/foo-2.0'), ); let spy = sinon.spy(packageInstaller, 'install'); check( await packageManager.resolve( 'foo', path.join(FIXTURES_DIR, 'has-foo/subpackage/index.js'), { range: '^2.0.0', shouldAutoInstall: true, }, ), { pkg: { name: 'foo', version: '2.0.0', }, resolved: path.join( FIXTURES_DIR, 'has-foo/subpackage/node_modules/foo/index.js', ), type: 1, invalidateOnFileChange: new Set([ path.join( FIXTURES_DIR, 'has-foo/subpackage/node_modules/foo/package.json', ), ]), invalidateOnFileCreate: [ { filePath: path.join( FIXTURES_DIR, 'has-foo/subpackage/node_modules/foo/index.ts', ), }, { filePath: path.join( FIXTURES_DIR, 'has-foo/subpackage/node_modules/foo/index.tsx', ), }, { fileName: 'node_modules/foo', aboveFilePath: path.join(FIXTURES_DIR, 'has-foo/subpackage'), }, { fileName: 'tsconfig.json', aboveFilePath: path.join(FIXTURES_DIR, 'has-foo/subpackage'), }, ], }, ); assert.deepEqual(spy.args, [ [ { cwd: path.join(FIXTURES_DIR, 'has-foo/subpackage'), packagePath: path.join( FIXTURES_DIR, 'has-foo/subpackage/package.json', ), fs, saveDev: true, modules: [{name: 'foo', range: '^2.0.0'}], }, ], ]); }); it("cannot autoinstall peer dependencies if there's an incompatible local requirement", async () => { packageManager.invalidate( 'peers', path.join(FIXTURES_DIR, 'has-foo/index.js'), ); packageInstaller.register( 'foo', fs, path.join(FIXTURES_DIR, 'packages/foo-2.0'), ); packageInstaller.register( 'peers', fs, path.join(FIXTURES_DIR, 'packages/peers-2.0'), ); // $FlowFixMe assert.rejects is Node 10+ await assert.rejects( () => packageManager.resolve( 'peers', path.join(FIXTURES_DIR, 'has-foo/index.js'), { range: '^2.0.0', shouldAutoInstall: true, }, ), err => { assert(err instanceof ThrowableDiagnostic); assert.equal( err.message, 'Could not install the peer dependency "foo" for "peers", installed version 1.1.0 is incompatible with ^2.0.0', ); return true; }, ); }); }); });