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 12 commits
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
2 changes: 1 addition & 1 deletion code/builders/builder-webpack5/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"prep": "../../../scripts/prepare/bundle.ts"
},
"dependencies": {
"@babel/core": "^7.22.0",
"@babel/core": "^7.22.9",
"@storybook/addons": "7.1.0",
"@storybook/api": "7.1.0",
"@storybook/channel-postmessage": "7.1.0",
Expand Down
16 changes: 8 additions & 8 deletions code/frameworks/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/plugin-syntax-bigint": "^7.8.3",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-assertions": "^7.20.0",
"@babel/plugin-transform-runtime": "^7.22.0",
"@babel/preset-env": "^7.22.0",
"@babel/preset-react": "^7.22.0",
"@babel/preset-typescript": "^7.21.0",
"@babel/runtime": "^7.22.0",
"@babel/plugin-syntax-import-assertions": "^7.22.5",
"@babel/plugin-transform-runtime": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@babel/preset-react": "^7.22.5",
"@babel/preset-typescript": "^7.22.5",
"@babel/runtime": "^7.22.6",
"@storybook/addon-actions": "7.1.0",
"@storybook/builder-webpack5": "7.1.0",
"@storybook/core-common": "7.1.0",
Expand Down Expand Up @@ -97,8 +97,8 @@
"tsconfig-paths-webpack-plugin": "^4.0.1"
},
"devDependencies": {
"@babel/core": "^7.22.0",
"@babel/types": "^7.22.0",
"@babel/core": "^7.22.9",
"@babel/types": "^7.22.5",
"@types/babel__core": "^7",
"@types/babel__plugin-transform-runtime": "^7",
"@types/babel__preset-env": "^7",
Expand Down
2 changes: 1 addition & 1 deletion code/frameworks/web-components-webpack5/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"prep": "../../../scripts/prepare/bundle.ts"
},
"dependencies": {
"@babel/preset-env": "^7.22.0",
"@babel/preset-env": "^7.22.9",
"@storybook/builder-webpack5": "7.1.0",
"@storybook/core-common": "7.1.0",
"@storybook/preset-web-components-webpack": "7.1.0",
Expand Down
5 changes: 3 additions & 2 deletions code/lib/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@
"test": "jest test/**/*.test.js"
},
"dependencies": {
"@babel/core": "^7.22.0",
"@babel/preset-env": "^7.22.0",
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@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",
"@storybook/core-common": "7.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import path from 'path';

const wrapForPnp = (packageName) =>
path.dirname(require.resolve(path.join(packageName, 'package.json')));

const config = {
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
wrapForPnp('@storybook/addon-links'),
wrapForPnp('@storybook/addon-essentials'),
wrapForPnp('@storybook/addon-interactions'),
],
framework: {
name: wrapForPnp('@storybook/angular'),
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const config = {
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/angular',
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;
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];
181 changes: 181 additions & 0 deletions code/lib/cli/src/automigrate/fixes/wrap-require-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/* eslint-disable no-param-reassign */
import * as t from '@babel/types';
import type { ConfigFile } from '@storybook/csf-tools';

const defaultRequireWrapperName = 'getAbsolutePath';

/**
* Checks if the following node declarations exists in the main config file.
* @example
* const <name> = () => {};
* function <name>() {}
*/
function doesVariableOrFunctionDeclarationExist(node: t.Node, name: string) {
return (
(t.isVariableDeclaration(node) &&
node.declarations.length === 1 &&
t.isVariableDeclarator(node.declarations[0]) &&
t.isIdentifier(node.declarations[0].id) &&
node.declarations[0].id?.name === name) ||
(t.isFunctionDeclaration(node) && t.isIdentifier(node.id) && node.id.name === name)
);
}

/**
* Wrap a value with require wrapper.
* @example
* // Before
* { framework: 'react' }
*
* // After
* { framework: wrapForPnp('react') }
*/
function getReferenceToRequireWrapper(config: ConfigFile, value: string) {
return t.callExpression(
t.identifier(getRequireWrapperName(config) ?? defaultRequireWrapperName),
[t.stringLiteral(value)]
);
}

/**
* Returns the name of the require wrapper function if it exists in the main config file.
* @returns Name of the require wrapper function.
*/
export function getRequireWrapperName(config: ConfigFile) {
const declarationName = config.getBodyDeclarations().flatMap((node) =>
// eslint-disable-next-line no-nested-ternary
doesVariableOrFunctionDeclarationExist(node, 'wrapForPnp')
? ['wrapForPnp']
: doesVariableOrFunctionDeclarationExist(node, defaultRequireWrapperName)
? [defaultRequireWrapperName]
: []
);

if (declarationName.length) {
return declarationName[0];
}

return null;
}

/**
* Check if the node needs to be wrapped with require wrapper.
*/
export function isRequireWrapperNecessary(
node: t.Node,
cb: (node: t.StringLiteral | t.ObjectProperty | t.ArrayExpression) => void = () => {}
) {
if (t.isStringLiteral(node)) {
// value will be converted from StringLiteral to CallExpression.
cb(node);
return true;
}

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)) {
cb(nameProperty);
return true;
}
}

