Skip to content

Commit 8ce6b17

Browse files
committed
rework package-manager
1 parent a4f6a13 commit 8ce6b17

File tree

4 files changed

+219
-246
lines changed

4 files changed

+219
-246
lines changed

src/environment.js

+25-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import fs from 'node:fs';
2-
import path, { isAbsolute } from 'node:path';
2+
import path, { isAbsolute, join } from 'node:path';
33
import EventEmitter from 'node:events';
44
import { pathToFileURL } from 'node:url';
55
import { createRequire } from 'node:module';
@@ -17,7 +17,6 @@ import { create as createMemFs } from 'mem-fs';
1717
import { create as createMemFsEditor } from 'mem-fs-editor';
1818
import createdLogger from 'debug';
1919
import isScoped from 'is-scoped';
20-
import { execa } from 'execa';
2120
import slash from 'slash';
2221
// eslint-disable-next-line n/file-extension-in-import
2322
import { isFilePending } from 'mem-fs-editor/state';
@@ -29,7 +28,7 @@ import resolver from './resolver.js';
2928
import YeomanRepository from './util/repository.js';
3029
import YeomanCommand from './util/command.js';
3130
import commandMixin from './command.js';
32-
import packageManagerMixin from './package-manager.js';
31+
import { packageManagerInstallTask } from './package-manager.js';
3332
import { ComposedStore } from './composed-store.js';
3433
// eslint-disable-next-line import/order
3534
import namespaceCompasibilityMixin from './namespace-composability.js';
@@ -73,7 +72,7 @@ function getGeneratorHint(namespace) {
7372
return `generator-${namespace}`;
7473
}
7574

76-
const mixins = [commandMixin, packageManagerMixin];
75+
const mixins = [commandMixin];
7776

7877
const Base = mixins.reduce((a, b) => b(a), EventEmitter);
7978

@@ -1248,7 +1247,28 @@ class Environment extends Base {
12481247
* Queue environment's package manager install task.
12491248
*/
12501249
queuePackageManagerInstall() {
1251-
this.queueTask('install', () => this.packageManagerInstallTask(), { once: 'package manager install' });
1250+
const { adapter, sharedFs: memFs } = this;
1251+
const { skipInstall, nodePackageManager } = this.options;
1252+
const { customInstallTask } = this.composedStore;
1253+
this.queueTask(
1254+
'install',
1255+
() => {
1256+
if (this.compatibilityMode === 'v4') {
1257+
debug('Running in generator < 5 compatibility. Package manager install is done by the generator.');
1258+
return false;
1259+
}
1260+
1261+
return packageManagerInstallTask({
1262+
adapter,
1263+
memFs,
1264+
packageJsonFile: join(this.cwd, 'package.json'),
1265+
skipInstall,
1266+
nodePackageManager,
1267+
customInstallTask,
1268+
});
1269+
},
1270+
{ once: 'package manager install' },
1271+
);
12521272
}
12531273

12541274
/**
@@ -1290,19 +1310,6 @@ class Environment extends Base {
12901310
}
12911311
this.runLoop.addSubQueue(priority, before);
12921312
}
1293-
1294-
/**
1295-
* @private
1296-
* Normalize a command across OS and spawn it (asynchronously).
1297-
*
1298-
* @param {String} command program to execute
1299-
* @param {Array} args list of arguments to pass to the program
1300-
* @param {object} [opt] any execa options
1301-
* @return {String} spawned process reference
1302-
*/
1303-
spawnCommand(command, args, opt) {
1304-
return execa(command, args, { stdio: 'inherit', cwd: this.cwd, ...opt });
1305-
}
13061313
}
13071314

13081315
Object.assign(Environment.prototype, resolver);

src/package-manager.js

+75-92
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,93 @@
1-
import path from 'node:path';
1+
import path, { dirname } from 'node:path';
22
import createdLogger from 'debug';
33
import preferredPm from 'preferred-pm';
4+
import { execa } from 'execa';
45

56
const debug = createdLogger('yeoman:environment:package-manager');
67

