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

WIP: Angular: Fix csf-plugin usage #24660

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 22 additions & 12 deletions code/addons/docs/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import remarkExternalLinks from 'remark-external-links';
import { dedent } from 'ts-dedent';

import type { DocsOptions, Indexer, Options, PresetProperty } from '@storybook/types';
import type { CsfPluginOptions } from '@storybook/csf-plugin';
import type { CsfOptions } from '@storybook/csf-plugin';
import type { JSXOptions, CompileOptions } from '@storybook/mdx2-csf';
import { global } from '@storybook/global';
import { loadCsf } from '@storybook/csf-tools';
Expand All @@ -27,7 +27,7 @@ async function webpack(
mdxBabelOptions?: any;
/** @deprecated */
sourceLoaderOptions: any;
csfPluginOptions: CsfPluginOptions | null;
csfPluginOptions: CsfOptions | null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the point of this breaking change?

jsxOptions?: JSXOptions;
mdxPluginOptions?: CompileOptions;
} /* & Parameters<
Expand Down Expand Up @@ -92,17 +92,22 @@ async function webpack(

const result = {
...webpackConfig,
plugins: [
...(webpackConfig.plugins || []),

...(csfPluginOptions
? [(await import('@storybook/csf-plugin')).webpack(csfPluginOptions)]
: []),
],

module: {
...module,
rules: [
...(csfPluginOptions
? [
{
test: /\.(story|stories)\.[tj]sx?$/,
use: [
{
loader: require.resolve('@storybook/csf-plugin/webpack'),
options: csfPluginOptions,
},
],
},
]
: []),
...(module.rules || []),
{
test: /(stories|story)\.mdx$/,
Expand Down Expand Up @@ -141,7 +146,7 @@ export const createStoriesMdxIndexer = (legacyMdx1?: boolean): Indexer => ({
? await import('@storybook/mdx1-csf')
: await import('@storybook/mdx2-csf');
code = await compile(code, {});
const csf = loadCsf(code, { ...opts, fileName }).parse();
const csf = loadCsf(code, code, { ...opts, fileName }).parse();

const { indexInputs, stories } = csf;

Expand Down Expand Up @@ -202,4 +207,9 @@ const optimizeViteDeps = [
'markdown-to-jsx',
];

export { webpackX as webpack, indexersX as experimental_indexers, docsX as docs, optimizeViteDeps };
export {
webpackX as webpackFinal,
indexersX as experimental_indexers,
docsX as docs,
optimizeViteDeps,
};
6 changes: 3 additions & 3 deletions code/builders/builder-vite/src/plugins/csf-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Plugin } from 'vite';
import { vite } from '@storybook/csf-plugin';
import type { Options } from '@storybook/types';
// @ts-expect-error - The tsconfig.json in code sets moduleResolution: Node. But to respect `exports` fields from package.json's, we would need to set the moduleResolution field to either "Node16" or "nodenext", which introduces another wave of errors
import CsfVitePlugin from '@storybook/csf-plugin/vite';

export async function csfPlugin(config: Options): Promise<Plugin> {
const { presets } = config;
Expand All @@ -10,6 +11,5 @@ export async function csfPlugin(config: Options): Promise<Plugin> {
// @ts-expect-error - not sure what type to use here
addons.find((a) => [a, a.name].includes('@storybook/addon-docs'))?.options ?? {};

// TODO: looks like unplugin can return an array of plugins
return vite(docsOptions?.csfPluginOptions) as Plugin;
return CsfVitePlugin(docsOptions?.csfPluginOptions) as Plugin;
}
59 changes: 32 additions & 27 deletions code/lib/codemod/src/transforms/csf-2-to-3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import prettier from 'prettier';
import * as t from '@babel/types';
import { isIdentifier, isTSTypeAnnotation, isTSTypeReference } from '@babel/types';
import type { CsfFile } from '@storybook/csf-tools';
import { loadCsf, printCsf } from '@storybook/csf-tools';
import { loadCsf, printCsfOriginal } from '@storybook/csf-tools';
import type { API, FileInfo } from 'jscodeshift';
import type { BabelFile, NodePath } from '@babel/core';
import * as babel from '@babel/core';
Expand Down Expand Up @@ -89,9 +89,9 @@ const isSimpleCSFStory = (init: t.Expression, annotations: t.ObjectProperty[]) =
annotations.length === 0 && t.isArrowFunctionExpression(init) && init.params.length === 0;

function removeUnusedTemplates(csf: CsfFile) {
Object.entries(csf._templates).forEach(([template, templateExpression]) => {
Object.entries(csf._originalTemplates).forEach(([template, templateExpression]) => {
const references: NodePath[] = [];
babel.traverse(csf._ast, {
babel.traverse(csf._astOriginal, {
Identifier: (path) => {
if (path.node.name === template) references.push(path as NodePath);
},
Expand All @@ -114,7 +114,7 @@ export default function transform(info: FileInfo, api: API, options: { parser?:
const makeTitle = (userTitle?: string) => {
return userTitle || 'FIXME';
};
const csf = loadCsf(info.source, { makeTitle });
const csf = loadCsf(info.source, info.source, { makeTitle });

try {
csf.parse();
Expand All @@ -128,16 +128,18 @@ export default function transform(info: FileInfo, api: API, options: { parser?:

const file: BabelFile = new babel.File(
{ filename: info.path },
{ code: info.source, ast: csf._ast }
{ code: info.source, ast: csf._astOriginal }
);

const importHelper = new StorybookImportHelper(file, info);

const objectExports: Record<string, t.Statement> = {};
Object.entries(csf._storyExports).forEach(([key, decl]) => {
const annotations = Object.entries(csf._storyAnnotations[key]).map(([annotation, val]) => {
return t.objectProperty(t.identifier(renameAnnotation(annotation)), val as t.Expression);
});
Object.entries(csf._originalStoryExports).forEach(([key, decl]) => {
const annotations = Object.entries(csf._originalStoryAnnotations[key]).map(
([annotation, val]) => {
return t.objectProperty(t.identifier(renameAnnotation(annotation)), val as t.Expression);
}
);

if (t.isVariableDeclarator(decl as t.Node)) {
const { init, id } = decl as any;
Expand All @@ -162,7 +164,7 @@ export default function transform(info: FileInfo, api: API, options: { parser?:
// export const A = Template.bind({});
const renderAnnotation = isReactGlobalRenderFn(
csf,
template ? csf._templates[template] : storyFn
template ? csf._originalTemplates[template] : storyFn
)
? []
: [t.objectProperty(t.identifier('render'), storyFn)];
Expand All @@ -178,30 +180,33 @@ export default function transform(info: FileInfo, api: API, options: { parser?:
}
});

csf._ast.program.body = csf._ast.program.body.reduce((acc: t.Statement[], stmt: t.Statement) => {
const statement = stmt;
// remove story annotations & template declarations
if (isStoryAnnotation(statement, objectExports)) {
return acc;
}
csf._astOriginal.program.body = csf._astOriginal.program.body.reduce(
(acc: t.Statement[], stmt: t.Statement) => {
const statement = stmt as t.Statement;
// remove story annotations & template declarations
if (isStoryAnnotation(statement, objectExports)) {
return acc;
}

// replace story exports with new object exports
const newExport = getNewExport(statement, objectExports);
if (newExport) {
acc.push(newExport);
return acc;
}
// replace story exports with new object exports
const newExport = getNewExport(statement, objectExports);
if (newExport) {
acc.push(newExport);
return acc;
}

// include unknown statements
acc.push(statement);
return acc;
}, []);
// include unknown statements
acc.push(statement);
return acc;
},
[]
);

upgradeDeprecatedTypes(file);
importHelper.removeDeprecatedStoryImport();
removeUnusedTemplates(csf);

let output = printCsf(csf).code;
let output = printCsfOriginal(csf).code;

try {
const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function migrateType(oldType: string) {

export default function transform(info: FileInfo, api: API, options: { parser?: string }) {
// TODO what do I need to with the title?
const csf = loadCsf(info.source, { makeTitle: (title) => title });
const csf = loadCsf(info.source, info.source, { makeTitle: (title) => title });
const fileNode = csf._ast;
// @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606
const file: BabelFile = new babel.File(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ const getStorySortParameterMock = getStorySortParameter as jest.Mock<

const csfIndexer = async (fileName: string, opts: any) => {
const code = (await fs.readFile(fileName, 'utf-8')).toString();
return loadCsf(code, { ...opts, fileName }).parse();
return loadCsf(code, code, { ...opts, fileName }).parse();
};

const storiesMdxIndexer = async (fileName: string, opts: any) => {
let code = (await fs.readFile(fileName, 'utf-8')).toString();
const { compile } = await import('@storybook/mdx2-csf');
code = await compile(code, {});
return loadCsf(code, { ...opts, fileName }).parse();
return loadCsf(code, code, { ...opts, fileName }).parse();
};

const options: StoryIndexGeneratorOptions = {
Expand Down Expand Up @@ -1168,7 +1168,7 @@ describe('StoryIndexGenerator with deprecated indexer API', () => {
test: /\.stories\.(m?js|ts)x?$/,
createIndex: async (fileName, options) => {
const code = (await fs.readFile(fileName, 'utf-8')).toString();
const csf = loadCsf(code, { ...options, fileName }).parse();
const csf = loadCsf(code, code, { ...options, fileName }).parse();

// eslint-disable-next-line no-underscore-dangle
return Object.entries(csf._stories).map(([exportName, story]) => ({
Expand Down
25 changes: 20 additions & 5 deletions code/lib/csf-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,24 @@
"license": "MIT",
"sideEffects": false,
"exports": {
".": {
"./index": {
"types": "./dist/index.d.ts",
"node": "./dist/index.js",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./webpack": {
"types": "./dist/webpack.d.ts",
"node": "./dist/webpack.js",
"require": "./dist/webpack.js",
"import": "./dist/webpack.mjs"
},
"./vite": {
"types": "./dist/vite.d.ts",
"node": "./dist/vite.js",
"require": "./dist/vite.js",
"import": "./dist/vite.mjs"
},
"./package.json": "./package.json"
},
"main": "dist/index.js",
Expand All @@ -44,18 +56,21 @@
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts"
},
"dependencies": {
"@storybook/csf-tools": "workspace:*",
"unplugin": "^1.3.1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use unplugin?

"@storybook/csf-tools": "workspace:*"
},
"devDependencies": {
"typescript": "^5.3.2"
"typescript": "^5.3.2",
"vite": "^4.5.0",
"webpack": "^5.89.0"
},
"publishConfig": {
"access": "public"
},
"bundler": {
"entries": [
"./src/index.ts"
"./src/index.ts",
"./src/vite.ts",
"./src/webpack.ts"
],
"externals": [
"webpack",
Expand Down
39 changes: 2 additions & 37 deletions code/lib/csf-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,5 @@
import { createUnplugin } from 'unplugin';
import fs from 'fs/promises';
import { loadCsf, enrichCsf, formatCsf } from '@storybook/csf-tools';
import type { EnrichCsfOptions } from '@storybook/csf-tools';

export type CsfPluginOptions = EnrichCsfOptions;
export default {};

const STORIES_REGEX = /\.(story|stories)\.[tj]sx?$/;

const logger = console;

export const unplugin = createUnplugin<CsfPluginOptions>((options) => {
return {
name: 'unplugin-csf',
enforce: 'pre',
loadInclude(id) {
return STORIES_REGEX.test(id);
},
async load(fname) {
const code = await fs.readFile(fname, 'utf-8');
try {
const csf = loadCsf(code, { makeTitle: (userTitle) => userTitle || 'default' }).parse();
enrichCsf(csf, options);
return formatCsf(csf, { sourceMaps: true });
} catch (err: any) {
// This can be called on legacy storiesOf files, so just ignore
// those errors. But warn about other errors.
if (!err.message?.startsWith('CSF:')) {
logger.warn(err.message);
}
return code;
}
},
};
});

export const { esbuild } = unplugin;
export const { webpack } = unplugin;
export const { rollup } = unplugin;
export const { vite } = unplugin;
export type CsfOptions = EnrichCsfOptions;
45 changes: 45 additions & 0 deletions code/lib/csf-plugin/src/vite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { loadCsf, enrichCsf, formatCsf } from '@storybook/csf-tools';
import fs from 'fs/promises';
import type { Plugin } from 'vite';
import type { CsfOptions } from '.';

const STORIES_REGEX = /\.(story|stories)\.[tj]sx?$/;

const logger = console;

function CsfVitePluginFn(options: CsfOptions = {}): Plugin {
return {
name: 'csf-vite-plugin',

async transform(code: string, id: string) {
if (!STORIES_REGEX.test(id)) {
return null;
}

try {
const originalCode = await fs.readFile(id, 'utf-8');
const csf = loadCsf(code, originalCode, {
makeTitle: (userTitle) => userTitle || 'default',
}).parse();
enrichCsf(csf, options);
const result = formatCsf(csf, { sourceMaps: true });
if (typeof result === 'string') {
return result;
}
return {
code: result.code,
map: result.map,
};
} catch (err: any) {
if (!err.message?.startsWith('CSF:')) {
logger.warn(err.message);
}
return {
code,
};
}
},
};
}

export default CsfVitePluginFn as any;
Loading