Skip to content

Commit 6a87f5d

Browse files
committed
feat: eslint desktop support first draft
1 parent 7bb1d88 commit 6a87f5d

9 files changed

Lines changed: 614 additions & 2 deletions

File tree

src-node/ESLint/constants.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
exports.ESLINT_ERROR_MODULE_LOAD_FAILED = "ESLINT_MODULE_LOAD_FAILED";
2+
exports.ESLINT_ERROR_MODULE_NOT_FOUND = "ESLINT_MODULE_NOT_FOUND";
3+
exports.ESLINT_ERROR_LINT_FAILED = "ESLINT_LINT_FAILED";
4+
5+
exports.OPERATION_LINT_TEXT = "LINT_TEXT";
6+
exports.OPERATION_QUIT = "QUIT";
7+
exports.OPERATION_RESPONSE = "ESLINT_RESPONSE";
8+
9+
// https://eslint.org/docs/latest/integrate/nodejs-api#-lintmessage-type
10+
exports.SEVERITY_WARNING = 1;
11+
exports.SEVERITY_ERROR = 2;

src-node/ESLint/runner.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
const readline = require('readline');
2+
const path = require('path');
3+
const fsPromises = require('fs').promises;
4+
const {ESLINT_ERROR_LINT_FAILED, ESLINT_ERROR_MODULE_LOAD_FAILED, ESLINT_ERROR_MODULE_NOT_FOUND,
5+
OPERATION_LINT_TEXT, OPERATION_QUIT, OPERATION_RESPONSE}
6+
= require("./constants");
7+
8+
if (!process.argv[2]) {
9+
console.error('Error: ESLintModulePath first argument is not set when running ESLint/runner.js');
10+
process.exit(1);
11+
}
12+
if (!process.argv[3]) {
13+
console.error('Error: projectRootPath second argument is not set when running ESLint/runner.js');
14+
process.exit(1);
15+
}
16+
17+
// Get ESLint full path from the environment variable
18+
const ESLintModulePath = path.resolve(process.argv[2]);
19+
const projectRootPath = path.resolve(process.argv[3]);
20+
const lintFilePath = process.argv[4] ? path.resolve(process.argv[4]) : null; // this is just for testing with console
21+
const fs = require('fs');
22+
23+
function sendToPHNode(jsObj) {
24+
console.log(JSON.stringify(jsObj));
25+
}
26+
27+
async function checkExists(directoryPath, isDir = true) {
28+
try {
29+
const stats = await fsPromises.stat(directoryPath);
30+
return isDir ? stats.isDirectory() : stats.isFile();
31+
} catch (error) {
32+
return false;
33+
}
34+
}
35+
36+
// Dynamically require the ESLint module
37+
let ESLintModule;
38+
// if the user changed his eslint version while we were caching an old eslint version, we need to update.
39+
// Function to observe version changes in ESLint
40+
async function observeVersionChanges() {
41+
try {
42+
const packageJsonPath = path.join(ESLintModulePath, 'package.json');
43+
const packageJson = await fsPromises.readFile(packageJsonPath, 'utf8');
44+
const packageData = JSON.parse(packageJson);
45+
const currentEsLintModuleVersion = packageData.version;
46+
47+
if (ESLintModule && currentEsLintModuleVersion && ESLintModule.version !== currentEsLintModuleVersion) {
48+
console.error('ESLint runner: ESLint version has changed. Requesting restart with new version...');
49+
process.exit(0);
50+
}
51+
} catch (error) {
52+
console.error('ESLint runner: Error reading ESLint version:', error.message);
53+
}
54+
}
55+
56+
async function getESLintModule() {
57+
if(ESLintModule){
58+
observeVersionChanges();
59+
return ESLintModule;
60+
}
61+
try{
62+
const directoryExists = await checkExists(ESLintModulePath);
63+
if(!directoryExists){
64+
return null;
65+
}
66+
const {ESLint} = require(ESLintModulePath);
67+
ESLintModule = ESLint;
68+
return ESLint;
69+
} catch (e) {
70+
console.error("ESLint runner: failed to load ESLintModule");
71+
}
72+
return null;
73+
}
74+
75+
async function getESLinter() {
76+
const ESLint = await getESLintModule();
77+
if(!ESLint){
78+
return null;
79+
}
80+
return new ESLint({ cwd: projectRootPath });
81+
// consider caching this if performance is not adequate.
82+
// when caching, make sure that a new eslint object is created when any of the eslint config file changes!!!.
83+
}
84+
85+
async function lintTextWithPath(text, fullFilePath) {
86+
// Create an ESLint instance
87+
const eslinter = await getESLinter();
88+
if(!eslinter){
89+
const directoryExists = await checkExists(ESLintModulePath);
90+
return {
91+
isError: true,
92+
errorCode: directoryExists ? ESLINT_ERROR_MODULE_LOAD_FAILED : ESLINT_ERROR_MODULE_NOT_FOUND
93+
};
94+
}
95+
96+
const isPathIgnored = await eslinter.isPathIgnored(fullFilePath);
97+
if(isPathIgnored) {
98+
return {
99+
isPathIgnored
100+
};
101+
}
102+
103+
// Lint the project directory
104+
const result = (await eslinter.lintText(text, {
105+
filePath: fullFilePath
106+
}))[0];
107+
108+
// Return the results as an array
109+
delete result.source;
110+
return {
111+
result
112+
};
113+
}
114+
115+
if(lintFilePath) {
116+
const text = fs.readFileSync(lintFilePath, { encoding: 'utf8' });
117+
lintTextWithPath(text, lintFilePath)
118+
.then(console.log);
119+
}
120+
121+
const rl = readline.createInterface({
122+
input: process.stdin,
123+
output: process.stdout
124+
});
125+
126+
// each event is a json object
127+
rl.on('line', (input) => {
128+
try {
129+
if(!input.trim()){
130+
return; // empty line
131+
}
132+
// {operation, text, fullFilePath, requestID}
133+
const eslintRequest = JSON.parse(input);
134+
if(eslintRequest.operation === OPERATION_LINT_TEXT) {
135+
lintTextWithPath(eslintRequest.text, eslintRequest.fullFilePath)
136+
.then(result=>{
137+
result.requestID = eslintRequest.requestID;
138+
result.operation = OPERATION_RESPONSE;
139+
sendToPHNode(result);
140+
}).catch(err=>{
141+
console.error("ESlint Runner error:", err);
142+
sendToPHNode({
143+
operation: OPERATION_RESPONSE,
144+
requestID: eslintRequest.requestID,
145+
isError: true,
146+
errorMessage: err.message,
147+
errorCode: ESLINT_ERROR_LINT_FAILED
148+
});
149+
});
150+
} else if(eslintRequest.operation === OPERATION_QUIT) {
151+
process.exit(0);
152+
} else {
153+
console.error("ESLint runner: Unknown operation: ", eslintRequest);
154+
}
155+
} catch (error) {
156+
console.error('ESLint runner:: Error while processing message:', error.message);
157+
}
158+
});

