@storybook/api
Version:
Core Storybook API & Context
277 lines (246 loc) • 8.25 kB
JavaScript
const _excluded = ["stories", "v"];
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
import "core-js/modules/es.array.reduce.js";
import global from 'global';
import dedent from 'ts-dedent';
import { transformStoriesRawToStoriesHash, transformStoryIndexToStoriesHash } from '../lib/stories';
const {
location,
fetch
} = global;
// eslint-disable-next-line no-useless-escape
const findFilename = /(\/((?:[^\/]+?)\.[^\/]+?)|\/)$/;
export const getSourceType = (source, refId) => {
const {
origin: localOrigin,
pathname: localPathname
} = location;
const {
origin: sourceOrigin,
pathname: sourcePathname
} = new URL(source || location.origin);
const localFull = `${localOrigin + localPathname}`.replace(findFilename, '');
const sourceFull = `${sourceOrigin + sourcePathname}`.replace(findFilename, '');
if (localFull === sourceFull) {
return ['local', sourceFull];
}
if (refId || source) {
return ['external', sourceFull];
}
return [null, null];
};
export const defaultStoryMapper = (b, a) => {
return Object.assign({}, a, {
kind: a.kind.replace('|', '/')
});
};
const addRefIds = (input, ref) => {
return Object.entries(input).reduce((acc, [id, item]) => {
return Object.assign({}, acc, {
[id]: Object.assign({}, item, {
refId: ref.id
})
});
}, {});
};
const handle = async request => {
if (request) {
return Promise.resolve(request).then(response => response.ok ? response.json() : {}).catch(error => ({
error
}));
}
return {};
};
const map = (input, ref, options) => {
const {
storyMapper
} = options;
if (storyMapper) {
return Object.entries(input).reduce((acc, [id, item]) => {
return Object.assign({}, acc, {
[id]: storyMapper(ref, item)
});
}, {});
}
return input;
};
export const init = ({
store,
provider,
singleStory
}, {
runCheck = true
} = {}) => {
const api = {
findRef: source => {
const refs = api.getRefs();
return Object.values(refs).find(({
url
}) => url.match(source));
},
changeRefVersion: (id, url) => {
const {
versions,
title
} = api.getRefs()[id];
const ref = {
id,
url,
versions,
title,
stories: {}
};
api.checkRef(ref);
},
changeRefState: (id, ready) => {
const _api$getRefs = api.getRefs(),
{
[id]: ref
} = _api$getRefs,
updated = _objectWithoutPropertiesLoose(_api$getRefs, [id].map(_toPropertyKey));
updated[id] = Object.assign({}, ref, {
ready
});
store.setState({
refs: updated
});
},
checkRef: async ref => {
const {
id,
url,
version,
type
} = ref;
const isPublic = type === 'server-checked'; // ref's type starts as either 'unknown' or 'server-checked'
// "server-checked" happens when we were able to verify the storybook is accessible from node (without cookies)
// "unknown" happens if the request was declined of failed (this can happen because the storybook doesn't exists or authentication is required)
//
// we then make a request for stories.json
//
// if this request fails when storybook is server-checked we mark the ref as "auto-inject", this is a fallback mechanism for local storybook, legacy storybooks, and storybooks that lack stories.json
// if the request fails with type "unknown" we give up and show an error
// if the request succeeds we set the ref to 'lazy' type, and show the stories in the sidebar without injecting the iframe first
//
// then we fetch metadata if the above fetch succeeded
const loadedData = {};
const query = version ? `?version=${version}` : '';
const credentials = isPublic ? 'omit' : 'include'; // In theory the `/iframe.html` could be private and the `stories.json` could not exist, but in practice
// the only private servers we know about (Chromatic) always include `stories.json`. So we can tell
// if the ref actually exists by simply checking `stories.json` w/ credentials.
const storiesFetch = await fetch(`${url}/stories.json${query}`, {
headers: {
Accept: 'application/json'
},
credentials
});
if (!storiesFetch.ok && !isPublic) {
loadedData.error = {
message: dedent`
Error: Loading of ref failed
at fetch (lib/api/src/modules/refs.ts)
URL: ${url}
We weren't able to load the above URL,
it's possible a CORS error happened.
Please check your dev-tools network tab.
`
};
} else if (storiesFetch.ok) {
const [stories, metadata] = await Promise.all([handle(storiesFetch), handle(fetch(`${url}/metadata.json${query}`, {
headers: {
Accept: 'application/json'
},
credentials,
cache: 'no-cache'
}).catch(() => false))]);
Object.assign(loadedData, Object.assign({}, stories, metadata));
}
const versions = ref.versions && Object.keys(ref.versions).length ? ref.versions : loadedData.versions;
await api.setRef(id, Object.assign({
id,
url
}, loadedData, versions ? {
versions
} : {}, {
error: loadedData.error,
type: !loadedData.stories ? 'auto-inject' : 'lazy'
}));
},
getRefs: () => {
const {
refs = {}
} = store.getState();
return refs;
},
setRef: (id, _ref, ready = false) => {
let {
stories,
v
} = _ref,
rest = _objectWithoutPropertiesLoose(_ref, _excluded);
if (singleStory) return;
const {
storyMapper = defaultStoryMapper
} = provider.getConfig();
const ref = api.getRefs()[id];
let storiesHash;
if (stories) {
if (v === 2) {
storiesHash = transformStoriesRawToStoriesHash(map(stories, ref, {
storyMapper
}), {
provider
});
} else if (!v) {
throw new Error('Composition: Missing stories.json version');
} else {
const index = stories;
storiesHash = transformStoryIndexToStoriesHash({
v,
stories: index
}, {
provider
});
}
storiesHash = addRefIds(storiesHash, ref);
}
api.updateRef(id, Object.assign({
stories: storiesHash
}, rest, {
ready
}));
},
updateRef: (id, data) => {
const _api$getRefs2 = api.getRefs(),
{
[id]: ref
} = _api$getRefs2,
updated = _objectWithoutPropertiesLoose(_api$getRefs2, [id].map(_toPropertyKey));
updated[id] = Object.assign({}, ref, data);
/* eslint-disable no-param-reassign */
const ordered = Object.keys(initialState).reduce((obj, key) => {
obj[key] = updated[key];
return obj;
}, {});
/* eslint-enable no-param-reassign */
store.setState({
refs: ordered
});
}
};
const refs = !singleStory && provider.getConfig().refs || {};
const initialState = refs;
if (runCheck) {
Object.entries(refs).forEach(([k, v]) => {
api.checkRef(v);
});
}
return {
api,
state: {
refs: initialState
}
};
};