UNPKG

v8-hot-reload-kit

Version:
250 lines (228 loc) 10.7 kB
import * as CDP from "chrome-remote-interface"; import {Protocol} from "devtools-protocol/types/protocol"; import * as path from "path"; import * as fs from "fs"; import * as chokidar from 'chokidar'; const MAX_SCRIPTS_CACHE_SIZE = 10000000; export class ScriptSourcesMgr { private _client: CDP.Client; private _scriptsDB = new Map<string, string>(); private _trace: (msg: string) => void; private _puerts: boolean; private _localRoot:string[]; private _remoteRoot:string[]; private _connecting:boolean = false; private _host:string; private _port:number; private _updateTask:{pathname: string, content: string}; private _watcher: chokidar.FSWatcher; private _forceSrcType: 'cjs'|'mjs'|undefined; private _ignorePattern: RegExp; private _partFolders:string[]; constructor(params?: Partial<{ trace: boolean, localRoot:string[], remoteRoot:string[], forceSrcType: 'cjs'|'mjs'|undefined, ignorePattern: string, partFolders: string[]}>) { const { trace, localRoot, remoteRoot, forceSrcType, ignorePattern } = params ?? {}; this._trace = trace ? console.log : () => {}; this._localRoot = localRoot || []; this._remoteRoot = remoteRoot; this._forceSrcType = forceSrcType; this._ignorePattern = ignorePattern ? new RegExp(ignorePattern) : undefined; this._partFolders = params?.partFolders || []; } public async connect(host: string, port?: number) { this._host = host; this._port = port; if (this._client) { throw new Error("connected or connecting"); } if (this._connecting) { console.warn(`${host}:${port} is connecting, skipped`); } console.log(`connecting ${host}:${port} ...`); this._connecting = true; try { this._watcher = new chokidar.FSWatcher({awaitWriteFinish: {stabilityThreshold: 200}}); const local = true; const cfg = { host, port, local }; let version = await CDP.Version(cfg); const isNode = version.Browser?.includes("node.js"); this._puerts = version.Browser?.includes("Puerts"); this._trace(`is node: ${isNode}, is puerts: ${this._puerts}`); let client = await CDP(cfg); //let client = await CDP({ target: "ws://127.0.0.1:8080/bc2a9035-bfa9-46f7-8ef2-8f304483eeae"}); if (isNode) { // for node --inspect-brk client.Debugger.paused(() => { client.Debugger.resume(); }); } const { Runtime, Debugger, Profiler } = client; Debugger.on("scriptParsed", this._onScriptParsed); Debugger.on("scriptFailedToParse", this._onScriptFailedToParse); await Runtime.enable(); await Debugger.enable({ "maxScriptsCacheSize": MAX_SCRIPTS_CACHE_SIZE }); await Debugger.setSkipAllPauses({skip:true}); //await Debugger.setPauseOnExceptions({state:"none"}); //await Debugger.setAsyncCallStackDepth({maxDepth:32}) if (isNode) { await Profiler.enable(); // nodejs要打开这个才能live-edit生效 //await Debugger.setBlackboxPatterns({patterns: ["/node_modules/|/bower_components/"]}); // for node --inspect-brk await client.Runtime.runIfWaitingForDebugger(); } client.on("disconnect", this._onDisconnect); this._client = client; console.log(`${host}:${port} connented.`); if (this._updateTask) { try { await this.reload(this._updateTask.pathname, this._updateTask.content); } catch {}; this.close(); } else { this._watcher.on('change', (filePath) => { let fullFilePath = `${path.resolve(filePath)}`; if (fs.existsSync(fullFilePath) && !this._ignorePattern?.test(filePath)) { this.reload(fullFilePath, fs.readFileSync(filePath).toString()); } }); } } catch (err) { console.error(`CONNECT_FAIL: ${err}`); this._client = undefined; this._watcher = undefined; this.retryConnect(2); await this.close(); } this._connecting = false; } public setUpdateTask(pathname: string, content: string):void { this._updateTask = {pathname, content}; } private retryConnect(delay:number) { console.log(`retry connect after ${delay} seconds`); setTimeout(()=>this.connect(this._host, this._port), delay * 1000); } public async reload(pathname: string, source: string): Promise<void> { const pathNormalized = path.normalize(pathname); if (!this._client) { console.warn(`remote not connected, not ready for ${pathNormalized}, retry later!`); } if (this._scriptsDB.has(pathNormalized)) { const scriptId = this._scriptsDB.get(pathNormalized); this._trace(`reloading ${pathNormalized}, scriptId: ${scriptId}`); try { const updateSource = (this._puerts && (this.isCJS(pathNormalized))) ? `(function (exports, require, module, __filename, __dirname) { ${source}\n});` : source; const {scriptSource} = await this._client.Debugger.getScriptSource({scriptId}); if (scriptSource == updateSource) { this._trace(`source not changed, skip ${pathNormalized}`); return; } //this._trace(`old src: ${scriptSource}`); //this._trace(`new src: ${updateSource}`); const rsp = await this._client.Debugger.setScriptSource({scriptId: scriptId, scriptSource: updateSource}); this._trace(`reload ${pathNormalized}, scriptId: ${scriptId}, response:${JSON.stringify(rsp)}`); } catch (err) { console.error(`RELOAD_SOURCE_FAIL: ${err}, script(${scriptId}):${pathNormalized}`); } } else { this._trace(`can not find scriptId for ${pathNormalized}.`); } } private isCJS(url: string) { if (this._forceSrcType) { return this._forceSrcType === 'cjs'; } return url.endsWith(".js") && !url.startsWith("http:"); } private convertUrlToLocalPath(url: string, partFolders: string[] = []): [boolean, string] { let pathname = url; let concatLocalRoot = false; for (let i = 0; i < this._remoteRoot.length; i++) { const remoteRoot = this._remoteRoot[i]; const localRoot = this._localRoot[i]; if (pathname.startsWith(remoteRoot)) { if (partFolders.length > 0) { for (let j = 0; j < partFolders.length; j++) { const partFolder = partFolders[j]; const pathnameByPartFolder = pathname.replace(remoteRoot, path.join(localRoot, partFolder)); const pathNormalized = path.normalize(pathnameByPartFolder); if (fs.existsSync(pathNormalized)) { // 文件存在 可以返回该文件 return [true, pathnameByPartFolder]; } } } pathname = pathname.replace(remoteRoot, localRoot); concatLocalRoot = true; break; } } return [concatLocalRoot, pathname]; } private setScriptInfo(scriptId: string, url: string) { let pathname = url; let isHttp = false; if (!this._puerts) { try { const parseUrl = new URL(url); if (parseUrl.protocol == "node:") { return; } pathname = parseUrl.pathname; if (["http:", "https:"].includes(parseUrl.protocol)) { isHttp = true; } else if (process.platform == "win32" && pathname.startsWith("/")) { pathname = pathname.substring(1); } } catch { console.warn(``) return; } } const [concatLocalRoot, localPathname] = this.convertUrlToLocalPath(pathname, this._partFolders); // if (this._remoteRoot && this._remoteRoot != this._localRoot) { // if(pathname.startsWith(this._remoteRoot)) { // pathname = pathname.replace(this._remoteRoot, this._localRoot); // concatLocalRoot = true; // } // } pathname = localPathname; if (isHttp && !concatLocalRoot) { pathname = path.join(this._localRoot[0], pathname); } // console.log(`url:${url}, path:${path}`); const pathNormalized = path.normalize(pathname); this._scriptsDB.set(scriptId, pathNormalized); this._scriptsDB.set(pathNormalized, scriptId); if (!fs.existsSync(pathNormalized)) { console.warn(`${pathNormalized} not exist! scriptId: ${scriptId}, url: ${url}`); } else { this._watcher.add(pathNormalized); console.log(`${pathNormalized} watched, scriptId: ${scriptId}, url: ${url}`); } } private _onScriptParsed = (params: Protocol.Debugger.ScriptParsedEvent) => { this.setScriptInfo(params.scriptId, params.url); } private _onScriptFailedToParse = (params: Protocol.Debugger.ScriptFailedToParseEvent) => { this.setScriptInfo(params.scriptId, params.url); } private _onDisconnect = () => { this._trace('>>> disconnected!'); this._watcher.close(); this._watcher = undefined; this._client = undefined; this.retryConnect(1); } public async close() { if (this._client) { this._connecting = false; this._watcher.close(); this._client = undefined; let client = this._client; this._client = undefined; this._trace('closing client...'); await client.close(); } } }