Skip to content

Commit 51d7896

Browse files
yannbfvalentinpalkovic
authored andcommitted
Merge pull request #31743 from storybookjs/yann/fix-empty-dir-init
CLI: Fix package manager instantiation in empty directories (cherry picked from commit ee73339)
1 parent fd74caa commit 51d7896

File tree

3 files changed

+74
-70
lines changed

3 files changed

+74
-70
lines changed

code/core/src/common/js-package-manager/JsPackageManagerFactory.ts

Lines changed: 50 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -45,28 +45,14 @@ export class JsPackageManagerFactory {
4545
this.cache.clear();
4646
}
4747

48-
public static getPackageManager(
49-
{
50-
force,
51-
configDir = '.storybook',
52-
storiesPaths,
53-
}: { force?: PackageManagerName; configDir?: string; storiesPaths?: string[] } = {},
54-
cwd = process.cwd()
55-
): JsPackageManager {
56-
// Check cache first
57-
const cacheKey = this.getCacheKey(force, configDir, cwd, storiesPaths);
58-
const cached = this.cache.get(cacheKey);
59-
if (cached) {
60-
return cached;
61-
}
62-
63-
// Option 1: If the user has provided a forcing flag, we use it
64-
if (force && force in this.PROXY_MAP) {
65-
const packageManager = new this.PROXY_MAP[force]({ cwd, configDir, storiesPaths });
66-
this.cache.set(cacheKey, packageManager);
67-
return packageManager;
68-
}
69-
48+
/**
49+
* Determine which package manager type to use based on lockfiles, commands, and environment
50+
*
51+
* @param cwd - Current working directory
52+
* @returns Package manager type as string: 'npm', 'pnpm', 'bun', 'yarn1', or 'yarn2'
53+
* @throws Error if no usable package manager is found
54+
*/
55+
public static getPackageManagerType(cwd = process.cwd()): PackageManagerName {
7056
const root = getProjectRoot();
7157

7258
const lockFiles = [
@@ -94,66 +80,81 @@ export class JsPackageManagerFactory {
9480
return 1;
9581
});
9682

97-
// Option 2: We try to infer the package manager from the closest lockfile
83+
// Option 1: We try to infer the package manager from the closest lockfile
9884
const closestLockfilePath = lockFiles[0];
99-
10085
const closestLockfile = closestLockfilePath && basename(closestLockfilePath);
10186

10287
const yarnVersion = getYarnVersion(cwd);
10388

10489
if (yarnVersion && closestLockfile === YARN_LOCKFILE) {
105-
const packageManager =
106-
yarnVersion === 1
107-
? new Yarn1Proxy({ cwd, configDir, storiesPaths })
108-
: new Yarn2Proxy({ cwd, configDir, storiesPaths });
109-
this.cache.set(cacheKey, packageManager);
110-
return packageManager;
90+
return yarnVersion === 1 ? 'yarn1' : 'yarn2';
11191
}
11292

11393
if (hasPNPM(cwd) && closestLockfile === PNPM_LOCKFILE) {
114-
const packageManager = new PNPMProxy({ cwd, configDir, storiesPaths });
115-
this.cache.set(cacheKey, packageManager);
116-
return packageManager;
94+
return 'pnpm';
11795
}
11896

11997
const isNPMCommandOk = hasNPM(cwd);
12098

12199
if (isNPMCommandOk && closestLockfile === NPM_LOCKFILE) {
122-
const packageManager = new NPMProxy({ cwd, configDir, storiesPaths });
123-
this.cache.set(cacheKey, packageManager);
124-
return packageManager;
100+
return 'npm';
125101
}
126102

127103
if (
128104
hasBun(cwd) &&
129105
(closestLockfile === BUN_LOCKFILE || closestLockfile === BUN_LOCKFILE_BINARY)
130106
) {
131-
const packageManager = new BUNProxy({ cwd, configDir, storiesPaths });
132-
this.cache.set(cacheKey, packageManager);
133-
return packageManager;
107+
return 'bun';
134108
}
135109

136-
// Option 3: If the user is running a command via npx/pnpx/yarn create/etc, we infer the package manager from the command
110+
// Option 2: If the user is running a command via npx/pnpx/yarn create/etc, we infer the package manager from the command
137111
const inferredPackageManager = this.inferPackageManagerFromUserAgent();
138112
if (inferredPackageManager && inferredPackageManager in this.PROXY_MAP) {
139-
const packageManager = new this.PROXY_MAP[inferredPackageManager]({
140-
cwd,
141-
storiesPaths,
142-
configDir,
143-
});
144-
this.cache.set(cacheKey, packageManager);
145-
return packageManager;
113+
return inferredPackageManager;
146114
}
147115

148116
// Default fallback, whenever users try to use something different than NPM, PNPM, Yarn,
149117
// but still have NPM installed
150118
if (isNPMCommandOk) {
151-
const packageManager = new NPMProxy({ cwd, configDir, storiesPaths });
119+
return 'npm';
120+
}
121+
122+
throw new Error('Unable to find a usable package manager within NPM, PNPM, Yarn and Yarn 2');
123+
}
124+
125+
public static getPackageManager(
126+
{
127+
force,
128+
configDir = '.storybook',
129+
storiesPaths,
130+
ignoreCache = false,
131+
}: {
132+
force?: PackageManagerName;
133+
configDir?: string;
134+
storiesPaths?: string[];
135+
ignoreCache?: boolean;
136+
} = {},
137+
cwd = process.cwd()
138+
): JsPackageManager {
139+
// Check cache first, unless ignored
140+
const cacheKey = this.getCacheKey(force, configDir, cwd, storiesPaths);
141+
const cached = this.cache.get(cacheKey);
142+
if (cached && !ignoreCache) {
143+
return cached;
144+
}
145+
146+
// Option 1: If the user has provided a forcing flag, we use it
147+
if (force && force in this.PROXY_MAP) {
148+
const packageManager = new this.PROXY_MAP[force]({ cwd, configDir, storiesPaths });
152149
this.cache.set(cacheKey, packageManager);
153150
return packageManager;
154151
}
155152

156-
throw new Error('Unable to find a usable package manager within NPM, PNPM, Yarn and Yarn 2');
153+
// Option 2: Detect package managers based on some heuristics
154+
const packageManagerType = this.getPackageManagerType(cwd);
155+
const packageManager = new this.PROXY_MAP[packageManagerType]({ cwd, configDir, storiesPaths });
156+
this.cache.set(cacheKey, packageManager);
157+
return packageManager;
157158
}
158159

159160
/** Look up map of package manager proxies by name */

code/lib/create-storybook/src/initiate.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -387,24 +387,28 @@ export async function doInitiate(options: CommandOptions): Promise<
387387
> {
388388
const { packageManager: pkgMgr } = options;
389389

390-
let packageManager = JsPackageManagerFactory.getPackageManager({
391-
force: pkgMgr,
392-
});
390+
const isEmptyDirProject = options.force !== true && currentDirectoryIsEmpty();
391+
let packageManagerType = JsPackageManagerFactory.getPackageManagerType();
393392

394393
// Check if the current directory is empty.
395-
if (options.force !== true && currentDirectoryIsEmpty(packageManager.type)) {
394+
if (isEmptyDirProject) {
396395
// Initializing Storybook in an empty directory with yarn1
397-
// will very likely fail due to different kind of hoisting issues
396+
// will very likely fail due to different kinds of hoisting issues
398397
// which doesn't get fixed anymore in yarn1.
399398
// We will fallback to npm in this case.
400-
if (packageManager.type === 'yarn1') {
401-
packageManager = JsPackageManagerFactory.getPackageManager({ force: 'npm' });
399+
if (packageManagerType === 'yarn1') {
400+
packageManagerType = 'npm';
402401
}
402+
403403
// Prompt the user to create a new project from our list.
404-
await scaffoldNewProject(packageManager.type, options);
404+
await scaffoldNewProject(packageManagerType, options);
405405
invalidateProjectRootCache();
406406
}
407407

408+
const packageManager = JsPackageManagerFactory.getPackageManager({
409+
force: pkgMgr,
410+
});
411+
408412
if (!options.skipInstall) {
409413
await packageManager.installDependencies();
410414
}

code/lib/create-storybook/src/scaffold-new-project.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -231,22 +231,21 @@ export const scaffoldNewProject = async (
231231
logger.line(1);
232232
};
233233

234-
const BASE_IGNORED_FILES = ['.git', '.gitignore', '.DS_Store', '.cache', 'node_modules'];
235-
236-
const IGNORED_FILES_BY_PACKAGE_MANAGER: Record<CoercedPackageManagerName, string[]> = {
237-
npm: [...BASE_IGNORED_FILES],
238-
yarn: [...BASE_IGNORED_FILES, '.yarnrc.yml', '.yarn'],
239-
pnpm: [...BASE_IGNORED_FILES],
240-
};
241-
242-
export const currentDirectoryIsEmpty = (packageManager: PackageManagerName) => {
243-
const packageManagerName = packageManagerToCoercedName(packageManager);
234+
const FILES_TO_IGNORE = [
235+
'.git',
236+
'.gitignore',
237+
'.DS_Store',
238+
'.cache',
239+
'node_modules',
240+
'.yarnrc.yml',
241+
'.yarn',
242+
];
243+
244+
export const currentDirectoryIsEmpty = () => {
244245
const cwdFolderEntries = readdirSync(process.cwd());
245246

246-
const filesToIgnore = IGNORED_FILES_BY_PACKAGE_MANAGER[packageManagerName];
247-
248247
return (
249248
cwdFolderEntries.length === 0 ||
250-
cwdFolderEntries.every((entry) => filesToIgnore.includes(entry))
249+
cwdFolderEntries.every((entry) => FILES_TO_IGNORE.includes(entry))
251250
);
252251
};

0 commit comments

Comments
 (0)