Initial commit

This commit is contained in:
Aditya Gupta
2026-06-29 19:29:19 +05:30
committed by GitHub
commit aabb8986aa
292 changed files with 17976 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
export const LOCAL_RELOAD_SOCKET_PORT = 8081;
export const LOCAL_RELOAD_SOCKET_URL = `ws://localhost:${LOCAL_RELOAD_SOCKET_PORT}`;
export const DO_UPDATE = 'do_update';
export const DONE_UPDATE = 'done_update';
export const BUILD_COMPLETE = 'build_complete';

View File

@@ -0,0 +1,17 @@
import { DO_UPDATE, DONE_UPDATE, LOCAL_RELOAD_SOCKET_URL } from '../consts.js';
import MessageInterpreter from '../interpreter/index.js';
export default ({ id, onUpdate }: { id: string; onUpdate: () => void }) => {
const ws = new WebSocket(LOCAL_RELOAD_SOCKET_URL);
ws.onopen = () => {
ws.addEventListener('message', event => {
const message = MessageInterpreter.receive(String(event.data));
if (message.type === DO_UPDATE && message.id === id) {
onUpdate();
ws.send(MessageInterpreter.send({ type: DONE_UPDATE }));
}
});
};
};

View File

@@ -0,0 +1,53 @@
import {
BUILD_COMPLETE,
DO_UPDATE,
DONE_UPDATE,
LOCAL_RELOAD_SOCKET_PORT,
LOCAL_RELOAD_SOCKET_URL,
} from '../consts.js';
import MessageInterpreter from '../interpreter/index.js';
import { WebSocketServer } from 'ws';
import type { WebSocket } from 'ws';
const clientsThatNeedToUpdate: Set<WebSocket> = new Set();
(() => {
const wss = new WebSocketServer({ port: LOCAL_RELOAD_SOCKET_PORT });
wss.on('listening', () => {
console.log(`[HMR] Server listening at ${LOCAL_RELOAD_SOCKET_URL}`);
});
wss.on('connection', ws => {
clientsThatNeedToUpdate.add(ws);
ws.addEventListener('close', () => {
clientsThatNeedToUpdate.delete(ws);
});
ws.addEventListener('message', event => {
if (typeof event.data !== 'string') return;
const message = MessageInterpreter.receive(event.data);
if (message.type === DONE_UPDATE) {
ws.close();
}
if (message.type === BUILD_COMPLETE) {
clientsThatNeedToUpdate.forEach((ws: WebSocket) =>
ws.send(MessageInterpreter.send({ type: DO_UPDATE, id: message.id })),
);
}
});
});
wss.on('error', (error: Error & { code: string }) => {
if (error.code === 'EADDRINUSE') {
console.info(`[HMR] Server already running at ${LOCAL_RELOAD_SOCKET_URL}, skipping reload server initialization`);
} else {
console.error(`[HMR] Failed to start server at ${LOCAL_RELOAD_SOCKET_URL}`);
throw error;
}
});
})();

View File

@@ -0,0 +1,33 @@
import initClient from '../initializers/init-client.js';
(() => {
let pendingReload = false;
initClient({
// @ts-expect-error That's because of the dynamic code loading
id: __HMR_ID,
onUpdate: () => {
// disable reload when tab is hidden
if (document.hidden) {
pendingReload = true;
return;
}
reload();
},
});
// reload
const reload = (): void => {
pendingReload = false;
window.location.reload();
};
// reload when tab is visible
const reloadWhenTabIsVisible = (): void => {
if (!document.hidden && pendingReload) {
reload();
}
};
document.addEventListener('visibilitychange', reloadWhenTabIsVisible);
})();

View File

@@ -0,0 +1,13 @@
import initClient from '../initializers/init-client.js';
(() => {
const reload = () => {
chrome.runtime.reload();
};
initClient({
// @ts-expect-error That's because of the dynamic code loading
id: __HMR_ID,
onUpdate: reload,
});
})();

View File

@@ -0,0 +1,6 @@
import type { WebSocketMessageType } from '../types.js';
export default {
send: (message: WebSocketMessageType): string => JSON.stringify(message),
receive: (serializedMessage: string): WebSocketMessageType => JSON.parse(serializedMessage),
};

View File

@@ -0,0 +1,3 @@
export * from './watch-rebuild-plugin.js';
export * from './watch-public-plugin.js';
export * from './make-entry-point-plugin.js';

View File