src-node/ESLint/service.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
const { spawn, exec } = require('child_process');
2+
const readline = require('readline');
3+
const path = require('path');
4+
const {OPERATION_RESPONSE, OPERATION_QUIT, OPERATION_LINT_TEXT}
5+
= require("./constants");
6+
7+
let requestID = 1;
8+
let queuedReq = new Map();
9+
function newRequestCallback(resolve, reject) {
10+
const newRequestID = requestID++;
11+
queuedReq.set(newRequestID, {resolve, reject});
12+
return newRequestID;
13+
}
14+
15+
let eslintServiceProcess, nodeBinPath, currentProjectPath;
16+
17+
function sendToESLintProcess(jsObj) {
18+
try{
19+
if(!eslintServiceProcess) {
20+
console.error('sendToESLintProcess: eslintServiceProcess not found');
21+
return;
22+
}
23+
eslintServiceProcess.stdin.write(JSON.stringify(jsObj) + "\n");
24+
} catch (e) {
25+
console.error('sendToESLintProcess: send error', e);
26+
}
27+
}
28+
29+
// Function to check if Node.js is installed
30+
function getNodeJSBinPath() {
31+
return new Promise((resolve) => {
32+
if(nodeBinPath){
33+
resolve(nodeBinPath);
34+
return;
35+
}
36+
exec('node -v', (error, stdout, stderr) => {
37+
if (error) {
38+
console.error('System Node.js is not installed, using PHNode for ESLint');
39+
nodeBinPath = process.argv[0]; // phnode itself
40+
} else {
41+
console.log(`Node.js is installed. Version: ${stdout.trim()}`);
42+
nodeBinPath = "node"; // system node
43+
}
44+
resolve(nodeBinPath);
45+
});
46+
});
47+
}
48+
49+
async function createESLintService(projectFullPath) {
50+
const nodeJSBinPath = await getNodeJSBinPath();
51+
return new Promise(resolve => {
52+
const args = [ path.join(__dirname, "runner.js"),
53+
path.join(projectFullPath, "node_modules", "eslint"), projectFullPath];
54+
if(eslintServiceProcess) {
55+
sendToESLintProcess({
56+
operation: OPERATION_QUIT
57+
});
58+
}
59+
eslintServiceProcess = spawn(nodeJSBinPath, args, {
60+
stdio: ['pipe', 'pipe', 'pipe']
61+
});
62+
eslintServiceProcess.on('spawn', () => {
63+
console.log('ESLint process started successfully');
64+
resolve();
65+
});
66+
const eslineServiceHandler = eslintServiceProcess;
67+
const rl = readline.createInterface({
68+
input: eslintServiceProcess.stdout,
69+
output: process.stdout,
70+
terminal: false
71+
});
72+
rl.on('line', (line) => {
73+
if(!line.trim()){
74+
return; // empty line
75+
}
76+
// {operation, text, fullFilePath, requestID}
77+
const eslintResponse = JSON.parse(line);
78+
if(eslintResponse.operation === OPERATION_RESPONSE) {
79+
const sentRequestID = eslintResponse.requestID;
80+
const promiseResoover = queuedReq.get(sentRequestID);
81+
if(!promiseResoover){
82+
console.error("ESLint service: request ID not found to process request!!: ", eslintResponse);
83+
return;
84+
}
85+
delete eslintResponse.requestID;
86+
delete eslintResponse.operation;
87+
promiseResoover.resolve(eslintResponse);
88+
} else {
89+
console.error("ESLint service: Unknown operation: ", eslintResponse);
90+
}
91+
});
92+
93+
eslintServiceProcess.on('error', (error) => {
94+
console.error(`ESLint Runner Process Error: ${error}`);
95+
});
96+
97+
eslintServiceProcess.on('close', (code) => {
98+
if(eslineServiceHandler === eslintServiceProcess) {
99+
eslintServiceProcess = null;
100+
}
101+
console.log(`ESLint Runner process exited with code ${code}`);
102+
});
103+
});
104+
}
105+
106+
async function lintFile(text, fullFilePath, projectFullPath) {
107+
if(currentProjectPath !== projectFullPath) {
108+
// on project change, we should create a new es linter
109+
currentProjectPath = projectFullPath;
110+
if(eslintServiceProcess) {
111+
sendToESLintProcess({
112+
operation: OPERATION_QUIT
113+
});
114+
eslintServiceProcess = null;
115+
}
116+
}
117+
if(!eslintServiceProcess){
118+
await createESLintService(projectFullPath);
119+
}
120+
return new Promise((resolve, reject) => {
121+
sendToESLintProcess({
122+
requestID: newRequestCallback(resolve, reject),
123+
operation: OPERATION_LINT_TEXT,
124+
text,
125+
fullFilePath
126+
});
127+
});
128+
}
129+
130+
exports.lintFile = lintFile;

src-node/utils.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const { exec, execFile } = require('child_process');
33
const fs = require('fs');
44
const fsPromise = require('fs').promises;
55
const path = require('path');
6+
const {lintFile} = require("./ESLint/service");
67
let openModule, open; // dynamic import when needed
78

89
async function _importOpen() {
@@ -145,10 +146,15 @@ async function _npmInstallInFolder({moduleNativeDir}) {
145146
});
146147
}
147148

149+
async function ESLintFile({text, fullFilePath, projectFullPath}) {
150+
return lintFile(text, fullFilePath, projectFullPath);
151+
}
152+
148153
exports.getURLContent = getURLContent;
149154
exports.setLocaleStrings = setLocaleStrings;
150155
exports.getPhoenixBinaryVersion = getPhoenixBinaryVersion;
151156
exports.getLinuxOSFlavorName = getLinuxOSFlavorName;
152157
exports.openUrlInBrowser = openUrlInBrowser;
158+
exports.ESLintFile = ESLintFile;
153159
exports._loadNodeExtensionModule = _loadNodeExtensionModule;
154160
exports._npmInstallInFolder = _npmInstallInFolder;

0 commit comments

Comments
 (0)