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

Core: Clear manager cache on runtime error #13230

Merged
merged 4 commits into from
Nov 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 49 additions & 18 deletions lib/core/src/server/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import webpackHotMiddleware from 'webpack-hot-middleware';

// eslint-disable-next-line import/no-extraneous-dependencies
import { NextHandleFunction } from 'connect';
import { FileSystemCache } from 'file-system-cache';
import { getMiddleware } from './utils/middleware';
import { logConfig } from './logConfig';
import loadConfig from './config';
Expand Down Expand Up @@ -196,6 +197,23 @@ const useProgressReporting = async (
new ProgressPlugin({ handler, modulesCount }).apply(compiler);
};

const useManagerCache = async (fsc: FileSystemCache, managerConfig: webpack.Configuration) => {
// Drop the `cache` property because it'll change as a result of writing to the cache.
const { cache: _, ...baseConfig } = managerConfig;
const configString = stringify(baseConfig);
const cachedConfig = await fsc.get('managerConfig');
await fsc.set('managerConfig', configString);
return configString === cachedConfig;
};

const clearManagerCache = async (fsc: FileSystemCache) => {
if (fsc && fsc.fileExists('managerConfig')) {
await fsc.remove('managerConfig');
return true;
}
return false;
};

const startManager = async ({
startTime,
options,
Expand All @@ -222,31 +240,28 @@ const startManager = async ({

if (options.cache) {
if (options.managerCache) {
// Drop the `cache` property because it'll change as a result of writing to the cache.
const { cache: _, ...baseConfig } = managerConfig;
const configString = stringify(baseConfig);
const cachedConfig = await options.cache.get('managerConfig');
options.cache.set('managerConfig', configString);
if (configString === cachedConfig && (await pathExists(outputDir))) {
const [useCache, hasOutput] = await Promise.all([
// must run even if outputDir doesn't exist, otherwise the 2nd run won't use cache
useManagerCache(options.cache, managerConfig),
pathExists(outputDir),
]);
if (useCache && hasOutput) {
logger.info('=> Using cached manager');
managerConfig = null;
}
} else {
logger.info('=> Removing cached managerConfig');
options.cache.remove('managerConfig');
} else if (await clearManagerCache(options.cache)) {
logger.info('=> Cleared cached manager config');
}
}
}

if (!managerConfig) {
// FIXME: This object containing default values should match ManagerResult
// @ts-ignore
return { managerStats: {}, managerTotalTime: 0 } as ManagerResult;
return { managerStats: {}, managerTotalTime: [0, 0] } as ManagerResult;
}

const compiler = webpack(managerConfig);
const middleware = webpackDevMiddleware(compiler, {
publicPath: managerConfig.output.publicPath,
publicPath: managerConfig.output?.publicPath,
writeToDisk: true,
watchOptions: {
aggregateTimeout: 2000,
Expand All @@ -264,11 +279,26 @@ const startManager = async ({
next();
});

router.post('/runtime-error', (request, response) => {
if (request.body?.error) {
logger.error('Runtime error! Check your browser console.');
logger.error(request.body.error.stack || request.body.message);
if (request.body.origin === 'manager') clearManagerCache(options.cache);
}
response.sendStatus(200);
});

router.use(middleware);

const managerStats: Stats = await new Promise((resolve) => middleware.waitUntilValid(resolve));
if (!managerStats) throw new Error('no stats after building preview');
if (managerStats.hasErrors()) throw managerStats;
if (!managerStats) {
await clearManagerCache(options.cache);
throw new Error('no stats after building manager');
}
if (managerStats.hasErrors()) {
await clearManagerCache(options.cache);
throw managerStats;
}
return { managerStats, managerTotalTime: process.hrtime(startTime) };
};

Expand All @@ -279,9 +309,7 @@ const startPreview = async ({
outputDir,
}: any): Promise<PreviewResult> => {
if (options.ignorePreview) {
// FIXME: This object containing default values should match PreviewResult
// @ts-ignore
return { previewStats: {}, previewTotalTime: 0 } as PreviewResult;
return { previewStats: {}, previewTotalTime: [0, 0] } as PreviewResult;
}

const previewConfig = await loadConfig({
Expand Down Expand Up @@ -343,6 +371,9 @@ export async function storybookDevServer(options: any) {
options.extendServer(server);
}

// Used to report back any client-side (runtime) errors
app.use(express.json());

app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
Expand Down
11 changes: 11 additions & 0 deletions lib/core/src/server/templates/base-manager-head.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,15 @@
// eslint-disable-next-line no-console
console.warn('unable to connect to top frame for connecting dev tools');
}

window.onerror = function onerror(message, source, line, column, err) {
if (window.CONFIG_TYPE !== 'DEVELOPMENT') return;
// eslint-disable-next-line no-var, vars-on-top
var error = { message: err.message, name: err.name, stack: err.stack };
window.fetch('/runtime-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, source, line, column, error, origin: 'manager' }),
});
};
</script>
11 changes: 11 additions & 0 deletions lib/core/src/server/templates/base-preview-head.html
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,15 @@
// eslint-disable-next-line no-console
console.warn('unable to connect to top frame for connecting dev tools');
}

window.onerror = function onerror(message, source, line, column, err) {
if (window.CONFIG_TYPE !== 'DEVELOPMENT') return;
// eslint-disable-next-line no-var, vars-on-top
var error = { message: err.message, name: err.name, stack: err.stack };
window.fetch('/runtime-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, source, line, column, error, origin: 'preview' }),
});
};
</script>
28 changes: 27 additions & 1 deletion lib/core/src/typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,32 @@ declare module 'lazy-universal-dotenv';
declare module 'pnp-webpack-plugin';
declare module '@storybook/theming/paths';
declare module '@storybook/ui/paths';
declare module 'file-system-cache';
declare module 'better-opn';
declare module '@storybook/ui';

declare module 'file-system-cache' {
export interface Options {
basePath?: string;
ns?: string | string[];
extension?: string;
}

export declare class FileSystemCache {
constructor(options: Options);
path(key: string): string;
fileExists(key: string): Promise<boolean>;
ensureBasePath(): Promise<void>;
get(key: string, defaultValue?: any): Promise<any | typeof defaultValue>;
getSync(key: string, defaultValue?: any): any | typeof defaultValue;
set(key: string, value: any): Promise<{ path: string }>
setSync(key: string, value: any): this;
remove(key: string): Promise<void>;
clear(): Promise<void>;
save(): Promise<{ paths: string[] }>;
load(): Promise<{ files: Array<{ path: string, value: any }> }>;
}

function create(options: Options): FileSystemCache;

export = create;
}