if (
t.isArrayExpression(node) &&
node.elements.some((element) => isRequireWrapperNecessary(element))
) {
cb(node);
return true;
}

return false;
}

/**
* Get all fields that need to be wrapped with require wrapper.
* @returns Array of fields that need to be wrapped with require wrapper.
*/
export function getFieldsForRequireWrapper(config: ConfigFile) {
const frameworkNode = config.getFieldNode(['framework']);
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved
const builderNode = config.getFieldNode(['core', 'builder']);
const rendererNode = config.getFieldNode(['core', 'renderer']);
const addons = config.getFieldNode(['addons']);

const fieldsWithRequireWrapper = [
...(frameworkNode ? [frameworkNode] : []),
...(builderNode ? [builderNode] : []),
...(rendererNode ? [rendererNode] : []),
...(addons && t.isArrayExpression(addons) ? [addons] : []),
];

return fieldsWithRequireWrapper;
}

/**
* Returns AST for the following function
* @example
* function getAbsolutePath(value) {
* return dirname(require.resolve(join(value, 'package.json')))
* }
*/
export function getRequireWrapperAsCallExpression(
isConfigTypescript: boolean
): t.FunctionDeclaration {
const functionDeclaration = {
...t.functionDeclaration(
t.identifier(defaultRequireWrapperName),
[
{
...t.identifier('value'),
...(isConfigTypescript
? { typeAnnotation: t.tsTypeAnnotation(t.tSStringKeyword()) }
: {}),
},
],
t.blockStatement([
t.returnStatement(
t.callExpression(t.identifier('dirname'), [
t.callExpression(t.memberExpression(t.identifier('require'), t.identifier('resolve')), [
t.callExpression(t.identifier('join'), [
t.identifier('value'),
t.stringLiteral('package.json'),
]),
]),
])
),
])
),
...(isConfigTypescript ? { returnType: t.tSTypeAnnotation(t.tsAnyKeyword()) } : {}),
};

t.addComment(
functionDeclaration,
'leading',
'*\n * This function is used to resolve the absolute path of a package.\n * It is needed in projects that use Yarn PnP or are set up within a monorepo.\n'
);

return functionDeclaration;
}

export function wrapValueWithRequireWrapper(config: ConfigFile, node: t.Node) {
isRequireWrapperNecessary(node, (n) => {
if (t.isStringLiteral(n)) {
n.value = getReferenceToRequireWrapper(config, n.value) as any;
}

if (t.isObjectProperty(n) && t.isStringLiteral(n.value)) {
n.value = getReferenceToRequireWrapper(config, n.value.value) as any;
}

if (t.isArrayExpression(n)) {
n.elements.forEach((element, index, elements) => {
if (t.isStringLiteral(element)) {
elements[index] = getReferenceToRequireWrapper(config, element.value);
}
});
}
});
}
75 changes: 75 additions & 0 deletions code/lib/cli/src/automigrate/fixes/wrap-require.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { wrapRequire } from './wrap-require';
import * as detect from '../../detect';

jest.mock('../../detect', () => ({
...jest.requireActual('../../detect'),
detectPnp: jest.fn(),
}));

describe('wrapRequire', () => {
describe('check', () => {
it('should return null if not in a monorepo and pnp is not enabled', async () => {
(detect.detectPnp as any as jest.SpyInstance).mockResolvedValue(false);

const check = wrapRequire.check({
packageManager: {
isStorybookInMonorepo: () => false,
},
storybookVersion: '7.0.0',
mainConfigPath: require.resolve('./__test__/main-config-without-wrappers.js'),
} as any);

await expect(check).resolves.toBeNull();
});

it('should return the configuration object if in a pnp environment', async () => {
(detect.detectPnp as any as jest.SpyInstance).mockResolvedValue(true);

const check = wrapRequire.check({
packageManager: {
isStorybookInMonorepo: () => false,
},
storybookVersion: '7.0.0',
mainConfigPath: require.resolve('./__test__/main-config-without-wrappers.js'),
} as any);

await expect(check).resolves.toEqual({
isPnp: true,
isStorybookInMonorepo: false,
storybookVersion: '7.0.0',
});
});

it('should return the configuration object if in a monorepo environment', async () => {
(detect.detectPnp as any as jest.SpyInstance).mockResolvedValue(false);

const check = wrapRequire.check({
packageManager: {
isStorybookInMonorepo: () => true,
},
storybookVersion: '7.0.0',
mainConfigPath: require.resolve('./__test__/main-config-without-wrappers.js'),
} as any);

await expect(check).resolves.toEqual({
isPnp: false,
isStorybookInMonorepo: true,
storybookVersion: '7.0.0',
});
});

it('should return null, if all fields have the require wrapper', async () => {
(detect.detectPnp as any as jest.SpyInstance).mockResolvedValue(true);

const check = wrapRequire.check({
packageManager: {
isStorybookInMonorepo: () => true,
},
storybookVersion: '7.0.0',
mainConfigPath: require.resolve('./__test__/main-config-with-wrappers.js'),
} as any);

await expect(check).resolves.toBeNull();
});
});
});