Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composition: Verify refs in node #12085

Merged
merged 10 commits into from
Aug 20, 2020
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';
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
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