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

refactor: exclusively restart ViteManager when required and improve error handling #2053

Merged
merged 6 commits into from
Sep 28, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading