uweb1075: global redirection works for AI sites and login
[uweb.git] / misc / ebrowser / webview.js
blob24a6b4b1d7c29b70e9e6175f7cb9f73901200973
1 /* Copyright (C) 2024 Richard Hao Cao
2 Ebrowser is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4 Ebrowser is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
6 You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
7 */
8 const {
9 app, BrowserWindow, Menu, shell, clipboard,
10 session, protocol, dialog, ipcMain
11 } = require('electron')
12 let win;
14 if(!app.requestSingleInstanceLock())
15 app.quit()
16 else {
17 app.on('ready', createWindow);
18 app.on('second-instance', (event, args, cwd) => {
19 if (win) {
20 if (win.isMinimized()) {
21 win.restore()
23 win.show()
24 win.focus()
25 cmdlineProcess(args,cwd,1);
26 }else
27 createWindow();
30 Menu.setApplicationMenu(null);
31 const fs = require('fs');
32 const path = require('path')
33 const https = require('https');
34 const url = require('url');
35 var translateRes;
37 let langs = app.getPreferredSystemLanguages();
38 if(langs.length==0 || langs[0].startsWith('en'))
39 topMenu();
40 else
41 initTranslateRes(langs[0]);
44 var repositoryurl = "https://gitlab.com/jamesfengcao/uweb/-/raw/master/misc/ebrowser/";
45 const readline = require('readline');
46 const process = require('process')
47 var gredirects = [];
48 var gredirect;
49 var redirects;
50 var bRedirect = true;
51 var bJS = true;
52 var bForwardCookie = true;
53 var proxies = {};
54 var proxy;
55 var useragents = {};
56 var downloadMenus; //[]
57 var selectMenus = [];
58 var defaultUA;
60 let sys = "X11; Linux x86_64";
61 if (process.platform === "win32")
62 sys = "Window NT 10.0; Win64; x64";
63 else if (process.platform === "darwin")
64 sys = "Macintosh; Intel Mac OS X 10_15_7";
65 defaultUA =
66 `Mozilla/5.0 (${sys}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/` +
67 process.versions.chrome +" Safari/537.36";
69 app.userAgentFallback = defaultUA;
71 fs.readFile(path.join(__dirname,'redirect.json'), 'utf8', (err, jsonString) => {
72 if (err) return;
73 try {
74 redirects = JSON.parse(jsonString);
75 } catch (e){console.log(e)}
76 });
78 async function createWindow () {
79 try {
80 let json = await fs.promises.readFile(path.join(__dirname,'uas.json'), 'utf8');
81 useragents = JSON.parse(json);
82 } catch (e){console.log(e)}
84 protocol.handle("i",(req)=>{return null;});
85 await (async ()=>{
86 try{
87 const readInterface = readline.createInterface ({
88 input: fs.createReadStream (path.join(__dirname,'config'), 'utf8'),
89 });
91 for await (const line of readInterface) {
92 addrCommand(line);
94 }catch(e){console.log(e);}
95 })();
97 win = new BrowserWindow(
98 {width: 800, height: 600,autoHideMenuBar: true,
99 webPreferences: {
100 nodeIntegration: true,
101 contextIsolation: false,
102 webviewTag: true,
103 }});
104 win.setMenuBarVisibility(false);
105 win.on('closed', function () {
106 win = null
109 win.loadFile('index.html');
110 fs.readFile(path.join(__dirname,'gredirect.json'), 'utf8', (err, jsonString) => {
111 if (err) return;
112 try {
113 gredirects = JSON.parse(jsonString);
114 } catch (e){console.log(e)}
117 fs.readFile(path.join(__dirname,'proxy.json'), 'utf8', (err, jsonString) => {
118 if (err) return;
119 try {
120 proxies = JSON.parse(jsonString);
121 let match = jsonString.match(/"([^"]+)"/);
122 if(match)
123 proxy = proxies[match[1]];
124 } catch (e){console.log(e)}
127 cmdlineProcess(process.argv, process.cwd(), 0);
128 //app.commandLine.appendSwitch ('trace-warnings');
130 fs.readFile(path.join(__dirname,'download.json'), 'utf8', (err, jsonStr) => {
131 if (err) return;
132 try {
133 downloadMenus = JSON.parse(jsonStr);
134 }catch (e){console.log(e)}
137 fs.readFile(path.join(__dirname,'select.json'), 'utf8', (err, jsonStr) => {
138 if (err) return;
139 try {
140 selectMenus = JSON.parse(jsonStr);
141 }catch (e){console.log(e)}
144 win.webContents.on('page-title-updated',(event,cmd)=>{
145 addrCommand(cmd);
148 session.defaultSession.on("will-download", async (e, item) => {
149 //item.setSavePath(save)
150 if(!downloadMenus) return;
151 let menuT = downloadContextMenuTemp(item.getURL());
152 let button = await promiseContextMenu(menuT);
153 if(-1===button) return;
154 e.preventDefault();
157 win.webContents.on('console-message',cbConsoleMsg);
160 app.on('window-all-closed', function () {
161 app.quit()
164 app.on('activate', function () {
165 if (win === null) {
166 createWindow()
170 app.on('will-quit', () => {
173 app.on ('web-contents-created', (event, contents) => {
174 if (contents.getType () === 'webview') {
175 contents.setWindowOpenHandler(cbWindowOpenHandler);
176 contents.on('context-menu',onContextMenu);
177 contents.on('page-title-updated',cbTitleUpdate);
178 contents.session.webRequest.onBeforeRequest(interceptRequest);
182 ipcMain.on('command', (event, cmd) => {
183 addrCommand(cmd);
186 function addrCommand(cmd){
187 if(cmd.length<3) return;
188 let c0 = cmd.charCodeAt(0);
189 switch(c0){
190 case 33://"!"
191 bangcommand(q,1);
192 return;
193 case 58: //':'
194 args = cmd.substring(1).split(/\s+/);
195 switch(args[0]){
196 case "cert":
197 if(args.length==1)
198 session.defaultSession.setCertificateVerifyProc((request, callback) => {
199 callback(0);
201 else
202 session.defaultSession.setCertificateVerifyProc(null);
203 return;
204 case "clear":
205 if(args.length==1){
206 session.defaultSession.clearData();
207 return;
209 switch(args[1]){
210 case "cache":
211 session.defaultSession.clearCache();
212 return;
213 case "dns":
214 session.defaultSession.clearHostResolverCache();
215 return;
216 case "storage":
217 session.defaultSession.clearStorageData();
218 return;
219 default:
220 try {
221 let opts = JSON.parse(args.slice(1).join(""));
222 session.defaultSession.clearData(opts);
223 }catch(e){console.log(e)}
225 return;
226 case "exit":
227 win.close();
228 return;
229 case "ext":
230 session.defaultSession.loadExtension(args[1]);
231 return;
232 case "gr":
233 if(args.length<2) {
234 gredirect_enable(0);
235 return;
237 let i = parseInt(args[1]);
238 if(i>=0 && i<gredirects.length)
239 gredirect_enable(i);
240 else
241 gredirect_disable();
242 return;
243 case "js"://execute js
244 eval(cmd.slice(4));
245 return;
246 case "nc":
247 bForwardCookie = false;
248 msgbox_info("Cookie forwarding disabled");
249 return;
250 case "uc":
251 if(bForwardCookie) {
252 msgbox_info("Cookie forwarding enabled for global redirection");
253 return;
255 forwardCookie();
256 return;
257 case "np":
258 session.defaultSession.setProxy ({mode:"direct"});
259 bRedirect = true;
260 return;
261 case "up":
262 if(args.length>1)
263 proxy = proxies[args[1]]; //retrieve proxy
264 if(proxy){
265 session.defaultSession.setProxy(proxy)
266 .then(() => {gredirect_disable()})
267 .catch((error) => {
268 console.error('Failed to set proxy:', error);
271 return;
272 case "nr":
273 bRedirect = false; return;
274 case "ur":
275 bRedirect = true; return;
276 case "ua":
277 if(args.length==2){
278 let ua = useragents[args[1]];
279 if(ua)
280 session.defaultSession.setUserAgent(ua);
281 }else
282 session.defaultSession.setUserAgent(defaultUA);
283 return;
284 case "update":
285 let updateurl;
286 if(1==args.length)
287 updateApp(repositoryurl);
288 else {
289 filename = args[1];
290 let iSlash = filename.lastIndexOf('/');
291 if(iSlash>0){
292 let folder = path.join(__dirname,filename.substring(0,iSlash));
293 fs.mkdirSync(folder,{ recursive: true });
295 fetch2file(repositoryurl,filename);
297 return;
302 function gredirect_disable(){
303 if(gredirect){
304 gredirect=null;
305 unregisterHandler();
307 bRedirect = true;
309 function gredirect_enable(i){
310 if(i>=gredirects.length) return;
311 if(!gredirect) registerHandler();
312 gredirect=gredirects[i];
315 function cbConsoleMsg(e, level, msg, line, sourceid){
316 console.log(line);
317 console.log(sourceid);
318 console.log(msg);
321 function interceptRequest(details, callback){
322 let url = details.url;
323 if(58===url.charCodeAt(1) || (!bJS && url.endsWith(".js"))){
324 callback({ cancel: true });
325 return;
327 do {
328 if(gredirect || !bRedirect ||(details.resourceType !== 'mainFrame' &&
329 details.resourceType !== 'subFrame')) break;
330 let oURL = new URL(url);
331 let domain = oURL.hostname;
332 let newUrl;
333 try{
334 let newDomain = redirects[domain];
335 if(!newDomain) break;
336 newUrl = "https://"+newDomain+oURL.pathname+oURL.search+oURL.hash;
337 }catch(e){break;}
338 callback({ cancel: false, redirectURL: newUrl });
339 return;
340 }while(false);
341 callback({ cancel: false });
344 function cbWindowOpenHandler(details){
345 let url = details.url;
346 let js = "newTab();tabs.children[tabs.children.length-1].src='"+
347 url+"';";
348 switch(details.disposition){
349 case "foreground-tab":
350 case "new-window":
351 js = js + "switchTab(tabs.children.length-1)";
353 win.webContents.executeJavaScript(js,false);
354 return { action: "deny" };
356 function cbTitleUpdate(event,title){
357 win.setTitle(title);
359 function menuSelection(menuTemplate, text){
360 for(let i=0; i<selectMenus.length-1;i++){
361 menuTemplate.push({
362 label: selectMenus[i],
363 click: () => {
364 let cmd = selectMenus[i+1].replace('%s',text);
365 let js = `handleQueries(\`${cmd}\`)`;
366 win.webContents.executeJavaScript(js,false);
371 function menuDownload(menuTemplate, labelprefix, linkUrl){
372 for(let i=0; i<downloadMenus.length-1;i++){
373 menuTemplate.push({
374 label: labelprefix+downloadMenus[i],
375 click: () => {
376 let cmd = downloadMenus[i+1].replace('%u',linkUrl);
377 let js = `handleQueries(\`${cmd}\`)`;
378 win.webContents.executeJavaScript(js,false);
383 function menuArray(labelprefix, linkUrl){
384 let menuTemplate = [
386 label: labelprefix+translate('Open'),
387 click: () => {
388 shell.openExternal(linkUrl);
392 label: labelprefix+translate('Copy'),
393 click: () => {
394 clipboard.writeText(linkUrl);
398 label: labelprefix+translate('Download'),
399 click: () => {
400 win.webContents.downloadURL(linkUrl);
404 if(downloadMenus)
405 menuDownload(menuTemplate, labelprefix, linkUrl);
406 return menuTemplate;
409 function onContextMenu(event, params){
410 let url = params.linkURL;
411 let mTemplate = [];
412 if (url) {
413 mTemplate.push({label:url,enabled:false});
414 mTemplate.push.apply(mTemplate,menuArray("",url));
415 if((url=params.srcURL))
416 mTemplate.push.apply(mTemplate,menuArray("src: ",url));
417 }else if((url=params.srcURL)){
418 mTemplate.push({label:url,enabled:false});
419 mTemplate.push.apply(mTemplate,menuArray("src: ",url));
420 }else if((url=params.selectionText)){
421 menuSelection(mTemplate,url);
422 }else
423 return;
425 const contextMenu = Menu.buildFromTemplate(mTemplate);
426 contextMenu.popup();
429 async function topMenu(){
430 const menuTemplate = [];
431 try {
432 let json = await fs.promises.readFile(path.join(__dirname,'menu.json'), 'utf8');
433 let menus = JSON.parse(json);
434 if(menus.length>1){
435 let submenu = [];
436 for(let i=0;i<menus.length-1; i=i+2){
437 let cmd = menus[i+1];
438 let js = `handleQueries("${cmd}")`;
439 submenu.push({
440 label: menus[i], click: ()=>{
441 win.webContents.executeJavaScript(js,false);
442 }});
444 menuTemplate.push({
445 label: translate('Tools'),
446 submenu: submenu,
449 }catch(e){console.log(e)}
450 menuTemplate.push(
452 label: translate('Edit'),
453 submenu: [
454 { label: translate('Config folder'), click: ()=>{
455 shell.openPath(__dirname);
460 label: translate('Help'),
461 submenu: [
462 { label: translate('Check for updates'), click: ()=>{
463 addrCommand(":update");
465 { label: translate('Help'), accelerator: 'F1', click: ()=>{
466 help();
468 { label: translate('Stop'), accelerator: 'Ctrl+C', click: ()=>{
469 let js="tabs.children[iTab].stop()"
470 win.webContents.executeJavaScript(js,false)
472 { label: translate('getURL'), accelerator: 'Ctrl+G', click: ()=>{
473 let js="{let q=document.forms[0].q;q.focus();q.value=tabs.children[iTab].getURL();getWinTitle()}"
474 win.webContents.executeJavaScript(js,false).then((r)=>{
475 win.setTitle(r);
478 { label: translate('Select'), accelerator: 'Ctrl+L', click:()=>{
479 win.webContents.executeJavaScript("document.forms[0].q.select()",false);
481 { label: translate('New Tab'), accelerator: 'Ctrl+T', click:()=>{
482 let js = "newTab();document.forms[0].q.select();switchTab(tabs.children.length-1)";
483 win.webContents.executeJavaScript(js,false);
485 { label: translate('Restore Tab'), accelerator: 'Ctrl+Shift+T', click:()=>{
486 let js = "{let u=closedUrls.pop();if(u){newTab();switchTab(tabs.children.length-1);tabs.children[iTab].src=u}}";
487 win.webContents.executeJavaScript(js,false);
489 { label: translate('No redirect'), accelerator: 'Ctrl+R', click: ()=>{
490 gredirect_disable();
492 { label: translate('Redirect'), accelerator: 'Ctrl+Shift+R', click: ()=>{
493 gredirect_enable(0);
495 { label: translate('Close tab'), accelerator: 'Ctrl+W', click: ()=>{
496 win.webContents.executeJavaScript("tabClose()",false).then((r)=>{
497 if(""===r) win.close();
498 else win.setTitle(r);
501 { label: translate('Next Tab'), accelerator: 'Ctrl+Tab', click: ()=>{
502 let js="tabInc(1);getWinTitle()";
503 win.webContents.executeJavaScript(js,false).then((r)=>{
504 win.setTitle(r);
507 { label: translate('Previous Tab'), accelerator: 'Ctrl+Shift+Tab', click: ()=>{
508 let js="tabDec(-1);getWinTitle()";
509 win.webContents.executeJavaScript(js,false).then((r)=>{
510 win.setTitle(r);
513 { label: translate('Go backward'), accelerator: 'Alt+Left', click: ()=>{
514 let js="tabs.children[iTab].goBack()";
515 win.webContents.executeJavaScript(js,false);
517 { label: translate('Go forward'), accelerator: 'Alt+Right', click: ()=>{
518 let js="tabs.children[iTab].goForward()";
519 win.webContents.executeJavaScript(js,false);
521 { label: translate('Zoom in'), accelerator: 'Ctrl+Shift+=', click: ()=>{
522 let js="{let t=tabs.children[iTab];let s=t.getZoomFactor()*1.2;t.setZoomFactor(s)}";
523 win.webContents.executeJavaScript(js,false);
525 { label: translate('Zoom out'), accelerator: 'Ctrl+-', click: ()=>{
526 let js="{let t=tabs.children[iTab];let s=t.getZoomFactor()/1.2;t.setZoomFactor(s)}";
527 win.webContents.executeJavaScript(js,false);
529 { label: translate('Default zoom'), accelerator: 'Ctrl+0', click: ()=>{
530 let js="tabs.children[iTab].setZoomFactor(1)";
531 win.webContents.executeJavaScript(js,false);
533 { label: translate('No focus'), accelerator: 'Esc', click: ()=>{
534 let js = `{let e=document.activeElement;
535 if(e)e.blur();try{tabs.children[iTab].stopFindInPage('clearSelection')}catch(er){}}`;
536 win.webContents.executeJavaScript(js,false);
538 { label: translate('Reload'), accelerator: 'F5', click: ()=>{
539 win.webContents.executeJavaScript("tabs.children[iTab].reload()",false);
541 { label: translate('Devtools'), accelerator: 'F12', click: ()=>{
542 let js = "try{tabs.children[iTab].openDevTools()}catch(e){console.log(e)}";
543 win.webContents.executeJavaScript(js,false);
549 const menu = Menu.buildFromTemplate(menuTemplate);
550 Menu.setApplicationMenu(menu);
553 function cmdlineProcess(argv,cwd,extra){
554 let i1st = 2+extra; //index for the first query item
555 if(argv.length>i1st){
556 if(i1st+1==argv.length){//local file
557 let fname = path.join(cwd, argv[i1st]);
558 if(fs.existsSync(fname)){
559 let js = "tabs.children[iTab].src='file://"+fname+"'";
560 win.webContents.executeJavaScript(js,false);
561 win.setTitle(argv[i1st]);
562 return;
565 let url=argv.slice(i1st).join(" ");
566 win.webContents.executeJavaScript("handleQuery(`"+url+"`)",false);
567 win.setTitle(url);
571 async function cbScheme_redir(req){
572 if(!gredirect) return null;
573 let oUrl = req.url;
574 let newurl = gredirect+oUrl;
575 const parsedUrl = url.parse(newurl);
576 let headers = new Headers();
577 for (var pair of req.headers.entries())
578 headers.set(pair[0],pair[1]);
579 if(bForwardCookie){
580 let cookies = await session.defaultSession.cookies.get({url: oUrl});
581 let cookieS = cookies.map (cookie => cookie.name + '=' + cookie.value ).join(';');
582 headers.set('Cookie',cookieS);
584 //missing referer header
585 //headers.set('referer',);
586 const options = {
587 hostname: parsedUrl.hostname,
588 port: parsedUrl.port,
589 path: parsedUrl.path,
590 method: req.method,
591 headers: headers
593 return new Promise(async (resolve, reject) => {
594 const nreq = https.request(options, (res) => {
595 let body = [];
596 res.on('data', (chunk) => {
597 body.push(chunk);
600 res.on('end', () => {
601 try {
602 body = Buffer.concat(body);
603 const response = new Response(body, res);
604 resolve(response);
605 } catch (e) {
606 reject(e);
610 nreq.on('error', (err) => {
611 reject(err);
613 if (req.body){
614 try {
615 const reader = req.body.getReader();
616 do {
617 const { done, value } = await reader.read();
618 if (done) {
619 nreq.end();
620 break;
622 nreq.write(value);
623 console.log(headers);
624 console.log(new TextDecoder("iso-8859-1").decode(value));
625 }while (true);
626 }catch(e){reject(e)}
627 }else
628 nreq.end();
632 function registerHandler(){
633 protocol.handle("http",cbScheme_redir);
634 protocol.handle("https",cbScheme_redir);
635 protocol.handle("ws",cbScheme_redir);
636 protocol.handle("wss",cbScheme_redir);
638 function unregisterHandler(){
639 protocol.unhandle("http",cbScheme_redir);
640 protocol.unhandle("https",cbScheme_redir);
641 protocol.unhandle("ws",cbScheme_redir);
642 protocol.unhandle("wss",cbScheme_redir);
645 function forwardCookie(){
646 const choice = dialog.showMessageBoxSync(null, {
647 type: 'warning',
648 title: 'Confirm cookie forwarding with global redirection',
649 message: 'Cookies are used to access your account. Forwarding cookies is vulnerable to global redirection server, proceed to enable cookie forwarding with global redirection?',
650 buttons: ['No','Yes']
652 if(1===choice) bForwardCookie=true;
654 function msgbox_info(msg){
655 dialog.showMessageBoxSync(null, {
656 type: 'info',
657 title: msg,
658 message: msg,
659 buttons: ['OK']
663 async function updateApp(url){//url must ending with "/"
664 let msg;
665 do {
666 try {
667 let res = await fetch(url+"package.json");
668 let packageS = await res.text();
669 {//the last part of version string is the version number, must keep increasing
670 let head = packageS.slice(2,40);
671 let iV = head.indexOf("version");
672 if(iV<0) {
673 msg = "remote package.json corrupted"
674 break;
676 iV = iV + 10;
677 let iE = head.indexOf('"',iV+4);
678 let iS = head.lastIndexOf('.',iE-1);
679 let nLatestVer = parseInt(head.substring(iS+1,iE));
681 let ver = app.getVersion();
682 iS = ver.lastIndexOf('.');
683 let nVer = parseInt(ver.substring(iS+1));
684 if(nVer>=nLatestVer){
685 msg = `Current version ${ver} is already up to date`;
686 break;
688 const choice = dialog.showMessageBoxSync(null, {
689 type: 'warning',
690 title: `Update from ${url}`,
691 message: `Proceed to update from ${ver} to ${head.substring(iV,iE)}?`,
692 buttons: ['YES','NO']
694 if(1===choice) return;
697 writeFile("package.json", packageS);
699 fetch2file(url,"webview.js");
700 fetch2file(url,"index.html");
701 msg = "Update completed";
702 }catch(e){
703 msg = "Fail to update"
705 }while(false);
706 dialog.showMessageBoxSync(null, {
707 type: 'info',
708 title: `Update from ${url}`,
709 message: msg,
710 buttons: ['OK']
714 async function fetch2file(urlFolder, filename, bOverwritten=true){
715 let pathname=path.join(__dirname,filename);
716 if(!bOverwritten && fs.existsSync(pathname)) return;
717 let res = await fetch(urlFolder+filename);
718 let str = await res.text();
719 writeFile(pathname, str);
722 async function writeFile(filename, str){
723 let pathname=filename+".new";
724 fs.writeFile(pathname, str, (err) => {
725 if(err) throw "Fail to write";
726 fs.rename(pathname,filename,(e1)=>{
727 if(e1) throw "Fail to rename";
732 function help(){
733 const readme = "README.md";
734 const htmlFN = path.join(__dirname,readme);
735 let js=`{let t=tabs.children[iTab];t.dataset.jsonce=BML_md;t.src="file://${htmlFN}"}`;
736 win.webContents.executeJavaScript(js,false)
739 function downloadContextMenuTemp(url){
740 let mTemplate =
741 [{label:url,enabled:false},
742 {label: translate('Download')},
744 label: translate('Copy'),
745 click: () => {
746 clipboard.writeText(url);
750 menuDownload(mTemplate, "", url);
751 return mTemplate;
753 async function initTranslateRes(lang){
754 let basename=path.join(__dirname,"translate.");
755 let fname = basename+lang;
756 if(!fs.existsSync(fname))
757 fname = basename+lang.slice(0,2);
758 try {
759 let json = await fs.promises.readFile(fname,'utf8');
760 translateRes = JSON.parse(json);
761 } catch (e){}
762 topMenu();
765 function translate(str){
766 let result;
767 if(translateRes && (result=translateRes[str])) return result;
768 return str;
771 function promiseContextMenu(menuTemplate) {
772 return new Promise((resolve, reject) => {
773 menuTemplate[1].click = () => resolve(-1);
774 const menu = Menu.buildFromTemplate(menuTemplate);
775 menu.on('menu-will-close', () => resolve(-2));
776 menu.popup();
780 function httpReq(url, method, filePath){
781 fs.readFile(filePath, (err, fileData) => {
782 if (err) {
783 console.error(`Error reading file: ${err.message}`);
784 return;
787 let opts = {
788 method: method,
789 headers: {
790 "Content-Type":'application/octet-stream',
792 body: fileData,
794 fetch(url,opts);
798 function bangcommand(q,offset){
799 let iS = q.indexOf(' ',offset);
800 if(iS<0) iS=q.length;
801 let fname = q.substring(offset,iS);
802 let fpath = path.join(__dirname,fname+'.js');
803 if (fs.existsSync(fpath)) {
804 fs.readFile(fpath, 'utf8',(err, js)=>{
805 if (err) {
806 console.log(err);
807 return;
809 const prefix = "(function(){";
810 const postfix = "})(`";
811 const end ="`)";
812 const fjs = `${prefix}${js}${postfix}${q}${end}`;
813 eval(fjs);