7-
const packageManagerMixin = cls =>
8-
class extends cls {
9-
/**
10-
* @private
11-
* Get the destination package.json file.
12-
* @return {Vinyl | undefined} a Vinyl file.
13-
*/
14-
getDestinationPackageJson() {
15-
return this.sharedFs.get(path.resolve(this.cwd, 'package.json'));
16-
}
17-
18-
/**
19-
* @private
20-
* Get the destination package.json commit status.
21-
* @return {boolean} package.json commit status.
22-
*/
23-
isDestinationPackageJsonCommitted() {
24-
const file = this.getDestinationPackageJson();
25-
return file && file.committed;
26-
}
27-
28-
/**
29-
* @private
30-
* Detect the package manager based on files or use the passed one.
31-
* @return {string} package manager.
32-
*/
33-
async detectPackageManager() {
34-
if (this.options.nodePackageManager) {
35-
return this.options.nodePackageManager;
36-
}
37-
38-
const pm = await preferredPm(this.cwd);
39-
return pm && pm.name;
40-
}
41-
42-
/**
43-
* Executes package manager install.
44-
* - checks if package.json was committed.
45-
* - uses a preferred package manager or try to detect.
46-
* @return {Promise<boolean>} Promise true if the install execution suceeded.
47-
*/
48-
async packageManagerInstallTask() {
49-
if (!this.getDestinationPackageJson()) {
50-
return false;
51-
}
52-
53-
if (this.compatibilityMode === 'v4') {
54-
debug('Running in generator < 5 compatibility. Package manager install is done by the generator.');
55-
return false;
56-
}
57-
58-
const { customInstallTask } = this.composedStore;
59-
if (customInstallTask && typeof customInstallTask !== 'function') {
60-
debug('Install disabled by customInstallTask');
61-
return false;
62-
}
63-
64-
if (!this.isDestinationPackageJsonCommitted()) {
65-
this.adapter.log(`
8+
/**
9+
* Executes package manager install.
10+
* - checks if package.json was committed.
11+
* - uses a preferred package manager or try to detect.
12+
* @return {Promise<boolean>} Promise true if the install execution suceeded.
13+
*/
14+
/*
15+
const { customInstallTask } = this.composedStore;
16+
packageJsonFile: join(this.cwd, 'package.json');
17+
18+
*/
19+
export async function packageManagerInstallTask({ memFs, packageJsonFile, customInstallTask, adapter, nodePackageManager, skipInstall }) {
20+
/**
21+
* @private
22+
* Get the destination package.json file.
23+
* @return {Vinyl | undefined} a Vinyl file.
24+
*/
25+
function getDestinationPackageJson() {
26+
return memFs.get(path.resolve(packageJsonFile));
27+
}
28+
29+
/**
30+
* @private
31+
* Get the destination package.json commit status.
32+
* @return {boolean} package.json commit status.
33+
*/
34+
function isDestinationPackageJsonCommitted() {
35+
const file = getDestinationPackageJson();
36+
return file.committed;
37+
}
38+
39+
if (!getDestinationPackageJson()) {
40+
return false;
41+
}
42+
43+
if (customInstallTask && typeof customInstallTask !== 'function') {
44+
debug('Install disabled by customInstallTask');
45+
return false;
46+
}
47+
48+
if (!isDestinationPackageJsonCommitted()) {
49+
adapter.log(`
6650
No change to package.json was detected. No package manager install will be executed.`);
67-
return false;
68-
}
51+
return false;
52+
}
6953

70-
this.adapter.log(`
54+
adapter.log(`
7155
Changes to package.json were detected.`);
7256

73-
if (this.options.skipInstall) {
74-
this.adapter.log(`Skipping package manager install.
57+
if (skipInstall) {
58+
adapter.log(`Skipping package manager install.
7559
`);
76-
return false;
77-
}
60+
return false;
61+
}
7862

79-
let packageManagerName = await this.detectPackageManager();
63+
// eslint-disable-next-line unicorn/no-await-expression-member
64+
let packageManagerName = nodePackageManager ?? (await preferredPm(dirname(packageJsonFile)))?.name;
8065

81-
const execPackageManager = async () => {
82-
if (!packageManagerName) {
83-
packageManagerName = 'npm';
84-
this.adapter.log('Error detecting the package manager. Falling back to npm.');
85-
}
66+
const execPackageManager = async () => {
67+
if (!packageManagerName) {
68+
packageManagerName = 'npm';
69+
adapter.log('Error detecting the package manager. Falling back to npm.');
70+
}
8671

87-
if (!['npm', 'yarn', 'pnpm'].includes(packageManagerName)) {
88-
this.adapter.log(`${packageManagerName} is not a supported package manager. Run it by yourself.`);
89-
return false;
90-
}
72+
if (!['npm', 'yarn', 'pnpm'].includes(packageManagerName)) {
73+
adapter.log(`${packageManagerName} is not a supported package manager. Run it by yourself.`);
74+
return false;
75+
}
9176

92-
this.adapter.log(`
77+
adapter.log(`
9378
Running ${packageManagerName} install for you to install the required dependencies.`);
94-
await this.spawnCommand(packageManagerName, ['install']);
95-
return true;
96-
};
79+
await execa(packageManagerName, ['install'], { stdio: 'inherit', cwd: dirname(packageJsonFile) });
80+
return true;
81+
};
9782

98-
if (customInstallTask) {
99-
const result = customInstallTask(packageManagerName, execPackageManager);
100-
if (!result || !result.then) {
101-
return true;
102-
}
83+
if (customInstallTask) {
84+
const result = customInstallTask(packageManagerName, execPackageManager);
85+
if (!result || !result.then) {
86+
return true;
87+
}
10388

104-
return result;
105-
}
89+
return result;
90+
}
10691

107-
return execPackageManager();
108-
}
109-
};
110-
export default packageManagerMixin;
92+
return execPackageManager();
93+
}

test/generator-features.js

+47-22
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ import { createRequire } from 'node:module';
33
import sinon from 'sinon';
44
import semver from 'semver';
55
import Generator from 'yeoman-generator';
6+
import { esmocha, expect, mock } from 'esmocha';
67
import helpers from './helpers.js';
78

9+
const { packageManagerInstallTask } = await mock('../src/package-manager.js');
10+
const { default: Environment } = await import('../src/environment.js');
11+
812
const require = createRequire(import.meta.url);
913
const { version } = require('yeoman-generator/package.json');
1014

11-
class FeaturesGenerator extends Generator {
12-
getFeatures() {
13-
return this.features;
14-
}
15-
}
15+
class FeaturesGenerator extends Generator {}
1616

1717
describe('environment (generator-features)', () => {
1818
before(function () {
@@ -21,6 +21,10 @@ describe('environment (generator-features)', () => {
2121
}
2222
});
2323

24+
beforeEach(() => {
25+
esmocha.resetAllMocks();
26+
});
27+
2428
describe('customCommitTask feature', () => {
2529
describe('without customInstallTask', () => {
2630
let runContext;
@@ -108,9 +112,9 @@ describe('environment (generator-features)', () => {
108112
describe('customInstallTask feature', () => {
109113
describe('without customInstallTask', () => {
110114
let runContext;
111-
before(async () => {
115+
beforeEach(async () => {
112116
runContext = helpers
113-
.create('custom-install')
117+
.create('custom-install', undefined, { createEnv: Environment.createEnv.bind(Environment) })
114118
.withOptions({ skipInstall: false })
115119
.withGenerators([
116120
[
@@ -121,24 +125,49 @@ describe('environment (generator-features)', () => {
121125
},
122126
'custom-install:app',
123127
],
124-
])
125-
.withEnvironment(env => {
126-
env.isDestinationPackageJsonCommitted = sinon.stub().returns(true);
127-
env.spawnCommand = sinon.stub().returns(Promise.resolve());
128-
});
128+
]);
129+
await runContext.run();
130+
});
131+
132+
it('should call packageManagerInstallTask', () => {
133+
expect(packageManagerInstallTask).toHaveBeenCalledWith(
134+
expect.not.objectContaining({
135+
customInstallTask: expect.any(Function),
136+
}),
137+
);
138+
});
139+
});
140+
141+
describe('v4 compatibility', () => {
142+
let runContext;
143+
beforeEach(async () => {
144+
runContext = helpers
145+
.create('custom-install', undefined, { createEnv: Environment.createEnv.bind(Environment) })
146+
.withOptions({ skipInstall: false })
147+
.withGenerators([
148+
[
149+
class extends FeaturesGenerator {
150+
packageJsonTask() {
151+
this.env.compatibilityMode = 'v4';
152+
this.packageJson.set({ name: 'foo' });
153+
}
154+
},
155+
'custom-install:app',
156+
],
157+
]);
129158
await runContext.run();
130159
});
131160

132-
it('should call spawnCommand', () => {
133-
assert.equal(runContext.env.spawnCommand.callCount, 1, 'should have been called once');
161+
it('should not call packageManagerInstallTask', () => {
162+
expect(packageManagerInstallTask).not.toHaveBeenCalled();
134163
});
135164
});
136165

137166
describe('with true customInstallTask', () => {
138167
let runContext;
139168
before(async () => {
140169
runContext = helpers
141-
.create('custom-install')
170+
.create('custom-install', undefined, { createEnv: Environment.createEnv.bind(Environment) })
142171
.withOptions({ skipInstall: false })
143172
.withGenerators([
144173
[
@@ -153,16 +182,12 @@ describe('environment (generator-features)', () => {
153182
},
154183
'custom-install:app',
155184
],
156-
])
157-
.withEnvironment(env => {
158-
env.isDestinationPackageJsonCommitted = sinon.stub().returns(true);
159-
env.spawnCommand = sinon.stub().returns(Promise.resolve());
160-
});
185+
]);
161186
await runContext.run();
162187
});
163188

164-
it('should not call spawnCommand', () => {
165-
assert.equal(runContext.env.spawnCommand.callCount, 0, 'should not have been called');
189+
it('should not call packageManagerInstallTask', () => {
190+
expect(packageManagerInstallTask).not.toHaveBeenCalled();
166191
});
167192
});
168193

0 commit comments

Comments
 (0)