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

Use vite internally instead of handling live update with websocket, chokidar by ourself #184

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
298 changes: 132 additions & 166 deletions cli/server/previewServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,185 +5,151 @@ const fs = require('fs');
const connect = require('connect');
const sirv = require('sirv');
const app = connect();
const chokidar = require('chokidar');
const { openBrowser } = require('./browser');
const { WebSocketServer } = require('ws');

const port = process.env.PORT || 3336;
// TODO: Can we reuse `port`, I think Vite they can do that
// https://github.com/vitejs/vite/blob/50a876537cc7b934ec5c1d11171b5ce02e3891a8/packages/vite/src/node/server/ws.ts#L97
// TODO: Increase port by 1 is not a good strategy, we should check if it's also available
const wsPort = Number(port) + 1;

const CACHE_DIRECTORY = './node_modules/.cache/jest-preview';
const INDEX_BASENAME = 'index.html';
const INDEX_PATH = path.join(CACHE_DIRECTORY, INDEX_BASENAME);
const PUBLIC_CONFIG_BASENAME = 'cache-public.config';
const PUBLIC_CONFIG_PATH = path.join(CACHE_DIRECTORY, PUBLIC_CONFIG_BASENAME);
const FAV_ICON_PATH = './node_modules/jest-preview/cli/server/favicon.ico';

// Always set default public folder to `public` if not specified
let publicFolder = 'public';

if (fs.existsSync(PUBLIC_CONFIG_PATH)) {
publicFolder = fs.readFileSync(PUBLIC_CONFIG_PATH, 'utf8').trim();
}

const wss = new WebSocketServer({ port: wsPort });
const { createServer: createViteServer } = require('vite');

