Skip to content

Commit d0d717c

Browse files
authored
Merge pull request storybookjs#35423 from storybookjs/task/c1adc443-regression-bug--upgrade--CLI
CLI: Fix silent hang in deferred addon configuration during upgrade
2 parents 12a9299 + 43213e6 commit d0d717c

3 files changed

Lines changed: 70 additions & 20 deletions

File tree

code/addons/a11y/src/postinstall.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,12 @@ export default async function postinstall(options: PostinstallOptions) {
2929
configDir: options.configDir,
3030
});
3131

32-
await jsPackageManager.runPackageCommand({ args, useRemotePkg: !!options.skipInstall });
32+
// stdin must not be left as an open pipe: the nested CLI (and the npm exec layers in between)
33+
// never receive EOF on it and block forever, which freezes the outer upgrade/add command
34+
// without any output. stdout/stderr stay piped so a failure still carries the child's output.
35+
await jsPackageManager.runPackageCommand({
36+
args,
37+
stdio: ['ignore', 'pipe', 'pipe'],
38+
useRemotePkg: !!options.skipInstall,
39+
});
3340
}

code/lib/cli-storybook/src/postinstallAddon.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,73 @@
1+
import { spawnSync } from 'node:child_process';
12
import { createRequire } from 'node:module';
2-
import { fileURLToPath } from 'node:url';
3+
import { join } from 'node:path';
4+
import { fileURLToPath, pathToFileURL } from 'node:url';
35

46
import { importModule } from '../../../core/src/shared/utils/module.ts';
57
import type { PostinstallOptions } from './add.ts';
68

79
const DIR_CWD = process.cwd();
8-
const require = createRequire(DIR_CWD);
10+
// createRequire treats a bare directory as a file path and resolves from its parent,
11+
// so anchor it to a file inside the project instead.
12+
const require = createRequire(join(DIR_CWD, 'package.json'));
913

1014
export const postinstallAddon = async (addonName: string, options: PostinstallOptions) => {
1115
const hookPath = `${addonName}/postinstall`;
12-
let modulePath: string;
16+
const logger = options.logger;
17+
let modulePath: string | undefined;
1318
try {
14-
modulePath = import.meta.resolve(hookPath, DIR_CWD);
19+
// Resolve from the user's project: import.meta.resolve would resolve relative to THIS file,
20+
// which points at the wrong copy (or none at all) when the CLI runs from a different tree
21+
// than the project, e.g. via npx or in a monorepo.
22+
modulePath = require.resolve(hookPath, { paths: [DIR_CWD] });
1523
} catch (e) {
24+
// When the addon was installed while this process was already running (the upgrade command
25+
// installs dependencies mid-run), Node's module resolution has cached the earlier negative
26+
// lookup and keeps failing. A fresh child process resolves from a clean cache.
27+
const result = spawnSync(
28+
process.execPath,
29+
['-p', `require.resolve(${JSON.stringify(hookPath)}, { paths: [process.cwd()] })`],
30+
{ cwd: DIR_CWD, encoding: 'utf8', timeout: 30_000 }
31+
);
32+
if (result.status === 0 && result.stdout.trim()) {
33+
modulePath = result.stdout.trim();
34+
}
35+
}
36+
37+
if (!modulePath) {
1638
try {
17-
modulePath = require.resolve(hookPath, { paths: [DIR_CWD] });
39+
modulePath = import.meta.resolve(hookPath);
1840
} catch (e) {
41+
logger.warn(
42+
`Could not resolve the postinstall hook of ${addonName}, skipping its configuration. Run \`npx storybook add ${addonName}\` to set it up manually.`
43+
);
1944
return;
2045
}
2146
}
2247

48+
// Prefer the module resolved from the user's project (modulePath): a bare `import(hookPath)`
49+
// resolves relative to THIS file, which points at the wrong copy (or none at all) when the CLI
50+
// runs from a different tree than the project, e.g. via npx or in a monorepo.
51+
const moduleFilePath = modulePath.startsWith('file:') ? fileURLToPath(modulePath) : modulePath;
52+
2353
let moduledLoaded: any;
2454
try {
25-
moduledLoaded = await import(hookPath)
55+
moduledLoaded = await import(pathToFileURL(moduleFilePath).href)
56+
.catch(() => importModule(moduleFilePath))
57+
.catch(() => require(moduleFilePath))
58+
.catch(() => import(hookPath))
2659
.catch(() => importModule(hookPath))
27-
.catch(() => importModule(modulePath))
28-
.catch(() => importModule(fileURLToPath(modulePath)))
29-
.catch(() => require(hookPath))
30-
.catch(() => require(modulePath))
31-
.catch(() => require(fileURLToPath(modulePath)));
60+
.catch(() => require(hookPath));
3261
} catch (e) {
62+
logger.warn(
63+
`Could not load the postinstall hook of ${addonName} (${String(
64+
e
65+
)}), skipping its configuration. Run \`npx storybook add ${addonName}\` to set it up manually.`
66+
);
3367
return;
3468
}
3569

3670
const postinstall = moduledLoaded?.default || moduledLoaded?.postinstall || moduledLoaded;
37-
const logger = options.logger;
3871

3972
if (!postinstall || typeof postinstall !== 'function') {
4073
logger.error(`Error finding postinstall function for ${addonName}`);

code/lib/cli-storybook/src/upgrade.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -500,13 +500,23 @@ export async function upgrade(options: UpgradeOptions): Promise<void> {
500500
for (const project of storybookProjects) {
501501
const addonsToPostinstall = automigrationResults[project.configDir]?.addonsToPostinstall;
502502
if (addonsToPostinstall?.length) {
503-
await configureDeferredAddons(addonsToPostinstall, {
504-
packageManager: project.packageManager.type,
505-
configDir: project.configDir,
506-
yes: options.yes,
507-
logger,
508-
prompt,
509-
});
503+
logger.step(`Configuring addons: ${addonsToPostinstall.join(', ')}..`);
504+
try {
505+
await configureDeferredAddons(addonsToPostinstall, {
506+
packageManager: project.packageManager.type,
507+
configDir: project.configDir,
508+
yes: options.yes,
509+
logger,
510+
prompt,
511+
});
512+
} catch (error) {
513+
logger.warn(
514+
`Configuring ${addonsToPostinstall.join(', ')} failed: ${String(
515+
error
516+
)}. Run "npx storybook add <addon>" manually for each addon to finish the setup.`
517+
);
518+
logger.debug(error instanceof Error ? (error.stack ?? error.message) : String(error));
519+
}
510520
}
511521
}
512522
}

0 commit comments

Comments
 (0)