Skip to content

Commit

Permalink
refactor: reuse the same Vite dev server with multiple index.html ent…
Browse files Browse the repository at this point in the history
…ry points (#1689)
  • Loading branch information
fwouts committed Jun 4, 2023
1 parent 70ea3f8 commit 5fedcc5
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 97 deletions.
113 changes: 40 additions & 73 deletions core/src/previewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { Reader, ReaderListenerInfo } from "@previewjs/vfs";
import { createFileSystemReader, createStackedReader } from "@previewjs/vfs";
import assertNever from "assert-never";
import axios from "axios";
import { exclusivePromiseRunner } from "exclusive-promises";
import express from "express";
import { escape } from "html-escaper";
import path from "path";
Expand Down Expand Up @@ -59,13 +58,12 @@ const SHUTDOWN_AFTER_INACTIVITY = 10000;
export class Previewer {
private readonly transformingReader: Reader;
private appServer: Server | null = null;
private viteManagers: Record<string, ViteManager> = {};
private viteManager: ViteManager | null = null;
private status: PreviewerStatus = { kind: "stopped" };
private shutdownCheckInterval: NodeJS.Timeout | null = null;
private disposeObserver: (() => Promise<void>) | null = null;
private config: PreviewConfig | null = null;
private lastPingTimestamp = 0;
private locking = exclusivePromiseRunner();

constructor(
private readonly options: {
Expand Down Expand Up @@ -175,63 +173,26 @@ export class Previewer {
);
const router = express.Router();
router.get(/^\/preview\/.*:[^/]+\/$/, async (req, res) => {
const componentId = req.path.substring(9, req.path.length - 1);
if (req.header("Accept") === "text/x-vite-ping") {
// This is triggered as part of HMR. It may come after we've
// switched to rendering a different component, so we want to
// make sure this doesn't accidentally restart the Vite manager for it.
// This is triggered as part of HMR. Exit early.
res.writeHead(204).end();
return;
}
if (!this.viteManager) {
res.status(404).end(`Uh-Oh! Vite server is not running.`);
return;
}
try {
const server = this.appServer?.server;
if (!server) {
throw new Error(`No server running`);
}
const componentId = req.path.substring(9, req.path.length - 1);
// Note: we only ever have a single active Vite manager.
// We must prevent race conditions here when we quickly switch back to a
// component whose Vite manager hasn't stopped yet.
let viteManager!: ViteManager;
await this.locking(async () => {
const existingViteManager = this.viteManagers[componentId];
if (existingViteManager) {
viteManager = existingViteManager;
} else {
await Promise.all(
Object.values(this.viteManagers).map((v) => v.stop())
);
this.viteManagers = {};
this.options.logger.debug(`Initializing Vite manager`);
viteManager = new ViteManager({
componentId,
base: req.path,
rootDirPath: this.options.rootDirPath,
shadowHtmlFilePath: path.join(
this.options.previewDirPath,
"index.html"
),
detectedGlobalCssFilePaths: globalCssAbsoluteFilePaths.map(
(absoluteFilePath) =>
path.relative(this.options.rootDirPath, absoluteFilePath)
),
reader: this.transformingReader,
cacheDir: path.join(
getCacheDir(this.options.rootDirPath),
"vite"
),
config,
logger: this.options.logger,
frameworkPlugin: this.options.frameworkPlugin,
});
this.viteManagers[componentId] = viteManager;
this.options.logger.debug(`Starting Vite manager`);
await viteManager.start(server, port);
}
});
res
.status(200)
.set({ "Content-Type": "text/html" })
.end(await viteManager.loadIndexHtml(req.originalUrl));
.end(
await this.viteManager.loadIndexHtml(
req.originalUrl,
componentId
)
);
} catch (e: any) {
res
.status(500)
Expand Down Expand Up @@ -273,11 +234,7 @@ export class Previewer {
...(this.options.middlewares || []),
router,
(req, res, next) => {
// Note: this isn't a problem because there is only ever at most one
// Vite manager at any given time.
for (const viteManager of Object.values(this.viteManagers)) {
viteManager.middleware(req, res, next);
}
this.viteManager?.middleware(req, res, next);
},
],
});
Expand All @@ -292,8 +249,26 @@ export class Previewer {
}
this.transformingReader.listeners.add(this.onFileChangeListener);
}
this.viteManager = new ViteManager({
rootDirPath: this.options.rootDirPath,
shadowHtmlFilePath: path.join(
this.options.previewDirPath,
"index.html"
),
detectedGlobalCssFilePaths: globalCssAbsoluteFilePaths.map(
(absoluteFilePath) =>
path.relative(this.options.rootDirPath, absoluteFilePath)
),
reader: this.transformingReader,
cacheDir: path.join(getCacheDir(this.options.rootDirPath), "vite"),
config,
logger: this.options.logger,
frameworkPlugin: this.options.frameworkPlugin,
});
this.options.logger.debug(`Starting server`);
await this.appServer.start(port);
const server = await this.appServer.start(port);
this.options.logger.debug(`Starting Vite manager`);
await this.viteManager.start(server, port);
this.options.logger.debug(`Previewer ready`);
this.status = {
kind: "started",
Expand Down Expand Up @@ -381,14 +356,10 @@ export class Previewer {
this.disposeObserver = null;
}
}
await Promise.all(
Object.values(this.viteManagers).map((v) => v.stop())
);
this.viteManagers = {};
if (this.appServer) {
await this.appServer.stop();
this.appServer = null;
}
await this.viteManager?.stop();
this.viteManager = null;
await this.appServer?.stop();
this.appServer = null;
this.status = {
kind: "stopped",
};
Expand All @@ -406,9 +377,7 @@ export class Previewer {
absoluteFilePath ===
path.resolve(this.options.rootDirPath, this.config.wrapper.path)
) {
for (const viteManager of Object.values(this.viteManagers)) {
viteManager.triggerFullReload();
}
this.viteManager?.triggerFullReload();
}
if (
!info.virtual &&
Expand All @@ -431,10 +400,8 @@ export class Previewer {
return;
}
if (info.virtual) {
for (const viteManager of Object.values(this.viteManagers)) {
viteManager.triggerReload(absoluteFilePath);
viteManager.triggerReload(absoluteFilePath + ".ts");
}
this.viteManager?.triggerReload(absoluteFilePath);
this.viteManager?.triggerReload(absoluteFilePath + ".ts");
} else if (this.options.onFileChanged) {
this.options.onFileChanged(absoluteFilePath);
}
Expand Down
29 changes: 5 additions & 24 deletions core/src/vite/vite-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ export class ViteManager {
private readonly options: {
logger: Logger;
reader: Reader;
base: string;
componentId: string;
rootDirPath: string;
shadowHtmlFilePath: string;
detectedGlobalCssFilePaths: string[];
Expand All @@ -56,7 +54,7 @@ export class ViteManager {
this.middleware = router;
}

async loadIndexHtml(url: string) {
async loadIndexHtml(url: string, componentId: string) {
const template = await fs.readFile(
this.options.shadowHtmlFilePath,
"utf-8"
Expand All @@ -65,7 +63,7 @@ export class ViteManager {
if (!this.viteServer) {
throw new Error(`Vite server is not running.`);
}
const { filePath } = decodeComponentId(this.options.componentId);
const { filePath } = decodeComponentId(componentId);
const componentPath = filePath.replace(/\\/g, "/");
const wrapper = this.options.config.wrapper;
const wrapperPath =
Expand Down Expand Up @@ -118,7 +116,7 @@ export class ViteManager {
latestWrapperModule = wrapperModule;
refresh = initPreview({
componentModule,
componentId: ${JSON.stringify(this.options.componentId)},
componentId: ${JSON.stringify(componentId)},
wrapperModule,
wrapperName: ${JSON.stringify(wrapper?.componentName || null)},
});
Expand Down Expand Up @@ -290,29 +288,12 @@ export class ViteManager {
...this.options.config.vite,
configFile: false,
root: this.options.rootDirPath,
base: this.options.base,
base: "/preview/",
server: {
middlewareMode: true,
hmr: {
overlay: false,
server: {
...server,
on: (event, listener) => {
if (event === "upgrade") {
// Vite doesn't check req.url, so it ends up trying to upgrade the same
// socket multiple times if multiple Vite managers are running.
// See https://github.com/vitejs/vite/blob/5c3fa057f10b1adea4e28f58f69bf6b636eac4aa/packages/vite/src/node/server/ws.ts#L105
server.on("upgrade", (req, socket, head) => {
if (req.url === this.options.base) {
// TODO: Why doesn't this.options.logger.error() work?
listener(req, socket, head);
}
});
} else {
server.on(event, listener);
}
},
} as Server,
server,
clientPort: port,
...(typeof this.options.config.vite?.server?.hmr === "object"
? this.options.config.vite?.server?.hmr
Expand Down

0 comments on commit 5fedcc5

Please sign in to comment.