Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Improve support of mono repositories #23458

Merged
merged 19 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7281218
Improve support of monorepositories
valentinpalkovic Jul 14, 2023
37f468e
Improve wrap-require automigration to only trigger prompt if necessary
valentinpalkovic Jul 17, 2023
73a8d24
Upgrade babel dependencies
valentinpalkovic Jul 17, 2023
66f2f85
Improve automigration of wrap-for-pnp for corner cases
valentinpalkovic Jul 18, 2023
282be33
Merge remote-tracking branch 'origin/next' into valentin/fix-monorepo…
valentinpalkovic Jul 18, 2023
4ea52e3
Apply types for wrapper in Typescript projects
valentinpalkovic Jul 18, 2023
be68922
Apply requireWrapper on initialization
valentinpalkovic Jul 19, 2023
7096fc7
Apply requireWrapper also on core.renderer field
valentinpalkovic Jul 19, 2023
49d9485
Add a comment section the the requireWrapper function
valentinpalkovic Jul 19, 2023
1768568
Only add imports for requireWrapper if necessary
valentinpalkovic Jul 19, 2023
e2a33c5
Merge remote-tracking branch 'origin/next' into valentin/fix-monorepo…
valentinpalkovic Jul 19, 2023
3ad4aad
Remove unused import
valentinpalkovic Jul 19, 2023
6071bb1
Update docs
valentinpalkovic Jul 19, 2023
5186063
Fix unsound types
valentinpalkovic Jul 19, 2023
f95de25
Merge remote-tracking branch 'origin/next' into valentin/fix-monorepo…
valentinpalkovic Jul 19, 2023
c122c6e
Fix tests
valentinpalkovic Jul 19, 2023
94bd46c
Merge remote-tracking branch 'origin/next' into valentin/fix-monorepo…
valentinpalkovic Jul 19, 2023
4c3e892
Apply getAbsolutePath wrapper to all framework package preset files
valentinpalkovic Jul 20, 2023
bae9f2a
Fix wrapper replacement for framework field
valentinpalkovic Jul 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions code/lib/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"dependencies": {
"@babel/core": "^7.22.0",
"@babel/preset-env": "^7.22.0",
"@babel/types": "^7.22.5",
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved
"@ndelangen/get-tarball": "^3.0.7",
"@storybook/codemod": "7.1.0-rc.2",
"@storybook/core-common": "7.1.0-rc.2",
Expand Down
2 changes: 2 additions & 0 deletions code/lib/cli/src/automigrate/fixes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { missingBabelRc } from './missing-babelrc';
import { angularBuilders } from './angular-builders';
import { incompatibleAddons } from './incompatible-addons';
import { angularBuildersMultiproject } from './angular-builders-multiproject';
import { wrapRequire } from './wrap-require';

export * from '../types';

Expand All @@ -40,6 +41,7 @@ export const allFixes: Fix[] = [
missingBabelRc,
angularBuildersMultiproject,
angularBuilders,
wrapRequire,
];

export const initFixes: Fix[] = [missingBabelRc, eslintPlugin];
85 changes: 85 additions & 0 deletions code/lib/cli/src/automigrate/fixes/wrap-require.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/* eslint-disable no-param-reassign */
import chalk from 'chalk';
import { dedent } from 'ts-dedent';
import * as t from '@babel/types';
import type { Fix } from '../types';
import { detectPnp } from '../../detect';
import { updateMainConfig } from '../helpers/mainConfigFile';

interface WrapRequireRunOptions {
storybookVersion: string;
}

export const wrapRequire: Fix<WrapRequireRunOptions> = {
id: 'wrap-require',

async check({ packageManager, storybookVersion }) {
const isStorybookInMonorepo = await packageManager.isStorybookInMonorepo();
yannbf marked this conversation as resolved.
Show resolved Hide resolved
const isPnp = await detectPnp();

if (!isStorybookInMonorepo && !isPnp) {
return null;
}

return { storybookVersion };
},

prompt({ storybookVersion }) {
const sbFormatted = chalk.cyan(`Storybook ${storybookVersion}`);

return dedent`We've have detected, that you're using ${sbFormatted} in a monorepo or with PnP enabled.
We will apply some tweaks in your main config file to make it work in this special environment.`;
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved
},

async run({ dryRun, mainConfigPath }) {
updateMainConfig({ dryRun, mainConfigPath }, (mainConfig) => {
const frameworkNode = mainConfig.getFieldNode(['framework']);
const builderNode = mainConfig.getFieldNode(['core', 'builder']);
const addons = mainConfig.getFieldNode(['addons']);

const getRequireWrapperAsCallExpression = (value: string) => {
// callExpression for "dirname(require.resolve(join(value, 'package.json')))""
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved
return t.callExpression(t.identifier('dirname'), [
t.callExpression(t.memberExpression(t.identifier('require'), t.identifier('resolve')), [
t.callExpression(t.identifier('join'), [
t.stringLiteral(value),
t.stringLiteral('package.json'),
]),
]),
]);
};

const wrapValueWithRequireWrapper = (node: t.Node) => {
if (t.isStringLiteral(node)) {
// value will be converted from StringLiteral to CallExpression.
node.value = getRequireWrapperAsCallExpression(node.value) as any;
} else if (t.isObjectExpression(node)) {
const nameProperty = node.properties.find(
(property) =>
t.isObjectProperty(property) &&
t.isIdentifier(property.key) &&
property.key.name === 'name'
) as t.ObjectProperty;

if (nameProperty && t.isStringLiteral(nameProperty.value)) {
nameProperty.value = getRequireWrapperAsCallExpression(nameProperty.value.value);
yannbf marked this conversation as resolved.
Show resolved Hide resolved
}
}
};

if (frameworkNode) {
wrapValueWithRequireWrapper(frameworkNode);
}

if (builderNode) {
wrapValueWithRequireWrapper(builderNode);
}

if (addons && t.isArrayExpression(addons)) {
addons.elements.forEach(wrapValueWithRequireWrapper);
}

mainConfig.setImport(['dirname, join'], 'path');
});
},
};
2 changes: 1 addition & 1 deletion code/lib/cli/src/automigrate/helpers/mainConfigFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export type GetStorybookData = typeof getStorybookData;
*/
export const updateMainConfig = async (
{ mainConfigPath, dryRun }: { mainConfigPath: string; dryRun: boolean },
callback: (main: ConfigFile) => Promise<void>
callback: (main: ConfigFile) => Promise<void> | void
) => {
try {
const main = await readConfig(mainConfigPath);
Expand Down
18 changes: 12 additions & 6 deletions code/lib/cli/src/generators/baseGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ const getFrameworkDetails = (
renderer: SupportedRenderers,
builder: Builder,
pnp: boolean,
framework?: SupportedFrameworks
framework?: SupportedFrameworks,
isStorybookInMonorepository?: boolean
): {
type: 'framework' | 'renderer';
packages: string[];
Expand All @@ -115,15 +116,18 @@ const getFrameworkDetails = (
renderer?: string;
rendererId: SupportedRenderers;
} => {
const applyRequireWrapper = pnp || isStorybookInMonorepository;
const frameworkPackage = getFrameworkPackage(framework, renderer, builder);

const frameworkPackagePath = pnp ? wrapForPnp(frameworkPackage) : frameworkPackage;
const frameworkPackagePath = applyRequireWrapper
? wrapForPnp(frameworkPackage)
: frameworkPackage;

const rendererPackage = getRendererPackage(framework, renderer);
const rendererPackagePath = pnp ? wrapForPnp(rendererPackage) : rendererPackage;
const rendererPackagePath = applyRequireWrapper ? wrapForPnp(rendererPackage) : rendererPackage;

const builderPackage = getBuilderDetails(builder);
const builderPackagePath = pnp ? wrapForPnp(builderPackage) : builderPackage;
const builderPackagePath = applyRequireWrapper ? wrapForPnp(builderPackage) : builderPackage;

const isExternalFramework = !!getExternalFramework(frameworkPackage);
const isKnownFramework =
Expand Down Expand Up @@ -187,6 +191,8 @@ export async function baseGenerator(
};
process.on('SIGINT', setNodeProcessExiting);

const isStorybookInMonorepository = packageManager.isStorybookInMonorepo();

const stopIfExiting = async <T>(callback: () => Promise<T>) => {
if (isNodeProcessExiting) {
throw new HandledError('Canceled by the user');
Expand Down Expand Up @@ -226,7 +232,7 @@ export async function baseGenerator(
rendererId,
framework: frameworkInclude,
builder: builderInclude,
} = getFrameworkDetails(renderer, builder, pnp, framework);
} = getFrameworkDetails(renderer, builder, pnp, framework, isStorybookInMonorepository);

// added to main.js
const addons = [
Expand Down Expand Up @@ -365,7 +371,7 @@ export async function baseGenerator(
framework: { name: frameworkInclude, options: options.framework || {} },
storybookConfigFolder,
docs: { autodocs: 'tag' },
addons: pnp ? addons.map(wrapForPnp) : addons,
addons: pnp || isStorybookInMonorepository ? addons.map(wrapForPnp) : addons,
extensions,
language,
...(staticDir ? { staticDirs: [path.join('..', staticDir)] } : null),
Expand Down
47 changes: 45 additions & 2 deletions code/lib/cli/src/js-package-manager/JsPackageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { gt, satisfies } from 'semver';
import type { CommonOptions } from 'execa';
import { command as execaCommand, sync as execaCommandSync } from 'execa';
import path from 'path';
import fs from 'fs';
import fs, { read } from 'fs';

import dedent from 'ts-dedent';
import { readFile, writeFile } from 'fs-extra';
import { readFile, readFileSync, writeFile } from 'fs-extra';
import { commandLog } from '../helpers';
import type { PackageJson, PackageJsonWithDepsAndDevDeps } from './PackageJson';
import storybookPackagesVersions from '../versions';
Expand Down Expand Up @@ -76,6 +76,49 @@ export abstract class JsPackageManager {
this.cwd = options?.cwd || process.cwd();
}

/** Detect whether Storybook gets initialized in a monorepository/workspace environment
* The cwd doesn't have to be the root of the monorepo, it can be a subdirectory
* @returns true, if Storybook is initialized inside a monorepository/workspace
*/
public isStorybookInMonorepo() {
let cwd = process.cwd();

// eslint-disable-next-line no-constant-condition
while (true) {
yannbf marked this conversation as resolved.
Show resolved Hide resolved
try {
const turboJsonPath = `${cwd}/turbo.json`;
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved
if (fs.existsSync(turboJsonPath)) {
return true;
}

const packageJsonPath = require.resolve(`${cwd}/package.json`);

// read packagejson with readFileSync
const packageJsonFile = readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonFile) as PackageJsonWithDepsAndDevDeps;

if (packageJson.workspaces) {
return true;
}
} catch (err) {
// Package.json not found or invalid in current directory
}

// Move up to the parent directory
const parentDir = path.dirname(cwd);

// Check if we have reached the root of the filesystem
if (parentDir === cwd) {
break;
}

// Update cwd to the parent directory
cwd = parentDir;
}

return false;
}

/**
* Install dependencies listed in `package.json`
*/
Expand Down
70 changes: 70 additions & 0 deletions code/lib/csf-tools/src/ConfigFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { dedent } from 'ts-dedent';
import { formatConfig, loadConfig } from './ConfigFile';
import { babelPrint } from './babelParse';

expect.addSnapshotSerializer({
print: (val: any) => val,
Expand Down Expand Up @@ -1049,4 +1050,73 @@ describe('ConfigFile', () => {
expect(config.getNamesFromPath(['addons'])).toBeUndefined();
});
});

describe('setImport', () => {
it(`supports setting a default import for a field that does not exist`, () => {
const source = dedent`
const config: StorybookConfig = { };
export default config;
`;
const config = loadConfig(source).parse();
config.setImport('path', 'path');
// eslint-disable-next-line no-underscore-dangle
const parsed = babelPrint(config._ast);
expect(parsed).toMatchInlineSnapshot(`
import path from 'path';
const config: StorybookConfig = { };
export default config;
`);
});

it(`supports setting a default import for a field that does exist`, () => {
const source = dedent`
const config: StorybookConfig = { };
export default config;
`;
const config = loadConfig(source).parse();
config.setImport('path', 'path');
// eslint-disable-next-line no-underscore-dangle
const parsed = babelPrint(config._ast);
expect(parsed).toMatchInlineSnapshot(`
import path from 'path';
const config: StorybookConfig = { };
export default config;
`);
});

it(`supports setting a named import for a field that does not exist`, () => {
const source = dedent`
const config: StorybookConfig = { };
export default config;
`;
const config = loadConfig(source).parse();
config.setImport(['dirname'], 'path');
// eslint-disable-next-line no-underscore-dangle
const parsed = babelPrint(config._ast);
expect(parsed).toMatchInlineSnapshot(`
import { dirname } from 'path';
const config: StorybookConfig = { };
export default config;
`);
});

it(`supports setting a named import for a filed where the source already exists`, () => {
const source = dedent`
import { dirname } from 'path';

const config: StorybookConfig = { };
export default config;
`;
const config = loadConfig(source).parse();
config.setImport(['dirname'], 'path');
// eslint-disable-next-line no-underscore-dangle
const parsed = babelPrint(config._ast);
expect(parsed).toMatchInlineSnapshot(`
import { dirname } from 'path';

const config: StorybookConfig = { };
export default config;
`);
});
});
});