Skip to content

Commit

Permalink
improve: handle build/request so that delays are not necessary to mit…
Browse files Browse the repository at this point in the history
…igate ENOENT preview server errors
  • Loading branch information
Shakeskeyboarde committed Jun 20, 2024
1 parent 044d2b7 commit 96ba2c8
Show file tree
Hide file tree
Showing 20 changed files with 581 additions and 289 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ By default, the plugin is automatically enabled when the mode starts with `previ

## Debugging

the `--debug live-preview` flag can be used to enable debug logging for the plugin.
The vite `-d, --debug [feat]` option is supported, and the following feature names are available for this tool.

- `live-preview`: General debugging information.
- `live-preview-request`: Log preview server requests.

## The Problem

Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vite-live-preview",
"version": "0.1.12",
"version": "0.1.13",
"description": "Vite preview watch mode.",
"license": "ISC",
"repository": {
Expand Down Expand Up @@ -51,11 +51,14 @@
"dependencies": {
"@commander-js/extra-typings": "^12.1.0",
"@types/ansi-html": "^0.0.0",
"@types/debug": "^4.1.12",
"@types/ws": "^8.5.10",
"ansi-html": "^0.0.9",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"debug": "^4.3.5",
"escape-goat": "^4.0.0",
"p-defer": "^4.0.1",
"ws": "^8.17.0"
},
"peerDependencies": {
Expand Down
27 changes: 27 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 6 additions & 31 deletions src/bin.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { createCommand, InvalidArgumentError } from '@commander-js/extra-typings';
import { build, type InlineConfig, loadConfigFromFile, type LogLevel, mergeConfig } from 'vite';
import type { LogLevel } from 'vite';

import { description, version } from './bin.data.js';
import plugin from './plugin-build.js';

const cli = createCommand('vite-live-preview')
.description(description)
Expand All @@ -28,16 +27,9 @@ const cli = createCommand('vite-live-preview')

const [root] = cli.processedArgs;
const {
config: configFile,
logLevel,
clearScreen,
debug,
filter,
mode = 'development',
base,
outDir,
reload,
...preview
...options
} = cli.opts();

if (debug) {
Expand All @@ -52,28 +44,11 @@ if (debug) {
}
}

// Load the configuration manually so that the `env` is correct.
const config: InlineConfig = mergeConfig<InlineConfig, InlineConfig>(
await loadConfigFromFile(
{ command: 'build', mode, isPreview: true, isSsrBuild: false },
configFile,
root,
logLevel,
).then((value) => value?.config ?? {}),
{
plugins: [plugin({ enable: true, reload })],
root,
configFile: false,
logLevel,
clearScreen,
mode,
base,
build: { outDir },
preview,
},
);
// XXX: Lazy load the main function so that environment variables which are
// greedily evaluated can take effect.
const { main } = await import('./main.js');

await build(config);
await main(root, options);

function parsePortArg(value: string): number {
const int = Number.parseInt(value, 10);
Expand Down
13 changes: 0 additions & 13 deletions src/debug.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './plugin-build.js';
export { default } from './plugin-build.js';
export * from './plugin/build.js';
export { default } from './plugin/build.js';
51 changes: 51 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { build, type InlineConfig, loadConfigFromFile, type LogLevel, mergeConfig } from 'vite';

import plugin from './plugin/build.js';

interface Options {
readonly mode?: string;
readonly config?: string;
readonly logLevel?: LogLevel;
readonly reload?: boolean;
readonly clearScreen?: boolean;
readonly base?: string;
readonly outDir?: string;
readonly host?: string | true;
readonly port?: number;
readonly strictPort?: true;
readonly open?: string | true;
}

export const main = async (root: string | undefined, {
config: configFile,
mode = 'development',
logLevel,
reload,
clearScreen,
base,
outDir,
...preview
}: Options): Promise<void> => {
// Load the configuration manually so that the `env` is correct.
const config: InlineConfig = mergeConfig<InlineConfig, InlineConfig>(
await loadConfigFromFile(
{ command: 'build', mode, isPreview: true, isSsrBuild: false },
configFile,
root,
logLevel,
).then((value) => value?.config ?? {}),
{
root,
configFile: false,
plugins: [plugin({ enable: true, reload })],
logLevel,
clearScreen,
mode,
base,
build: { outDir },
preview,
},
);

await build(config);
};
31 changes: 31 additions & 0 deletions src/middleware/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import path from 'node:path';

import { type Connect } from 'vite';

import TEMPLATE_CLIENT_SCRIPT from '../template/client.js?raw';
import { createDebugger } from '../util/create-debugger.js';

interface Options {
readonly base: string;
}

export const CLIENT_SCRIPT_NAME = 'vite-live-preview/client.ts';

/**
* Middleware that serves the client script.
*/
export default ({ base }: Options): Connect.NextHandleFunction => {
const debug = createDebugger('live-preview');
const clientScript = TEMPLATE_CLIENT_SCRIPT.replace(/(?<=const base *= *)'\/'/u, JSON.stringify(base));
const clientScriptLength = Buffer.byteLength(clientScript, 'utf8');
const clientScriptRoute = path.posix.join(base, CLIENT_SCRIPT_NAME);

return (req, res, next) => {
if (req.url !== clientScriptRoute) return next();

res.setHeader('Content-Type', 'text/javascript');
res.setHeader('Content-Length', clientScriptLength);
res.end(clientScript);
debug?.('served client script.');
};
};
14 changes: 14 additions & 0 deletions src/middleware/delay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { type Connect } from 'vite';

interface Options {
readonly getPromise: () => Promise<void>;
}

/**
* Middleware that delays the response until a promise resolves.
*/
export default ({ getPromise }: Options): Connect.NextHandleFunction => {
return (req, res, next) => {
void getPromise().finally(() => next());
};
};
41 changes: 41 additions & 0 deletions src/middleware/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import path from 'node:path';

import ansiHtml from 'ansi-html';
import { htmlEscape } from 'escape-goat';
import { type Connect } from 'vite';

import TEMPLATE_ERROR_HTML from '../template/error.html?raw';
import { createDebugger } from '../util/create-debugger.js';
import { CLIENT_SCRIPT_NAME } from './client.js';

interface Options {
readonly base: string;
readonly getError: () => Error | undefined;
}

/**
* Middleware that serves an error page when an error is present.
*/
export default ({ base, getError }: Options): Connect.NextHandleFunction => {
const debug = createDebugger('live-preview');
const clientSrc = JSON.stringify(path.posix.join(base, CLIENT_SCRIPT_NAME));

return (req, res, next) => {
if (!req.headers.accept?.includes('html')) return next();

const error = getError();

if (!error) return next();

const errorMessage = ansiHtml(htmlEscape(error.message));
const errorHtml = TEMPLATE_ERROR_HTML
.replace(/(?=<\/head>)|$/iu, `<script crossorigin="" src=${clientSrc}></script>\n`)
.replace(/(?=<\/body>)|$/iu, `<pre class="error"><code>${errorMessage}</code></pre>\n`);

res.statusCode = 500;
res.setHeader('Content-Type', 'text/html');
res.setHeader('Content-Length', Buffer.byteLength(errorHtml, 'utf8'));
res.end(errorHtml);
debug?.(`served error page for "${req.url}".`);
};
};
Loading

0 comments on commit 96ba2c8

Please sign in to comment.