Skip to content

Commit

Permalink
JS-400 Single entrypoint for worker and server (#4893)
Browse files Browse the repository at this point in the history
  • Loading branch information
vdiez authored Nov 8, 2024
1 parent 4fe1c52 commit 0e9590e
Show file tree
Hide file tree
Showing 15 changed files with 543 additions and 340 deletions.
60 changes: 35 additions & 25 deletions bin/server.mjs
Original file line number Diff line number Diff line change
@@ -1,32 +1,42 @@
#!/usr/bin/env node

/**
* This script expects following arguments
*
* port - port number on which server.mjs should listen
* host - host address on which server.mjs should listen
* workDir - working directory from SonarQube API
* shouldUseTypeScriptParserForJS - whether TypeScript parser should be used for JS code (default true, can be set to false in case of perf issues)
* sonarlint - when running in SonarLint (used to not compute metrics, highlighting, etc)
* bundles - ; or : delimited paths to additional rule bundles
*/

import * as server from '../lib/server.js';
import { isMainThread } from 'node:worker_threads';
import * as server from '../lib/bridge/src/server.js';
import path from 'path';
import * as context from '../lib/shared/src/helpers/context.js';
import { pathToFileURL } from 'node:url';
import { createWorker } from '../lib/shared/src/helpers/worker.js';
import { getContext } from '../lib/shared/src/helpers/context.js';

const port = process.argv[2];
const host = process.argv[3];
const workDir = process.argv[4];
const shouldUseTypeScriptParserForJS = process.argv[5] !== 'false';
const sonarlint = process.argv[6] === 'true';
const debugMemory = process.argv[7] === 'true';
// import containing code which is only executed if it's a child process
import '../lib/bridge/src/worker.js';

let bundles = [];
if (process.argv[8]) {
bundles = process.argv[8].split(path.delimiter).map(bundleDir => pathToFileURL(bundleDir).href);
}
if (isMainThread) {
/**
* This script expects following arguments
*
* port - port number on which server.mjs should listen
* host - host address on which server.mjs should listen
* workDir - working directory from SonarQube API
* shouldUseTypeScriptParserForJS - whether TypeScript parser should be used for JS code (default true, can be set to false in case of perf issues)
* sonarlint - when running in SonarLint (used to not compute metrics, highlighting, etc)
* bundles - ; or : delimited paths to additional rule bundles
*/

const port = process.argv[2];
const host = process.argv[3];
const workDir = process.argv[4];
const shouldUseTypeScriptParserForJS = process.argv[5] !== 'false';
const sonarlint = process.argv[6] === 'true';
const debugMemory = process.argv[7] === 'true';

context.setContext({ workDir, shouldUseTypeScriptParserForJS, sonarlint, debugMemory, bundles });
server.start(Number.parseInt(port), host).catch(() => {});
let bundles = [];
if (process.argv[8]) {
bundles = process.argv[8].split(path.delimiter).map(bundleDir => pathToFileURL(bundleDir).href);
}

context.setContext({ workDir, shouldUseTypeScriptParserForJS, sonarlint, debugMemory, bundles });

server
.start(Number.parseInt(port), host, createWorker(new URL(import.meta.url), getContext()))
.catch(() => {});
}
97 changes: 97 additions & 0 deletions packages/bridge/src/delegate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import formData from 'form-data';
import express from 'express';
import { Worker } from 'node:worker_threads';
import { JsTsAnalysisOutputWithAst } from '../../jsts/src/analysis/analysis.js';
import { handleRequest } from './handle-request.js';
import { AnalysisOutput } from '../../shared/src/types/analysis.js';
import { RequestResult, RequestType } from './request.js';

/**
* Returns a delegate function to handle an HTTP request
*/
export function createDelegator(worker: Worker | undefined) {
return function (type: RequestType) {
return worker ? createWorkerHandler(worker, type) : createHandler(type);
};
}

/**
* Handler to analyze in the same thread as HTTP server. Used for testing purposes
* @param type
*/
function createHandler(type: RequestType) {
return async (
request: express.Request,
response: express.Response,
next: express.NextFunction,
) => {
handleResult(await handleRequest({ type, data: request.body }), response, next);
};
}

function createWorkerHandler(worker: Worker, type: RequestType) {
return async (
request: express.Request,
response: express.Response,
next: express.NextFunction,
) => {
worker.once('message', message => {
handleResult(message, response, next);
});
worker.postMessage({ type, data: request.body });
};
}

function handleResult(
message: RequestResult,
response: express.Response,
next: express.NextFunction,
) {
switch (message.type) {
case 'success':
if (typeof message.result === 'object' && outputContainsAst(message.result)) {
sendFormData(message.result, response);
} else {
response.send(message.result);
}
break;

case 'failure':
next(message.error);
break;
}
}

function sendFormData(result: JsTsAnalysisOutputWithAst, response: express.Response) {
const { ast, ...rest } = result;
const fd = new formData();
fd.append('ast', Buffer.from(ast), { filename: 'ast' });
fd.append('json', JSON.stringify(rest));
// this adds the boundary string that will be used to separate the parts
response.set('Content-Type', fd.getHeaders()['content-type']);
response.set('Content-Length', `${fd.getLengthSync()}`);
fd.pipe(response);
}

function outputContainsAst(result: AnalysisOutput): result is JsTsAnalysisOutputWithAst {
return 'ast' in result;
}
117 changes: 117 additions & 0 deletions packages/bridge/src/handle-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { analyzeCSS } from '../../css/src/analysis/analyzer.js';
import { analyzeHTML } from '../../html/src/index.js';
import { analyzeJSTS } from '../../jsts/src/analysis/analyzer.js';
import { analyzeProject } from '../../jsts/src/analysis/projectAnalysis/projectAnalyzer.js';
import { analyzeYAML } from '../../yaml/src/index.js';
import { logHeapStatistics } from './memory.js';
import {
createAndSaveProgram,
createProgramOptions,
deleteProgram,
writeTSConfigFile,
} from '../../jsts/src/program/program.js';
import { initializeLinter } from '../../jsts/src/linter/linters.js';
import { clearTypeScriptESLintParserCaches } from '../../jsts/src/parsers/eslint.js';
import { BridgeRequest, readFileLazily, RequestResult, serializeError } from './request.js';

export async function handleRequest(request: BridgeRequest): Promise<RequestResult> {
try {
switch (request.type) {
case 'on-init-linter': {
const { rules, environments, globals, linterId, baseDir } = request.data;
await initializeLinter(rules, environments, globals, baseDir, linterId);
return { type: 'success', result: 'OK!' };
}
case 'on-analyze-js': {
const output = analyzeJSTS(await readFileLazily(request.data), 'js');
return {
type: 'success',
result: output,
};
}
case 'on-analyze-ts':
case 'on-analyze-with-program': {
const output = analyzeJSTS(await readFileLazily(request.data), 'ts');
return {
type: 'success',
result: output,
};
}
case 'on-create-program': {
logHeapStatistics();
const { programId, files, projectReferences, missingTsConfig } = createAndSaveProgram(
request.data.tsConfig,
);
return {
type: 'success',
result: { programId, files, projectReferences, missingTsConfig },
};
}
case 'on-delete-program': {
deleteProgram(request.data.programId);
logHeapStatistics();
return { type: 'success', result: 'OK!' };
}
case 'on-create-tsconfig-file': {
const tsConfigContent = request.data;
const tsConfigFile = await writeTSConfigFile(tsConfigContent);
return { type: 'success', result: tsConfigFile };
}
// Clean typescript-eslint cache in SonarLint. not used currently
case 'on-new-tsconfig': {
clearTypeScriptESLintParserCaches();
return { type: 'success', result: 'OK!' };
}
case 'on-tsconfig-files': {
const options = createProgramOptions(request.data.tsConfig);
return {
type: 'success',
result: {
files: options.rootNames,
projectReferences: options.projectReferences
? options.projectReferences.map(ref => ref.path)
: [],
},
};
}
case 'on-analyze-css': {
const output = await analyzeCSS(await readFileLazily(request.data));
return { type: 'success', result: output };
}
case 'on-analyze-yaml': {
const output = await analyzeYAML(await readFileLazily(request.data));
return { type: 'success', result: output };
}

case 'on-analyze-html': {
const output = await analyzeHTML(await readFileLazily(request.data));
return { type: 'success', result: output };
}
case 'on-analyze-project': {
const output = await analyzeProject(request.data);
return { type: 'success', result: output };
}
}
} catch (err) {
return { type: 'failure', error: serializeError(err) };
}
}
Loading

0 comments on commit 0e9590e

Please sign in to comment.