@andersundsehr/storybook-typo3
Version:
The one and only Storybook Renderer for TYPO3 Fluid Components
181 lines (175 loc) ⢠7.45 kB
JavaScript
/*
We want to make it obvious for the user what he has to do.
We have 2 different use cases:
- background fetches (componentMeta & preview)
- html Fetches
most of the time the user has to do something in their IDE to fix the problem.
So we want to show a message that tells the user what he has to do.
In the case of a background fetch, we want to show the full error in the console.
And give the user a alert with the error message and a retry button.
In the case of a html fetch, we want to show the error as html response?
- yes: because the user can see the error in the browser and can fix it
- no: because storybook does not understand that it is an error
In any case we should get the error in json so we can handle it in the frontend as we like.
Sometimes it can still be text/html so we need to handle that as well.
*/
let updatableApiKey = import.meta.env.STORYBOOK_TYPO3_KEY;
function anySignal(...signals) {
const controller = new AbortController();
for (const s of signals) {
if (!s) {
continue;
}
if (s.aborted) {
controller.abort(s.reason);
break;
}
s.addEventListener('abort', () => controller.abort(s.reason), { once: true });
}
return controller.signal;
}
export async function fetchWithUserRetry(url, options, message, resultType = 'json') {
try {
options = { ...options }; // Clone options to avoid mutating the original
options.signal = anySignal(options.signal, AbortSignal.timeout(5000));
options.headers = {
...options.headers,
'Content-Type': 'application/json',
Accept: resultType === 'json' ? 'application/json' : 'text/html',
'X-Storybook-TYPO3-Key': updatableApiKey,
};
const response = await fetch(url, options);
if (!response.ok) {
return retry(response, url, options, message, resultType);
}
return (await response[resultType]());
}
catch (error) {
return retry(error, url, options, message, resultType);
}
}
async function retry(errorOrResponse, url, options, message, resultType) {
const errorType = await getErrorExplanation(errorOrResponse);
let exception = errorOrResponse instanceof Error ? errorOrResponse : undefined;
let retry = false;
let confirmationMessage = '';
if (errorType.errorType === 'abort') {
// we do not ask the user to retry on aborts:
throw exception || new Error(`Request aborted: ${errorType.message}`);
}
else if (errorType.errorType === 'key') {
retry = true;
confirmationMessage = `š ${errorType.message}\n\n`
+ `š url: ${url}\n\n`
+ `š¬ Please insert the correct API key:\ncan be found in the .env file of your TYPO3:\n\n`;
// if there was no api key, it will be set into the .env from the php code automatically
// we wait 1 second to give HMR time to update the .env file and reload the page, otherwise the user has to provide the key
if (!updatableApiKey) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
updatableApiKey = prompt(confirmationMessage) || updatableApiKey;
}
else if (errorType.errorType === 'network') {
confirmationMessage = `š+š« A network error occurred while fetching ${message} from TYPO3:\n\n`
+ `š¬ ${errorType.message}\n\n`
+ `ā© Please check your network connection and try again.\n\n`
+ `š url: ${url}\n\n`;
exception = new Error(`Network error: ${errorType.message}`);
}
else if (errorType.errorType === 'extension') {
if (resultType === 'text') {
// no retry via confirm()
return errorType.errorHtml;
}
confirmationMessage = `š„ An error occurred while fetching ${message} from TYPO3:\n\n`
+ `š¬ ${errorType.reason}\n\n`
+ `${errorType.stackTrace ? `šµš»āāļø Stack trace: ${errorType.stackTrace}\n\n` : ''}`
+ `š url: ${url}\n\n`;
exception = new Error(`Extension error: ${errorType.reason}`);
}
else {
if (resultType === 'text' && errorType.message) {
// no retry via confirm()
return errorType.message;
}
// unknown error
confirmationMessage = `š³ An error occurred while fetching ${message} from TYPO3:\n\n`
+ `š¬ ${errorType.message}\n\n`
+ `#ļøā£ Status code: ${errorType.statusCode || 'unknown'}\n\n`
+ `š url: ${url}\n\n`;
}
confirmationMessage = confirmationMessage.trim();
confirmationMessage = confirmationMessage.length > 700 ? confirmationMessage.substring(0, 700 - 3) + '\nā¦' : confirmationMessage;
if (!retry) {
retry = confirm(confirmationMessage + '\n\n Do you want to retry šā');
}
if (retry) {
options.signal = undefined;
return fetchWithUserRetry(url, options, message, resultType);
}
throw exception || new Error(`Failed to fetch ${message} from TYPO3: ${JSON.stringify(errorType)}`);
}
async function getErrorExplanation(errorOrResponse) {
if (errorOrResponse instanceof Response) {
let text = undefined;
try {
text = await errorOrResponse.text();
}
catch (e) {
}
if (errorOrResponse.status === 401) {
return {
errorType: 'key',
message: text || `The API key is not set or invalid. Maybe you only need to restart Storybook?`,
};
}
if (!text) {
return {
errorType: 'unknown',
statusCode: errorOrResponse.status,
message: `Failed to read response from TYPO3: ${errorOrResponse.status}\n ${errorOrResponse.statusText}`,
};
}
try {
const extensionError = JSON.parse(text);
if (extensionError && typeof extensionError === 'object' && 'errorType' in extensionError && extensionError.errorType === 'extension') {
return extensionError;
}
return {
errorType: 'unknown',
statusCode: errorOrResponse.status,
message: `Received an unexpected response from TYPO3: ${errorOrResponse.status}\n ${JSON.stringify(extensionError)}`,
};
}
catch (e) {
console.warn('āļø Failed to parse JSON from TYPO3 response:', e);
}
return {
errorType: 'unknown',
statusCode: errorOrResponse.status,
message: `Received an unexpected response from TYPO3: ${errorOrResponse.status}\n ${text}`,
};
}
if (errorOrResponse instanceof Error) {
if (errorOrResponse.name === 'AbortError') {
return {
errorType: 'abort',
message: `The request was aborted (timeout or cancellation).`,
};
}
return {
errorType: 'network',
message: errorOrResponse.message,
};
}
if (typeof errorOrResponse === 'string') {
return {
errorType: 'unknown',
message: errorOrResponse,
};
}
return {
errorType: 'unknown',
message: `An unknown error occurred: ${JSON.stringify(errorOrResponse) || ''}`,
};
}