Skip to content

Commit

Permalink
Merge pull request #12085 from storybookjs/tech/refs-verify-existence…
Browse files Browse the repository at this point in the history
…-in-node

Composition: Verify refs in node
  • Loading branch information
shilman authored Aug 20, 2020
2 parents 4d27456 + f412ad1 commit 48c4b62
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 179 deletions.
106 changes: 46 additions & 60 deletions lib/api/src/modules/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,21 @@ export interface ComposedRef {
id: string;
title?: string;
url: string;
type?: 'auto-inject' | 'unknown' | 'lazy';
type?: 'auto-inject' | 'unknown' | 'lazy' | 'server-checked';
stories: StoriesHash;
versions?: Versions;
loginUrl?: string;
version?: string;
ready?: boolean;
error?: any;
}
export interface ComposedRefUpdate {
title?: string;
type?: 'auto-inject' | 'unknown' | 'lazy';
stories?: StoriesHash;
versions?: Versions;
loginUrl?: string;
version?: string;
ready?: boolean;
error?: any;
}

export type ComposedRefUpdate = Partial<
Pick<
ComposedRef,
'title' | 'type' | 'stories' | 'versions' | 'loginUrl' | 'version' | 'ready' | 'error'
>
>;

export type Refs = Record<string, ComposedRef>;
export type RefId = string;
Expand All @@ -62,16 +59,6 @@ export type RefUrl = string;
// eslint-disable-next-line no-useless-escape
const findFilename = /(\/((?:[^\/]+?)\.[^\/]+?)|\/)$/;

const allSettled = (promises: Promise<Response>[]): Promise<(Response | false)[]> =>
Promise.all(
promises.map((promise) =>
promise.then(
(r) => (r.ok ? r : (false as const)),
() => false as const
)
)
);

export const getSourceType = (source: string, refId: string) => {
const { origin: localOrigin, pathname: localPathname } = location;
const { origin: sourceOrigin, pathname: sourcePathname } = new URL(source);
Expand All @@ -98,6 +85,15 @@ const addRefIds = (input: StoriesHash, ref: ComposedRef): StoriesHash => {
}, {} as StoriesHash);
};

const handle = async (request: Response | false): Promise<SetRefData> => {
if (request) {
return Promise.resolve(request)
.then((response) => (response.ok ? response.json() : {}))
.catch((error) => ({ error }));
}
return {};
};

const map = (
input: StoriesRaw,
ref: ComposedRef,
Expand Down Expand Up @@ -135,40 +131,37 @@ export const init: ModuleFn = ({ store, provider, fullAPI }, { runCheck = true }
});
},
checkRef: async (ref) => {
const { id, url, version } = 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: { error?: Error; stories?: StoriesRaw; loginUrl?: string } = {};
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 [included, omitted, iframe] = await allSettled([
fetch(`${url}/stories.json${query}`, {
headers: {
Accept: 'application/json',
},
credentials: 'include',
}),
fetch(`${url}/stories.json${query}`, {
headers: {
Accept: 'application/json',
},
credentials: 'omit',
}),
fetch(`${url}/iframe.html${query}`, {
mode: 'no-cors',
credentials: 'omit',
}),
]);

const handle = async (request: Response | false): Promise<SetRefData> => {
if (request) {
return Promise.resolve(request)
.then((response) => (response.ok ? response.json() : {}))
.catch((error) => ({ error }));
}
return {};
};

if (!included && !omitted && !iframe) {
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
Expand All @@ -182,11 +175,9 @@ export const init: ModuleFn = ({ store, provider, fullAPI }, { runCheck = true }
Please check your dev-tools network tab.
`,
} as Error;
} else if (omitted || included) {
const credentials = included ? 'include' : 'omit';

} else if (storiesFetch.ok) {
const [stories, metadata] = await Promise.all([
included ? handle(included) : handle(omitted),
handle(storiesFetch),
handle(
fetch(`${url}/metadata.json${query}`, {
headers: {
Expand Down Expand Up @@ -244,11 +235,6 @@ export const init: ModuleFn = ({ store, provider, fullAPI }, { runCheck = true }

const initialState: SubState['refs'] = refs;

Object.values(refs).forEach((r) => {
// eslint-disable-next-line no-param-reassign
r.type = 'unknown';
});

if (runCheck) {
Object.entries(refs).forEach(([k, v]) => {
api.checkRef(v as SetRefData);
Expand Down
Loading

0 comments on commit 48c4b62

Please sign in to comment.