Skip to content

Commit

Permalink
refactor: exclusively restart ViteManager when required and improve e…
Browse files Browse the repository at this point in the history
…rror handling (#2053)
  • Loading branch information
fwouts authored Sep 28, 2023
1 parent ec2eab1 commit e3bd502
Show file tree
Hide file tree
Showing 5 changed files with 479 additions and 445 deletions.
23 changes: 23 additions & 0 deletions core/src/html-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { escape } from "html-escaper";

export function generateHtmlError(message: string) {
return `<html>
<head>
<style>
body {
background: #FCA5A5
}
pre {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
font-size: 12px;
line-height: 1.5em;
color: #7F1D1D;
}
</style>
</head>
<body>
<pre>${escape(message)}</pre>
</body>
</html>`;
}
4 changes: 0 additions & 4 deletions core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,6 @@ export async function createWorkspace({
logger,
middlewares,
port,
onFileChanged: (absoluteFilePath) => {
const filePath = path.relative(rootDir, absoluteFilePath);
frameworkPlugin.typeAnalyzer.invalidateCachedTypesForFile(filePath);
},
});
await previewer.start();
activePreviewers.add(previewer);
Expand Down
202 changes: 25 additions & 177 deletions core/src/previewer.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,21 @@
import type { PreviewConfig } from "@previewjs/config";
import { PREVIEW_CONFIG_NAME, readConfig } from "@previewjs/config";
import type { Reader, ReaderListenerInfo } from "@previewjs/vfs";
import type { Reader } from "@previewjs/vfs";
import { createFileSystemReader, createStackedReader } from "@previewjs/vfs";
import assertNever from "assert-never";
import axios from "axios";
import express from "express";
import { escape } from "html-escaper";
import path from "path";
import type { Logger } from "pino";
import { getCacheDir } from "./caching";
import { FILES_REQUIRING_REDETECTION } from "./crawl-files";
import { findFiles } from "./find-files";
import type { FrameworkPlugin } from "./plugins/framework";
import { Server } from "./server";
import { ViteManager } from "./vite/vite-manager";

const POSTCSS_CONFIG_FILE = [
".postcssrc",
".postcssrc.json",
".postcssrc.yml",
".postcssrc.js",
".postcssrc.mjs",
".postcssrc.cjs",
".postcssrc.ts",
"postcss.config.js",
"postcss.config.mjs",
"postcss.config.cjs",
"postcss.config.ts",
];
const GLOBAL_CSS_FILE_NAMES_WITHOUT_EXT = [
"index",
"global",
"globals",
"style",
"styles",
"app",
];
const GLOBAL_CSS_EXTS = ["css", "sass", "scss", "less", "styl", "stylus"];
const GLOBAL_CSS_FILE = GLOBAL_CSS_FILE_NAMES_WITHOUT_EXT.flatMap((fileName) =>
GLOBAL_CSS_EXTS.map((ext) => `${fileName}.${ext}`)
);

const FILES_REQUIRING_RESTART = new Set([
PREVIEW_CONFIG_NAME,
...FILES_REQUIRING_REDETECTION,
...POSTCSS_CONFIG_FILE,
...GLOBAL_CSS_FILE,
"vite.config.js",
"vite.config.ts",
// TODO: Make plugins contribute files requiring restart to make core agnostic of Svelte config files.
"svelte.config.js",
]);

export class Previewer {
private readonly transformingReader: Reader;
private appServer: Server | null = null;
private viteManager: ViteManager | null = null;
private status: PreviewerStatus = { kind: "stopped" };
private disposeObserver: (() => Promise<void>) | null = null;
private config: PreviewConfig | null = null;

constructor(
private readonly options: {
Expand All @@ -69,7 +26,6 @@ export class Previewer {
frameworkPlugin: FrameworkPlugin;
middlewares: express.RequestHandler[];
port: number;
onFileChanged?(absoluteFilePath: string): void;
}
) {
this.transformingReader = createStackedReader([
Expand Down Expand Up @@ -119,38 +75,17 @@ export class Previewer {
await this.start(options);
break;
case "stopped":
await this.startFromStopped(options);
await this.startFromStopped();
break;
default:
throw assertNever(statusBeforeStart);
}
}

private async startFromStopped({
restarting,
}: { restarting?: boolean } = {}) {
private async startFromStopped() {
this.status = {
kind: "starting",
promise: (async () => {
// PostCSS requires the current directory to change because it relies
// on the `import-cwd` package to resolve plugins.
process.chdir(this.options.rootDir);
const configFromProject = await readConfig(this.options.rootDir);
const config = (this.config = {
...configFromProject,
wrapper: configFromProject.wrapper || {
path: this.options.frameworkPlugin.defaultWrapperPath,
},
});
const globalCssAbsoluteFilePaths = await findFiles(
this.options.rootDir,
`**/@(${GLOBAL_CSS_FILE_NAMES_WITHOUT_EXT.join(
"|"
)}).@(${GLOBAL_CSS_EXTS.join("|")})`,
{
maxDepth: 3,
}
);
const router = express.Router();
router.get(/^\/.*:[^/]+\/$/, async (req, res, next) => {
if (req.url.includes("?html-proxy")) {
Expand All @@ -167,42 +102,15 @@ export class Previewer {
res.status(404).end(`Uh-Oh! Vite server is not running.`);
return;
}
try {
res
.status(200)
.set({ "Content-Type": "text/html" })
.end(
await this.viteManager.loadIndexHtml(
req.originalUrl,
previewableId
)
);
} catch (e: any) {
res
.status(500)
.set({ "Content-Type": "text/html" })
.end(
`<html>
<head>
<style>
body {
background: #FCA5A5
}
pre {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
font-size: 12px;
line-height: 1.5em;
color: #7F1D1D;
}
</style>
</head>
<body>
<pre>${escape(`${e}` || "An unknown error has occurred")}</pre>
</body>
</html>`
);
}
res
.status(200)
.set({ "Content-Type": "text/html" })
.end(
await this.viteManager.loadIndexHtml(
req.originalUrl,
previewableId
)
);
});
router.use("/ping", async (req, res) => {
res.json(
Expand All @@ -221,40 +129,28 @@ export class Previewer {
},
],
});
if (!restarting) {
// When we restart, we must not stop the file observer otherwise a crash while restarting
// (e.g. due to an incomplete preview.config.js) would mean that we stop listening altogether,
// and we will never know to restart.
if (this.transformingReader.observe) {
this.disposeObserver = await this.transformingReader.observe(
this.options.rootDir
);
}
this.transformingReader.listeners.add(this.onFileChangeListener);
if (this.transformingReader.observe) {
this.disposeObserver = await this.transformingReader.observe(
this.options.rootDir
);
}
this.options.logger.debug(`Starting server`);
const server = await this.appServer.start(this.options.port);
this.options.logger.debug(`Starting Vite manager`);
this.viteManager = new ViteManager({
rootDir: this.options.rootDir,
shadowHtmlFilePath: path.join(
this.options.previewDirPath,
"index.html"
),
detectedGlobalCssFilePaths: globalCssAbsoluteFilePaths.map(
(absoluteFilePath) =>
path.relative(this.options.rootDir, absoluteFilePath)
),
reader: this.transformingReader,
cacheDir: path.join(getCacheDir(this.options.rootDir), "vite"),
config,
logger: this.options.logger,
frameworkPlugin: this.options.frameworkPlugin,
server,
port: this.options.port,
});
this.options.logger.debug(`Starting server`);
const server = await this.appServer.start(this.options.port);
this.options.logger.debug(`Starting Vite manager`);
this.viteManager.start(server, this.options.port).catch((e) => {
this.options.logger.error(`Vite manager failed to start: ${e}`);
this.stop();
});
this.viteManager.start();
this.options.logger.debug(`Previewer ready`);
this.status = {
kind: "started",
Expand All @@ -278,11 +174,7 @@ export class Previewer {
}
}

async stop({
restarting,
}: {
restarting?: boolean;
} = {}) {
async stop() {
if (this.status.kind === "starting") {
try {
await this.status.promise;
Expand All @@ -298,13 +190,8 @@ export class Previewer {
this.status = {
kind: "stopping",
promise: (async () => {
if (!restarting) {
this.transformingReader.listeners.remove(this.onFileChangeListener);
if (this.disposeObserver) {
await this.disposeObserver();
this.disposeObserver = null;
}
}
await this.disposeObserver?.();
this.disposeObserver = null;
await this.viteManager?.stop();
this.viteManager = null;
await this.appServer?.stop();
Expand All @@ -316,45 +203,6 @@ export class Previewer {
};
await this.status.promise;
}

private readonly onFileChangeListener = {
onChange: (absoluteFilePath: string, info: ReaderListenerInfo) => {
absoluteFilePath = path.resolve(absoluteFilePath);
if (
!info.virtual &&
this.config?.wrapper &&
absoluteFilePath ===
path.resolve(this.options.rootDir, this.config.wrapper.path)
) {
this.viteManager?.triggerFullReload();
}
if (
!info.virtual &&
FILES_REQUIRING_RESTART.has(path.basename(absoluteFilePath))
) {
if (this.status.kind === "starting" || this.status.kind === "started") {
// Packages were updated. Restart.
this.options.logger.info(
"New dependencies were detected. Restarting..."
);
this.stop({
restarting: true,
})
.then(async () => {
await this.start({ restarting: true });
})
.catch(this.options.logger.error.bind(this.options.logger));
}
return;
}
if (info.virtual) {
this.viteManager?.triggerReload(absoluteFilePath);
// this.viteManager?.triggerReload(absoluteFilePath + ".ts");
} else if (this.options.onFileChanged) {
this.options.onFileChanged(absoluteFilePath);
}
},
};
}

type PreviewerStatus =
Expand Down
Loading

0 comments on commit e3bd502

Please sign in to comment.