diff --git a/.eslintignore b/.eslintignore index acaec38f0b07..76cb54f8b1ee 100644 --- a/.eslintignore +++ b/.eslintignore @@ -10,6 +10,7 @@ lib/cli/test *.bundle.js *.js.map *.ts +*.tsx !.remarkrc.js !.babelrc.js diff --git a/addons/notes/package.json b/addons/notes/package.json index 1824f9a46395..c10435e5a913 100644 --- a/addons/notes/package.json +++ b/addons/notes/package.json @@ -18,13 +18,13 @@ "license": "MIT", "main": "dist/public_api.js", "types": "dist/public_api.d.ts", - "jsnext:main": "src/public_api.ts", "scripts": { "prepare": "node ../../scripts/prepare.js" }, "dependencies": { "@emotion/styled": "^0.10.6", "@storybook/addons": "4.2.0-alpha.7", + "@storybook/channels": "4.2.0-alpha.7", "core-js": "^2.5.7", "marked": "^0.5.2", "prop-types": "^15.6.2" diff --git a/addons/notes/src/index.ts b/addons/notes/src/index.ts index 57749ebb5434..c12ad3c8c7c6 100644 --- a/addons/notes/src/index.ts +++ b/addons/notes/src/index.ts @@ -1,5 +1,38 @@ -import addons, { makeDecorator } from '@storybook/addons'; import { parse as renderMarkdown } from 'marked'; +import { + addons, + makeDecorator, + StoryContext, + StoryGetter, + WrapperSettings, +} from '@storybook/addons'; + +function wrapper( + getStory: StoryGetter, + context: StoryContext, + { options, parameters }: WrapperSettings +) { + const channel = addons.getChannel(); + + const storyOptions = parameters || options; + + const { text, markdown, markdownOptions } = + typeof storyOptions === 'string' + ? { + text: storyOptions, + markdown: undefined, + markdownOptions: undefined, + } + : storyOptions; + + if (!text && !markdown) { + throw new Error('You must set of one of `text` or `markdown` on the `notes` parameter'); + } + + channel.emit('storybook/notes/add_notes', text || renderMarkdown(markdown, markdownOptions)); + + return getStory(context); +} // todo resolve any after @storybook/addons and @storybook/channels are migrated to TypeScript export const withNotes = makeDecorator({ @@ -7,28 +40,7 @@ export const withNotes = makeDecorator({ parameterName: 'notes', skipIfNoParametersOrOptions: true, allowDeprecatedUsage: true, - wrapper: (getStory: (context: any) => any, context: any, { options, parameters }: any) => { - const channel = addons.getChannel(); - - const storyOptions = parameters || options; - - const { text, markdown, markdownOptions } = - typeof storyOptions === 'string' - ? { - text: storyOptions, - markdown: undefined, - markdownOptions: undefined, - } - : storyOptions; - - if (!text && !markdown) { - throw new Error('You must set of one of `text` or `markdown` on the `notes` parameter'); - } - - channel.emit('storybook/notes/add_notes', text || renderMarkdown(markdown, markdownOptions)); - - return getStory(context); - }, + wrapper, }); export const withMarkdownNotes = (text: string, options: any) => diff --git a/addons/notes/src/register.tsx b/addons/notes/src/register.tsx index 9b6d15364ce0..7c0694fbaaa9 100644 --- a/addons/notes/src/register.tsx +++ b/addons/notes/src/register.tsx @@ -1,15 +1,10 @@ import * as React from 'react'; -import addons from '@storybook/addons'; + import * as PropTypes from 'prop-types'; import styled from '@emotion/styled'; - -// todo this is going to be refactored after the migration of @storybook/channel to TypeScript -interface NotesChannel { - emit: any; - on(listener: string, callback: (text: string) => void): any; - removeListener(listener: string, callback: (text: string) => void): void; -} +import { Channel } from '@storybook/channels'; +import { addons } from '@storybook/addons'; interface NotesApi { setQueryParams: any; // todo check correct definition @@ -20,7 +15,7 @@ interface NotesApi { interface NotesProps { active: boolean; - channel: NotesChannel; + channel: Channel; api: NotesApi; } @@ -35,7 +30,6 @@ const Panel = styled.div({ }); export class Notes extends React.Component { - static propTypes = { active: PropTypes.bool.isRequired, channel: PropTypes.shape({ @@ -91,9 +85,9 @@ export class Notes extends React.Component { const { text } = this.state; const textAfterFormatted = text ? text - .trim() - .replace(/(<\S+.*>)\n/g, '$1') - .replace(/\n/g, '
') + .trim() + .replace(/(<\S+.*>)\n/g, '$1') + .replace(/\n/g, '
') : ''; return active ? ( @@ -109,6 +103,8 @@ addons.register('storybook/notes', (api: any) => { const channel = addons.getChannel(); addons.addPanel('storybook/notes/panel', { title: 'Notes', - render: ({ active }: { active: boolean }) => , + render: ({ active }: { active: boolean }) => ( + + ), }); }); diff --git a/addons/notes/src/typings.d.ts b/addons/notes/src/typings.d.ts index 84ab86f76adc..1bf20a5640be 100644 --- a/addons/notes/src/typings.d.ts +++ b/addons/notes/src/typings.d.ts @@ -1,5 +1,3 @@ -// Fixes 'noImplicitAny' lint error because @storybook/addons isn't migrated to typescript yet -declare module '@storybook/addons'; - // Only necessary for 0.x.x. Version 10.x.x has definition files included declare module '@emotion/styled'; +declare module 'global'; diff --git a/addons/notes/tsconfig.json b/addons/notes/tsconfig.json index 91baa3e9a3ac..30079a2f7435 100644 --- a/addons/notes/tsconfig.json +++ b/addons/notes/tsconfig.json @@ -4,8 +4,7 @@ "rootDir": "./src" }, "include": [ - "src/**/*.ts", - "src/**/*.tsx" + "src/**/*" ], "exclude": [ "src/__tests__/**/*" diff --git a/addons/storyshots/storyshots-core/package.json b/addons/storyshots/storyshots-core/package.json index edad4675ce0a..62265534823f 100644 --- a/addons/storyshots/storyshots-core/package.json +++ b/addons/storyshots/storyshots-core/package.json @@ -35,7 +35,6 @@ "devDependencies": { "@storybook/addon-actions": "4.2.0-alpha.7", "@storybook/addon-links": "4.2.0-alpha.7", - "@storybook/addons": "4.2.0-alpha.1", "@storybook/react": "4.2.0-alpha.7", "enzyme-to-json": "^3.3.4", "react": "^16.6.0" diff --git a/lib/addons/package.json b/lib/addons/package.json index c66854c36588..8c42c707b446 100644 --- a/lib/addons/package.json +++ b/lib/addons/package.json @@ -14,8 +14,8 @@ "url": "https://github.com/storybooks/storybook.git" }, "license": "MIT", - "main": "dist/index.js", - "jsnext:main": "src/index.js", + "main": "dist/public_api.js", + "types": "dist/public_api.d.ts", "scripts": { "prepare": "node ../../scripts/prepare.js" }, @@ -25,6 +25,9 @@ "global": "^4.3.2", "util-deprecate": "^1.0.2" }, + "devDependencies": { + "@types/util-deprecate": "^1.0.0" + }, "publishConfig": { "access": "public" } diff --git a/lib/addons/src/index.js b/lib/addons/src/index.js deleted file mode 100644 index bee676ea0f2a..000000000000 --- a/lib/addons/src/index.js +++ /dev/null @@ -1,88 +0,0 @@ -// Resolves to window in browser and to global in node -import global from 'global'; -// import { TabWrapper } from '@storybook/components'; - -export mockChannel from './storybook-channel-mock'; -export { makeDecorator } from './make-decorator'; - -export class AddonStore { - constructor() { - this.loaders = {}; - this.panels = {}; - this.channel = null; - this.preview = null; - this.database = null; - } - - getChannel() { - // this.channel should get overwritten by setChannel. If it wasn't called (e.g. in non-browser environment), throw. - if (!this.channel) { - throw new Error( - 'Accessing nonexistent addons channel, see https://storybook.js.org/basics/faq/#why-is-there-no-addons-channel' - ); - } - return this.channel; - } - - hasChannel() { - return Boolean(this.channel); - } - - setChannel(channel) { - this.channel = channel; - } - - getPreview() { - return this.preview; - } - - setPreview(preview) { - this.preview = preview; - } - - getDatabase() { - return this.database; - } - - setDatabase(database) { - this.database = database; - } - - getPanels() { - return this.panels; - } - - addPanel(name, panel) { - // supporting legacy addons, which have not migrated to the active-prop - // const original = panel.render; - // if (original && original.toString() && !original.toString().match(/active/)) { - // this.panels[name] = { - // ...panel, - // render: ({ active }) => TabWrapper({ active, render: original }), - // }; - // } else { - this.panels[name] = panel; - // } - } - - register(name, loader) { - this.loaders[name] = loader; - } - - loadAddons(api) { - Object.keys(this.loaders) - .map(name => this.loaders[name]) - .forEach(loader => loader(api)); - } -} - -// Enforce addons store to be a singleton -const KEY = '__STORYBOOK_ADDONS'; -function getAddonsStore() { - if (!global[KEY]) { - global[KEY] = new AddonStore(); - } - return global[KEY]; -} - -export default getAddonsStore(); diff --git a/lib/addons/src/index.ts b/lib/addons/src/index.ts new file mode 100644 index 000000000000..c8432df6f6ce --- /dev/null +++ b/lib/addons/src/index.ts @@ -0,0 +1,80 @@ +import global from 'global'; +import { Channel } from '@storybook/channels'; +import { ReactElement } from 'react'; + +export interface PanelOptions { + active: boolean; +} + +export interface Panel { + title: string; + + render(options: PanelOptions): ReactElement; +} + +export type Loader = (callback: (api: any) => void) => void; + +interface LoaderKeyValue { + [key: string]: Loader; +} + +interface PanelKeyValue { + [key: string]: Panel; +} + +export class AddonStore { + private loaders: LoaderKeyValue = {}; + private panels: PanelKeyValue = {}; + private channel: Channel | undefined; + + getChannel() { + // this.channel should get overwritten by setChannel. If it wasn't called (e.g. in non-browser environment), throw. + if (!this.channel) { + throw new Error( + 'Accessing nonexistent addons channel, see https://storybook.js.org/basics/faq/#why-is-there-no-addons-channel' + ); + } + + return this.channel; + } + + hasChannel() { + return !!this.channel; + } + + setChannel(channel: Channel) { + this.channel = channel; + } + + getPanels() { + return this.panels; + } + + addPanel(name: string, panel: Panel) { + this.panels[name] = panel; + } + + register(name: string, registerCallback: (api: any) => void) { + this.loaders[name] = registerCallback; + } + + loadAddons(api: any) { + Object.values(this.loaders).forEach(value => value(api)); + } +} + +// Enforce addons store to be a singleton +const KEY = '__STORYBOOK_ADDONS'; + +function getAddonsStore() { + if (!global[KEY]) { + global[KEY] = new AddonStore(); + } + return global[KEY]; +} + +// Exporting this twice in order to to be able to import it like { addons } instead of 'addons' +// prefer import { addons } from '@storybook/addons' over import addons from '@storybook/addons' +// +// See public_api.ts +export const addons = getAddonsStore(); diff --git a/lib/addons/src/make-decorator.test.js b/lib/addons/src/make-decorator.test.ts similarity index 90% rename from lib/addons/src/make-decorator.test.js rename to lib/addons/src/make-decorator.test.ts index 53f3dd8cfeb5..4b640ac6636c 100644 --- a/lib/addons/src/make-decorator.test.js +++ b/lib/addons/src/make-decorator.test.ts @@ -1,10 +1,17 @@ import deprecate from 'util-deprecate'; -import { makeDecorator } from './make-decorator'; -import { defaultDecorateStory } from '../../core/src/client/preview/client_api'; +import { makeDecorator, StoryContext } from './make-decorator'; + +// Copy & paste from internal api: core/client/preview/client_api +export const defaultDecorateStory = (getStory: Function, decorators: Function[]) => + decorators.reduce( + (decorated, decorator) => (context: StoryContext) => + decorator(() => decorated(context), context), + getStory + ); jest.mock('util-deprecate'); -let deprecatedFns = []; -deprecate.mockImplementation((fn, warning) => { +let deprecatedFns: any[] = []; +(deprecate as any).mockImplementation((fn: (...args: any) => any, warning: string) => { const deprecatedFn = jest.fn(fn); deprecatedFns.push({ deprecatedFn, diff --git a/lib/addons/src/make-decorator.js b/lib/addons/src/make-decorator.ts similarity index 52% rename from lib/addons/src/make-decorator.js rename to lib/addons/src/make-decorator.ts index e7df28f726f1..15d16687e881 100644 --- a/lib/addons/src/make-decorator.js +++ b/lib/addons/src/make-decorator.ts @@ -1,23 +1,41 @@ import deprecate from 'util-deprecate'; -// Create a decorator that can be used both in the (deprecated) old "hoc" style: -// .add('story', decorator(options)(() => )); -// -// And in the new, "parameterized" style: -// .addDecorator(decorator) -// .add('story', () => , { name: { parameters } }); -// -// *And* in the older, but not deprecated, "pass options to decorator" style: -// .addDecorator(decorator(options)) - -export const makeDecorator = ({ +export interface StoryContext { + story: string; + kind: string; +} + +export interface WrapperSettings { + options: object; + parameters: any; +} + +export type StoryGetter = (context: StoryContext) => any; + +export type StoryWrapper = ( + getStory: StoryGetter, + context: StoryContext, + settings: WrapperSettings +) => any; + +type MakeDecoratorResult = (...args: any) => any; + +interface MakeDecoratorOptions { + name: string; + parameterName: string; + allowDeprecatedUsage: boolean; + skipIfNoParametersOrOptions: boolean; + wrapper: StoryWrapper; +} + +export const makeDecorator: MakeDecoratorResult = ({ name, parameterName, wrapper, skipIfNoParametersOrOptions = false, allowDeprecatedUsage = false, -}) => { - const decorator = options => (getStory, context) => { +}: MakeDecoratorOptions) => { + const decorator: any = (options: object) => (getStory: any, context: any) => { const parameters = context.parameters && context.parameters[parameterName]; if (parameters && parameters.disable) { @@ -27,19 +45,20 @@ export const makeDecorator = ({ if (skipIfNoParametersOrOptions && !options && !parameters) { return getStory(context); } + return wrapper(getStory, context, { options, parameters, }); }; - return (...args) => { + return (...args: any) => { // Used without options as .addDecorator(decorator) if (typeof args[0] === 'function') { return decorator()(...args); } - return (...innerArgs) => { + return (...innerArgs: any): any => { // Used as [.]addDecorator(decorator(options)) if (innerArgs.length > 1) { return decorator(...args)(...innerArgs); @@ -49,13 +68,15 @@ export const makeDecorator = ({ // Used to wrap a story directly .add('story', decorator(options)(() => )) // This is now deprecated: return deprecate( - context => decorator(...args)(innerArgs[0], context), - `Passing stories directly into ${name}() is deprecated, instead use addDecorator(${name}) and pass options with the '${parameterName}' parameter` + (context: any) => decorator(...args)(innerArgs[0], context), + `Passing stories directly into ${name}() is deprecated, + instead use addDecorator(${name}) and pass options with the '${parameterName}' parameter` ); } throw new Error( - `Passing stories directly into ${name}() is not allowed, instead use addDecorator(${name}) and pass options with the '${parameterName}' parameter` + `Passing stories directly into ${name}() is not allowed, + instead use addDecorator(${name}) and pass options with the '${parameterName}' parameter` ); }; }; diff --git a/lib/addons/src/public_api.ts b/lib/addons/src/public_api.ts new file mode 100644 index 000000000000..22adacc28201 --- /dev/null +++ b/lib/addons/src/public_api.ts @@ -0,0 +1,11 @@ +export * from './make-decorator'; +export * from '.'; +export * from './storybook-channel-mock'; + +// There can only be 1 default export per entry point and it has to be directly from public_api +// Exporting this twice in order to to be able to import it like { addons } instead of 'addons' +// prefer import { addons } from '@storybook/addons' over import addons from '@storybook/addons' +// +// See index.ts +import { addons } from '.'; +export default addons; diff --git a/lib/addons/src/storybook-channel-mock.js b/lib/addons/src/storybook-channel-mock.ts similarity index 78% rename from lib/addons/src/storybook-channel-mock.js rename to lib/addons/src/storybook-channel-mock.ts index 7358fc7e4bef..7d43f7b025c0 100644 --- a/lib/addons/src/storybook-channel-mock.js +++ b/lib/addons/src/storybook-channel-mock.ts @@ -1,6 +1,6 @@ import Channel from '@storybook/channels'; -export default function createChannel() { +export function mockChannel() { const transport = { setHandler: () => {}, send: () => {}, diff --git a/lib/addons/src/typings.d.ts b/lib/addons/src/typings.d.ts new file mode 100644 index 000000000000..2f4eb9cf4fd9 --- /dev/null +++ b/lib/addons/src/typings.d.ts @@ -0,0 +1 @@ +declare module 'global'; diff --git a/lib/addons/tsconfig.json b/lib/addons/tsconfig.json new file mode 100644 index 000000000000..64eed74f8ea8 --- /dev/null +++ b/lib/addons/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "src/**.test.ts" + ] +} diff --git a/lib/channels/package.json b/lib/channels/package.json index 32d176873c7c..b2bed02364bf 100644 --- a/lib/channels/package.json +++ b/lib/channels/package.json @@ -15,7 +15,7 @@ }, "license": "MIT", "main": "dist/index.js", - "jsnext:main": "src/index.ts", + "types": "dist/index.d.ts", "scripts": { "prepare": "node ../../scripts/prepare.js" }, diff --git a/lib/channels/tsconfig.json b/lib/channels/tsconfig.json index f7c7ea71c14c..64eed74f8ea8 100644 --- a/lib/channels/tsconfig.json +++ b/lib/channels/tsconfig.json @@ -4,10 +4,9 @@ "rootDir": "./src" }, "include": [ - "src/**/*.ts", - "src/**/*.tsx" + "src/**/*" ], "exclude": [ - "src/index.test.ts" + "src/**.test.ts" ] } diff --git a/package.json b/package.json index f44b2ceae377..30f642b746b6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "bootstrap": "node ./scripts/bootstrap.js", "bootstrap:crna-kitchen-sink": "npm --prefix examples-native/crna-kitchen-sink install", "bootstrap:docs": "yarn install --cwd docs", - "build-packs": "lerna exec --scope '@storybook/*' --parallel -- \\$LERNA_ROOT_PATH/scripts/build-pack.sh \\$LERNA_ROOT_PATH/packs", + "build-packs": "lerna exec --scope '@storybook/*' -- \\$LERNA_ROOT_PATH/scripts/build-pack.sh \\$LERNA_ROOT_PATH/packs", "build-storybooks": "./scripts/build-storybooks.sh", "changelog": "pr-log --sloppy --cherry-pick", "changelog:next": "pr-log --sloppy --since-prerelease", diff --git a/scripts/build-pack.sh b/scripts/build-pack.sh index f77e609c161a..e33e7ae4c089 100755 --- a/scripts/build-pack.sh +++ b/scripts/build-pack.sh @@ -5,4 +5,7 @@ PACK_DIR=$1 FILE=$(npm pack | tail -n 1) +echo $PACK_DIR +echo $FILE + mv "$FILE" "$PACK_DIR/${FILE%-[0-9]*\.[0-9]*\.[0-9]*\.tgz}.tgz" diff --git a/scripts/prepare.js b/scripts/prepare.js index 59a6f856f4c5..177e1c5cd1e7 100644 --- a/scripts/prepare.js +++ b/scripts/prepare.js @@ -38,8 +38,12 @@ function logError(type, packageJson) { const packageJson = getPackageJson(); removeDist(); -babelify({ errorCallback: () => logError('js', packageJson) }); -removeTsFromDist(); -tscfy({ errorCallback: () => logError('ts', packageJson) }); +if (packageJson && packageJson.types && packageJson.types.indexOf('d.ts') !== -1) { + tscfy({ errorCallback: () => logError('ts', packageJson) }); +} else { + babelify({ errorCallback: () => logError('js', packageJson) }); + removeTsFromDist(); + tscfy({ errorCallback: () => logError('ts', packageJson) }); +} console.log(chalk.gray(`Built: ${chalk.bold(`${packageJson.name}@${packageJson.version}`)}`)); diff --git a/tsconfig.json b/tsconfig.json index 55ea5bb93317..c2e1fbdc8a35 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,13 +9,15 @@ "noImplicitAny": true, "jsx": "react", "module": "commonjs", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, "target": "es5", "types": [ "node", "jest" ], "lib": [ - "es2016", + "es2017", "dom" ] } diff --git a/yarn.lock b/yarn.lock index ad65005a4857..e6fd2a7a55ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2115,6 +2115,11 @@ version "2.0.2" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.2.tgz#5dc0a7f76809b7518c0df58689cd16a19bd751c6" +"@types/util-deprecate@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/util-deprecate/-/util-deprecate-1.0.0.tgz#341d0815fe5a661b94e3ea738d182b4c359e3958" + integrity sha512-I2vixiQ+mrmKxfdLNvaa766nulrMVDoUQiSQoNeTjFUNAt8klnMgDh3yy/bH/r275357q30ACOEUaxFOR8YVrA== + "@types/vfile-message@*": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/vfile-message/-/vfile-message-1.0.1.tgz#e1e9895cc6b36c462d4244e64e6d0b6eaf65355a"