@@ -0,0 +1,73 @@
import { IS_FIREFOX } from '@extension/env';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { basename, resolve, sep } from 'node:path';
import type { PluginOption } from 'vite';
/**
* Extract content directory from output directory for Firefox
* @param outputDir
*/
const extractContentDir = (outputDir: string) => {
const parts = outputDir.split(sep);
const distIndex = parts.indexOf('dist');
if (distIndex !== -1 && distIndex < parts.length - 1) {
return parts.slice(distIndex + 1);
}
throw new Error('Output directory does not contain "dist"');
};
const safeWriteFileSync = (path: string, data: string) => {
const folder = path.split(sep).slice(0, -1).join(sep);
if (!existsSync(folder)) {
mkdirSync(folder, { recursive: true });
}
writeFileSync(path, data);
};
/**
* Make an entry point file for content script cache busting
*/
export const makeEntryPointPlugin = (): PluginOption => ({
name: 'make-entry-point-plugin',
generateBundle(options, bundle) {
const outputDir = options.dir;
if (!outputDir) {
throw new Error('Output directory not found');
}
for (const module of Object.values(bundle)) {
const fileName = module.fileName;
const newFileName = fileName.replace('.js', '_dev.js');
switch (module.type) {
case 'asset':
if (fileName.endsWith('.map')) {
const originalFileName = fileName.replace('.map', '');
const replacedSource = String(module.source).replaceAll(originalFileName, newFileName);
module.source = '';
safeWriteFileSync(resolve(outputDir, newFileName), replacedSource);
break;
}
break;
case 'chunk': {
safeWriteFileSync(resolve(outputDir, newFileName), module.code);
const newFileNameBase = basename(newFileName);
if (IS_FIREFOX) {
const contentDirectory = extractContentDir(outputDir);
module.code = `import(browser.runtime.getURL("${contentDirectory}/${newFileNameBase}"));`;
} else {
module.code = `import('./${newFileNameBase}');`;
}
break;
}
}
}
},
});

View File

@@ -0,0 +1,13 @@
import fg from 'fast-glob';
import type { PluginOption } from 'vite';
export const watchPublicPlugin = (): PluginOption => ({
name: 'watch-public-plugin',
async buildStart() {
const files = await fg(['public/**/*']);
for (const file of files) {
this.addWatchFile(file);
}
},
});

View File

@@ -0,0 +1,68 @@
import { BUILD_COMPLETE, LOCAL_RELOAD_SOCKET_URL } from '../consts.js';
import MessageInterpreter from '../interpreter/index.js';
import { WebSocket } from 'ws';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import type { PluginConfigType } from '../types.js';
import type { PluginOption } from 'vite';
const injectionsPath = resolve(import.meta.dirname, '..', 'injections');
const refreshCode = readFileSync(resolve(injectionsPath, 'refresh.js'), 'utf-8');
const reloadCode = readFileSync(resolve(injectionsPath, 'reload.js'), 'utf-8');
export const watchRebuildPlugin = (config: PluginConfigType): PluginOption => {
const { refresh, reload, id: _id, onStart } = config;
const hmrCode = (refresh ? refreshCode : '') + (reload ? reloadCode : '');
let ws: WebSocket | null = null;
const id = _id ?? Math.random().toString(36);
let reconnectTries = 0;
const initializeWebSocket = () => {
ws = new WebSocket(LOCAL_RELOAD_SOCKET_URL);
ws.onopen = () => {
console.log(`[HMR] Connected to dev-server at ${LOCAL_RELOAD_SOCKET_URL}`);
};
ws.onerror = () => {
console.error(`[HMR] Failed to connect server at ${LOCAL_RELOAD_SOCKET_URL}`);
console.warn('Retrying in 3 seconds...');
ws = null;
if (reconnectTries <= 2) {
setTimeout(() => {
reconnectTries++;
initializeWebSocket();
}, 3_000);
} else {
console.error(`[HMR] Cannot establish connection to server at ${LOCAL_RELOAD_SOCKET_URL}`);
}
};
};
return {
name: 'watch-rebuild',
closeBundle() {
onStart?.();
if (!ws) {
initializeWebSocket();
return;
}
/**
* When the build is complete, send a message to the reload server.
* The reload server will send a message to the client to reload or refresh the extension.
*/
ws.send(MessageInterpreter.send({ type: BUILD_COMPLETE, id }));
},
generateBundle(_options, bundle) {
for (const module of Object.values(bundle)) {
if (module.type === 'chunk') {
module.code = `(function() {let __HMR_ID = "${id}";\n` + hmrCode + '\n' + '})();' + '\n' + module.code;
}
}
},
};
};

18
packages/hmr/lib/types.ts Normal file
View File

@@ -0,0 +1,18 @@
import type { BUILD_COMPLETE, DO_UPDATE, DONE_UPDATE } from './consts.js';
type UpdateRequestMessageType = {
type: typeof DO_UPDATE;
id: string;
};
type UpdateCompleteMessageType = { type: typeof DONE_UPDATE };
type BuildCompletionMessageType = { type: typeof BUILD_COMPLETE; id: string };
export type WebSocketMessageType = UpdateCompleteMessageType | UpdateRequestMessageType | BuildCompletionMessageType;
export type PluginConfigType = {
onStart?: () => void;
reload?: boolean;
refresh?: boolean;
id?: string;
};