Skip to content

Commit

Permalink
Merge pull request #23644 from storybookjs/valentin/support-cjs-in-wr…
Browse files Browse the repository at this point in the history
…ap-require-automigration

Automigration: Fix wrap-require automigration for common js main.js files
(cherry picked from commit dc9ac08)
  • Loading branch information
valentinpalkovic authored and storybook-bot committed Jul 31, 2023
1 parent c75aeee commit 457e058
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 1 deletion.
11 changes: 10 additions & 1 deletion code/lib/cli/src/automigrate/fixes/wrap-require.ts
Expand Up @@ -61,7 +61,16 @@ export const wrapRequire: Fix<WrapRequireRunOptions> = {
});

if (getRequireWrapperName(mainConfig) === null) {
mainConfig.setImport(['dirname', 'join'], 'path');
if (
mainConfig.fileName.endsWith('.cjs') ||
mainConfig.fileName.endsWith('.cts') ||
mainConfig.fileName.endsWith('.cjsx') ||
mainConfig.fileName.endsWith('.ctsx')
) {
mainConfig.setRequireImport(['dirname', 'join'], 'path');
} else {
mainConfig.setImport(['dirname', 'join'], 'path');
}
mainConfig.setBodyDeclaration(
getRequireWrapperAsCallExpression(result.isConfigTypescript)
);
Expand Down
88 changes: 88 additions & 0 deletions code/lib/csf-tools/src/ConfigFile.test.ts
Expand Up @@ -1131,4 +1131,92 @@ describe('ConfigFile', () => {
`);
});
});

describe('setRequireImport', () => {
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.setRequireImport('path', 'path');

// eslint-disable-next-line no-underscore-dangle
const parsed = babelPrint(config._ast);

expect(parsed).toMatchInlineSnapshot(`
const path = require('path');
const config: StorybookConfig = { };
export default config;
`);
});

it(`supports setting a default import for a field that does exist`, () => {
const source = dedent`
const path = require('path');
const config: StorybookConfig = { };
export default config;
`;

const config = loadConfig(source).parse();
config.setRequireImport('path', 'path');

// eslint-disable-next-line no-underscore-dangle
const parsed = babelPrint(config._ast);

expect(parsed).toMatchInlineSnapshot(`
const path = require('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.setRequireImport(['dirname'], 'path');

// eslint-disable-next-line no-underscore-dangle
const parsed = babelPrint(config._ast);

expect(parsed).toMatchInlineSnapshot(`
const {
dirname,
} = require('path');
const config: StorybookConfig = { };
export default config;
`);
});

it(`supports setting a named import for a field where the source already exists`, () => {
const source = dedent`
const { dirname } = require('path');
const config: StorybookConfig = { };
export default config;
`;

const config = loadConfig(source).parse();
config.setRequireImport(['dirname', 'basename'], 'path');

// eslint-disable-next-line no-underscore-dangle
const parsed = babelPrint(config._ast);

expect(parsed).toMatchInlineSnapshot(`
const {
dirname,
basename,
} = require('path');
const config: StorybookConfig = { };
export default config;
`);
});
});
});
101 changes: 101 additions & 0 deletions code/lib/csf-tools/src/ConfigFile.ts
Expand Up @@ -523,6 +523,107 @@ export class ConfigFile {
this._ast.program.body.push(declaration);
}

/**
* Import specifiers for a specific require import
* @param importSpecifiers - The import specifiers to set. If a string is passed in, a default import will be set. Otherwise, an array of named imports will be set
* @param fromImport - The module to import from
* @example
* // const { foo } = require('bar');
* setRequireImport(['foo'], 'bar');
*
* // const foo = require('bar');
* setRequireImport('foo', 'bar');
*
*/
setRequireImport(importSpecifier: string[] | string, fromImport: string) {
const requireDeclaration = this._ast.program.body.find(
(node) =>
t.isVariableDeclaration(node) &&
node.declarations.length === 1 &&
t.isVariableDeclarator(node.declarations[0]) &&
t.isCallExpression(node.declarations[0].init) &&
t.isIdentifier(node.declarations[0].init.callee) &&
node.declarations[0].init.callee.name === 'require' &&
t.isStringLiteral(node.declarations[0].init.arguments[0]) &&
node.declarations[0].init.arguments[0].value === fromImport
) as t.VariableDeclaration | undefined;

/**
* Returns true, when the given import declaration has the given import specifier
* @example
* // const { foo } = require('bar');
* hasImportSpecifier(declaration, 'foo');
*/
const hasRequireSpecifier = (name: string) =>
t.isObjectPattern(requireDeclaration?.declarations[0].id) &&
requireDeclaration?.declarations[0].id.properties.find(
(specifier) =>
t.isObjectProperty(specifier) &&
t.isIdentifier(specifier.key) &&
specifier.key.name === name
);

/**
* Returns true, when the given import declaration has the given default import specifier
* @example
* // import foo from 'bar';
* hasImportSpecifier(declaration, 'foo');
*/
const hasDefaultRequireSpecifier = (declaration: t.VariableDeclaration, name: string) =>
declaration.declarations.length === 1 &&
t.isVariableDeclarator(declaration.declarations[0]) &&
t.isIdentifier(declaration.declarations[0].id) &&
declaration.declarations[0].id.name === name;

// if the import specifier is a string, we're dealing with default imports
if (typeof importSpecifier === 'string') {
// If the import declaration with the given source exists
const addDefaultRequireSpecifier = () => {
this._ast.program.body.unshift(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(importSpecifier),
t.callExpression(t.identifier('require'), [t.stringLiteral(fromImport)])
),
])
);
};

if (requireDeclaration) {
if (!hasDefaultRequireSpecifier(requireDeclaration, importSpecifier)) {
// If the import declaration hasn't the specified default identifier, we add a new variable declaration
addDefaultRequireSpecifier();
}
// If the import declaration with the given source doesn't exist
} else {
// Add the import declaration to the top of the file
addDefaultRequireSpecifier();
}
// if the import specifier is an array, we're dealing with named imports
} else if (requireDeclaration) {
importSpecifier.forEach((specifier) => {
if (!hasRequireSpecifier(specifier)) {
(requireDeclaration.declarations[0].id as t.ObjectPattern).properties.push(
t.objectProperty(t.identifier(specifier), t.identifier(specifier), undefined, true)
);
}
});
} else {
this._ast.program.body.unshift(
t.variableDeclaration('const', [
t.variableDeclarator(
t.objectPattern(
importSpecifier.map((specifier) =>
t.objectProperty(t.identifier(specifier), t.identifier(specifier), undefined, true)
)
),
t.callExpression(t.identifier('require'), [t.stringLiteral(fromImport)])
),
])
);
}
}

/**
* Set import specifiers for a given import statement.
* @description Does not support setting type imports (yet)
Expand Down

0 comments on commit 457e058

Please sign in to comment.