Added Required logic if not found, and added better update logic
This commit is contained in:
@@ -1,25 +1,34 @@
|
||||
// The module 'vscode' contains the VS Code extensibility API
|
||||
// Import the module and reference it with the alias vscode in your code below
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { ScriptCompiler, ScriptCompilerOptions, CompilationError, ICompilationLogger } from 'dcs-script-compiler';
|
||||
import { Logger } from './logger';
|
||||
import * as luaAddonsManager from './lua-addons-manager';
|
||||
import { CompilationErrorType } from '../../compiler/src/CompilationError';
|
||||
|
||||
const luaWorkSpaceSettingKey = "Lua.workspace";
|
||||
const librarySettingsKey = "library";
|
||||
const diagnosticCollection = vscode.languages.createDiagnosticCollection('lua-transpiler');
|
||||
|
||||
// Diagnostic codes
|
||||
const DIAGNOSTIC_CODE_MISSING_GLOBAL = 'lua-missing-global-dependency';
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
const luaAddonNames: string[] = [ "dcs-types" ];
|
||||
|
||||
const extensionName = 'dutchies-dcs-scripting-tools';
|
||||
const publisherName = 'dutchie031';
|
||||
const extensionSettingsFilter = `@ext:${publisherName}.${extensionName}`;
|
||||
|
||||
let luaAddonsManagerInstance: luaAddonsManager.LuaAddonsManager | undefined;
|
||||
|
||||
// State tracking for addon updates
|
||||
let extensionPath: string | undefined;
|
||||
let workspaceRoot: string | undefined;
|
||||
let luaAddonsTargetPath: string | undefined;
|
||||
let currentExtensionVersion: string | undefined;
|
||||
let installedAddonPaths: Map<string, string> = new Map();
|
||||
let isUpdatingAddons = false;
|
||||
|
||||
//TODO:
|
||||
@@ -32,9 +41,22 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
extensionPath = context.extensionPath;
|
||||
workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
|
||||
if (workspaceRoot === undefined) {
|
||||
vscode.window.showErrorMessage('Workspace root not found. Lua addons manager cannot be initialized.');
|
||||
return;
|
||||
}
|
||||
|
||||
luaAddonsTargetPath = path.join(workspaceRoot, '.vscode' , 'lua-addons');
|
||||
luaAddonsManagerInstance = new luaAddonsManager.LuaAddonsManager(luaAddonsTargetPath, context.extensionPath);
|
||||
|
||||
if (!luaAddonsManagerInstance) {
|
||||
vscode.window.showErrorMessage('Failed to initialize Lua addons manager.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize extension version
|
||||
try {
|
||||
currentExtensionVersion = await luaAddonsManager.getExtensionVersion(extensionPath);
|
||||
currentExtensionVersion = await luaAddonsManagerInstance.getPackageVersion();
|
||||
logger.debug(`Extension version: ${currentExtensionVersion}`);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to determine extension version: ${err instanceof Error ? err.message : String(err)}`);
|
||||
@@ -70,6 +92,23 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
})
|
||||
);
|
||||
|
||||
// Register code actions provider for quick fixes
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeActionsProvider('lua', new LuaQuickFixProvider(), {
|
||||
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix]
|
||||
})
|
||||
);
|
||||
|
||||
// Register command to add global requirable
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
'dutchies-dcs-scripting-tools.addGlobalRequirable',
|
||||
async (dependency: string) => {
|
||||
await addGlobalRequirable(dependency);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
vscode.workspace.onDidSaveTextDocument(async(document) => {
|
||||
logger.info(`Document saved: ${document.uri.fsPath} | Language: ${document.languageId}`);
|
||||
if (document.languageId === 'lua') {
|
||||
@@ -164,16 +203,40 @@ async function compileLuaScripts() {
|
||||
logger.error('Compilation failed: ' + (err as Error).message + '\n' + (err as Error).stack);
|
||||
}
|
||||
|
||||
const globalRequirables: string[] = config.get<string[]>('globalRequirables') || [];
|
||||
|
||||
// Update diagnostics
|
||||
diagnosticCollection.clear();
|
||||
for (const [filePath, errors] of errorsByFile) {
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
const diagnostics = errors.map(error => {
|
||||
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);
|
||||
diagnostic.source = 'DCS Lua Transpiler';
|
||||
return diagnostic;
|
||||
});
|
||||
if (error.type === CompilationErrorType.DependencyNotFound) {
|
||||
const dependencyStr = error.metaData?.get("dependency") ?? undefined;
|
||||
if (dependencyStr) {
|
||||
if (globalRequirables.includes(dependencyStr)) {
|
||||
// Do nothing, it is a globally requirable dependency
|
||||
const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0);
|
||||
const diagnostic = new vscode.Diagnostic(range, "Unchecked: Globally marked dependency", vscode.DiagnosticSeverity.Hint);
|
||||
diagnostic.source = 'DCS Lua Transpiler';
|
||||
return diagnostic;
|
||||
} else {
|
||||
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);
|
||||
diagnostic.code = { value: DIAGNOSTIC_CODE_MISSING_GLOBAL, target: vscode.Uri.parse('https://example.com') };
|
||||
diagnostic.source = 'DCS Lua Transpiler';
|
||||
// Store the dependency name for the quick fix to access
|
||||
(diagnostic as any).dependency = dependencyStr;
|
||||
return diagnostic;
|
||||
}
|
||||
}
|
||||
return undefined; //Something weird happened, let's ignore it for now.
|
||||
} else {
|
||||
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);
|
||||
diagnostic.source = 'DCS Lua Transpiler';
|
||||
return diagnostic;
|
||||
}
|
||||
}).filter(diagnostic => diagnostic !== undefined);
|
||||
diagnosticCollection.set(uri, diagnostics);
|
||||
}
|
||||
}
|
||||
@@ -186,6 +249,7 @@ async function enableIntellisense() {
|
||||
}
|
||||
|
||||
await updateLuaAddons();
|
||||
await addPluginPathsToSettings();
|
||||
await vscode.commands.executeCommand(
|
||||
"lua.startServer"
|
||||
);
|
||||
@@ -199,18 +263,8 @@ async function disableIntellisense() {
|
||||
}
|
||||
|
||||
await removeVersionedPluginPathsFromSettings();
|
||||
|
||||
// Clean up all addon versions from workspace
|
||||
if (workspaceRoot) {
|
||||
try {
|
||||
const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath || '');
|
||||
for (const addonName of discoveredAddons.keys()) {
|
||||
await luaAddonsManager.deleteAllVersionsOfAddon(workspaceRoot, addonName);
|
||||
logger.debug(`Deleted all versions of ${addonName} from workspace.`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Failed to clean up addons: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
if(luaAddonsManagerInstance){
|
||||
await luaAddonsManagerInstance.removeAllExtensions(luaAddonNames);
|
||||
}
|
||||
|
||||
await vscode.commands.executeCommand(
|
||||
@@ -237,41 +291,28 @@ async function updateLuaAddons(): Promise<void> {
|
||||
|
||||
isUpdatingAddons = true;
|
||||
|
||||
if (luaAddonsManagerInstance === undefined) {
|
||||
logger.warn('Lua Addons Manager instance is not available.');
|
||||
isUpdatingAddons = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('Starting lua-addons update...');
|
||||
|
||||
// Discover bundled addons
|
||||
const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath);
|
||||
|
||||
if (discoveredAddons.size === 0) {
|
||||
logger.info('No lua-addons found in extension bundle.');
|
||||
isUpdatingAddons = false;
|
||||
return;
|
||||
for (const addonName of luaAddonNames) {
|
||||
const packageVersion = await luaAddonsManagerInstance.getPackageVersion();
|
||||
const addonVersion = await luaAddonsManagerInstance.getExtensionVersion(addonName);
|
||||
|
||||
if (packageVersion && packageVersion !== addonVersion) {
|
||||
logger.warn(
|
||||
`Version mismatch for addon ${addonName}: package=${packageVersion}, installed=${addonVersion}`
|
||||
);
|
||||
|
||||
luaAddonsManagerInstance.removeExtension(addonName);
|
||||
luaAddonsManagerInstance.installExtension(addonName);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Discovered ${discoveredAddons.size} lua-addon(s): ${Array.from(discoveredAddons.keys()).join(', ')}`);
|
||||
|
||||
// Copy addons with versioned names
|
||||
const copiedAddons = await luaAddonsManager.copyLuaAddons(
|
||||
extensionPath,
|
||||
workspaceRoot,
|
||||
currentExtensionVersion,
|
||||
discoveredAddons
|
||||
);
|
||||
|
||||
// Store paths for settings management
|
||||
installedAddonPaths = copiedAddons;
|
||||
logger.debug(`Copied ${copiedAddons.size} addon(s) to workspace.`);
|
||||
|
||||
// Clean up old versions
|
||||
for (const addonName of discoveredAddons.keys()) {
|
||||
await luaAddonsManager.deleteOldVersions(workspaceRoot, addonName, currentExtensionVersion);
|
||||
logger.debug(`Cleaned old versions of ${addonName}.`);
|
||||
}
|
||||
|
||||
// Update Lua settings with new paths
|
||||
await addVersionedPluginPathsToSettings(copiedAddons);
|
||||
|
||||
logger.info(`Lua-addons update completed successfully (version ${currentExtensionVersion}).`);
|
||||
|
||||
} catch (err) {
|
||||
@@ -304,27 +345,22 @@ async function checkAndUpdateAddonsOnStartup(): Promise<void> {
|
||||
|
||||
logger.debug('Checking lua-addons versions on startup...');
|
||||
|
||||
// Get installed addons
|
||||
const installedAddons = await luaAddonsManager.getInstalledAddons(workspaceRoot);
|
||||
const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath);
|
||||
if (!luaAddonsManagerInstance) {
|
||||
logger.debug('Lua Addons Manager instance is not available, skipping addon version check.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check each discovered addon
|
||||
let updateNeeded = false;
|
||||
for (const addonName of discoveredAddons.keys()) {
|
||||
const installed = installedAddons.get(addonName);
|
||||
const installedVersion = installed?.version;
|
||||
|
||||
if (luaAddonsManager.requiresUpdate(installedVersion, currentExtensionVersion)) {
|
||||
logger.info(
|
||||
`Version mismatch for ${addonName}: installed=${installedVersion || 'none'}, current=${currentExtensionVersion}`
|
||||
);
|
||||
for (const addonName of luaAddonNames) {
|
||||
const packageVersion = await luaAddonsManagerInstance.getPackageVersion();
|
||||
const addonVersion = await luaAddonsManagerInstance.getExtensionVersion(addonName);
|
||||
if (packageVersion && packageVersion !== addonVersion) {
|
||||
updateNeeded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateNeeded) {
|
||||
logger.info('Lua-addons update needed, prompting user...');
|
||||
const userChoice = await vscode.window.showInformationMessage(
|
||||
'DCS-Types are not up to date with the current extension version. Do you want to update them?',
|
||||
{ modal: false },
|
||||
@@ -337,17 +373,7 @@ async function checkAndUpdateAddonsOnStartup(): Promise<void> {
|
||||
} else {
|
||||
logger.info('User declined DCS-Types update.');
|
||||
}
|
||||
} else {
|
||||
logger.debug('Lua-addons versions are current, no update needed.');
|
||||
// Ensure paths are in settings even if no update needed
|
||||
const pathsMap = new Map<string, string>();
|
||||
for (const [name, info] of installedAddons.entries()) {
|
||||
pathsMap.set(name, info.path);
|
||||
}
|
||||
installedAddonPaths = pathsMap;
|
||||
await addVersionedPluginPathsToSettings(installedAddonPaths);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Error checking lua-addons on startup: ${err instanceof Error ? err.message : String(err)}`
|
||||
@@ -377,14 +403,12 @@ function convertToWorkspaceFolderPath(absolutePath: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds versioned addon paths to the Lua workspace library settings
|
||||
* @param addonPaths - Map of addon name to installed path
|
||||
* Adds the lua-addons directory to the Lua workspace library settings.
|
||||
* Lua Language Server automatically discovers versioned addons within this directory.
|
||||
*/
|
||||
async function addVersionedPluginPathsToSettings(addonPaths?: Map<string, string>): Promise<void> {
|
||||
const pathsToAdd = addonPaths || installedAddonPaths;
|
||||
|
||||
if (pathsToAdd.size === 0) {
|
||||
logger.debug('No addon paths to add to settings.');
|
||||
async function addPluginPathsToSettings(): Promise<void> {
|
||||
if (!luaAddonsTargetPath) {
|
||||
logger.warn('Lua addons target path not available.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -392,58 +416,110 @@ async function addVersionedPluginPathsToSettings(addonPaths?: Map<string, string
|
||||
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
||||
let librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
||||
|
||||
// Add each addon path if not already present
|
||||
for (const addonPath of pathsToAdd.values()) {
|
||||
const workspaceFolderPath = convertToWorkspaceFolderPath(addonPath);
|
||||
if (!librarySettings.includes(workspaceFolderPath)) {
|
||||
librarySettings.push(workspaceFolderPath);
|
||||
logger.debug(`Added addon path to settings: ${workspaceFolderPath}`);
|
||||
}
|
||||
const workspaceFolderPath = convertToWorkspaceFolderPath(luaAddonsTargetPath);
|
||||
|
||||
if (!librarySettings.includes(workspaceFolderPath)) {
|
||||
librarySettings.push(workspaceFolderPath);
|
||||
logger.debug(`Added lua-addons path to Lua settings: ${workspaceFolderPath}`);
|
||||
await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
||||
} else {
|
||||
logger.debug('Lua-addons path already in settings.');
|
||||
}
|
||||
|
||||
await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to add addon paths to settings: ${err instanceof Error ? err.message : String(err)}`);
|
||||
logger.error(`Failed to add lua-addons path to settings: ${err instanceof Error ? err.message : String(err)}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all addon paths from the Lua workspace library settings
|
||||
* Removes the lua-addons directory from the Lua workspace library settings
|
||||
*/
|
||||
async function removeVersionedPluginPathsFromSettings(): Promise<void> {
|
||||
if (!luaAddonsTargetPath) {
|
||||
logger.debug('Lua addons target path not available, skipping removal.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
||||
let librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
||||
|
||||
const workspaceFolderPath = convertToWorkspaceFolderPath(luaAddonsTargetPath);
|
||||
const filteredSettings = librarySettings.filter(path => path !== workspaceFolderPath);
|
||||
|
||||
if (filteredSettings.length !== librarySettings.length) {
|
||||
await luaSettings.update(librarySettingsKey, filteredSettings, vscode.ConfigurationTarget.Workspace);
|
||||
logger.debug(`Removed lua-addons path from Lua settings: ${workspaceFolderPath}`);
|
||||
} else {
|
||||
logger.debug('Lua-addons path not found in settings.');
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Failed to remove lua-addons path from settings: ${err instanceof Error ? err.message : String(err)}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (workspaceRoot) {
|
||||
// Get all installed addons to know what paths to remove
|
||||
const installedAddons = await luaAddonsManager.getInstalledAddons(workspaceRoot);
|
||||
/**
|
||||
* Code actions provider for Lua quick fixes
|
||||
*/
|
||||
class LuaQuickFixProvider implements vscode.CodeActionProvider {
|
||||
provideCodeActions(
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range | vscode.Selection,
|
||||
context: vscode.CodeActionContext,
|
||||
): vscode.CodeAction[] {
|
||||
const codeActions: vscode.CodeAction[] = [];
|
||||
|
||||
// Check for missing global dependency diagnostics
|
||||
for (const diagnostic of context.diagnostics) {
|
||||
const codeValue = typeof diagnostic.code === 'object' && diagnostic.code !== null
|
||||
? (diagnostic.code as any).value
|
||||
: diagnostic.code;
|
||||
|
||||
for (const addonInfo of installedAddons.values()) {
|
||||
// Try both absolute and workspace-relative paths for compatibility
|
||||
const absolutePath = addonInfo.path;
|
||||
const relativePath = convertToWorkspaceFolderPath(absolutePath);
|
||||
|
||||
// Remove absolute path if present
|
||||
let index = librarySettings.indexOf(absolutePath);
|
||||
if (index !== -1) {
|
||||
librarySettings.splice(index, 1);
|
||||
logger.debug(`Removed addon path from settings: ${absolutePath}`);
|
||||
}
|
||||
|
||||
// Remove relative path if present
|
||||
index = librarySettings.indexOf(relativePath);
|
||||
if (index !== -1) {
|
||||
librarySettings.splice(index, 1);
|
||||
logger.debug(`Removed addon path from settings: ${relativePath}`);
|
||||
if (codeValue === DIAGNOSTIC_CODE_MISSING_GLOBAL) {
|
||||
const dependency = (diagnostic as any).dependency;
|
||||
if (dependency) {
|
||||
const action = new vscode.CodeAction(
|
||||
`Mark '${dependency}' as globally available`,
|
||||
vscode.CodeActionKind.QuickFix
|
||||
);
|
||||
action.command = {
|
||||
title: `Add '${dependency}' to global requireables`,
|
||||
command: 'dutchies-dcs-scripting-tools.addGlobalRequirable',
|
||||
arguments: [dependency]
|
||||
};
|
||||
action.diagnostics = [diagnostic];
|
||||
codeActions.push(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
||||
return codeActions;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a dependency to the global requireables list in workspace settings
|
||||
* @param dependency - The dependency name to add
|
||||
*/
|
||||
async function addGlobalRequirable(dependency: string): Promise<void> {
|
||||
try {
|
||||
const config = vscode.workspace.getConfiguration(extensionName);
|
||||
const globalRequirables = config.get<string[]>('globalRequirables') || [];
|
||||
|
||||
if (!globalRequirables.includes(dependency)) {
|
||||
globalRequirables.push(dependency);
|
||||
await config.update('globalRequirables', globalRequirables, vscode.ConfigurationTarget.Workspace);
|
||||
vscode.window.showInformationMessage(`Added '${dependency}' to global requireables.`);
|
||||
|
||||
// Recompile to update diagnostics
|
||||
await compileLuaScripts();
|
||||
} else {
|
||||
vscode.window.showInformationMessage(`'${dependency}' is already in global requireables.`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Failed to remove addon paths from settings: ${err instanceof Error ? err.message : String(err)}`);
|
||||
throw err;
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
vscode.window.showErrorMessage(`Failed to add global requirable: ${errorMsg}`);
|
||||
logger.error(`Error adding global requirable: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,354 +1,149 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
/**
|
||||
* Manages lua-addons: discovery, versioning, copying, and cleanup.
|
||||
* All operations assume semver versioning (X.Y.Z format).
|
||||
* Addon folder names follow the pattern: `<addon-name>.<version>` (e.g., dcs-types.0.0.5)
|
||||
*/
|
||||
|
||||
interface LuaAddonInfo {
|
||||
name: string;
|
||||
sourceDir: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface InstalledAddonInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the extension's version from package.json
|
||||
* @param extensionRoot - Absolute path to the extension root directory
|
||||
* @returns The semantic version string (e.g., "0.0.5")
|
||||
* @throws Error if package.json cannot be read or version is not found
|
||||
*/
|
||||
export async function getExtensionVersion(extensionRoot: string): Promise<string> {
|
||||
try {
|
||||
const packageJsonPath = path.join(extensionRoot, 'package.json');
|
||||
const content = await fs.readFile(packageJsonPath, 'utf-8');
|
||||
const packageJson = JSON.parse(content);
|
||||
|
||||
const version = packageJson.version as string | undefined;
|
||||
if (!version) {
|
||||
throw new Error('Version field not found in package.json');
|
||||
}
|
||||
|
||||
return version;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to read extension version: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers all lua-addons in the bundled lua-addons directory
|
||||
* @param extensionPath - Absolute path to the extension installation directory
|
||||
* @returns Map of addon name to source directory path
|
||||
* @throws Error if lua-addons directory cannot be read
|
||||
*/
|
||||
export async function discoverLuaAddons(extensionPath: string): Promise<Map<string, string>> {
|
||||
const luaAddonsDir = path.join(extensionPath, 'lua-addons');
|
||||
const addons = new Map<string, string>();
|
||||
|
||||
try {
|
||||
// Check if lua-addons directory exists
|
||||
if (!existsSync(luaAddonsDir)) {
|
||||
return addons; // Empty map if no lua-addons
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const addonName = entry.name;
|
||||
const addonPath = path.join(luaAddonsDir, addonName);
|
||||
addons.set(addonName, addonPath);
|
||||
}
|
||||
}
|
||||
|
||||
return addons;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to discover lua-addons: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the workspace lua-addons directory path (.vscode/lua-addons)
|
||||
* @param workspaceRoot - Absolute path to the workspace root
|
||||
* @returns Absolute path to .vscode/lua-addons
|
||||
*/
|
||||
export function getWorkspaceLuaAddonsDir(workspaceRoot: string): string {
|
||||
return path.join(workspaceRoot, '.vscode', 'lua-addons');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses installed addon versions from the workspace lua-addons directory
|
||||
* Expects folder names in format: `<addon-name>.<version>` (e.g., dcs-types.0.0.5)
|
||||
* @param workspaceRoot - Absolute path to the workspace root
|
||||
* @returns Map of addon name to version string; returns empty map if directory doesn't exist
|
||||
* @throws Error if directory cannot be read
|
||||
*/
|
||||
export async function getInstalledVersions(
|
||||
workspaceRoot: string
|
||||
): Promise<Map<string, InstalledAddonInfo>> {
|
||||
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||
const installed = new Map<string, InstalledAddonInfo>();
|
||||
|
||||
try {
|
||||
// Return empty map if directory doesn't exist yet
|
||||
if (!existsSync(luaAddonsDir)) {
|
||||
return installed;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const folderName = entry.name;
|
||||
const parsed = parseAddonFolderName(folderName);
|
||||
|
||||
if (parsed) {
|
||||
installed.set(parsed.name, {
|
||||
name: parsed.name,
|
||||
version: parsed.version,
|
||||
path: path.join(luaAddonsDir, folderName)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return installed;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to read installed versions: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies lua-addons from the extension bundle to the workspace with versioned folder names
|
||||
* @param extensionPath - Absolute path to the extension installation directory
|
||||
* @param workspaceRoot - Absolute path to the workspace root
|
||||
* @param extensionVersion - The current extension version string
|
||||
* @param addonsToCopy - Map of addon name to source directory (from discoverLuaAddons)
|
||||
* @returns Map of addon name to installed path in workspace
|
||||
* @throws Error if copy fails or directory creation fails
|
||||
*/
|
||||
export async function copyLuaAddons(
|
||||
extensionPath: string,
|
||||
workspaceRoot: string,
|
||||
extensionVersion: string,
|
||||
addonsToCopy: Map<string, string>
|
||||
): Promise<Map<string, string>> {
|
||||
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||
const results = new Map<string, string>();
|
||||
|
||||
try {
|
||||
// Ensure .vscode directory exists
|
||||
const vscodeDir = path.join(workspaceRoot, '.vscode');
|
||||
await ensureDirectoryExists(vscodeDir);
|
||||
|
||||
// Ensure lua-addons directory exists
|
||||
await ensureDirectoryExists(luaAddonsDir);
|
||||
|
||||
// Copy each addon
|
||||
for (const [addonName, sourceDir] of addonsToCopy) {
|
||||
const versionedFolderName = `${addonName}.${extensionVersion}`;
|
||||
const destDir = path.join(luaAddonsDir, versionedFolderName);
|
||||
|
||||
// Remove destination if it already exists (shouldn't happen, but be safe)
|
||||
if (existsSync(destDir)) {
|
||||
await fs.rm(destDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Copy the addon
|
||||
await copyDirectory(sourceDir, destDir);
|
||||
results.set(addonName, destDir);
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to copy lua-addons: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all old versions of a specific addon
|
||||
* @param workspaceRoot - Absolute path to the workspace root
|
||||
* @param addonName - Name of the addon (e.g., "dcs-types")
|
||||
* @param currentVersion - Version to keep (e.g., "0.0.5")
|
||||
* @throws Error if directory operations fail
|
||||
*/
|
||||
export async function deleteOldVersions(
|
||||
workspaceRoot: string,
|
||||
addonName: string,
|
||||
currentVersion: string
|
||||
): Promise<void> {
|
||||
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||
|
||||
try {
|
||||
// Return silently if directory doesn't exist
|
||||
if (!existsSync(luaAddonsDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const parsed = parseAddonFolderName(entry.name);
|
||||
|
||||
// Delete if it's an old version of this addon
|
||||
if (parsed && parsed.name === addonName && parsed.version !== currentVersion) {
|
||||
const oldPath = path.join(luaAddonsDir, entry.name);
|
||||
await fs.rm(oldPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to delete old versions of ${addonName}: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an addon needs to be updated (version mismatch)
|
||||
* @param installedVersion - Currently installed version or undefined if not installed
|
||||
* @param currentExtensionVersion - Current extension version to compare against
|
||||
* @returns true if no version is installed or version doesn't match
|
||||
*/
|
||||
export function requiresUpdate(
|
||||
installedVersion: string | undefined,
|
||||
currentExtensionVersion: string
|
||||
): boolean {
|
||||
return !installedVersion || installedVersion !== currentExtensionVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all versioned addon folders for a specific addon name
|
||||
* @param workspaceRoot - Absolute path to the workspace root
|
||||
* @param addonName - Name of the addon to clean up completely
|
||||
* @throws Error if directory operations fail
|
||||
*/
|
||||
export async function deleteAllVersionsOfAddon(
|
||||
workspaceRoot: string,
|
||||
addonName: string
|
||||
): Promise<void> {
|
||||
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||
|
||||
try {
|
||||
// Return silently if directory doesn't exist
|
||||
if (!existsSync(luaAddonsDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const parsed = parseAddonFolderName(entry.name);
|
||||
|
||||
// Delete all versions of this addon
|
||||
if (parsed && parsed.name === addonName) {
|
||||
const addonPath = path.join(luaAddonsDir, entry.name);
|
||||
await fs.rm(addonPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to delete all versions of ${addonName}: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently installed addon info for all addons
|
||||
* @param workspaceRoot - Absolute path to the workspace root
|
||||
* @returns Map of addon name to its latest installed version info
|
||||
*/
|
||||
export async function getInstalledAddons(
|
||||
workspaceRoot: string
|
||||
): Promise<Map<string, InstalledAddonInfo>> {
|
||||
return getInstalledVersions(workspaceRoot);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Private Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parses an addon folder name in format `<name>.<version>`
|
||||
* @param folderName - The folder name to parse
|
||||
* @returns Object with name and version, or null if format doesn't match
|
||||
*/
|
||||
function parseAddonFolderName(
|
||||
folderName: string
|
||||
): { name: string; version: string } | null {
|
||||
// Split only on the last dot to handle addon names with dots (unlikely, but safe)
|
||||
const lastDotIndex = folderName.lastIndexOf('.');
|
||||
if (lastDotIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = folderName.substring(0, lastDotIndex);
|
||||
const version = folderName.substring(lastDotIndex + 1);
|
||||
|
||||
// Validate semver format (basic check)
|
||||
if (!/^\d+\.\d+\.\d+/.test(version)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { name, version };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a directory exists, creating it if necessary
|
||||
* @param dirPath - Absolute path to the directory
|
||||
*/
|
||||
async function ensureDirectoryExists(dirPath: string): Promise<void> {
|
||||
try {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
} catch (err) {
|
||||
// Ignore EEXIST errors
|
||||
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively copies a directory and its contents
|
||||
* @param srcDir - Source directory path
|
||||
* @param destDir - Destination directory path
|
||||
*/
|
||||
async function copyDirectory(srcDir: string, destDir: string): Promise<void> {
|
||||
await ensureDirectoryExists(destDir);
|
||||
|
||||
const entries = await fs.readdir(srcDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(srcDir, entry.name);
|
||||
const destPath = path.join(destDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyDirectory(srcPath, destPath);
|
||||
} else {
|
||||
await fs.copyFile(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
import path from "path";
|
||||
import * as fs from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
/*
|
||||
* Lua addon naming conventions:
|
||||
* <addon-name>.<major>.<minor>.<patch>
|
||||
*/
|
||||
|
||||
export class LuaAddonsManager {
|
||||
|
||||
/** Path where addons are installed */
|
||||
private targetPath: string;
|
||||
|
||||
/** Path where addon sources are located */
|
||||
private sourceExtensionPath: string;
|
||||
|
||||
private extensionContextPath: string;
|
||||
|
||||
/**
|
||||
* Creates a new LuaAddonsManager instance.
|
||||
*
|
||||
* @param extensionPath - Directory where addons are installed
|
||||
* @param sourceExtensionPath - Directory where addon sources are located
|
||||
*/
|
||||
public constructor(luaTargetPath: string, extensionPath: string) {
|
||||
this.targetPath = luaTargetPath;
|
||||
this.sourceExtensionPath = path.join(extensionPath, "lua-addons");
|
||||
this.extensionContextPath = extensionPath;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the version of an installed addon by name.
|
||||
*
|
||||
* Searches for a folder matching the naming pattern `<name>.<major>.<minor>.<patch>`
|
||||
* and returns the version string.
|
||||
*
|
||||
* @param name - The addon name to search for (without version suffix)
|
||||
* @returns The version string in format "major.minor.patch", or undefined if not found
|
||||
* @throws Never throws, returns undefined if addon not found
|
||||
*/
|
||||
async getExtensionVersion(name: string): Promise<string | undefined> {
|
||||
const folders = await fs.readdir(this.targetPath, { withFileTypes: true });
|
||||
for (const folder of folders) {
|
||||
if (!folder.isDirectory()) { continue; }
|
||||
|
||||
const versionMatch = folder.name.match(/^(.+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!versionMatch) { continue; }
|
||||
|
||||
const addonName = versionMatch[1];
|
||||
if (addonName === name) {
|
||||
return `${versionMatch[2]}.${versionMatch[3]}.${versionMatch[4]}`;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the version from the package.json file in the extension path.
|
||||
*
|
||||
* @returns The version string from package.json
|
||||
* @throws Error if package.json cannot be read or is missing the version field
|
||||
*/
|
||||
async getPackageVersion(): Promise<string | undefined> {
|
||||
try {
|
||||
const packageJsonPath = path.join(this.extensionContextPath,'package.json');
|
||||
const content = await fs.readFile(packageJsonPath, 'utf-8');
|
||||
const packageJson = JSON.parse(content);
|
||||
|
||||
const version = packageJson.version as string | undefined;
|
||||
if (!version) {
|
||||
throw new Error('Version field not found in package.json');
|
||||
}
|
||||
|
||||
return version;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to read extension version: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single addon by name.
|
||||
*
|
||||
* Deletes the addon directory matching the given name, regardless of version.
|
||||
*
|
||||
* @param name - The addon name to remove (without version suffix)
|
||||
* @throws Error if the removal operation fails
|
||||
*/
|
||||
async removeExtension(name: string): Promise<void> {
|
||||
return this.removeAllExtensions([name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes multiple addons by name.
|
||||
*
|
||||
* Scans the extension directory for folders matching the addon naming pattern
|
||||
* `<name>.<major>.<minor>.<patch>` and removes those whose base name is in the
|
||||
* provided list. Removes all versions of matched addons.
|
||||
*
|
||||
* @param names - Array of addon names to remove (without version suffix)
|
||||
* @throws Error if the removal operation fails
|
||||
*/
|
||||
async removeAllExtensions(names: string[]): Promise<void> {
|
||||
const folders = await fs.readdir(this.targetPath, { withFileTypes: true });
|
||||
for (const folder of folders) {
|
||||
if (!folder.isDirectory()) { continue; }
|
||||
|
||||
// Check if folder name matches versioning pattern: name.major.minor.patch
|
||||
const versionMatch = folder.name.match(/^(.+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!versionMatch) { continue; }
|
||||
|
||||
// Extract addon name from the versioned folder name
|
||||
const addonName = versionMatch[1];
|
||||
|
||||
if (names.includes(addonName)) {
|
||||
const folderPath = path.join(this.targetPath, folder.name);
|
||||
await fs.rm(folderPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs an addon from source to the target installation directory.
|
||||
*
|
||||
* Copies the addon directory from sourceExtensionPath to extensionPath.
|
||||
* The source directory name should match the addon name.
|
||||
*
|
||||
* @param name - The addon name/directory to install (without version suffix)
|
||||
* @throws Error if source directory not found or copy operation fails
|
||||
*/
|
||||
async installExtension(name: string): Promise<void> {
|
||||
const sourceFolderPath = path.join(this.sourceExtensionPath, name);
|
||||
|
||||
const version = await this.getPackageVersion();
|
||||
const versionedName = `${name}.${version}`;
|
||||
const targetFolderPath = path.join(this.targetPath, versionedName);
|
||||
|
||||
if (!existsSync(sourceFolderPath)) {
|
||||
throw new Error(`Source extension folder not found: ${sourceFolderPath}`);
|
||||
}
|
||||
await fs.mkdir(targetFolderPath, { recursive: true });
|
||||
await fs.cp(sourceFolderPath, targetFolderPath, { recursive: true });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,14 +3,14 @@ import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { existsSync } from 'fs';
|
||||
import * as luaAddonsManager from '../lua-addons-manager';
|
||||
import { LuaAddonsManager } from '../lua-addons-manager';
|
||||
|
||||
/**
|
||||
* Integration tests for lua-addons-manager
|
||||
* Integration tests for LuaAddonsManager
|
||||
* Uses temporary directories to avoid polluting the file system
|
||||
*/
|
||||
|
||||
suite('lua-addons-manager', () => {
|
||||
suite('LuaAddonsManager', () => {
|
||||
let tempDir: string;
|
||||
|
||||
suiteSetup(async () => {
|
||||
@@ -24,287 +24,232 @@ suite('lua-addons-manager', () => {
|
||||
});
|
||||
|
||||
suite('getExtensionVersion', () => {
|
||||
test('should read version from package.json', async () => {
|
||||
const testDir = path.join(tempDir, 'getExtensionVersion-1');
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
const packageJson = path.join(testDir, 'package.json');
|
||||
await fs.writeFile(packageJson, JSON.stringify({ version: '1.2.3' }));
|
||||
test('should return undefined if addon not found', async () => {
|
||||
const extensionDir = path.join(tempDir, 'getExtensionVersion-1');
|
||||
const sourceDir = path.join(tempDir, 'getExtensionVersion-1-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
|
||||
const version = await luaAddonsManager.getExtensionVersion(testDir);
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
const version = await manager.getExtensionVersion('nonexistent');
|
||||
|
||||
assert.strictEqual(version, undefined);
|
||||
});
|
||||
|
||||
test('should return version string for installed addon', async () => {
|
||||
const extensionDir = path.join(tempDir, 'getExtensionVersion-2');
|
||||
const sourceDir = path.join(tempDir, 'getExtensionVersion-2-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'dcs-types.1.2.3'), { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
const version = await manager.getExtensionVersion('dcs-types');
|
||||
|
||||
assert.strictEqual(version, '1.2.3');
|
||||
});
|
||||
|
||||
test('should throw error if package.json not found', async () => {
|
||||
const testDir = path.join(tempDir, 'getExtensionVersion-2');
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
test('should return first matching version if multiple versions exist', async () => {
|
||||
const extensionDir = path.join(tempDir, 'getExtensionVersion-3');
|
||||
const sourceDir = path.join(tempDir, 'getExtensionVersion-3-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon.1.0.0'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon.2.0.0'), { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
const version = await manager.getExtensionVersion('addon');
|
||||
|
||||
// Should return one of the versions (first found)
|
||||
assert.match(version!, /^[12]\.0\.0$/);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getPackageVersion', () => {
|
||||
test('should read version from package.json', async () => {
|
||||
const extensionDir = path.join(tempDir, 'getPackageVersion-1');
|
||||
const sourceDir = path.join(tempDir, 'getPackageVersion-1-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
const packageJson = path.join(extensionDir, 'package.json');
|
||||
await fs.writeFile(packageJson, JSON.stringify({ version: '2.1.0' }));
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
const version = await manager.getPackageVersion();
|
||||
|
||||
assert.strictEqual(version, '2.1.0');
|
||||
});
|
||||
|
||||
test('should throw error if package.json not found', async () => {
|
||||
const extensionDir = path.join(tempDir, 'getPackageVersion-2');
|
||||
const sourceDir = path.join(tempDir, 'getPackageVersion-2-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await assert.rejects(
|
||||
() => luaAddonsManager.getExtensionVersion(testDir),
|
||||
() => manager.getPackageVersion(),
|
||||
/Failed to read extension version/
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw error if version field missing', async () => {
|
||||
const testDir = path.join(tempDir, 'getExtensionVersion-3');
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
const packageJson = path.join(testDir, 'package.json');
|
||||
const extensionDir = path.join(tempDir, 'getPackageVersion-3');
|
||||
const sourceDir = path.join(tempDir, 'getPackageVersion-3-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
const packageJson = path.join(extensionDir, 'package.json');
|
||||
await fs.writeFile(packageJson, JSON.stringify({ name: 'test' }));
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await assert.rejects(
|
||||
() => luaAddonsManager.getExtensionVersion(testDir),
|
||||
() => manager.getPackageVersion(),
|
||||
/Version field not found/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('discoverLuaAddons', () => {
|
||||
test('should discover lua-addons directories', async () => {
|
||||
const testDir = path.join(tempDir, 'discoverLuaAddons-1');
|
||||
const luaAddonsDir = path.join(testDir, 'lua-addons');
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1'), { recursive: true });
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'addon2'), { recursive: true });
|
||||
suite('removeExtension', () => {
|
||||
test('should remove addon by name', async () => {
|
||||
const extensionDir = path.join(tempDir, 'removeExtension-1');
|
||||
const sourceDir = path.join(tempDir, 'removeExtension-1-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.1.0.0'), { recursive: true });
|
||||
|
||||
const addons = await luaAddonsManager.discoverLuaAddons(testDir);
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.removeExtension('addon1');
|
||||
|
||||
assert.strictEqual(addons.size, 2);
|
||||
assert.strictEqual(addons.has('addon1'), true);
|
||||
assert.strictEqual(addons.has('addon2'), true);
|
||||
const remaining = await fs.readdir(extensionDir);
|
||||
assert.strictEqual(remaining.length, 0);
|
||||
});
|
||||
|
||||
test('should return empty map if lua-addons directory does not exist', async () => {
|
||||
const testDir = path.join(tempDir, 'discoverLuaAddons-2');
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
test('should not affect other addons', async () => {
|
||||
const extensionDir = path.join(tempDir, 'removeExtension-2');
|
||||
const sourceDir = path.join(tempDir, 'removeExtension-2-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.1.0.0'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon2.1.0.0'), { recursive: true });
|
||||
|
||||
const addons = await luaAddonsManager.discoverLuaAddons(testDir);
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.removeExtension('addon1');
|
||||
|
||||
assert.strictEqual(addons.size, 0);
|
||||
});
|
||||
|
||||
test('should ignore non-directory entries', async () => {
|
||||
const testDir = path.join(tempDir, 'discoverLuaAddons-3');
|
||||
const luaAddonsDir = path.join(testDir, 'lua-addons');
|
||||
await fs.mkdir(luaAddonsDir, { recursive: true });
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1'), { recursive: true });
|
||||
await fs.writeFile(path.join(luaAddonsDir, 'file.txt'), 'test');
|
||||
|
||||
const addons = await luaAddonsManager.discoverLuaAddons(testDir);
|
||||
|
||||
assert.strictEqual(addons.size, 1);
|
||||
assert.strictEqual(addons.has('addon1'), true);
|
||||
const remaining = await fs.readdir(extensionDir);
|
||||
assert.strictEqual(remaining.length, 1);
|
||||
assert.strictEqual(remaining[0], 'addon2.1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
suite('getWorkspaceLuaAddonsDir', () => {
|
||||
test('should return correct .vscode/lua-addons path', () => {
|
||||
const workspaceRoot = '/path/to/workspace';
|
||||
const result = luaAddonsManager.getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||
suite('removeAllExtensions', () => {
|
||||
test('should remove multiple addons by name', async () => {
|
||||
const extensionDir = path.join(tempDir, 'removeAllExtensions-1');
|
||||
const sourceDir = path.join(tempDir, 'removeAllExtensions-1-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.1.0.0'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon2.1.0.0'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon3.1.0.0'), { recursive: true });
|
||||
|
||||
assert.strictEqual(result, path.join(workspaceRoot, '.vscode', 'lua-addons'));
|
||||
});
|
||||
});
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.removeAllExtensions(['addon1', 'addon2']);
|
||||
|
||||
suite('getInstalledVersions', () => {
|
||||
test('should parse installed addon versions from folder names', async () => {
|
||||
const testDir = path.join(tempDir, 'getInstalledVersions-1');
|
||||
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'dcs-types.0.0.5'), { recursive: true });
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'mission-utils.1.2.3'), { recursive: true });
|
||||
|
||||
const installed = await luaAddonsManager.getInstalledVersions(testDir);
|
||||
|
||||
assert.strictEqual(installed.size, 2);
|
||||
assert.strictEqual(installed.has('dcs-types'), true);
|
||||
assert.strictEqual(installed.has('mission-utils'), true);
|
||||
assert.strictEqual(installed.get('dcs-types')?.version, '0.0.5');
|
||||
assert.strictEqual(installed.get('mission-utils')?.version, '1.2.3');
|
||||
const remaining = await fs.readdir(extensionDir);
|
||||
assert.deepStrictEqual(remaining.sort(), ['addon3.1.0.0']);
|
||||
});
|
||||
|
||||
test('should return empty map if directory does not exist', async () => {
|
||||
const testDir = path.join(tempDir, 'getInstalledVersions-2');
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
test('should delete all versions of matching addons', async () => {
|
||||
const extensionDir = path.join(tempDir, 'removeAllExtensions-2');
|
||||
const sourceDir = path.join(tempDir, 'removeAllExtensions-2-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.0.0.1'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.0.0.2'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.0.0.3'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon2.1.0.0'), { recursive: true });
|
||||
|
||||
const installed = await luaAddonsManager.getInstalledVersions(testDir);
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.removeAllExtensions(['addon1']);
|
||||
|
||||
assert.strictEqual(installed.size, 0);
|
||||
const remaining = await fs.readdir(extensionDir);
|
||||
assert.deepStrictEqual(remaining.sort(), ['addon2.1.0.0']);
|
||||
});
|
||||
|
||||
test('should ignore folders with invalid version format', async () => {
|
||||
const testDir = path.join(tempDir, 'getInstalledVersions-3');
|
||||
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'dcs-types.0.0.5'), { recursive: true });
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'invalid-addon'), { recursive: true });
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'bad-format.v1'), { recursive: true });
|
||||
const extensionDir = path.join(tempDir, 'removeAllExtensions-3');
|
||||
const sourceDir = path.join(tempDir, 'removeAllExtensions-3-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.1.0.0'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'invalid-folder'), { recursive: true });
|
||||
|
||||
const installed = await luaAddonsManager.getInstalledVersions(testDir);
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.removeAllExtensions(['addon1']);
|
||||
|
||||
assert.strictEqual(installed.size, 1);
|
||||
assert.strictEqual(installed.has('dcs-types'), true);
|
||||
assert.strictEqual(installed.has('invalid-addon'), false);
|
||||
assert.strictEqual(installed.has('bad-format'), false);
|
||||
const remaining = await fs.readdir(extensionDir);
|
||||
assert.deepStrictEqual(remaining.sort(), ['invalid-folder']);
|
||||
});
|
||||
|
||||
test('should not throw if addon does not exist', async () => {
|
||||
const extensionDir = path.join(tempDir, 'removeAllExtensions-4');
|
||||
const sourceDir = path.join(tempDir, 'removeAllExtensions-4-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
// Should not throw
|
||||
await manager.removeAllExtensions(['nonexistent']);
|
||||
});
|
||||
});
|
||||
|
||||
suite('copyLuaAddons', () => {
|
||||
test('should copy addons with versioned folder names', async () => {
|
||||
const testDir = path.join(tempDir, 'copyLuaAddons-1');
|
||||
const sourceAddon1 = path.join(testDir, 'source-addons', 'addon1');
|
||||
await fs.mkdir(sourceAddon1, { recursive: true });
|
||||
await fs.writeFile(path.join(sourceAddon1, 'file1.lua'), 'content1');
|
||||
suite('installExtension', () => {
|
||||
test('should copy addon from source to target', async () => {
|
||||
const extensionDir = path.join(tempDir, 'installExtension-1');
|
||||
const sourceDir = path.join(tempDir, 'installExtension-1-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
const sourceAddon = path.join(sourceDir, 'addon1');
|
||||
await fs.mkdir(sourceAddon, { recursive: true });
|
||||
await fs.writeFile(path.join(sourceAddon, 'file.lua'), 'content');
|
||||
|
||||
const addonsToCopy = new Map<string, string>([
|
||||
['addon1', sourceAddon1]
|
||||
]);
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.installExtension('addon1');
|
||||
|
||||
const result = await luaAddonsManager.copyLuaAddons(
|
||||
testDir,
|
||||
testDir,
|
||||
'1.0.0',
|
||||
addonsToCopy
|
||||
);
|
||||
|
||||
assert.strictEqual(result.size, 1);
|
||||
const copiedPath = result.get('addon1');
|
||||
assert.strictEqual(copiedPath !== undefined, true);
|
||||
if (copiedPath) {
|
||||
assert.strictEqual(copiedPath.includes('addon1.1.0.0'), true);
|
||||
assert.strictEqual(existsSync(path.join(copiedPath, 'file1.lua')), true);
|
||||
}
|
||||
const targetAddon = path.join(extensionDir, 'addon1');
|
||||
assert.strictEqual(existsSync(targetAddon), true);
|
||||
assert.strictEqual(existsSync(path.join(targetAddon, 'file.lua')), true);
|
||||
});
|
||||
|
||||
test('should create .vscode directory if it does not exist', async () => {
|
||||
const testDir = path.join(tempDir, 'copyLuaAddons-2');
|
||||
const sourceAddon = path.join(testDir, 'source', 'addon1');
|
||||
await fs.mkdir(sourceAddon, { recursive: true });
|
||||
test('should throw error if source directory does not exist', async () => {
|
||||
const extensionDir = path.join(tempDir, 'installExtension-2');
|
||||
const sourceDir = path.join(tempDir, 'installExtension-2-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
|
||||
const addonsToCopy = new Map<string, string>([
|
||||
['addon1', sourceAddon]
|
||||
]);
|
||||
|
||||
await luaAddonsManager.copyLuaAddons(
|
||||
testDir,
|
||||
testDir,
|
||||
'1.0.0',
|
||||
addonsToCopy
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await assert.rejects(
|
||||
() => manager.installExtension('nonexistent'),
|
||||
/Source extension folder not found/
|
||||
);
|
||||
|
||||
const vscodeDir = path.join(testDir, '.vscode');
|
||||
assert.strictEqual(existsSync(vscodeDir), true);
|
||||
});
|
||||
|
||||
test('should copy nested directories recursively', async () => {
|
||||
const testDir = path.join(tempDir, 'copyLuaAddons-3');
|
||||
const sourceAddon = path.join(testDir, 'source', 'addon1');
|
||||
const extensionDir = path.join(tempDir, 'installExtension-3');
|
||||
const sourceDir = path.join(tempDir, 'installExtension-3-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
const sourceAddon = path.join(sourceDir, 'addon1');
|
||||
await fs.mkdir(path.join(sourceAddon, 'subdir'), { recursive: true });
|
||||
await fs.writeFile(path.join(sourceAddon, 'file.lua'), 'root');
|
||||
await fs.writeFile(path.join(sourceAddon, 'subdir', 'file.lua'), 'nested');
|
||||
|
||||
const addonsToCopy = new Map<string, string>([
|
||||
['addon1', sourceAddon]
|
||||
]);
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.installExtension('addon1');
|
||||
|
||||
const result = await luaAddonsManager.copyLuaAddons(
|
||||
testDir,
|
||||
testDir,
|
||||
'1.0.0',
|
||||
addonsToCopy
|
||||
);
|
||||
|
||||
const copiedPath = result.get('addon1')!;
|
||||
assert.strictEqual(existsSync(path.join(copiedPath, 'file.lua')), true);
|
||||
assert.strictEqual(existsSync(path.join(copiedPath, 'subdir', 'file.lua')), true);
|
||||
});
|
||||
});
|
||||
|
||||
suite('deleteOldVersions', () => {
|
||||
test('should delete old versions of an addon', async () => {
|
||||
const testDir = path.join(tempDir, 'deleteOldVersions-1');
|
||||
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.1'), { recursive: true });
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.2'), { recursive: true });
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.3'), { recursive: true });
|
||||
|
||||
await luaAddonsManager.deleteOldVersions(testDir, 'addon1', '0.0.3');
|
||||
|
||||
const remaining = await fs.readdir(luaAddonsDir);
|
||||
assert.deepStrictEqual(remaining.sort(), ['addon1.0.0.3']);
|
||||
});
|
||||
|
||||
test('should not delete other addons', async () => {
|
||||
const testDir = path.join(tempDir, 'deleteOldVersions-2');
|
||||
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.1'), { recursive: true });
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.2'), { recursive: true });
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'addon2.0.0.1'), { recursive: true });
|
||||
|
||||
await luaAddonsManager.deleteOldVersions(testDir, 'addon1', '0.0.2');
|
||||
|
||||
const remaining = await fs.readdir(luaAddonsDir);
|
||||
assert.strictEqual(remaining.includes('addon1.0.0.2'), true);
|
||||
assert.strictEqual(remaining.includes('addon2.0.0.1'), true);
|
||||
assert.strictEqual(remaining.includes('addon1.0.0.1'), false);
|
||||
});
|
||||
|
||||
test('should handle missing directory gracefully', async () => {
|
||||
const testDir = path.join(tempDir, 'deleteOldVersions-3');
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
|
||||
// Should not throw
|
||||
await luaAddonsManager.deleteOldVersions(testDir, 'addon1', '0.0.1');
|
||||
});
|
||||
});
|
||||
|
||||
suite('deleteAllVersionsOfAddon', () => {
|
||||
test('should delete all versions of an addon', async () => {
|
||||
const testDir = path.join(tempDir, 'deleteAllVersionsOfAddon-1');
|
||||
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.1'), { recursive: true });
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.2'), { recursive: true });
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'addon2.0.0.1'), { recursive: true });
|
||||
|
||||
await luaAddonsManager.deleteAllVersionsOfAddon(testDir, 'addon1');
|
||||
|
||||
const remaining = await fs.readdir(luaAddonsDir);
|
||||
assert.deepStrictEqual(remaining.sort(), ['addon2.0.0.1']);
|
||||
});
|
||||
});
|
||||
|
||||
suite('requiresUpdate', () => {
|
||||
test('should return true if no version installed', () => {
|
||||
const result = luaAddonsManager.requiresUpdate(undefined, '1.0.0');
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
|
||||
test('should return true if versions do not match', () => {
|
||||
const result = luaAddonsManager.requiresUpdate('1.0.0', '1.0.1');
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
|
||||
test('should return false if versions match', () => {
|
||||
const result = luaAddonsManager.requiresUpdate('1.0.0', '1.0.0');
|
||||
|
||||
assert.strictEqual(result, false);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getInstalledAddons', () => {
|
||||
test('should return installed addon information', async () => {
|
||||
const testDir = path.join(tempDir, 'getInstalledAddons-1');
|
||||
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
||||
await fs.mkdir(path.join(luaAddonsDir, 'dcs-types.0.0.5'), { recursive: true });
|
||||
|
||||
const installed = await luaAddonsManager.getInstalledAddons(testDir);
|
||||
|
||||
assert.strictEqual(installed.size, 1);
|
||||
const addonInfo = installed.get('dcs-types');
|
||||
assert.strictEqual(addonInfo !== undefined, true);
|
||||
if (addonInfo) {
|
||||
assert.strictEqual(addonInfo.name, 'dcs-types');
|
||||
assert.strictEqual(addonInfo.version, '0.0.5');
|
||||
assert.strictEqual(addonInfo.path.includes('dcs-types.0.0.5'), true);
|
||||
}
|
||||
const targetAddon = path.join(extensionDir, 'addon1');
|
||||
assert.strictEqual(existsSync(path.join(targetAddon, 'file.lua')), true);
|
||||
assert.strictEqual(existsSync(path.join(targetAddon, 'subdir', 'file.lua')), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user