Skip to content

Commit

Permalink
Brought changes lib/core/src/client/preview in #9870 over
Browse files Browse the repository at this point in the history
Thanks @kroeder!
  • Loading branch information
tmeasday committed Feb 20, 2020
1 parent da9576c commit f9d88da
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 46 deletions.
1 change: 1 addition & 0 deletions lib/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"license": "MIT",
"main": "dist/client/index.js",
"types": "dist/client/index.d.ts",
"files": [
"dist/**/*",
"dll/**/*",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { CSSProperties } from 'react';

const wrapper = {
fontSize: '14px',
Expand All @@ -13,7 +13,7 @@ const main = {
background: 'rgba(0,0,0,0.03)',
};

const heading = {
const heading: CSSProperties = {
textAlign: 'center',
};

Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { navigator, window, document } from 'global';
import { document, navigator, window } from 'global';
import React from 'react';
import ReactDOM from 'react-dom';
import deprecate from 'util-deprecate';
import AnsiToHtml from 'ansi-to-html';

import addons from '@storybook/addons';
import createChannel from '@storybook/channel-postmessage';
import { ClientApi, StoryStore, ConfigApi } from '@storybook/client-api';
import { toId, storyNameFromExport, isExportStory } from '@storybook/csf';
import { ClientApi, ConfigApi, StoryStore } from '@storybook/client-api';
import { isExportStory, storyNameFromExport, toId } from '@storybook/csf';
import { logger } from '@storybook/client-logger';
import Events from '@storybook/core-events';

import { initializePath, setPath } from './url';
import { NoDocs } from './NoDocs';
import { Loadable, LoaderFunction, PreviewError, RequireContext } from './types';

const ansiConverter = new AnsiToHtml({
escapeXML: true,
Expand All @@ -38,7 +39,7 @@ function showNopreview() {
document.body.classList.add(classes.NOPREVIEW);
}

function showErrorDisplay({ message = '', stack = '' }) {
function showErrorDisplay({ message = '', stack = '' }: PreviewError) {
document.getElementById('error-message').innerHTML = ansiConverter.toHtml(message);
document.getElementById('error-stack').innerHTML = ansiConverter.toHtml(stack);

Expand All @@ -50,7 +51,7 @@ function showErrorDisplay({ message = '', stack = '' }) {

// showError is used by the various app layers to inform the user they have done something
// wrong -- for instance returned the wrong thing from a story
function showError({ title, description }) {
function showError({ title, description }: { title: string; description: string }) {
addons.getChannel().emit(Events.STORY_ERRORED, { title, description });
showErrorDisplay({
message: title,
Expand All @@ -59,7 +60,7 @@ function showError({ title, description }) {
}

// showException is used if we fail to render the story and it is uncaught by the app layer
function showException(exception) {
function showException(exception: PreviewError) {
addons.getChannel().emit(Events.STORY_THREW_EXCEPTION, exception);
showErrorDisplay(exception);

Expand All @@ -75,11 +76,12 @@ const isBrowser =
!(navigator.userAgent.indexOf('jsdom') > -1);

export const getContext = (() => {
let cache;
return decorateStory => {
if (cache) {
return cache;
}
// let cache;
// todo add type
return (decorateStory: any) => {
// if (cache) {
// return cache;
// }
let channel = null;
if (isBrowser) {
try {
Expand Down Expand Up @@ -114,18 +116,17 @@ export const getContext = (() => {
};
})();

function focusInInput(event) {
return (
/input|textarea/i.test(event.target.tagName) ||
event.target.getAttribute('contenteditable') !== null
);
function focusInInput(event: Event) {
const target = event.target as Element;
return /input|textarea/i.test(target.tagName) || target.getAttribute('contenteditable') !== null;
}

type StyleKeyValue = { [key: string]: string };
const applyLayout = (() => {
let previousStyles;
let previousStyles: string | undefined;

return layout => {
const layouts = {
return (layout: string) => {
const layouts: StyleKeyValue = {
centered: `
display: flex;
justify-content: center;
Expand All @@ -152,7 +153,8 @@ const applyLayout = (() => {
};
})();

export default function start(render, { decorateStory } = {}) {
// todo improve typings
export default function start(render: any, { decorateStory }: any = {}) {
const context = getContext(decorateStory);

const { clientApi, channel, configApi, storyStore } = context;
Expand All @@ -162,9 +164,9 @@ export default function start(render, { decorateStory } = {}) {
let previousStory = '';
let previousRevision = -1;
let previousViewMode = '';
let previousId = null;
let previousId: string | null = null;

const renderMain = forceRender => {
const renderMain = (forceRender: boolean) => {
const revision = storyStore.getRevision();
const loadError = storyStore.getError();
const { storyId, viewMode: urlViewMode } = storyStore.getSelection();
Expand Down Expand Up @@ -225,7 +227,7 @@ export default function start(render, { decorateStory } = {}) {
break;
case 'story':
default:
if (previousId != null && (id !== previousId || viewMode !== previousViewMode)) {
if (previousId !== null && (id !== previousId || viewMode !== previousViewMode)) {
storyStore.cleanHooks(previousId);
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
Expand All @@ -238,14 +240,16 @@ export default function start(render, { decorateStory } = {}) {
switch (viewMode) {
case 'docs': {
showMain();
document.getElementById('root').setAttribute('hidden', true);
// todo discuss: takes NodeModule, passed boolean
document.getElementById('root').setAttribute('hidden', true as any);
document.getElementById('docs-root').removeAttribute('hidden');
break;
}
case 'story':
default: {
if (previousViewMode === 'docs') {
document.getElementById('docs-root').setAttribute('hidden', true);
// todo discuss: takes NodeModule, passed boolean
document.getElementById('docs-root').setAttribute('hidden', true as any);
document.getElementById('root').removeAttribute('hidden');
}
}
Expand All @@ -255,7 +259,8 @@ export default function start(render, { decorateStory } = {}) {
switch (viewMode) {
case 'docs': {
const docs = parameters.docs || {};
const DocsContainer = docs.container || (({ children }) => <>{children}</>);
// todo improve typings
const DocsContainer = docs.container || (({ children }: any) => <>{children}</>);
const Page = docs.page || NoDocs;
ReactDOM.render(
<DocsContainer context={renderContext}>
Expand Down Expand Up @@ -298,7 +303,7 @@ export default function start(render, { decorateStory } = {}) {
};

// initialize the UI
const renderUI = forceRender => {
const renderUI = (forceRender: boolean) => {
if (isBrowser) {
try {
renderMain(forceRender);
Expand Down Expand Up @@ -333,7 +338,7 @@ export default function start(render, { decorateStory } = {}) {
});

// Handle keyboard shortcuts
window.onkeydown = event => {
window.onkeydown = (event: KeyboardEvent) => {
if (!focusInInput(event)) {
// We have to pick off the keys of the event that we need on the other side
const { altKey, ctrlKey, metaKey, shiftKey, key, code, keyCode } = event;
Expand All @@ -357,28 +362,31 @@ export default function start(render, { decorateStory } = {}) {
window.__STORYBOOK_ADDONS_CHANNEL__ = channel; // may not be defined
}

let previousExports = new Map();
const loadStories = (loadable, framework) => () => {
let previousExports = new Map<any, string>();
const loadStories = (loadable: Loadable, framework: string) => () => {
let reqs = null;
// todo discuss / improve type check
if (Array.isArray(loadable)) {
reqs = loadable;
} else if (loadable.keys) {
reqs = [loadable];
} else if ((loadable as RequireContext).keys) {
reqs = [loadable as RequireContext];
}

let currentExports = new Map();
let currentExports = new Map<any, string>();
if (reqs) {
reqs.forEach(req => {
req.keys().forEach(filename => {
req.keys().forEach((filename: string) => {
const fileExports = req(filename);
currentExports.set(
fileExports,
typeof req.resolve === 'function' ? req.resolve(filename) : null
// todo discuss: types infer that this is RequireContext; no checks needed?
// typeof req.resolve === 'function' ? req.resolve(filename) : null
req.resolve(filename)
);
});
});
} else {
const exported = loadable();
const exported = (loadable as LoaderFunction)();
if (Array.isArray(exported) && exported.every(obj => obj.default != null)) {
currentExports = new Map(exported.map(fileExports => [fileExports, null]));
} else if (exported) {
Expand Down Expand Up @@ -437,7 +445,8 @@ export default function start(render, { decorateStory } = {}) {
subcomponents,
} = meta;
// We pass true here to avoid the warning about HMR. It's cool clientApi, we got this
const kind = clientApi.storiesOf(kindName, true);
// todo discuss: TS now wants a NodeModule; should we fix this differently?
const kind = clientApi.storiesOf(kindName, true as any);

// we should always have a framework, rest optional
kind.addParameters({
Expand All @@ -448,7 +457,8 @@ export default function start(render, { decorateStory } = {}) {
...params,
});

(decos || []).forEach(decorator => {
// todo add type
(decos || []).forEach((decorator: any) => {
kind.addDecorator(decorator);
});

Expand Down Expand Up @@ -484,7 +494,7 @@ export default function start(render, { decorateStory } = {}) {
* @param {*} m - ES module object for hot-module-reloading (HMR)
* @param {*} framework - name of framework in use, e.g. "react"
*/
const configure = (loadable, m, framework) => {
const configure = (loadable: Loadable, m: NodeModule, framework: string) => {
if (typeof m === 'string') {
throw new Error(
`Invalid module '${m}'. Did you forget to pass \`module\` as the second argument to \`configure\`"?`
Expand Down
22 changes: 22 additions & 0 deletions lib/core/src/client/preview/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export interface PreviewError {
message?: string;
stack?: string;
}

// Copy of require.context
// interface RequireContext {
// keys(): string[];
// (id: string): any;
// <T>(id: string): T;
// resolve(id: string): string;
// /** The module id of the context module. This may be useful for module.hot.accept. */
// id: string;
// }

export type RequireContext = {
keys: () => string[];
(id: string): any;
resolve(id: string): string;
};
export type LoaderFunction = () => void | any[];
export type Loadable = RequireContext | RequireContext[] | LoaderFunction;
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { history, document } from 'global';
import qs from 'qs';
import { toId } from '@storybook/csf';
import { StoryStore } from '@storybook/client-api';

export function pathToId(path) {
export function pathToId(path: string) {
const match = (path || '').match(/^\/story\/(.+)/);
if (!match) {
throw new Error(`Invalid path '${path}', must start with '/story/'`);
}
return match[1];
}

export const setPath = ({ storyId, viewMode }) => {
// todo add proper types
export const setPath = ({ storyId, viewMode }: any) => {
const { path, selectedKind, selectedStory, ...rest } = qs.parse(document.location.search, {
ignoreQueryPrefix: true,
});
Expand All @@ -22,7 +24,11 @@ export const setPath = ({ storyId, viewMode }) => {
history.replaceState({}, '', newPath);
};

export const getIdFromLegacyQuery = ({ path, selectedKind, selectedStory }, storyStore) => {
// todo add proper types
export const getIdFromLegacyQuery = (
{ path, selectedKind, selectedStory }: any,
storyStore: StoryStore
) => {
if (path) {
return pathToId(path);
}
Expand All @@ -40,12 +46,12 @@ export const getIdFromLegacyQuery = ({ path, selectedKind, selectedStory }, stor
return undefined;
};

export const parseQueryParameters = search => {
export const parseQueryParameters = (search: string) => {
const { id } = qs.parse(search, { ignoreQueryPrefix: true });
return id;
};

export const initializePath = storyStore => {
export const initializePath = (storyStore: StoryStore) => {
const query = qs.parse(document.location.search, { ignoreQueryPrefix: true });
let { id: storyId, viewMode } = query; // eslint-disable-line prefer-const
if (!storyId) {
Expand Down

0 comments on commit f9d88da

Please sign in to comment.