Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/composed-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import crypto from 'node:crypto';
import { toNamespace } from '@yeoman/namespace';
import type { BaseGenerator, Logger } from '@yeoman/types';
import createdLogger from 'debug';
import type { InstallTask } from './package-manager.ts';
import type { PackageManagerInstallTaskOptions } from './package-manager.ts';

const debug = createdLogger('yeoman:environment:composed-store');

Expand All @@ -22,7 +22,7 @@ export class ComposedStore {
return this.findUniqueFeature('customCommitTask');
}

get customInstallTask(): InstallTask | undefined {
get customInstallTask(): PackageManagerInstallTaskOptions['customInstallTask'] | undefined {
return this.findUniqueFeature('customInstallTask');
}

Expand Down
24 changes: 19 additions & 5 deletions src/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type PackageManagerInstallTaskOptions = {
packageJsonLocation: string;
adapter: InputOutputAdapter;
nodePackageManager?: string;
customInstallTask?: boolean | InstallTask;
customInstallTask?: boolean | InstallTask | 'ask';
skipInstall?: boolean;
};

Expand Down Expand Up @@ -46,7 +46,11 @@ export async function packageManagerInstallTask({
* @return {Vinyl | undefined} a Vinyl file.
*/
function getDestinationPackageJson() {
return memFs.get(join(packageJsonLocation, 'package.json'));
const packageJsonFile = join(packageJsonLocation, 'package.json');

if (memFs.existsInMemory(packageJsonFile)) {
return memFs.get(packageJsonFile);
}
}

/**
Expand All @@ -56,14 +60,14 @@ export async function packageManagerInstallTask({
*/
function isDestinationPackageJsonCommitted() {
const file = getDestinationPackageJson();
return file.committed;
return file?.committed;
}

if (!getDestinationPackageJson()) {
return false;
}

if (customInstallTask && typeof customInstallTask !== 'function') {
if (customInstallTask && typeof customInstallTask !== 'function' && customInstallTask !== 'ask') {
debug('Install disabled by customInstallTask');
return false;
}
Expand Down Expand Up @@ -102,9 +106,19 @@ Running ${packageManagerName} install for you to install the required dependenci
return true;
};

if (customInstallTask) {
if (typeof customInstallTask === 'function') {
return customInstallTask(packageManagerName, execPackageManager);
}
if (customInstallTask === 'ask') {
const { runInstall } = await adapter.prompt({
type: 'confirm',
name: 'runInstall',
message: `Do you want to run ${packageManagerName} install now?`,
});
if (!runInstall) {
return false;
}
}

return execPackageManager();
}
96 changes: 93 additions & 3 deletions test/generator-features.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import assert from 'node:assert';
import { stub } from 'sinon';
import { after, afterEach, before, beforeEach, describe, esmocha, expect, it } from 'esmocha';
import helpers, { getCreateEnv as getCreateEnvironment } from './helpers.js';
import helpers, { getCreateEnv as getCreateEnvironment, result } from './helpers.js';
import { greaterThan5 } from './generator-versions.js';

const { commitSharedFsTask } = await esmocha.mock('../src/commit.ts', import('../src/commit.ts'));
const { packageManagerInstallTask } = await esmocha.mock('../src/package-manager.ts', import('../src/package-manager.ts'));
const commitModule = await import('../src/commit.ts');
const { commitSharedFsTask: originalCommitSharedFsTask } = commitModule;
const { commitSharedFsTask } = await esmocha.mock('../src/commit.ts', Promise.resolve(commitModule));
const { execa } = await esmocha.mock('execa', import('execa'));
const packageManagerModule = await import('../src/package-manager.ts');
const { packageManagerInstallTask: originalPackageManagerInstallTask } = packageManagerModule;
const { packageManagerInstallTask } = await esmocha.mock('../src/package-manager.ts', Promise.resolve(packageManagerModule));
const { default: BasicEnvironment } = await import('../src/environment-base.ts');

for (const generatorVersion of greaterThan5) {
Expand Down Expand Up @@ -216,6 +220,92 @@ for (const generatorVersion of greaterThan5) {
});
});

describe('with ask customInstallTask', () => {
describe('accepting to run install', () => {
beforeEach(async () => {
commitSharedFsTask.mockImplementation(originalCommitSharedFsTask);
packageManagerInstallTask.mockImplementation(originalPackageManagerInstallTask);
await helpers
.run('custom-install', undefined, { createEnv: getCreateEnvironment(BasicEnvironment) })
.withOptions({ skipInstall: false })
.withAnswers({ runInstall: true })
.withGenerators([
[
class extends FeaturesGenerator {
constructor(arguments_, options, features) {
super(arguments_, options, { ...features, customInstallTask: 'ask' });
}

packageJsonTask() {
this.packageJson.set({ name: 'foo' });
}
},
{ namespace: 'custom-install:app' },
],
]);
});

it('should write package.json', () => {
result.assertFile('package.json');
});

it('should call packageManagerInstallTask', () => {
expect(packageManagerInstallTask).toHaveBeenCalledTimes(1);
expect(packageManagerInstallTask).toHaveBeenCalledWith(
expect.objectContaining({
customInstallTask: 'ask',
}),
);
});

it('should call execa', () => {
expect(execa).toHaveBeenCalled();
});
});

describe('declining to run install', () => {
beforeEach(async () => {
commitSharedFsTask.mockImplementation(originalCommitSharedFsTask);
packageManagerInstallTask.mockImplementation(originalPackageManagerInstallTask);
await helpers
.run('custom-install', undefined, { createEnv: getCreateEnvironment(BasicEnvironment) })
.withOptions({ skipInstall: false })
.withAnswers({ runInstall: false })
.withGenerators([
[
class extends FeaturesGenerator {
constructor(arguments_, options, features) {
super(arguments_, options, { ...features, customInstallTask: 'ask' });
}

packageJsonTask() {
this.packageJson.set({ name: 'foo' });
}
},
{ namespace: 'custom-install:app' },
],
]);
});

it('should write package.json', () => {
result.assertFile('package.json');
});

it('should call packageManagerInstallTask', () => {
expect(packageManagerInstallTask).toHaveBeenCalledTimes(1);
expect(packageManagerInstallTask).toHaveBeenCalledWith(
expect.objectContaining({
customInstallTask: 'ask',
}),
);
});

it('should not call execa', () => {
expect(execa).not.toHaveBeenCalled();
});
});
});

describe('with function customInstallTask and custom path', () => {
let runContext;
let customInstallTask;
Expand Down
2 changes: 2 additions & 0 deletions test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export const getCreateEnv =
export default createHelpers({
createEnv: getCreateEnv(Environment),
});

export { result } from 'yeoman-test';
2 changes: 1 addition & 1 deletion test/package-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('environment (package-manager)', () => {
beforeEach(() => {
adapter = { log: esmocha.fn() };
execa.mockReturnValue();
memFs = { get: esmocha.fn() };
memFs = { get: esmocha.fn(), existsInMemory: esmocha.fn().mockReturnValue(true) };
packageJsonLocation = path.join(__dirname, 'fixtures', 'package-manager', 'npm');
whichPackageManager.mockResolvedValue('npm');
});
Expand Down
Loading