This commit is contained in:
ex61wi
2026-04-05 22:56:09 +02:00
parent 8cf3b6640a
commit 9e64c6b8d2
5 changed files with 201 additions and 62 deletions
+134 -40
View File
@@ -4,6 +4,8 @@ import * as path from 'path';
export interface CompilationError { export interface CompilationError {
filePath: string; filePath: string;
line: number; line: number;
charStart?: number;
charEnd?: number;
message: string; message: string;
} }
@@ -15,48 +17,92 @@ export interface ScriptCompilerOptions {
onError?: (error: CompilationError) => void onError?: (error: CompilationError) => void
} }
export interface ICompilationLogger {
info(message: string): void;
error(message: string): void;
writeLine(message: string): void;
}
class Metrics {
public totalLinesRead : number = 0;
public totalLinesWritten: number = 0;
public readTimeMs : number = 0;
public writeTimeMs : number = 0;
public totalTimeMs : number = 0;
public filesRead : number = 0;
public filesWritten : number = 0;
log(logger: ICompilationLogger){
logger.writeLine("Compilation Metrics: ")
logger.writeLine(`=====================`)
logger.writeLine(`Lines Read: ${this.totalLinesRead}`)
logger.writeLine(`Lines Written: ${this.totalLinesWritten}`)
logger.writeLine(`Files Processed: ${this.filesRead} | ${this.filesWritten}`)
logger.writeLine(`=====================`)
logger.writeLine(`Read Time (ms): ${this.readTimeMs} ms`)
logger.writeLine(`Write Time (ms): ${this.writeTimeMs} ms`)
logger.writeLine(`Total Time (ms): ${this.totalTimeMs} ms`)
}
}
const LUA_SCRIPT_GLOBAL_KEYWORD = 'ScriptGlobals'; const LUA_SCRIPT_GLOBAL_KEYWORD = 'ScriptGlobals';
export class ScriptCompiler { export class ScriptCompiler {
constructor(private options: ScriptCompilerOptions) { constructor(private options: ScriptCompilerOptions, private logger: ICompilationLogger) {
if (options.outputFileName === undefined) { if (options.outputFileName === undefined) {
this.options.outputFileName = 'compiled.lua'; this.options.outputFileName = 'compiled.lua';
} }
} }
public async compile(): Promise<void> { public async compile(): Promise<void> {
const start = Date.now();
const metricsMeter = new Metrics();
const entries = fs.readdirSync(this.options.sourcePath, { recursive: true, withFileTypes: true }); const entries = fs.readdirSync(this.options.sourcePath, { recursive: true, withFileTypes: true });
const parsedFiles: Map<string, ParsedFile> = new Map(); const parsedFiles: Map<string, ParsedFile> = new Map();
const readStart = Date.now();
for (const entry of entries) { for (const entry of entries) {
if (entry.isFile() && entry.name.endsWith('.lua')) { if (entry.isFile() && entry.name.endsWith('.lua')) {
const fullPath = path.join(entry.parentPath ?? entry.path, entry.name); const fullPath = path.join(entry.parentPath ?? entry.path, entry.name);
const relativePath = path.relative(this.options.sourcePath, fullPath); const relativePath = path.relative(this.options.sourcePath, fullPath);
const content = fs.readFileSync(fullPath, 'utf-8'); const content = fs.readFileSync(fullPath, 'utf-8');
const parsedFile = this.parseFile(relativePath, content, fullPath); const parsedFile = this.parseFile(relativePath, content, fullPath, metricsMeter);
parsedFiles.set(parsedFile.fileKey, parsedFile); parsedFiles.set(parsedFile.fileKey, parsedFile);
metricsMeter.filesRead++;
} }
} }
const readEnd = Date.now();
metricsMeter.readTimeMs = readEnd - readStart;
const writer = new Writer( const writer = new Writer(
path.join(this.options.outputPath, this.options.outputFileName!), path.join(this.options.outputPath, this.options.outputFileName!),
parsedFiles, parsedFiles,
this.logger,
this.options.onError this.options.onError
); );
writer.write(); const writeStart = Date.now();
console.log(`Compilation complete. Output written to ${path.join(this.options.outputPath, this.options.outputFileName!)}`); writer.write(metricsMeter);
const writeEnd = Date.now();
metricsMeter.writeTimeMs = (writeEnd - writeStart);
writer.logDependencyTree();
this.logger.info(`Compilation complete. Output written to ${path.join(this.options.outputPath, this.options.outputFileName!)}`);
const end = Date.now();
metricsMeter.totalTimeMs = (end-start);
metricsMeter.log(this.logger);
} }
private reportError(filePath: string, line: number, message: string): void { private reportError(filePath: string, line: number, charStart: number | undefined, charEnd: number | undefined, message: string): void {
if (this.options.onError) { if (this.options.onError) {
this.options.onError({ filePath, line, message }); this.options.onError({ filePath, line, charStart, charEnd, message });
} }
} }
private parseFile(filePath: string, content: string, fullPath: string): ParsedFile { private parseFile(filePath: string, content: string, fullPath: string, metricsMeter: Metrics): ParsedFile {
const dependencies: Dependency[] = []; const dependencies: Dependency[] = [];
const newLines: string[] = [`do --${filePath}`]; const newLines: string[] = [`do --${filePath}`];
@@ -83,6 +129,7 @@ export class ScriptCompiler {
}; };
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
metricsMeter.totalLinesRead++;
let line = lines[i]; let line = lines[i];
const trimmedLine = line.trim(); const trimmedLine = line.trim();
@@ -103,7 +150,7 @@ export class ScriptCompiler {
} }
newLines.push(line); newLines.push(line);
} else { } else {
this.reportError(fullPath, i, `Code found after module-level return: ${trimmedLine}`); this.reportError(fullPath, i, undefined, undefined, `Code found after module-level return: ${trimmedLine}`);
newLines.push(line); // Continue processing despite error newLines.push(line); // Continue processing despite error
} }
continue; continue;
@@ -112,8 +159,10 @@ export class ScriptCompiler {
// Handle require statements // Handle require statements
const requireMatch = line.match(/require\(['"](.+?)['"]\)/); const requireMatch = line.match(/require\(['"](.+?)['"]\)/);
if (requireMatch) { if (requireMatch) {
const requiredModule = fileReferenceToLuaVariable(requireMatch[1]); const textMatch = requireMatch[1];
dependencies.push(new Dependency(requiredModule, fullPath, i + 1)); const requiredModule = fileReferenceToLuaVariable(textMatch);
dependencies.push(new Dependency(textMatch, i, requireMatch.index ?? 0, (requireMatch.index ?? 0) + requireMatch[0].length));
line = line.replace(requireMatch[0], requiredModule); line = line.replace(requireMatch[0], requiredModule);
} }
@@ -164,7 +213,7 @@ export class ScriptCompiler {
const afterReturn = trimmedLine.substring(afterReturnPos).trim(); const afterReturn = trimmedLine.substring(afterReturnPos).trim();
if (afterReturn === '') { if (afterReturn === '') {
this.reportError(fullPath, i, 'Empty return statement at module level'); this.reportError(fullPath, i, afterReturnPos, afterReturnPos, 'Empty return statement at module level');
continue; continue;
} }
@@ -207,7 +256,7 @@ export class ScriptCompiler {
} }
if (hasMultipleValues) { if (hasMultipleValues) {
this.reportError(fullPath, i, `Multiple return values not supported: ${trimmedLine}`); this.reportError(fullPath, i, afterReturnPos, afterReturnPos + afterReturn.length, `Multiple return values not supported: ${trimmedLine}`);
} else { } else {
// Replace return with assignment // Replace return with assignment
const moduleVariable = fileReferenceToLuaVariable(filePath); const moduleVariable = fileReferenceToLuaVariable(filePath);
@@ -242,12 +291,17 @@ function stripLuaMultilineComments(content: string): string {
} }
class Dependency { class Dependency {
public readonly fileKey: string;
constructor( constructor(
public readonly name: string, public readonly requiredModule: string,
public readonly filePath: string, public readonly requiredAtLine: number,
public readonly requiredAtLine: number public readonly charStart: number,
){} public readonly charEnd: number
)
{
this.fileKey = fileReferenceToLuaVariable(requiredModule);
}
} }
class ParsedFile { class ParsedFile {
@@ -298,38 +352,52 @@ class Writer {
constructor( constructor(
public location: string, public location: string,
public files: Map<string, ParsedFile>, public files: Map<string, ParsedFile>,
private readonly logger: ICompilationLogger,
public onError?: (error: CompilationError) => void public onError?: (error: CompilationError) => void
) {} ) {}
private getStartLines(): string[] { private getStartLines(): string[] {
return [`local ${LUA_SCRIPT_GLOBAL_KEYWORD} = {}`]; return [
`-- Transpiled at (UTC): ${new Date().toISOString()}`,
`local ${LUA_SCRIPT_GLOBAL_KEYWORD} = {}`
];
} }
write(): void { logDependencyTree(): void {
const visited: Set<string> = new Set();
const depth : number = 1;
this.logger.writeLine('Dependency Tree <root>:');
const logFileRecursive = (parsedFile: ParsedFile, currentDepth: number) => {
if (visited.has(parsedFile.fileKey)) {
return;
}
visited.add(parsedFile.fileKey);
let padding = ' '.repeat(currentDepth);
if (currentDepth > 0) {
padding = ' '.repeat(currentDepth) + '└─>';
}
const printable = parsedFile.fileKey.replace(LUA_SCRIPT_GLOBAL_KEYWORD + '.', '');
this.logger.writeLine(padding + printable);
for (const dep of parsedFile.dependencies) {
const depFile = this.files.get(dep.fileKey);
if (depFile) {
logFileRecursive(depFile, currentDepth + 1);
}
}
}
for (const parsedFile of this.files.values()) {
logFileRecursive(parsedFile, depth);
}
};
write(metrics: Metrics): void {
const writtenFiles: Set<string> = new Set(); const writtenFiles: Set<string> = new Set();
const outputLines: string[] = []; const outputLines: string[] = [];
outputLines.push(...this.getStartLines()); const startLines = this.getStartLines();
metrics.totalLinesWritten+=startLines.length;
const writeFileRecursive = (parsedFile: ParsedFile) => { outputLines.push(...startLines);
// Write dependencies first
for (const dep of parsedFile.dependencies) {
const depFile = this.files.get(dep.name);
if (depFile) {
writeFileRecursive(depFile);
} else {
this.onError?.({
filePath: parsedFile.fullPath,
line: 0,
message: `Missing dependency: ${dep}`
});
}
}
outputLines.push(...parsedFile.lines);
writtenFiles.add(parsedFile.fileKey);
};
//Check for circular dependencies before writing //Check for circular dependencies before writing
const visited: Set<string> = new Set(); const visited: Set<string> = new Set();
@@ -357,7 +425,7 @@ class Writer {
const file = this.files.get(key); const file = this.files.get(key);
if (file) { if (file) {
for (const dep of file.dependencies) { for (const dep of file.dependencies) {
if (checkCircular(dep.name)) { if (checkCircular(dep.fileKey)) {
return true; return true;
} }
} }
@@ -371,6 +439,32 @@ class Writer {
} }
} }
const writeFileRecursive = (parsedFile: ParsedFile) => {
if (writtenFiles.has(parsedFile.fileKey)) {
return;
}
// Write dependencies first
for (const dep of parsedFile.dependencies) {
const depFile = this.files.get(dep.fileKey);
if (depFile) {
writeFileRecursive(depFile);
} else {
this.onError?.({
filePath: parsedFile.fullPath,
line: dep.requiredAtLine,
charStart: dep.charStart,
charEnd: dep.charEnd,
message: `Missing dependency: ${dep.fileKey}`
});
}
}
metrics.totalLinesWritten += parsedFile.lines.length;
outputLines.push(...parsedFile.lines);
writtenFiles.add(parsedFile.fileKey);
metrics.filesWritten++;
};
// Write all files in dependency order // Write all files in dependency order
for (const parsedFile of this.files.values()) { for (const parsedFile of this.files.values()) {
writeFileRecursive(parsedFile); writeFileRecursive(parsedFile);
+12 -3
View File
@@ -22,6 +22,17 @@
"activationEvents": [], "activationEvents": [],
"main": "./dist/extension.js", "main": "./dist/extension.js",
"contributes": { "contributes": {
"configuration": {
"title": "Dutchies DCS Scripting Tools Settings",
"properties": {
"dutchiesDcsScriptingTools.compileAt" : {
"type": "string",
"enum": ["onSave", "onCompileCommand"],
"default": "onCompileCommand",
"description": "When to compile the Lua scripts"
}
}
},
"grammars": [ "grammars": [
{ {
"path": "./syntaxes/lua-slocal.json", "path": "./syntaxes/lua-slocal.json",
@@ -56,9 +67,7 @@
"lua-addons/**/*", "lua-addons/**/*",
"syntaxes/**/*" "syntaxes/**/*"
], ],
"dependencies": { "dependencies": {},
"dcs-script-compiler" : "*"
},
"scripts": { "scripts": {
"vscode:prepublish": "npm run build -w compiler && npm run package", "vscode:prepublish": "npm run build -w compiler && npm run package",
"compile": "npm run check-types && npm run lint && node esbuild.js", "compile": "npm run check-types && npm run lint && node esbuild.js",
+23 -4
View File
@@ -1,7 +1,8 @@
// The module 'vscode' contains the VS Code extensibility API // The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below // Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { ScriptCompiler, ScriptCompilerOptions, CompilationError } from 'dcs-script-compiler'; import { ScriptCompiler, ScriptCompilerOptions, CompilationError, ICompilationLogger } from 'dcs-script-compiler';
import { Logger } from './logger';
const luaWorkSpaceSettingKey = "Lua.workspace"; const luaWorkSpaceSettingKey = "Lua.workspace";
const librarySettingsKey = "library"; const librarySettingsKey = "library";
@@ -9,6 +10,7 @@ const diagnosticCollection = vscode.languages.createDiagnosticCollection('lua-tr
let pluginPath : string | undefined; let pluginPath : string | undefined;
const logger = new Logger();
// This method is called when your extension is activated // This method is called when your extension is activated
// Your extension is activated the very first time the command is executed // Your extension is activated the very first time the command is executed
@@ -36,12 +38,12 @@ export function activate(context: vscode.ExtensionContext) {
const start = Date.now(); const start = Date.now();
await compileLuaScripts(); await compileLuaScripts();
const end = Date.now(); const end = Date.now();
console.log(`Lua scripts compiled in ${(end - start) / 1000} seconds.`);
vscode.window.showInformationMessage(`Lua scripts compiled successfully in ${(end - start) / 1000} seconds.`); vscode.window.showInformationMessage(`Lua scripts compiled successfully in ${(end - start) / 1000} seconds.`);
}catch(err){ }catch(err){
vscode.window.showErrorMessage('Error compiling Lua scripts: ' + (err as Error).message); vscode.window.showErrorMessage('Error compiling Lua scripts: ' + (err as Error).message);
} }
}); });
} }
// This method is called when your extension is deactivated // This method is called when your extension is deactivated
@@ -50,6 +52,22 @@ export function deactivate()
removePluginPathFromSettings(); removePluginPathFromSettings();
} }
class CompilationLogger implements ICompilationLogger {
constructor(
private readonly logger: Logger) {
}
info(message: string): void {
this.logger.info(message);
}
error(message: string): void {
this.logger.error(message);
}
writeLine(message: string): void {
this.logger.log(message);
}
}
async function compileLuaScripts() { async function compileLuaScripts() {
const config = vscode.workspace.getConfiguration('dcsScriptingTools'); const config = vscode.workspace.getConfiguration('dcsScriptingTools');
@@ -74,7 +92,8 @@ async function compileLuaScripts() {
} }
}; };
const compiler = new ScriptCompiler(options); const compilationLogger = new CompilationLogger(logger);
const compiler = new ScriptCompiler(options, compilationLogger);
try { try {
await compiler.compile(); await compiler.compile();
} catch (err) { } catch (err) {
@@ -86,7 +105,7 @@ async function compileLuaScripts() {
for (const [filePath, errors] of errorsByFile) { for (const [filePath, errors] of errorsByFile) {
const uri = vscode.Uri.file(filePath); const uri = vscode.Uri.file(filePath);
const diagnostics = errors.map(error => { const diagnostics = errors.map(error => {
const range = new vscode.Range(error.line, 0, error.line, 0); const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0);
const diagnostic = new vscode.Diagnostic(range, error.message, vscode.DiagnosticSeverity.Error); const diagnostic = new vscode.Diagnostic(range, error.message, vscode.DiagnosticSeverity.Error);
diagnostic.source = 'DCS Lua Transpiler'; diagnostic.source = 'DCS Lua Transpiler';
return diagnostic; return diagnostic;
+26 -12
View File
@@ -8,11 +8,15 @@
"workspaces": [ "workspaces": [
"compiler", "compiler",
"dutchies-dcs-scripting-tools" "dutchies-dcs-scripting-tools"
] ],
"devDependencies": {
"@types/node": "^25.5.2"
}
}, },
"compiler": { "compiler": {
"name": "dcs-script-compiler", "name": "dcs-script-compiler",
"version": "1.0.0", "version": "1.0.0",
"extraneous": true,
"devDependencies": { "devDependencies": {
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"typescript": "^5.9.3" "typescript": "^5.9.3"
@@ -21,9 +25,6 @@
"dutchies-dcs-scripting-tools": { "dutchies-dcs-scripting-tools": {
"version": "0.0.1", "version": "0.0.1",
"license": "MIT", "license": "MIT",
"dependencies": {
"dcs-script-compiler": "*"
},
"devDependencies": { "devDependencies": {
"@types/mocha": "^10.0.10", "@types/mocha": "^10.0.10",
"@types/node": "22.x", "@types/node": "22.x",
@@ -40,6 +41,16 @@
"vscode": "^1.108.1" "vscode": "^1.108.1"
} }
}, },
"dutchies-dcs-scripting-tools/node_modules/@types/node": {
"version": "22.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
"integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@bcoe/v8-coverage": { "node_modules/@bcoe/v8-coverage": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
@@ -832,15 +843,22 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.19.7", "version": "25.5.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~7.18.0"
} }
}, },
"node_modules/@types/node/node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/vscode": { "node_modules/@types/vscode": {
"version": "1.108.1", "version": "1.108.1",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.108.1.tgz", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.108.1.tgz",
@@ -1713,10 +1731,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/dcs-script-compiler": {
"resolved": "compiler",
"link": true
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+4 -1
View File
@@ -4,5 +4,8 @@
"workspaces": [ "workspaces": [
"compiler", "compiler",
"dutchies-dcs-scripting-tools" "dutchies-dcs-scripting-tools"
] ],
"devDependencies": {
"@types/node": "^25.5.2"
}
} }