• Есть ли способ привязать деструктурированный импорт к свойствам модуля (концепция live bindings) в commonjs без esm?

    @EDiPensier Автор вопроса
    В конце концов пришёл к гибридному решению вопроса - совмещение import'ов для подключения изменяемых свойств и require для противоположных в связке с вебпаком.

    Однако такой способ совершенно не подходит, если требуется запустить неупакованное серверное приложение(без учёта специфики сабжа). Соответственно, финальным аккордом стало дописывание костыля, который временно преобразует импорт commonjs'а в esm , если видит на строке выше комментарий //useLiveBindings:

    example.js:

    //useLiveBindings
    const { a, b } = require("./c");
    //Преобразуется в import { a, b } from "./c"
    
    //Не будет преобразовано
    const { d } = require("./e");

    precompileLiveBindings.js:

    const nodePath = require("path");
    const fs = require("fs");
    
    const regexpToFindLB = /\/\/useLiveBinding(?:\r|\r\n|\n)(?:const|let|var)\s+\{([\s\S]+?)\}\s*=\s*require\s*\(\s*((["'`])[\s\S]+?\3)\s*\)\s*;?/g;
    
    const tempFilePathsStore = {};
    
    const precompileLB = nowDir => {
    	
    	const filesAndDirs = fs.readdirSync(nowDir);
    	
    	for(let i = 0; i < filesAndDirs.length; ++i) {
    		
    		const object = nodePath.join( nowDir, filesAndDirs[i] );
    		
    		if( fs.lstatSync( object ).isDirectory() ) {
    			
    			precompileLB(object);
    			
    		} else {
    			
    			let content = fs.readFileSync( object ).toString();
    			
    			const match = !!content.match(regexpToFindLB);
    			
    			if(match) {
    				
    				tempFilePathsStore[object] = content;
    				
    				content = content.replace(regexpToFindLB, `import {$1} from $2`);
    				
    				fs.writeFileSync( object, content );
    				
    			}
    			
    		}
    		
    	}
    	
    };
    
    const restoreOriginals = () => {
    	
    	for(const path in tempFilePathsStore) {
    		
    		fs.writeFileSync( path, tempFilePathsStore[path] );
    		
    	}
    	
    };
    
    module.exports = {
    	
    	precompileLB: () => precompileLB(__dirname),
    	
    	restoreOriginals,
    	
    };

    webpack.js:

    const webpack = require("webpack");
    const { precompileLB, restoreOriginals } = require("./_precompileLB");
    const configs = { ... };
    
    const pack = () => {
    	
    	precompileLB();
    	
    	webpack(configs, (err, stats) => {
    		
    		if (!err) err = stats.toJson().errors[0];
    		
    		if (err) {
    			
    			throw new Error(err);
    			
    		}
    		
    		console.log( stats.toString( { colors: true } ) );
    		
    		restoreOriginals();
    		
    	});
    	
    };
    
    pack();
    Ответ написан
    Комментировать
  • Как получить исходный ответ сайта в нужной кодировке?

    @EDiPensier Автор вопроса
    В общем, проблема была именно с Content-Type, который отдавался в text/plain.

    Самый схожий случай с близлежащей проблемой описан здесь, а популярные методы решения тут (не для вышеприведённого).

    Первое что пришло в голову после обнаружения корня зла - попытка модифицировать ответ на text/html, с которым не возникало подобного, по этому варианту, параллельно решая дополнительные задачи. Всё-бы ничего, но только к этому варианту нужен собственный body для корректной работы. Если же передавать пустой или попытаться получить с помощью Fetch.getResponseBody (в рамках описанного варианта), то подключение не пройдёт успешно (страница висит в ожидании ответа), а единственная ошибка, которая вероятнее всего прилетит - таймаут. Описанный ниже метод ещё более костыльный, так как отправляет запрос через request, от которого я изначально уходил.

    В конечном итоге посчастливилось столкнуться с тем, что нужно и вникать в суть проблемы более глубже. Собственно, рекомендуют использовать Fetch.getResponseBody более нестандартно, что может вызвать конфликты с плагинами ( кооперация появилась относительно недавно, но это лишь часть решения, кроме того до сих пор не интегрировано в популярный puppeteer... ), ибо puppeteer регулирует Fetch.enable самостоятельно для другой сессии клиента.

    Выложу в данном ответе 2 решения.

    Первое, используя единичную стратегию, через Chrome Devtools Protocol -> Fetch с последующим отключением:

    const URI = "<someUri>";
    
    //...launch puppeteer
    
    const next = async (browser) => {
    
    	let realResponse = null;
    	
    	const page = await browser.newPage()
    		
    	const client = await page.target().createCDPSession();
    
    	await client.send("Fetch.enable", {
    		patterns: [{ requestStage: "Response" }]
    	});
    
    	const requestPausedHandler = async (event) => {
    
    		const { url } = event.request;
    		
    		if(url == URI) {
    			
    			const { requestId } = event;
    
    			const responseCdp = await client.send("Fetch.getResponseBody", { requestId });
    			
    			realResponse = responseCdp.base64Encoded
    					? Buffer.from(responseCdp.body, "base64").toString("utf-8")
    					: responseCdp.body;
    			
    			//При правильном регулировании - возможен вызов закомментированного, но здесь принцип once.
    			//await client.send("Fetch.continueRequest", { requestId });
    			
    			//Поэтому следует использовать сразу использовать Fetch.disable для созданной сессии, дабы отключиться от фазы Response (отдаёт ответ параллельно).
    			await client.send("Fetch.disable");
    
    		}
    		
    	};
    
    	client.on("Fetch.requestPaused", requestPausedHandler);
    
    	page.goto(uri).then(async (response) => {
    		
    		client.off("Fetch.requestPaused", requestPausedHandler);
    		
    		console.log(realResponse);
    		
    	});
    	
    }

    И второе, которое описал в конце вопроса. Заключается в том, чтобы получать главную страницу сайта и оттуда выполнять XMLHttpRequest с помощью page.evaluate (предварительно сохраняя оригинальные вспомогательные методы воздействия):

    const MAIN_URI = "<someUri>";
    const NEED_URI = "<someUri>";
    
    const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    
    const primordials = ["Promise", "XMLHttpRequest"];
    const defaultLengthOfHiddenIdentifier = 50;
    
    const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
    
    const genRandomString = length => {
    	
    	let result = "";
    	const alphabetLMinusOne = alphabet.length - 1;
    	
    	for(let i = 0; i < length; ++i) {
    		
    		result += alphabet[rand(0, alphabetLMinusOne)];
    		
    	}
    	
    	return result;
    	
    };
    
    //...launch puppeteer
    
    const next = async (browser) => {
    	
    	const page = await browser.newPage();
    	
    	//Запрос на страницу должен идти после сохранения primordials
    	//await page.goto(MAIN_URI);
    	
    	//...possible waitUntil
    		
    	let nowLengthOfHI = defaultLengthOfHiddenIdentifier;
    	
    	for(let i = 0; i < primordials.length; ++i) {
    		
    		const object = primordials[i];
    		let possibleIdentifier = genRandomString(nowLengthOfHI);
    		
    		for(let j = i - 1; j > 0; --j) {
    			
    			const prevObject = primordials[j];
    			
    			if(prevObject[1] == possibleIdentifier) {
    				
    				possibleIdentifier = genRandomString(++nowLengthOfHI);
    				
    				break;
    				
    			}
    			
    		}
    		
    		primordials[i] = [
    		
    			object,
    			possibleIdentifier,
    		
    		];
    		
    	}
    	
    	//Сохранение оригинальных значений для Promise и XMLHttpRequest, чтобы быть уверенными, что сайт не перезаписал их.
    	await page.evaluateOnNewDocument(primordials => {
    		
    		const getOriginObject = object => {
    			
    			if(object.includes(".")) {
    				
    				const splitted = object.split(".");
    				
    				let ret = window;
    				
    				for(let i = 0; i < splitted.length; ++i) {
    					
    					ret = ret[splitted[i]];
    					
    				}
    				
    				return ret;
    				
    			}
    			
    			return window[object];
    			
    		};
    		
    		for(let i = 0; i < primordials.length; ++i) {
    			
    			const object = primordials[i][0];
    			const identifier = primordials[i][1];
    			
    			Object.defineProperty(window, identifier, { value: getOriginObject(object) });
    			
    		}
    		
    	}, primordials);
    
    	await page.goto(MAIN_URI);
    
    	const answerObject = await page.evaluate(async (url, primordials) => {
    		
    		const _Promise = window[ primordials.find(obj => obj[0] == "Promise")[1] ];
    		const _XMLHttpRequest = window[ primordials.find(obj => obj[0] == "XMLHttpRequest")[1] ];
    		
    		return new _Promise(resolve => {
    			
    			let wasCalled = false;
    		
    			const handleEvent = e => {
    				
    				if(wasCalled) return;
    				
    				wasCalled = true;
    				
    				const answer = {
    						
    					error: false,
    					
    					type: e.type,
    					status: xhr.status,
    					response: xhr.responseText,
    					
    				};
    				
    				if(e.type == "error" || e.type == "abort" || e.type == "timeout") {
    					
    					answer.error = true;
    					
    				}
    				
    				if(xhr.status >= 400 && xhr.status < 600) {
    					
    					answer.error = true;
    					
    				}
    				
    				return resolve(answer);
    				
    			};
    		
    			const xhr = new _XMLHttpRequest();
    			
    			xhr.addEventListener('error', handleEvent);
    			xhr.addEventListener('timeout', handleEvent);
    			xhr.addEventListener('load', handleEvent);
    				
    			xhr.open("GET", url);
    			
    			xhr.send();
    		
    		});
    		
    	}, NEED_URI, primordials);
    	
    	console.log(answerObject);
    	
    }
    Ответ написан
    Комментировать