diff --git a/.storybook/main.js b/.storybook/main.js index bf9b14e09f43e..da228e5d88142 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,7 +1,12 @@ const path = require('path'); const fs = require('fs'); -const { loadWorkspaceAddon, registerTsPaths, registerRules, rules } = require('@fluentui/scripts-storybook'); +const { + loadWorkspaceAddon, + registerTsPaths, + processBabelLoaderOptions, + getImportMappingsForExportToSandboxAddon, +} = require('@fluentui/scripts-storybook'); const tsConfigPath = path.resolve(__dirname, '../tsconfig.base.json'); @@ -62,13 +67,24 @@ module.exports = /** @type {Omit} */ ({ // internal monorepo custom addons - /** @see ../packages/react-components/react-storybook-addon */ + /** {@link file://./../packages/react-components/react-storybook-addon/package.json} */ loadWorkspaceAddon('@fluentui/react-storybook-addon', { tsConfigPath }), - loadWorkspaceAddon('@fluentui/react-storybook-addon-export-to-sandbox', { tsConfigPath }), + /** {@link file://./../packages/react-components/react-storybook-addon-export-to-sandbox/package.json} */ + loadWorkspaceAddon('@fluentui/react-storybook-addon-export-to-sandbox', { + tsConfigPath, + /** @type {import('../packages/react-components/react-storybook-addon-export-to-sandbox/src/public-types').PresetConfig} */ + options: { + importMappings: getImportMappingsForExportToSandboxAddon(), + babelLoaderOptionsUpdater: processBabelLoaderOptions, + webpackRule: { + test: /\.stories\.tsx$/, + include: /stories/, + }, + }, + }), ], webpackFinal: config => { registerTsPaths({ config, configFile: tsConfigPath }); - registerRules({ config, rules: [rules.codesandboxRule] }); if ((process.env.CI || process.env.TF_BUILD || process.env.LAGE_PACKAGE_NAME) && config.plugins) { // Disable ProgressPlugin in PR/CI builds to reduce log verbosity (warnings and errors are still logged) diff --git a/.storybook/preview.js b/.storybook/preview.js index 4b45512253073..f58d6eda11b0d 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -39,9 +39,6 @@ export const parameters = { excludeDecorators: true, type: 'source', }, - // This config reuses sources generated for CodeSandbox export feature - // (@fluentui/babel-preset-storybook-full-source). - transformSource: (snippet, story) => story.parameters.fullSource, }, exportToSandbox: { provider: 'codesandbox-browser', diff --git a/packages/react-components/babel-preset-storybook-full-source/etc/babel-preset-storybook-full-source.api.md b/packages/react-components/babel-preset-storybook-full-source/etc/babel-preset-storybook-full-source.api.md index e8386cf719c80..b224fa709bf0d 100644 --- a/packages/react-components/babel-preset-storybook-full-source/etc/babel-preset-storybook-full-source.api.md +++ b/packages/react-components/babel-preset-storybook-full-source/etc/babel-preset-storybook-full-source.api.md @@ -10,7 +10,8 @@ import * as Babel from '@babel/core'; export type BabelPluginOptions = Record; // @public -export function fullSourcePlugin(babel: typeof Babel, options: BabelPluginOptions): Babel.PluginObj; +function fullSourcePlugin(babel: typeof Babel, options: BabelPluginOptions): Babel.PluginObj; +export default fullSourcePlugin; // (No @packageDocumentation comment for this package) diff --git a/packages/react-components/babel-preset-storybook-full-source/package.json b/packages/react-components/babel-preset-storybook-full-source/package.json index b3d922223deff..59811dd6bcd21 100644 --- a/packages/react-components/babel-preset-storybook-full-source/package.json +++ b/packages/react-components/babel-preset-storybook-full-source/package.json @@ -3,8 +3,8 @@ "version": "0.0.1", "description": "Babel preset that adds the makes the full source code of stories available", "private": true, - "main": "./src/index.dev.js", - "typings": "./src/index.dev.d.ts", + "main": "./lib-commonjs/index.js", + "typings": "./dist/index.d.ts", "repository": { "type": "git", "url": "https://github.com/microsoft/fluentui" @@ -33,9 +33,14 @@ }, "exports": { ".": { - "types": "./src/index.dev.d.ts", - "node": "./src/index.dev.js", - "require": "./src/index.dev.js" + "types": "./dist/index.d.ts", + "node": "./lib-commonjs/index.js", + "require": "./lib-commonjs/index.js" + }, + "./__dev": { + "types": "./src/index.ts", + "node": "./src/index.ts", + "require": "./src/index.ts" }, "./package.json": "./package.json" } diff --git a/packages/react-components/babel-preset-storybook-full-source/src/index.dev.d.ts b/packages/react-components/babel-preset-storybook-full-source/src/index.dev.d.ts deleted file mode 100644 index 2cb9de9550e8f..0000000000000 --- a/packages/react-components/babel-preset-storybook-full-source/src/index.dev.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { fullSourcePlugin } from './index'; -export type { BabelPluginOptions } from './index'; diff --git a/packages/react-components/babel-preset-storybook-full-source/src/index.dev.js b/packages/react-components/babel-preset-storybook-full-source/src/index.dev.js deleted file mode 100644 index 62c09df0331a0..0000000000000 --- a/packages/react-components/babel-preset-storybook-full-source/src/index.dev.js +++ /dev/null @@ -1,11 +0,0 @@ -// This is internal code and should be a dev dependency -/* eslint-disable import/no-extraneous-dependencies */ -const { registerTsProject } = require('nx/src/utils/register'); - -// This is internal code and should be a dev dependency -const { joinPathFragments } = require('@nx/devkit'); -/* eslint-enable import/no-extraneous-dependencies */ - -registerTsProject(joinPathFragments(__dirname, '..'), 'tsconfig.lib.json'); - -module.exports = require('./index.ts'); diff --git a/packages/react-components/babel-preset-storybook-full-source/src/index.ts b/packages/react-components/babel-preset-storybook-full-source/src/index.ts index db45fa99c1841..c9e681c9fa9ee 100644 --- a/packages/react-components/babel-preset-storybook-full-source/src/index.ts +++ b/packages/react-components/babel-preset-storybook-full-source/src/index.ts @@ -1,2 +1,4 @@ -export { fullSourcePlugin } from './fullsource'; +import { fullSourcePlugin } from './fullsource'; + export type { BabelPluginOptions } from './types'; +export default fullSourcePlugin; diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/package.json b/packages/react-components/react-storybook-addon-export-to-sandbox/package.json index 4a15818421288..7701cfaef4716 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/package.json +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/package.json @@ -30,6 +30,7 @@ "@swc/helpers": "^0.5.1", "@types/dedent": "0.7.0", "codesandbox-import-utils": "2.2.3", + "@fluentui/babel-preset-storybook-full-source": "~0.0.1", "dedent": "0.7.0" }, "peerDependencies": { diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/preset.js b/packages/react-components/react-storybook-addon-export-to-sandbox/preset.js index a093401712087..c7c7bd61d4ead 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/preset.js +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/preset.js @@ -1,5 +1,9 @@ +/* eslint-disable no-shadow */ + +const preset = require('./lib/preset/preset'); + function config(entry = []) { return [...entry, require.resolve('./lib/preset/preview')]; } -module.exports = { config }; +module.exports = { config, ...preset }; diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/preset/preset.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/preset/preset.ts new file mode 100644 index 0000000000000..af4ba4a266c94 --- /dev/null +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/preset/preset.ts @@ -0,0 +1,5 @@ +import { webpack, WebpackFinalConfig, WebpackFinalOptions } from '../webpack'; + +export function webpackFinal(config: WebpackFinalConfig, options: WebpackFinalOptions) { + return webpack(config, options); +} diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/preset/preview.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/preset/preview.ts index 4a635b24ae47f..da489893782a1 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/src/preset/preview.ts +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/preset/preview.ts @@ -1,3 +1,19 @@ +import type { Parameters } from '@storybook/addons'; +import type { StoryContextForEnhancers } from '@storybook/csf'; + import { withExportToSandboxButton } from '../decorators/with-export-to-sandbox-button'; export const decorators = [withExportToSandboxButton]; + +export const parameters: Parameters = { + docs: { + /** + * Override source code shown within "Show Code" Docs tab. + * @see https://github.com/storybookjs/storybook/blob/release-6-5/addons/docs/docs/recipes.md#customizing-source-snippets + */ + transformSource: (source: string, storyContext: StoryContextForEnhancers) => { + // This config renders story source generated via `fullSource` parameter that is being added by @fluentui/babel-preset-storybook-full-source plugin, which is registered as part of this preset + return storyContext.parameters.fullSource; + }, + }, +}; diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/public-types.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/public-types.ts index 744d744d917fc..6a8974d09330f 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/src/public-types.ts +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/public-types.ts @@ -14,3 +14,9 @@ interface ParametersConfig { export interface ParametersExtension { exportToSandbox?: ParametersConfig; } + +export interface PresetConfig { + importMappings: import('@fluentui/babel-preset-storybook-full-source').BabelPluginOptions; + webpackRule?: import('webpack').RuleSetRule; + babelLoaderOptionsUpdater?: (value: import('@babel/core').TransformOptions) => typeof value; +} diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-utils.spec.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-utils.spec.ts index 9f13e2203a01d..d01403d49ed14 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-utils.spec.ts +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-utils.spec.ts @@ -44,7 +44,7 @@ describe(`sabdbox-utils`, () => { expect(actual).toBe(null); expect(consoleErrorSpy.mock.calls.flat()).toMatchInlineSnapshot(` Array [ - "Export to CodeSandbox: Couldn't find source for story Showcase. Did you install the babel plugin?", + "Export to Sandbox Addon: Couldn't find source for story Showcase. Did you install the babel plugin?", ] `); }); diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-utils.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-utils.ts index 6e15a9400c3f4..e5f534fc0f739 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-utils.ts +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-utils.ts @@ -71,7 +71,7 @@ export function prepareData(context: StoryContext): Data | null { if (!storyFile) { console.error( - dedent`Export to CodeSandbox: Couldn't find source for story ${context.story}. Did you install the babel plugin?`, + dedent`Export to Sandbox Addon: Couldn't find source for story ${context.story}. Did you install the babel plugin?`, ); return null; } diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/types.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/types.ts index dc41cad559b25..78519a904cbae 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/src/types.ts +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/types.ts @@ -1,8 +1,8 @@ import type { StoryContext as StoryContextOrigin, Parameters } from '@storybook/addons'; -import type { ParametersExtension } from './public-types'; +import type { ParametersExtension, PresetConfig } from './public-types'; export interface StoryContext extends StoryContextOrigin { parameters: Parameters & ParametersExtension; } -export type { ParametersExtension }; +export type { ParametersExtension, PresetConfig }; diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.spec.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.spec.ts new file mode 100644 index 0000000000000..bdbe31935fbe6 --- /dev/null +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.spec.ts @@ -0,0 +1,68 @@ +import { PresetConfig } from './public-types'; +import { webpack, WebpackFinalOptions } from './webpack'; +describe(`webpack`, () => { + it(`should register webpack preset with defaults`, () => { + const actual = webpack({ module: { rules: [] } }, { + presetsList: [ + { + name: 'node_modules/@fluentui/react-storybook-addon-export-to-sandbox/lib/preset.js', + preset: {}, + options: {}, + }, + ], + } as WebpackFinalOptions); + + expect(actual.module?.rules).toEqual([ + { + enforce: 'post', + test: /\.stories\.(jsx?$|tsx?$)/, + use: { + loader: 'babel-loader', + options: { + plugins: [[expect.stringContaining('babel-preset-storybook-full-source'), undefined]], + }, + }, + }, + ]); + }); + + it(`should register webpack preset with user provided options`, () => { + const actual = webpack({ module: { rules: [] } }, { + presetsList: [ + { + name: 'node_modules/@fluentui/react-storybook-addon-export-to-sandbox/lib/preset.js', + preset: {}, + options: { + importMappings: { + '@proj/foo': { replace: '@proj/moo' }, + }, + webpackRule: { test: /\.stories\.tsx?/, include: /foo-stories/ }, + babelLoaderOptionsUpdater: value => { + return Object.assign(value, { presets: ['babel-foo-bar-preset'] }); + }, + } as PresetConfig, + }, + ], + } as WebpackFinalOptions); + + expect(actual.module?.rules).toEqual([ + { + enforce: 'post', + test: /\.stories\.tsx?/, + include: /foo-stories/, + use: { + loader: 'babel-loader', + options: { + plugins: [ + [ + expect.stringContaining('babel-preset-storybook-full-source'), + { '@proj/foo': { replace: '@proj/moo' } }, + ], + ], + presets: ['babel-foo-bar-preset'], + }, + }, + }, + ]); + }); +}); diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts new file mode 100644 index 0000000000000..5fd2825c30f50 --- /dev/null +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts @@ -0,0 +1,70 @@ +import type { PresetConfig } from './types'; + +type WebpackFinalFn = NonNullable; +export type WebpackFinalConfig = Parameters[0]; +export type WebpackFinalOptions = Parameters[1]; + +export function webpack(config: WebpackFinalConfig, options: WebpackFinalOptions) { + const addonPresetConfig = getAddonOptions(options); + + registerRules({ config, rules: [createRule(addonPresetConfig)] }); + + return config; +} + +const identity = (value: T) => value; +const addonFilePattern = /react-storybook-addon-export-to-sandbox\/[a-z/]+.[jt]s$/; +const defaultOptions = { + webpackRule: {}, + babelLoaderOptionsUpdater: identity, +}; + +const PLUGIN_PATH = + process.env.NODE_ENV !== 'production' + ? '@fluentui/babel-preset-storybook-full-source/__dev' + : '@fluentui/babel-preset-storybook-full-source'; + +function createRule(config: Required): import('webpack').RuleSetRule { + const { babelLoaderOptionsUpdater, importMappings, webpackRule } = config; + + const plugin = [require.resolve(PLUGIN_PATH), importMappings]; + + return { + test: /\.stories\.(jsx?$|tsx?$)/, + ...webpackRule, + /** + * why the usage of 'post' ? - we need to run this loader after all storybook webpack rules/loaders have been executed. + * while we can use Array.prototype.unshift to "override" the indexes this approach is more declarative without additional hacks. + */ + enforce: 'post', + use: { + loader: 'babel-loader', + options: babelLoaderOptionsUpdater({ + plugins: [plugin], + }), + }, + }; +} + +/** + * + * register custom Webpack Rules to webpack config + */ +function registerRules(options: { rules: import('webpack').RuleSetRule[]; config: import('webpack').Configuration }) { + const { config, rules } = options; + config.module = config.module ?? {}; + config.module.rules = config.module.rules ?? []; + config.module.rules.push(...rules); + + return config; +} + +function getAddonOptions(options: WebpackFinalOptions): Required { + const presetRegistration = options.presetsList?.find(preset => { + return addonFilePattern.test(preset.name); + }); + + const addonOptions = presetRegistration?.options ?? {}; + + return { ...defaultOptions, ...addonOptions }; +} diff --git a/scripts/storybook/package.json b/scripts/storybook/package.json index 851127a2afe2e..708b97b2b442d 100644 --- a/scripts/storybook/package.json +++ b/scripts/storybook/package.json @@ -11,7 +11,6 @@ "type-check": "tsc -b tsconfig.json" }, "dependencies": { - "@fluentui/scripts-monorepo": "*", - "@fluentui/babel-preset-storybook-full-source": "*" + "@fluentui/scripts-monorepo": "*" } } diff --git a/scripts/storybook/src/index.d.ts b/scripts/storybook/src/index.d.ts index 8e89d8b7855d3..3cd51591fa2ad 100644 --- a/scripts/storybook/src/index.d.ts +++ b/scripts/storybook/src/index.d.ts @@ -4,6 +4,8 @@ export { registerTsPaths, registerRules, overrideDefaultBabelLoader, + processBabelLoaderOptions, + getImportMappingsForExportToSandboxAddon, } from './utils'; export * as rules from './rules'; diff --git a/scripts/storybook/src/index.js b/scripts/storybook/src/index.js index 364bb4019e518..0d064e63f9697 100644 --- a/scripts/storybook/src/index.js +++ b/scripts/storybook/src/index.js @@ -1,5 +1,12 @@ const rules = require('./rules'); -const { getPackageStoriesGlob, loadWorkspaceAddon, registerRules, registerTsPaths } = require('./utils'); +const { + getPackageStoriesGlob, + loadWorkspaceAddon, + registerRules, + registerTsPaths, + processBabelLoaderOptions, + getImportMappingsForExportToSandboxAddon, +} = require('./utils'); module.exports = { getPackageStoriesGlob, @@ -7,4 +14,6 @@ module.exports = { registerRules, registerTsPaths, rules, + getImportMappingsForExportToSandboxAddon, + processBabelLoaderOptions, }; diff --git a/scripts/storybook/src/rules.js b/scripts/storybook/src/rules.js index ab0f4f37c73b2..88c56ffd55af0 100644 --- a/scripts/storybook/src/rules.js +++ b/scripts/storybook/src/rules.js @@ -1,5 +1,3 @@ -const { _createCodesandboxRule } = require('./utils'); - /** * @type {import("webpack").RuleSetRule} */ @@ -92,13 +90,7 @@ const griffelRule = { ], }; -/** - * @type {import("webpack").RuleSetRule} - */ -const codesandboxRule = _createCodesandboxRule(); - exports.tsRule = tsRule; exports.scssRule = scssRule; exports.cssRule = cssRule; exports.griffelRule = griffelRule; -exports.codesandboxRule = codesandboxRule; diff --git a/scripts/storybook/src/rules.spec.ts b/scripts/storybook/src/rules.spec.ts index 1c6a65ccf9d2f..4d70ff3d7e4d5 100644 --- a/scripts/storybook/src/rules.spec.ts +++ b/scripts/storybook/src/rules.spec.ts @@ -1,49 +1,3 @@ -import { getAllPackageInfo } from '@fluentui/scripts-monorepo'; -import * as semver from 'semver'; - -import { codesandboxRule } from './rules'; - describe(`rules`, () => { - describe(`codesandbox`, () => { - it(`should generate rule definition with overridden babel loader`, () => { - const allPackagesInfo = getAllPackageInfo(); - const allPackagesInfoProjects = Object.values(allPackagesInfo); - const suitePackage = allPackagesInfo['@fluentui/react-components']; - const suitePackageDependencies = suitePackage.packageJson.dependencies ?? {}; - const unstablePackage = allPackagesInfoProjects.find(metadata => { - return ( - suitePackageDependencies[metadata.packageJson.name] && - semver.prerelease(metadata.packageJson.version) !== null - ); - }); - const stableSuitePackages = allPackagesInfoProjects.reduce((acc, metadata) => { - if ( - suitePackageDependencies[metadata.packageJson.name] && - semver.prerelease(metadata.packageJson.version) === null - ) { - acc[metadata.packageJson.name] = { replace: '@fluentui/react-components' }; - } - return acc; - }, {} as Record); - - const options = (codesandboxRule.use as { options: Record }).options; - - expect(options).toEqual( - expect.objectContaining({ - customize: expect.stringContaining('loaders/custom-loader.js'), - plugins: [ - [ - expect.any(Function), - expect.objectContaining({ - ...stableSuitePackages, - ...(unstablePackage - ? { [unstablePackage.packageJson.name]: { replace: '@fluentui/react-components/unstable' } } - : null), - }), - ], - ], - }), - ); - }); - }); + it.todo(`add tests if necessary...`); }); diff --git a/scripts/storybook/src/utils.js b/scripts/storybook/src/utils.js index af000bda1dd32..b6c561fc6370c 100644 --- a/scripts/storybook/src/utils.js +++ b/scripts/storybook/src/utils.js @@ -1,7 +1,6 @@ const fs = require('fs'); const path = require('path'); -const { fullSourcePlugin: babelPlugin } = require('@fluentui/babel-preset-storybook-full-source'); const { getAllPackageInfo } = require('@fluentui/scripts-monorepo'); const { stripIndents, offsetFromRoot, workspaceRoot, readProjectConfiguration } = require('@nx/devkit'); const { FsTree } = require('nx/src/generators/tree'); @@ -24,14 +23,26 @@ const loadWorkspaceAddonDefaultOptions = { workspaceRoot }; * } * ``` * + * @template {Record} AddonConfiguration * @param {string} addonName - package name of custom workspace addon * @param {Object} options * @param {string=} options.workspaceRoot * @param {string} options.tsConfigPath - absolute path to tsConfig that contains path aliases + * @param {AddonConfiguration=} options.options - addon preset configuration */ function loadWorkspaceAddon(addonName, options) { /* eslint-disable no-shadow */ - const { workspaceRoot, tsConfigPath } = { ...loadWorkspaceAddonDefaultOptions, ...options }; + const { workspaceRoot, tsConfigPath, options: addonConfig } = { ...loadWorkspaceAddonDefaultOptions, ...options }; + + const inMemoryTsTranspilationTemplate = stripIndents` + function registerInMemoryTsTranspilation(){ + const { registerTsProject } = require('nx/src/utils/register'); + const { joinPathFragments } = require('@nx/devkit'); + registerTsProject(joinPathFragments(__dirname, '..'), 'tsconfig.lib.json'); + } + + registerInMemoryTsTranspilation(); + `; function getPaths() { const addonMetadata = getProjectMetadata(addonName, workspaceRoot); @@ -92,8 +103,10 @@ function loadWorkspaceAddon(addonName, options) { const posixTsConfigPath = tsConfigPath.split(path.sep).join(path.posix.sep); const presetRelativePathToDistApiRegex = new RegExp(`\\./${packageDistPath}`, 'g'); + const presetConfigRegex = /const\s+([a-z]+)\s+=\s+require\('([a-z/.]+)\/preset'\)/i; const presetApiRegex = /module\.exports\s+=\s+({).+}/; const presetApiPathRegex = /(\/manager|\/preview)/g; + let modifiedPresetContent = presetContent .replace(presetRelativePathToDistApiRegex, relativePathToSource) .replace(presetApiPathRegex, '$1.ts') @@ -102,7 +115,6 @@ function loadWorkspaceAddon(addonName, options) { }); modifiedPresetContent = stripIndents` - // @ts-ignore const { registerTsPaths } = require('@fluentui/scripts-storybook'); function managerWebpack(config, options) { @@ -113,76 +125,69 @@ function loadWorkspaceAddon(addonName, options) { ${modifiedPresetContent} `; + if (presetConfigRegex.test(presetContent)) { + modifiedPresetContent = stripIndents` + ${inMemoryTsTranspilationTemplate} + + ${modifiedPresetContent} + `; + } + + modifiedPresetContent = stripIndents` + // @ts-nocheck + + ${modifiedPresetContent} + `; + if (!fs.existsSync(packageTempPath)) { fs.mkdirSync(packageTempPath, { recursive: true }); } fs.writeFileSync(presetMockedSourcePath, modifiedPresetContent, { encoding: 'utf-8' }); + if (addonConfig) { + return { name: presetMockedSourcePath, options: addonConfig }; + } + return presetMockedSourcePath; /* eslint-enable no-shadow */ } /** - * @private * @param {ReturnType} allPackageInfo - * @returns {import("webpack").RuleSetRule} + * @returns {Record}; */ -function _createCodesandboxRule(allPackageInfo = getAllPackageInfo()) { - const config = getCodesandboxBabelOptions(); - - return { - /** - * why the usage of 'post' ? - we need to run this loader after all storybook webpack rules/loaders have been executed. - * while we can use Array.prototype.unshift to "override" the indexes this approach is more declarative without additional hacks. - */ - enforce: 'post', - test: /\.stories\.tsx$/, - include: /stories/, - exclude: /node_modules/, - use: { - loader: 'babel-loader', - options: _processBabelLoaderOptions({ - plugins: [[babelPlugin, config]], - }), - }, - }; - +function getImportMappingsForExportToSandboxAddon(allPackageInfo = getAllPackageInfo()) { /** - * @returns {import('@fluentui/babel-preset-storybook-full-source').BabelPluginOptions} + * packages that are part of v9 but are not meant for platform:web */ - function getCodesandboxBabelOptions() { - /** - * packages that are part of v9 but are not meant for platform:web - */ - const excludePackages = [ - '@fluentui/babel-preset-storybook-full-source', - '@fluentui/react-storybook-addon', - '@fluentui/react-storybook-addon-export-to-sandbox', - '@fluentui/react-conformance-griffel', - ]; - - const importMappings = Object.values(allPackageInfo).reduce((acc, cur) => { - if (excludePackages.includes(cur.packageJson.name)) { - return acc; - } - - if (isPackagePartOfReactComponentsSuite(cur.packageJson.name)) { - // TODO: once all pre-release packages (deprecated approach) will be released as stable this logic will be removed - const isPrerelease = semver.prerelease(cur.packageJson.version) !== null; + const excludePackages = [ + '@fluentui/babel-preset-storybook-full-source', + '@fluentui/react-storybook-addon', + '@fluentui/react-storybook-addon-export-to-sandbox', + '@fluentui/react-conformance-griffel', + ]; + + const importMappings = Object.values(allPackageInfo).reduce((acc, cur) => { + if (excludePackages.includes(cur.packageJson.name)) { + return acc; + } - acc[cur.packageJson.name] = isPrerelease - ? { replace: '@fluentui/react-components/unstable' } - : { replace: '@fluentui/react-components' }; + if (isPackagePartOfReactComponentsSuite(cur.packageJson.name)) { + // TODO: once all pre-release packages (deprecated approach) will be released as stable this logic will be removed + const isPrerelease = semver.prerelease(cur.packageJson.version) !== null; - return acc; - } + acc[cur.packageJson.name] = isPrerelease + ? { replace: '@fluentui/react-components/unstable' } + : { replace: '@fluentui/react-components' }; return acc; - }, /** @type import('@fluentui/babel-preset-storybook-full-source').BabelPluginOptions*/ ({})); + } - return importMappings; - } + return acc; + }, /** @type {Record} */ ({})); + + return importMappings; /** * @@ -288,10 +293,9 @@ function registerRules(options) { * Why is this needed: * - `options.babelrc` is ignored by `babel-loader` thus we need to use `customize` api to exclude specific babel presets/plugins * - * @private * @param {BabelLoaderOptions} loaderConfig */ -function _processBabelLoaderOptions(loaderConfig) { +function processBabelLoaderOptions(loaderConfig) { const customLoaderPath = path.join(__dirname, './loaders/custom-loader.js'); const customOptions = { customize: customLoaderPath }; Object.assign(loaderConfig, customOptions); @@ -327,7 +331,7 @@ function overrideDefaultBabelLoader(options) { const loader = getBabelLoader(/** @type {import('webpack').RuleSetRule[]}*/ (config.module.rules)); - _processBabelLoaderOptions(loader.options); + processBabelLoaderOptions(loader.options); function getBabelLoader(/** @type {import('webpack').RuleSetRule[]} */ rules) { // eslint-disable-next-line no-shadow @@ -370,4 +374,5 @@ exports.loadWorkspaceAddon = loadWorkspaceAddon; exports.registerTsPaths = registerTsPaths; exports.registerRules = registerRules; exports.overrideDefaultBabelLoader = overrideDefaultBabelLoader; -exports._createCodesandboxRule = _createCodesandboxRule; +exports.processBabelLoaderOptions = processBabelLoaderOptions; +exports.getImportMappingsForExportToSandboxAddon = getImportMappingsForExportToSandboxAddon; diff --git a/scripts/storybook/src/utils.spec.js b/scripts/storybook/src/utils.spec.js index f91a544bb7544..665105fa10f0b 100644 --- a/scripts/storybook/src/utils.spec.js +++ b/scripts/storybook/src/utils.spec.js @@ -1,10 +1,18 @@ +/* eslint-disable no-shadow */ const fs = require('fs'); const path = require('path'); -const { stripIndents } = require('@nx/devkit'); +const { getAllPackageInfo } = require('@fluentui/scripts-monorepo'); +const { stripIndents, workspaceRoot } = require('@nx/devkit'); +const semver = require('semver'); const tmp = require('tmp'); -const { loadWorkspaceAddon, getPackageStoriesGlob } = require('./utils'); +const { + loadWorkspaceAddon, + getPackageStoriesGlob, + getImportMappingsForExportToSandboxAddon, + processBabelLoaderOptions, +} = require('./utils'); tmp.setGracefulCleanup(); @@ -12,7 +20,7 @@ describe(`utils`, () => { describe(`#loadWorkspacePlugin`, () => { /** * - * @param {{packageName:string}} options + * @param {{packageName:string, presetContent?: string}} options */ function setup(options) { const npmScope = '@proj'; @@ -61,8 +69,8 @@ describe(`utils`, () => { 'utf-8', ); - fs.writeFileSync( - paths.preset, + const presetTemplate = + options.presetContent ?? stripIndents` function config(entry = []) { return [...entry, require.resolve('./lib/preset/preview')]; @@ -73,9 +81,9 @@ describe(`utils`, () => { } module.exports = { managerEntries, config }; - `, - 'utf-8', - ); + `; + + fs.writeFileSync(paths.preset, presetTemplate, 'utf-8'); return { npmScope, @@ -97,6 +105,22 @@ describe(`utils`, () => { expect(actual).toBe(expected); }); + it(`should return path to in memory preset loader root with options if provided `, () => { + const { npmScope, workspaceRoot, tsConfigRoot } = setup({ packageName: 'storybook-custom-addon' }); + + const actual = loadWorkspaceAddon(`${npmScope}/storybook-custom-addon`, { + workspaceRoot, + tsConfigPath: tsConfigRoot, + options: { who: 'developers' }, + }); + const expected = { + name: `${workspaceRoot}/packages/storybook-custom-addon/temp/preset.ts`, + options: { who: 'developers' }, + }; + + expect(actual).toEqual(expected); + }); + it(`should create mocked preset registration module with in memory TS compilation`, () => { const { tsConfigRoot, npmScope, packageRoot, workspaceRoot } = setup({ packageName: 'storybook-custom-addon' }); @@ -104,12 +128,13 @@ describe(`utils`, () => { const mockedPreset = fs.readFileSync(path.join(packageRoot, 'temp', 'preset.ts'), 'utf-8'); - expect(mockedPreset).toMatchInlineSnapshot(` - "// @ts-ignore + expect(mockedPreset.replace(tsConfigRoot, 'Any')).toMatchInlineSnapshot(` + "// @ts-nocheck + const { registerTsPaths } = require('@fluentui/scripts-storybook'); function managerWebpack(config, options) { - registerTsPaths({config, configFile: '${tsConfigRoot}'}); + registerTsPaths({config, configFile: 'Any'}); return config; } @@ -124,6 +149,61 @@ describe(`utils`, () => { module.exports = { managerWebpack, managerEntries, config };" `); }); + + it(`should create mocked preset registration module with in memory TS compilation if webpack preset is part of api`, () => { + const { tsConfigRoot, npmScope, packageRoot, workspaceRoot } = setup({ + packageName: 'storybook-custom-addon', + + presetContent: stripIndents` + const preset = require('./lib/preset/preset'); + + function config(entry = []) { + return [...entry, require.resolve('./lib/preset/preview')]; + } + + function managerEntries(entry = []) { + return [...entry, require.resolve('./lib/preset/manager')]; + } + + module.exports = { managerEntries, config, ...preset }; + `, + }); + + loadWorkspaceAddon(`${npmScope}/storybook-custom-addon`, { workspaceRoot, tsConfigPath: tsConfigRoot }); + + const mockedPreset = fs.readFileSync(path.join(packageRoot, 'temp', 'preset.ts'), 'utf-8'); + + expect(mockedPreset.replace(tsConfigRoot, 'Any')).toMatchInlineSnapshot(` + "// @ts-nocheck + + function registerInMemoryTsTranspilation(){ + const { registerTsProject } = require('nx/src/utils/register'); + const { joinPathFragments } = require('@nx/devkit'); + registerTsProject(joinPathFragments(__dirname, '..'), 'tsconfig.lib.json'); + } + + registerInMemoryTsTranspilation(); + + const { registerTsPaths } = require('@fluentui/scripts-storybook'); + + function managerWebpack(config, options) { + registerTsPaths({config, configFile: 'Any'}); + return config; + } + + const preset = require('../src/preset/preset'); + + function config(entry = []) { + return [...entry, require.resolve('../src/preset/preview.ts')]; + } + + function managerEntries(entry = []) { + return [...entry, require.resolve('../src/preset/manager.ts')]; + } + + module.exports = { managerWebpack, managerEntries, config, ...preset };" + `); + }); }); describe(`#getPackageStoriesGlob`, () => { @@ -146,4 +226,57 @@ describe(`utils`, () => { expect(first.endsWith('**/@(index.stories.@(ts|tsx)|*.stories.mdx)')).toBeTruthy(); }); }); + + describe(`#processBabelLoaderOptions`, () => { + it(`should add customize property with loader`, () => { + const actual = processBabelLoaderOptions({ plugins: [['foo-babel-loader', { one: true }]] }); + + expect(actual).toEqual({ + customize: `${workspaceRoot}/scripts/storybook/src/loaders/custom-loader.js`, + plugins: [ + [ + 'foo-babel-loader', + { + one: true, + }, + ], + ], + }); + }); + }); + + describe(`#getImportMappingsForExportToSandboxAddon`, () => { + it(`should get import mappings for storybook sources`, () => { + const allPackagesInfo = getAllPackageInfo(); + const allPackagesInfoProjects = Object.values(allPackagesInfo); + const suitePackage = allPackagesInfo['@fluentui/react-components']; + const suitePackageDependencies = suitePackage.packageJson.dependencies ?? {}; + const unstablePackage = allPackagesInfoProjects.find(metadata => { + return ( + suitePackageDependencies[metadata.packageJson.name] && + semver.prerelease(metadata.packageJson.version) !== null + ); + }); + const stableSuitePackages = allPackagesInfoProjects.reduce((acc, metadata) => { + if ( + suitePackageDependencies[metadata.packageJson.name] && + semver.prerelease(metadata.packageJson.version) === null + ) { + acc[metadata.packageJson.name] = { replace: '@fluentui/react-components' }; + } + return acc; + }, /** @type {Record} */ ({})); + + const actual = getImportMappingsForExportToSandboxAddon(); + + expect(actual).toEqual( + expect.objectContaining({ + ...stableSuitePackages, + ...(unstablePackage + ? { [unstablePackage.packageJson.name]: { replace: '@fluentui/react-components/unstable' } } + : null), + }), + ); + }); + }); }); diff --git a/typings/storybook__addons/index.d.ts b/typings/storybook__addons/index.d.ts index fd135ac9a8127..b1e120cfcf5ec 100644 --- a/typings/storybook__addons/index.d.ts +++ b/typings/storybook__addons/index.d.ts @@ -45,9 +45,9 @@ declare module '@storybook/addons' { /** * Allows to override code that will be used for "Show Code" tab. - * @see https://github.com/storybookjs/storybook/blob/main/addons/docs/docs/recipes.md#customizing-source-snippets + * @see https://github.com/storybookjs/storybook/blob/release-6-5/addons/docs/docs/recipes.md#customizing-source-snippets */ - transformSource?: (snippet: string, story: StoryContextForEnhancers) => string | undefined; + transformSource?: (snippet: string, storyContext: StoryContextForEnhancers) => string | undefined; container?: React.ComponentType; page?: React.ComponentType;