wss.on('connection', function connection(ws) {
ws.on('message', function message(data) {
console.log('received: %s', data);
try {
const dataJSON = JSON.parse(data);
if (dataJSON.type === 'publicFolder') {
publicFolder = dataJSON.payload;
}
} catch (error) {
console.error(error);
}
async function createServer() {
const vite = await createViteServer({
server: { middlewareMode: 'ssr' },
configFile: path.resolve(__dirname, './vite.config.js'),
});
});

const watcher = chokidar.watch([INDEX_PATH, PUBLIC_CONFIG_PATH], {
// ignored: ['**/node_modules/**', '**/.git/**'],
ignoreInitial: true,
ignorePermissionErrors: true,
disableGlobbing: true,
});

function handleFileChange(filePath) {
const basename = path.basename(filePath);
// TODO: Check if this is the root cause for issue on linux
if (basename === INDEX_BASENAME) {
wss.clients.forEach((client) => {
if (client.readyState === 1) {
client.send(JSON.stringify({ type: 'reload' }));
}
});
}

if (basename === PUBLIC_CONFIG_BASENAME) {
publicFolder = fs.readFileSync(PUBLIC_CONFIG_PATH, 'utf8').trim();
}
}
const port = process.env.PORT || 3336;

watcher
.on('change', handleFileChange)
.on('add', handleFileChange)
.on('unlink', handleFileChange);

/**
*
* @param {string} string
* @param {string} word
* @param {string} injectWord
* @returns string
*/

function injectToString(string, word, injectWord) {
const breakPosition = string.indexOf(word) + word.length;
return (
string.slice(0, breakPosition) + injectWord + string.slice(breakPosition)
);
}
const CACHE_DIRECTORY = './node_modules/.cache/jest-preview';
const INDEX_BASENAME = 'index.html';
const INDEX_PATH = path.join(CACHE_DIRECTORY, INDEX_BASENAME);
const PUBLIC_CONFIG_BASENAME = 'cache-public.config';
const PUBLIC_CONFIG_PATH = path.join(CACHE_DIRECTORY, PUBLIC_CONFIG_BASENAME);
const FAV_ICON_PATH = './node_modules/jest-preview/cli/server/favicon.ico';

function injectToHead(html, content) {
return injectToString(html, '<head>', content);
}
// Always set default public folder to `public` if not specified
let publicFolder = 'public';

app.use((req, res, next) => {
// Learn from https://github.com/vitejs/vite/blob/2b7dad1ea1d78d7977e0569fcca4c585b4014e85/packages/vite/src/node/server/middlewares/static.ts#L38
const serve = sirv('.', {
dev: true,
etag: true,
});
// Do not serve index
if (req.url === '/') {
return next();
if (fs.existsSync(PUBLIC_CONFIG_PATH)) {
publicFolder = fs.readFileSync(PUBLIC_CONFIG_PATH, 'utf8').trim();
}

// Check if req.url is existed, if not, look up in public directory
const filePath = path.join('.', req.url);
if (!fs.existsSync(filePath)) {
const newPath = path.join(publicFolder, req.url);
if (fs.existsSync(newPath)) {
req.url = newPath;
} else {
// Cannot find the file, warns user about it
// Likely user has old Jest cached code transformations.
// Or just a bug in their source code
console.log('[WARN] File not found: ', req.url);
console.log(`[WARN] Please check if ${req.url} is existed.`);
console.log(
`[WARN] If it is existed, likely you forget to setup the code transformation, or you haven't flushed the old cache yet. Try to run "./node_modules/.bin/jest --clearCache" to clear the cache.\n`,
);
// TODO: To send those warning to browser as an overlay/ toast, the idea is similar to https://www.npmjs.com/package/vite-plugin-checker
// TODO: Known issue: in development, we can't find `favicon.ico` yet. So it will yell in the Preview Server logs
}
function injectToString(string, word, injectWord) {
const breakPosition = string.indexOf(word) + word.length;
return (
string.slice(0, breakPosition) + injectWord + string.slice(breakPosition)
);
}
serve(req, res, next);
});

app.use('/', (req, res) => {
const reloadScriptContent = fs
.readFileSync(path.join(__dirname, './ws-client.js'), 'utf-8')
.replace(/\$PORT/g, wsPort);

if (!fs.existsSync(INDEX_PATH)) {
// Make it looks nice
return res.end(`<!DOCTYPE html>
<html>
<head>
<link rel="shortcut icon" href="${FAV_ICON_PATH}">
<title>Jest Preview Dashboard</title>
</head>
<body>
No preview found.<br/>
Please add following lines to your test: <br /> <br />
<div style="background-color: grey;width: fit-content;padding: 8px;">
<code>
import { debug } from 'jest-preview';
<br />
<br />
// Inside your tests
<br />
debug();
</code>
</div>
<br />
Then rerun your tests.
<br />
See an example in the <a href="https://www.jest-preview.com/docs/getting-started/usage#3-preview-your-html-from-jest-following-code-demo-how-to-use-it-with-react-testing-library" target="_blank" rel="noopener noreferrer">documentation</a>
</body>
<script>${reloadScriptContent}</script>
</html>`);

function injectToHead(html, content) {
return injectToString(html, '<head>', content);
}
let indexHtml = fs.readFileSync(INDEX_PATH, 'utf8');
indexHtml += `<script>${reloadScriptContent}</script>`;
indexHtml = injectToHead(
indexHtml,
`<link rel="shortcut icon" href="${FAV_ICON_PATH}">

app.use(vite.middlewares);

app.use((req, res, next) => {
// Learn from https://github.com/vitejs/vite/blob/2b7dad1ea1d78d7977e0569fcca4c585b4014e85/packages/vite/src/node/server/middlewares/static.ts#L38
const serve = sirv('.', {
dev: true,
etag: true,
});
// Do not serve index
if (req.url === '/') {
return next();
}

// Check if req.url is existed, if not, look up in public directory
const filePath = path.join('.', req.url);
if (!fs.existsSync(filePath)) {
const newPath = path.join(publicFolder, req.url);
if (fs.existsSync(newPath)) {
req.url = newPath;
} else {
// Cannot find the file, warns user about it
// Likely user has old Jest cached code transformations.
// Or just a bug in their source code
console.log('[WARN] File not found: ', req.url);
console.log(`[WARN] Please check if ${req.url} is existed.`);
console.log(
`[WARN] If it is existed, likely you forget to setup the code transformation, or you haven't flushed the old cache yet. Try to run "./node_modules/.bin/jest --clearCache" to clear the cache.\n`,
);
// TODO: To send those warning to browser as an overlay/ toast, the idea is similar to https://www.npmjs.com/package/vite-plugin-checker
// TODO: Known issue: in development, we can't find `favicon.ico` yet. So it will yell in the Preview Server logs
}
}
serve(req, res, next);
});

app.use('/', async (req, res) => {
let indexHtml = fs.readFileSync(INDEX_PATH, 'utf8');
indexHtml = injectToHead(
indexHtml,
`<link rel="shortcut icon" href="${FAV_ICON_PATH}">
<title>Jest Preview Dashboard</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover">`,
);
res.end(indexHtml);
});

const server = http.createServer(app);

server.listen(port, () => {
if (fs.existsSync(INDEX_PATH)) {
// Remove old preview
const files = fs.readdirSync(CACHE_DIRECTORY);
files.forEach((file) => {
if (!file.startsWith('cache-')) {
fs.unlinkSync(path.join(CACHE_DIRECTORY, file));
}
});
}
);
indexHtml = injectToHead(
indexHtml,
`<script type="module" src="/@vite/client"></script>
<script type="module">
import RefreshRuntime from "/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>`,
);
res.end(indexHtml);
});

const server = http.createServer(app);

server.listen(port, () => {
if (fs.existsSync(INDEX_PATH)) {
// Remove old preview
const files = fs.readdirSync(CACHE_DIRECTORY);
files.forEach((file) => {
if (!file.startsWith('cache-')) {
fs.unlinkSync(path.join(CACHE_DIRECTORY, file));
}
});
}

if (!fs.existsSync(CACHE_DIRECTORY)) {
fs.mkdirSync(CACHE_DIRECTORY, {
recursive: true,
});
}
fs.writeFileSync(
INDEX_PATH,
`<!DOCTYPE html>
<html>
<head>
<link rel="shortcut icon" href="${FAV_ICON_PATH}">
<title>Jest Preview Dashboard</title>
</head>
<body>
No preview found.<br/>
Please add following lines to your test: <br /> <br />
<div style="background-color: grey;width: fit-content;padding: 8px;">
<code>
import { debug } from 'jest-preview';
<br />
<br />
// Inside your tests
<br />
debug();
</code>
</div>
<br />
Then rerun your tests.
<br />
See an example in the <a href="https://www.jest-preview.com/docs/getting-started/usage#3-preview-your-html-from-jest-following-code-demo-how-to-use-it-with-react-testing-library" target="_blank" rel="noopener noreferrer">documentation</a>
</body>
</html>`,
);

console.log(`Jest Preview Server listening on port ${port}`);
openBrowser(`http://localhost:${port}`);
});
}

console.log(`Jest Preview Server listening on port ${port}`);
openBrowser(`http://localhost:${port}`);
});
createServer();
9 changes: 9 additions & 0 deletions cli/server/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig, loadEnv } from 'vite';

export default defineConfig({
server: {
watch: {
ignored: ['!**/node_modules/.cache/jest-preview/**'],
},
},
});
Loading