Add Storybook documentation and remove deprecated knobs (https://github.com/woocommerce/woocommerce-blocks/pull/5407)

* Remove Storybook knobs

They are now fully replaced by controls

* Update Storybook to 6.4.9
* Add Storybook documentation and VSCode snippets
* Add VSCode snippets
* Convert stories from checkout packages and include them in Storybook
This commit is contained in:
Lucio Giannotta 2021-12-21 18:08:57 +01:00 committed by GitHub
parent 5730574fde
commit 35adae3fa4
29 changed files with 1202 additions and 43088 deletions

View File

@ -29,7 +29,6 @@ module.exports = {
'@woocommerce/settings',
'@woocommerce/shared-context',
'@woocommerce/shared-hocs',
'@woocommerce/knobs',
'@wordpress/a11y',
'@wordpress/api-fetch',
'@wordpress/block-editor',

View File

@ -8,6 +8,7 @@ project.properties
.idea
.vscode
!.vscode/extensions.json
!.vscode/storybook.code-snippets
*.sublime-project
*.sublime-workspace
.sublimelinterrc

View File

@ -0,0 +1,40 @@
{
"Storybook Story": {
"prefix": [ "storybook", "sbs" ],
"body": [
"/**",
" * External dependencies",
" */",
"import { Story, Meta } from '@storybook/react';",
"",
"/**",
" * Internal dependencies",
" */",
"import ${1:${TM_DIRECTORY/.*\\/(.*)\\/.*$/${1:/pascalcase}/}}, { ${2:${TM_DIRECTORY/.*\\/(.*)\\/.*$/${1:/pascalcase}/}Props} } from '..';",
"",
"export default {",
"\ttitle: 'WooCommerce Blocks/${3|@base-components,editor-components,woocommerce,Checkout Blocks|}/${1}',",
"\tcomponent: ${1},",
"} as Meta< ${2} >;",
"",
"const Template: Story< ${2} > = ( args ) => (",
"\t<${1} { ...args } />",
");",
"",
"export const Default = Template.bind( {} );",
"Default.args = {};",
""
],
"description": "Scaffolds a Storybook story",
"scope": "typescript, typescriptreact"
},
"Storybook Story from Template": {
"prefix": [ "sbt" ],
"body": [
"export const ${1:MyStory} = Template.bind( {} );",
"$1.args = {",
"\t$2",
"};"
]
}
}

View File

@ -104,7 +104,7 @@ const NoShippingPlaceholder = ( {
);
};
interface TotalShippingProps {
export interface TotalShippingProps {
currency: Currency;
values: {
total_shipping: string;
@ -115,7 +115,7 @@ interface TotalShippingProps {
className?: string;
}
const TotalsShipping = ( {
export const TotalsShipping = ( {
currency,
values,
showCalculator = true,

View File

@ -1,35 +0,0 @@
/**
* External dependencies
*/
import { boolean, text } from '@storybook/addon-knobs';
import { currencyKnob } from '@woocommerce/knobs';
/**
* Internal dependencies
*/
import TotalsShipping from '../';
export default {
title: 'WooCommerce Blocks/@blocks-checkout/TotalsShipping',
component: TotalsShipping,
};
export const Default = () => {
const currency = currencyKnob();
const showCalculator = boolean( 'Show calculator', true );
const showRateSelector = boolean( 'Show rate selector', true );
const totalShipping = text( 'Total shipping', '1000' );
const totalShippingTax = text( 'Total shipping tax', '200' );
return (
<TotalsShipping
currency={ currency }
showCalculator={ showCalculator }
showRateSelector={ showRateSelector }
values={ {
total_shipping: totalShipping,
total_shipping_tax: totalShippingTax,
} }
/>
);
};

File diff suppressed because it is too large Load Diff

View File

@ -83,14 +83,13 @@
"@babel/polyfill": "7.12.1",
"@babel/preset-typescript": "7.16.0",
"@octokit/graphql": "4.8.0",
"@storybook/addon-a11y": "6.4.4",
"@storybook/addon-essentials": "6.4.4",
"@storybook/addon-knobs": "6.4.0",
"@storybook/addon-links": "6.4.4",
"@storybook/addon-storysource": "6.4.4",
"@storybook/addons": "6.4.4",
"@storybook/client-api": "6.4.4",
"@storybook/react": "6.4.4",
"@storybook/addon-a11y": "6.4.9",
"@storybook/addon-essentials": "6.4.9",
"@storybook/addon-links": "6.4.9",
"@storybook/addon-storysource": "6.4.9",
"@storybook/addons": "6.4.9",
"@storybook/client-api": "6.4.9",
"@storybook/react": "6.4.9",
"@testing-library/jest-dom": "5.15.1",
"@testing-library/react": "12.1.2",
"@testing-library/react-hooks": "7.0.2",

View File

@ -9,7 +9,7 @@ import classNames from 'classnames';
*/
import './style.scss';
type CheckboxControlProps = {
export type CheckboxControlProps = {
className?: string;
label?: string;
id?: string;
@ -22,7 +22,7 @@ type CheckboxControlProps = {
/**
* Component used to show a checkbox control with styles.
*/
const CheckboxControl = ( {
export const CheckboxControl = ( {
className,
label,
id,

View File

@ -1,27 +0,0 @@
/**
* External dependencies
*/
import { text } from '@storybook/addon-knobs';
import { useState } from 'react';
/**
* Internal dependencies
*/
import CheckboxControl from '../';
export default {
title: 'WooCommerce Blocks/@base-components/CheckboxControl',
component: CheckboxControl,
};
export const Default = () => {
const [ checked, setChecked ] = useState( false );
return (
<CheckboxControl
label={ text( 'Label', 'Yes please' ) }
checked={ checked }
onChange={ ( value ) => setChecked( value ) }
/>
);
};

View File

@ -0,0 +1,25 @@
/**
* External dependencies
*/
import { Story, Meta } from '@storybook/react';
/**
* Internal dependencies
*/
import CheckboxControl, { CheckboxControlProps } from '..';
export default {
title: 'WooCommerce Blocks/Checkout Blocks/CheckboxControl',
component: CheckboxControl,
args: {
instanceId: 'my-checkbox-id',
label: 'Check me out',
},
} as Meta< CheckboxControlProps >;
const Template: Story< CheckboxControlProps > = ( args ) => (
<CheckboxControl { ...args } />
);
export const Default = Template.bind( {} );
Default.args = {};

View File

@ -13,7 +13,7 @@ import type { ReactElement } from 'react';
*/
import TotalsItem from '../item';
interface TotalsFeesProps {
export interface TotalsFeesProps {
currency: Currency;
cartFees: CartFeeItem[];
className?: string;

View File

@ -1,37 +0,0 @@
/**
* External dependencies
*/
import { text } from '@storybook/addon-knobs';
import { currencyKnob } from '@woocommerce/knobs';
/**
* Internal dependencies
*/
import TotalsFees from '../';
export default {
title: 'WooCommerce Blocks/@blocks-checkout/TotalsFees',
component: TotalsFees,
};
export const Default = () => {
const currency = currencyKnob();
const totalFees = text( 'Total fee', '1000' );
const totalFeesTax = text( 'Total fee tax', '200' );
return (
<TotalsFees
currency={ currency }
cartFees={ [
{
id: 'fee',
name: 'Fee',
totals: {
total: totalFees,
total_tax: totalFeesTax,
},
},
] }
/>
);
};

View File

@ -0,0 +1,41 @@
/**
* External dependencies
*/
import { Story, Meta } from '@storybook/react';
import {
currenciesAPIShape as currencies,
currencyControl,
} from '@woocommerce/storybook-controls';
/**
* Internal dependencies
*/
import Fees, { TotalsFeesProps } from '..';
export default {
title: 'WooCommerce Blocks/Checkout Blocks/totals/Fees',
component: Fees,
argTypes: {
currency: currencyControl,
},
args: {
cartFees: [
{
id: 'my-id',
name: 'Storybook fee',
totals: {
...currencies.USD,
total: '1000',
total_tax: '200',
},
},
],
},
} as Meta< TotalsFeesProps >;
const Template: Story< TotalsFeesProps > = ( args ) => <Fees { ...args } />;
export const Default = Template.bind( {} );
Default.args = {};
// @todo Revise Storybook entries for `Checkout Blocks/totals` components

View File

@ -12,7 +12,7 @@ import type { Currency } from '@woocommerce/price-format';
*/
import './style.scss';
interface TotalsItemProps {
export interface TotalsItemProps {
className?: string;
currency: Currency;
label: string;

View File

@ -1,34 +0,0 @@
/**
* External dependencies
*/
import { text } from '@storybook/addon-knobs';
import { currencyKnob } from '@woocommerce/knobs';
/**
* Internal dependencies
*/
import TotalsItem from '../';
export default {
title: 'WooCommerce Blocks/@blocks-checkout/TotalsItem',
component: TotalsItem,
};
export const Default = () => {
const currency = currencyKnob();
const description = text(
'Description',
'This is the amount that will be charged to your card.'
);
const label = text( 'Label', 'Amount' );
const value = text( 'Value', '1000' );
return (
<TotalsItem
currency={ currency }
description={ description }
label={ label }
value={ value }
/>
);
};

View File

@ -0,0 +1,35 @@
/**
* External dependencies
*/
import { Story, Meta } from '@storybook/react';
import { currencyControl } from '@woocommerce/storybook-controls';
/**
* Internal dependencies
*/
import Item, { TotalsItemProps } from '..';
export default {
title: 'WooCommerce Blocks/Checkout Blocks/totals/Item',
component: Item,
argTypes: {
currency: currencyControl,
description: { control: { type: 'text' } },
},
args: {
description: (
<span>
This item is <strong>so interesting</strong>
</span>
),
label: 'Intersting item',
value: 2000,
},
} as Meta< TotalsItemProps >;
const Template: Story< TotalsItemProps > = ( args ) => <Item { ...args } />;
export const Default = Template.bind( {} );
Default.args = {};
// @todo Revise Storybook entries for `Checkout Blocks/totals` components

View File

@ -16,7 +16,7 @@ interface Values {
total_items_tax: string;
}
interface SubtotalProps {
export interface SubtotalProps {
className?: string;
currency: Currency;
values: Values | Record< string, never >;

View File

@ -1,31 +0,0 @@
/**
* External dependencies
*/
import { text } from '@storybook/addon-knobs';
import { currencyKnob } from '@woocommerce/knobs';
/**
* Internal dependencies
*/
import Subtotal from '../';
export default {
title: 'WooCommerce Blocks/@blocks-checkout/Subtotal',
component: Subtotal,
};
export const Default = () => {
const currency = currencyKnob();
const totalItems = text( 'Total items', '1000' );
const totalItemsTax = text( 'Total items tax', '200' );
return (
<Subtotal
currency={ currency }
values={ {
total_items: totalItems,
total_items_tax: totalItemsTax,
} }
/>
);
};

View File

@ -0,0 +1,31 @@
/**
* External dependencies
*/
import { Story, Meta } from '@storybook/react';
import { currencyControl } from '@woocommerce/storybook-controls';
/**
* Internal dependencies
*/
import Subtotal, { SubtotalProps } from '..';
export default {
title: 'WooCommerce Blocks/Checkout Blocks/totals/Subtotal',
component: Subtotal,
argTypes: {
currency: currencyControl,
},
args: {
values: {
total_items: '1000',
total_items_tax: '200',
},
},
} as Meta< SubtotalProps >;
const Template: Story< SubtotalProps > = ( args ) => <Subtotal { ...args } />;
export const Default = Template.bind( {} );
Default.args = {};
// @todo Revise Storybook entries for `Checkout Blocks/totals` components

View File

@ -19,7 +19,7 @@ interface Values {
total_tax: string;
}
interface TotalsTaxesProps {
export interface TotalsTaxesProps {
className?: string;
currency: Currency;
showRateAfterTaxName: boolean;

View File

@ -1,29 +0,0 @@
/**
* External dependencies
*/
import { text } from '@storybook/addon-knobs';
import { currencyKnob } from '@woocommerce/knobs';
/**
* Internal dependencies
*/
import TotalsTaxes from '../';
export default {
title: 'WooCommerce Blocks/@blocks-checkout/TotalsTaxes',
component: TotalsTaxes,
};
export const Default = () => {
const currency = currencyKnob();
const totalTaxes = text( 'Total taxes', '1000' );
return (
<TotalsTaxes
currency={ currency }
values={ {
total_tax: totalTaxes,
} }
/>
);
};

View File

@ -0,0 +1,37 @@
/**
* External dependencies
*/
import { Story, Meta } from '@storybook/react';
import { currencyControl } from '@woocommerce/storybook-controls';
/**
* Internal dependencies
*/
import Taxes, { TotalsTaxesProps } from '..';
export default {
title: 'WooCommerce Blocks/Checkout Blocks/totals/Taxes',
component: Taxes,
argTypes: {
currency: currencyControl,
},
args: {
values: {
tax_lines: [
{
name: 'Expensive tax fee',
price: '1000',
rate: '500',
},
],
total_tax: '2000',
},
},
} as Meta< TotalsTaxesProps >;
const Template: Story< TotalsTaxesProps > = ( args ) => <Taxes { ...args } />;
export const Default = Template.bind( {} );
Default.args = {};
// @todo Revise Storybook entries for `Checkout Blocks/totals` components

View File

@ -0,0 +1,322 @@
# Working with Storybook
This document is meant to make contributing to our Storybook a bit easier by giving some tips, pointing out a few gotchas, making the editing experience smoother, and reducing the friction to adding a new story.
## Adding new stories
To easily scaffold a new story for your component you need a few steps, but most things are already configured to reduce friction.
1. Make sure you add a `stories` directory within your component directory.
2. You can name the files within the directory however you want. Usually, it's best to have one `index.tsx` file including all your stories. However, if you are having several components under the same directory (e.g. `Chip` and `RemovableChip`), it might be best to have one file per component ([see below](#one-file-per-component)).
3. You will need a `default` export which defines the metadata for the component and one named export for each story you want to create.
Let's see the code in detail.
### Scaffold tour
This is the minimal scaffold you need for your new Story:
```tsx
import { Story, Meta } from '@storybook/react';
import MyComponent, { MyComponentProps } from '..';
export default {
title: 'WooCommerce Blocks/${MyCategory}/${MyComponent}',
component: MyComponent,
} as Meta< MyComponentProps >;
const Template: Story< MyComponentProps > = ( args ) => (
<MyComponent { ...args } />
);
export const Default = Template.bind( {} );
Default.args = {};
```
The above code will already generate a story that you can tweak, but let's look at the code in-depth.
First of all, by using TypeScript and exporting your component's props, Storybook will automatically show the documentation associated with your props and also generate the controls which are more appropriate for each property.
Also, if you follow the naming convention of starting your event handlers with `on` (e.g. `onChange`, `onRemove`, etc.), Storybook will automatically make those props uncontrollable and generate events from them (this convention can be changed in `preview.js`).
Sometimes you will need to manually change those assumptions, and we'll see how in a moment. But let's go step-by-step:
#### The `default` export
The default export defines the metadata for your component. The most important things you should be aware of:
```ts
{
/**
* This is how your component is going to be named in the Storybook UI.
* You can use slashed-paths to define hierarchies and categories.
* At the time of writing, we don't have well defined categories, but we
* are working on making that clearer.
*/
title: string;
/**
* You should always pass your component here.
*/
component: ComponentType< any >;
/**
* You can define here the default props for the components on all your
* stories. This is pretty useful when you have required props.
*/
args: Partial< MyComponentProps >;
/**
* Here you can define how the Storybook controls look like. More info
* below.
*/
argTypes: Partial< ArgTypes< MyComponentProps > >;
}
```
#### Defining controls
As mentioned, Storybook will try to infer the best control for your property type (e.g. an on/off switch for a `boolean`, etc.). However, there are times in which you want different options.
Here is a link to the official Storybook documentation: https://storybook.js.org/docs/react/essentials/controls
But a TL;DR of most common usecases:
- The shape of the prop looks like so:
```ts
{
[$myPropName]: {
control: { type: $controlType },
// Only applicable for selects/radios and such
options: $controlOptions
}
}
```
- You can disable a control like so:
```ts
{
[$myPropName]: { control: false }
}
```
#### The Story template
The recommended way to create a story is by creating a template function and duplicating it for each story, to avoid extra scaffolding. The simplest form of the template is as follows:
```tsx
const Template: Story< MyComponentProps > = ( args ) => (
<MyComponent { ...args } />
);
```
In this way, you are rendering the component in the viewport and binding the component properties to the Storybook controls.
In this template function, however, you might put any extra logic that your component rendering might need. Common use-cases are [wrapping them in context providers](#context-providers), or [simulating controlled components](#controlled-components).
#### Defining Stories
If you have followed this scaffold, defining a new story is as simple as this:
```ts
export const Default = Template.bind( {} );
```
Usually, you want a story with the name `Default`, and other stories with interesting variations of your component ([see below](#what-should-constitute-a-story)). In order to create those variations, you should work with some of the properties of this function prototype. Here are the most common:
```ts
{
/**
* Define the properties to pass for this specific story. Often,
* `Default` stories wouldn't even need this.
*/
args: Partial< MyComponentProps >;
/**
* You will rarely need this, as story names are automatically generated
* from your constant name. But that's good to know if you ever need.
*/
storyName: string;
}
```
Full official docs: https://storybook.js.org/docs/react/writing-stories/introduction
### Snippets
If you are using [VSCode](https://code.visualstudio.com/), this repo includes some helpful snippets that will become available inside `.ts` and `.tsx` files. You can find them inside `.vscode/storybook.code-snippets`.
The `sbs` (“Storybook story”) will scaffold the entire code in the [section above](#scaffold-tour). If you have respected the naming conventions, it will also properly import your component and the properties with the correct names, saving you a bunch of time.
The `sbt` (“Storybook story template”) will create a new story by binding your default template and prompting you to provide specific arguments.
## FAQ
### What should constitute a story?
Stories should show the component in its different shapes.
The most obvious way is when a component has a few variations (such as a button with a `primary` attribute being larger and bolder, for example).
However, a common misconception is that, since full controls are enabled and allow the user to explore the component by playing around with its properties interactively, there is no need to provide stories which are just the component with different default properties.
While this is true for many cases (often you don't need to create a different story by replacing the `label` of a component, if that's just text; though it might be argued that it could be interesting to have a story showing how a very long text behaves, especially in [combined stories](#can-i-create-stories-with-mixed-components)), it is very useful to create stories for components in their loading or error states.
### Can I create stories with mixed components?
Yes, and it'd be awesome to see how our components interact with each other, especially in the context of blocks. At the time of writing, we have no such stories, however, here is a link to the official docs if you want to give a go at implementing this: https://storybook.js.org/docs/react/workflows/stories-for-multiple-components
## Tips
### One file per component
While stories for closely related components could technically be living within the same file, we advise that you keep one file per component, as this works best with the automatic organization of the components within storybook and using [`default` exports in the Story](https://storybook.js.org/docs/react/api/csf#default-export).
An exception to this, might be when there is only one mainly used component and the other defined ones are just used internally.
E.g. compare `Chip/RemovableChip` with `ProductPrice/Sale/Range`. In the latter case, `Sale` and `Range` components are actually rendered depending on the props passed to the `ProductPrice` component, so they might as well be stories of their parent component.
### Custom controls
Sometimes the inferred text input is not good enough, and you need something more complex. If those controls happen to be shared among many components, please write them in `storybook/custom-controls`.
An example there is the `currency` control. Since many of our components expect `Currency` objects in their props, we can make sure we give a few examples Storybook users can play with, without having to manually create the object themselves.
Let's take a look:
```ts
export const currencies: Record< string, Currency > = {
EUR: {
// ...
},
USD: {
// ...
},
} as const;
export const currencyControl = {
control: 'select',
defaultValue: currencies.USD,
// This maps string keys to their values
mapping: currencies,
// These are the options which will appear in the <select> control
options: Object.keys( currencies ),
};
```
Then we can just pass the `currencyControl` to our component `argTypes`.
```ts
export default {
// ...
argTypes: {
currency: currencyControl,
},
};
```
## Common gotchas and examples
### Named exports
Your component files can have `default` exports, but in order to play well with the automatic doc generation, they should **always** have named exports as well.
See: https://github.com/strothj/react-docgen-typescript-loader/issues/75
### Controlled components
Your component is not managing its own state and expects it to be passed as a prop, but you want to create a self-contained story. You can then edit your main `Template` function to manage the state, for example through hooks.
```tsx
const Template: Story< MyControlledComponentProps > = ( args ) => {
const [ myState, setMyState ] = useState( 0 );
const onChange = ( newVal ) => {
args.onChange?.( newVal );
setMyState( newVal );
};
return (
<MyControlledComponent
{ ...args }
onChange={ onChange }
state={ myState }
/>
);
};
```
You will notice that when you do this, your controls will not be in sync anymore: when you change the state through your story interaction, the control for `state` will not equal `myState`.
Often, it is enough to disable the control for `state` as it's not required.
If you want to keep them in sync, you'll have to use `useArgs` from the Storybook client API.
```tsx
import { useArgs } from '@storybook/client-api';
const Template: Story< MyControlledComponentProps > = ( args ) => {
const [ _, setArgs ] = useArgs();
const onChange = ( newVal ) => {
args.onChange?.( newVal );
setArgs( { state: newVal } );
};
return <MyControlledComponent { ...args } onChange={ onChange } />;
};
```
Note that this makes things a bit more complex, so it is recommended to do only when it makes sense for a controlled value to still have a Storybook control.
At the time of writing, there is a known bug that doesn't keep number inputs in sync: https://github.com/storybookjs/storybook/issues/15924
### Simulating interactions
First of all, note that all the props starting with `on` as described above trigger “actions” in Storybook, which is basically just a way to see how data gets passed to a handler via the _Actions_ tab.
You can also manually mark props as actions in the `argTypes`, like so:
```ts
export default {
// ...
argTypes: {
myHandler: {
// <- this doesn't start with `on` so it needs manual config
action: 'This text will show in the panel along with the data',
},
},
};
```
Full action docs: https://storybook.js.org/docs/react/essentials/actions
**However**, you might want to simulate some sort of behavior from your component, for example show how a `Retry` button triggers a loading state. In this case you can use `useArgs`:
```tsx
const Template: Story< MyComponentProps > = ( args ) => {
const [ { isLoading }, setArgs ] = useArgs();
const onRetry = () => {
args.onRetry?.();
setArgs( { isLoading: true } );
setTimeout(
() => setArgs( { isLoading: false } ),
INTERACTION_TIMEOUT
);
};
return (
<MyComponent { ...args } onRetry={ onRetry } isLoading={ isLoading } />
);
};
```
We expose the `INTERACTION_TIMEOUT` constant from `@woocommerce/storybook-controls'` as a simple way to set our timeouts across our stories.
### Context providers
See example: `assets/js/base/components/country-input/stories/index.tsx`

View File

@ -1,30 +0,0 @@
/**
* External dependencies
*/
import { select } from '@storybook/addon-knobs';
export const currencyKnob = () => {
const currencies = [
{
label: 'USD',
code: 'USD',
symbol: '$',
thousandSeparator: ',',
decimalSeparator: '.',
minorUnit: 2,
prefix: '$',
suffix: '',
},
{
label: 'EUR',
code: 'EUR',
symbol: '€',
thousandSeparator: '.',
decimalSeparator: ',',
minorUnit: 2,
prefix: '',
suffix: '€',
},
];
return select( 'Currency', currencies, currencies[ 0 ] );
};

View File

@ -1 +0,0 @@
export * from './currency-knob.js';

View File

@ -2,11 +2,11 @@ module.exports = {
stories: [
// WooCommerce Blocks stuff (anywhere in repo!)
'../assets/js/**/stories/*.@(js|jsx|ts|tsx)',
'../packages/**/stories/*.@(js|jsx|ts|tsx)',
],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
'@storybook/addon-knobs',
'@storybook/addon-links',
'storybook-addon-react-docgen',
],

View File

@ -2,7 +2,6 @@
* External dependencies
*/
import { withA11y } from '@storybook/addon-a11y';
import { withKnobs } from '@storybook/addon-knobs';
import { addDecorator } from '@storybook/react';
/**
@ -11,7 +10,6 @@ import { addDecorator } from '@storybook/react';
import './style.scss';
addDecorator( withA11y );
addDecorator( withKnobs );
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },

View File

@ -38,7 +38,6 @@ module.exports = ( { config: storybookConfig } ) => {
'@woocommerce/base-hooks': require.resolve(
'./__mocks__/woocommerce-base-hooks.js'
),
'@woocommerce/storybook': require.resolve( './knobs/index.js' ),
'wordpress-components': require.resolve(
'../node_modules/wordpress-components'
),

View File

@ -50,7 +50,6 @@
"@woocommerce/block-settings": [ "assets/js/settings/blocks" ],
"@woocommerce/icons": [ "assets/js/icons" ],
"@woocommerce/resource-previews": [ "assets/js/previews" ],
"@woocommerce/knobs": [ "storybook/knobs" ],
"@woocommerce/settings": [ "assets/js/settings/shared" ],
"@woocommerce/shared-context": [ "assets/js/shared/context" ],
"@woocommerce/shared-hocs": [ "assets/js/shared/hocs" ],