Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions src/devtools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ class DevToolsExtension {
* @returns {Promise<void>}
*/
async refreshMetadataTypesInBackground(): Promise<void> {
const sessionLogger = new VsceLogger();
const packageName = this.mcdev.getPackageName();
this.tooltipProvider.addCacheEntry("metadataTypes", "Metadata Types");
this.tooltipProvider.setCacheLoading("metadataTypes");
Expand All @@ -362,24 +363,51 @@ class DevToolsExtension {
try {
const workspace = this.vscodeEditor.getWorkspace();
const workspacePath = workspace.getWorkspaceFsPath();
const packageName = this.mcdev.getPackageName();

// Start a dedicated log session for this background operation so that any relevant logs are captured in the VSCE log file
sessionLogger.startSession(workspacePath);

// Handler function to process the output of the 'explainTypes' command execution
const onResult = ({ output, error }: TUtils.IOutputLogger): void => {
if (output) {
console.log(`[index_refreshMetadataTypesInBackground]: Command ExplainTypes output: ${output}`);
this.mcdev.updateMetadataTypes(output);
}
if (error) {
Comment on lines +371 to +377
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onResult calls mcdev.updateMetadataTypes(output) for every stdout chunk. Terminal.executeTerminalCommand streams data events (and trims per chunk), so output is not guaranteed to be the full JSON and may be corrupted, causing JSON parsing failures or partial updates. Buffer stdout in this method (or add a capture mode in Terminal/Mcdev) and call updateMetadataTypes once after execute() completes, with the complete, unmodified output.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls fix

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The executeTerminalCommand always returns the full json and not in chunks... I'm not sure what to do with this one.

this.writeLog(
packageName,
`[index_refreshMetadataTypesInBackground]: Command ExplainTypes returned an error: ${error}`,
EnumsExtension.LoggerLevel.ERROR,
sessionLogger
);
}
Comment thread
anasilva105 marked this conversation as resolved.
};

const types = await this.mcdev.runExplainTypes(workspacePath);
if (!types) return;
// Executes the 'explainTypes' command and updates metadata types if there are changes; logs any errors encountered
const { success } = await this.mcdev.execute("explainTypes", onResult, {
projectPath: workspacePath
});

const updated = this.mcdev.updateMetadataTypes(types);
if (updated) {
// Writes an error log if the command execution failed
if (!success) {
this.writeLog(
packageName,
`Metadata types updated from '${packageName} explainTypes --json' (${types.length} types loaded)`,
EnumsExtension.LoggerLevel.INFO
`[index_refreshMetadataTypesInBackground]: Error occurred while executing mcdev explainTypes.`,
EnumsExtension.LoggerLevel.ERROR,
sessionLogger
);
}
// End the log session, keeping the log file if there were errors (success = false) or if any logs were written; otherwise discard the log file
sessionLogger.endSession(success);
} catch (error) {
Comment thread
anasilva105 marked this conversation as resolved.
this.writeLog(
packageName,
`[index_refreshMetadataTypesInBackground]: ${error}`,
EnumsExtension.LoggerLevel.WARN
);
// End the log session, keeping the log file since an error was logged
sessionLogger.endSession(false);
} finally {
this.tooltipProvider.setCacheDone("metadataTypes");
this.writeLog(packageName, "Caching metadata types done", EnumsExtension.LoggerLevel.INFO);
Expand Down
41 changes: 9 additions & 32 deletions src/devtools/mcdev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,45 +130,22 @@ class Mcdev {
};
}

/**
* Runs 'mcdev explainTypes --json' asynchronously and returns the parsed type list.
* Returns null if the command fails or the output cannot be parsed.
*
* @public
* @param {string} projectPath - working directory path for mcdev
* @returns {Promise<TDevTools.IMetadataTypes[] | null>} parsed metadata types or null on failure
*/
public async runExplainTypes(projectPath: string): Promise<TDevTools.IMetadataTypes[] | null> {
try {
const result = await Terminal.executeTerminalCommandCapture({
command: this.packageName,
commandArgs: ["et", "--json"],
commandCwd: projectPath
});

if (!result.success || !result.stdStreams.output) return null;

// The output may include non-JSON lines before the JSON array; extract the JSON portion
const jsonStart = result.stdStreams.output.indexOf("[");
if (jsonStart === -1) return null;
const jsonOutput = result.stdStreams.output.slice(jsonStart);

return JSON.parse(jsonOutput) as TDevTools.IMetadataTypes[];
} catch {
return null;
}
}

/**
* Updates the internal metadata types list with a new set of types.
* Returns true when the list was changed (new, removed, or modified types detected).
*
* @public
* @param {TDevTools.IMetadataTypes[]} types - updated metadata types list
* @param {string} types - raw JSON output / string containing the updated metadata types list
* @returns {boolean} true if the list changed, false otherwise
*/
public updateMetadataTypes(types: TDevTools.IMetadataTypes[]): boolean {
return this.metadataTypes.updateMetadataTypes(types);
public updateMetadataTypes(types: string): boolean {
// The output may include non-JSON lines before the JSON array; extract the JSON portion
const jsonStart = types.indexOf("[");
if (jsonStart === -1)
throw new Error(`[mcdev_updateMetadataTypes]: Invalid output for metadata types: ${types}`);
Comment thread
JoernBerkefeld marked this conversation as resolved.
// Extract the JSON array from the output
const jsonTypes = types.slice(jsonStart).trim();
return this.metadataTypes.updateMetadataTypes(jsonTypes);
}

/**
Expand Down
71 changes: 43 additions & 28 deletions src/devtools/metadatatypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,36 +85,51 @@ class MetadataTypes {
* Updates the metadata types list with the provided types.
* Returns true if any change is detected: new types, removed types, or modified properties of existing types.
*
* @param {TDevTools.IMetadataTypes[]} types - updated list of metadata types
* @param {string} types - updated list of metadata types
* @returns {boolean} true if the list changed, false otherwise
*/
updateMetadataTypes(types: TDevTools.IMetadataTypes[]): boolean {
const currentApiNames = new Set(this.metadataTypes.map(t => t.apiName));
const newApiNames = new Set(types.map(t => t.apiName));

const hasNewTypes = types.some(t => !currentApiNames.has(t.apiName));
const hasRemovedTypes = this.metadataTypes.some(t => !newApiNames.has(t.apiName));

const currentMap = new Map(this.metadataTypes.map(t => [t.apiName, t]));
const hasChangedTypes = types.some(t => {
const current = currentMap.get(t.apiName);
if (current === undefined) return false;
if (current.name !== t.name || current.description !== t.description) return true;
const curRBD = current.retrieveByDefault;
const newRBD = t.retrieveByDefault;
if (Array.isArray(curRBD) !== Array.isArray(newRBD)) return true;
if (Array.isArray(curRBD) && Array.isArray(newRBD)) {
if (curRBD.length !== newRBD.length || curRBD.some((v, i) => v !== newRBD[i])) return true;
} else if (curRBD !== newRBD) return true;
const supportsKeys = Object.keys(current.supports) as (keyof TDevTools.MetadataTypesActionsMap)[];
return supportsKeys.some(k => current.supports[k] !== t.supports[k]);
});

if (hasNewTypes || hasRemovedTypes || hasChangedTypes) {
this.metadataTypes = types;
return true;
updateMetadataTypes(types: string): boolean {
console.log("== MetadataTypes: Update Metadata Types ==");

try {
// Parse the input JSON string into an array of metadata types
const typesJson = JSON.parse(types) as TDevTools.IMetadataTypes[];

// Create sets of API names for current and new metadata types to detect additions and removals
const currentApiNames = new Set(this.metadataTypes.map(t => t.apiName));
const newApiNames = new Set(typesJson.map(t => t.apiName));

// Check for new types, removed types, and changed type details
const hasNewTypes = typesJson.some(t => !currentApiNames.has(t.apiName));
const hasRemovedTypes = this.metadataTypes.some(t => !newApiNames.has(t.apiName));

const currentMap = new Map(this.metadataTypes.map(t => [t.apiName, t]));

// Check for changes in existing types by comparing their properties
const hasChangedTypes = typesJson.some(t => {
const current = currentMap.get(t.apiName);
if (current === undefined) return false;
if (current.name !== t.name || current.description !== t.description) return true;
const curRBD = current.retrieveByDefault;
const newRBD = t.retrieveByDefault;
if (Array.isArray(curRBD) !== Array.isArray(newRBD)) return true;
if (Array.isArray(curRBD) && Array.isArray(newRBD)) {
if (curRBD.length !== newRBD.length || curRBD.some((v, i) => v !== newRBD[i])) return true;
} else if (curRBD !== newRBD) return true;
const supportsKeys = Object.keys(current.supports) as (keyof TDevTools.MetadataTypesActionsMap)[];
return supportsKeys.some(k => current.supports[k] !== t.supports[k]);
});

// If there are any new, removed, or changed types, update the stored metadata types and return true; otherwise, return false
if (hasNewTypes || hasRemovedTypes || hasChangedTypes) {
this.metadataTypes = typesJson;
return true;
}
return false;
} catch {
// If parsing or updating fails, log the error and return false (no update performed)
return false;
}
return false;
}

/**
Expand Down Expand Up @@ -148,7 +163,7 @@ class MetadataTypes {
* @returns {{ filename?: string; metadataTypeName?: string }} file name and metadata type name configured
*/
handleFileConfiguration(mdt: string, files: string[]): { filename?: string; metadataTypeName?: string } {
console.log("== MetadataTypes ExtractFileName ==");
console.log("== MetadataTypes HandleFileConfiguration ==");
if (mdt === "asset") {
const [assetName, filename] = files;

Expand Down
42 changes: 1 addition & 41 deletions src/utils/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,46 +95,6 @@ function executeCommand({
});
}

/**
* Executes a terminal command asynchronously and captures the full output.
* Unlike executeCommand, this accumulates all stdout/stderr output and returns
* it in the resolved result, making it suitable for commands like 'mcdev et --json'.
*
* @param {TUtils.ITerminalCommandRunner} param.command - terminal command
* @param {TUtils.ITerminalCommandRunner} param.commandArgs - terminal arguments
* @param {TUtils.ITerminalCommandRunner} param.commandCwd - terminal working directory path
* @returns {Promise<TUtils.ITerminalCommandResult>} object configured with the full result of executing the terminal command
*/
function executeTerminalCommandCapture({
command,
commandArgs,
commandCwd
}: TUtils.ITerminalCommandRunner): Promise<TUtils.ITerminalCommandResult> {
return new Promise(resolve => {
let output = "";
let error = "";
const cwd = commandCwd ? Lib.removeLeadingRootDrivePath(commandCwd) : commandCwd;
const proc = spawn(command, commandArgs, { shell: true, cwd });

if (proc.stdout)
proc.stdout.on("data", (data: Buffer) => {
output += data.toString();
});
if (proc.stderr)
proc.stderr.on("data", (data: Buffer) => {
error += data.toString();
});

proc.on("error", () => {
resolve({ success: false, stdStreams: { output: output.trim(), error: error.trim() } });
});

proc.on("close", code => {
resolve({ success: code === 0, stdStreams: { output: output.trim(), error: error.trim() } });
});
});
}

/**
* Executes terminal command either synchronously or asynchronously
*
Expand Down Expand Up @@ -201,4 +161,4 @@ function installPackage(packageName: string): TUtils.ITerminalCommandResult {
return terminal;
}

export { executeTerminalCommand, executeTerminalCommandCapture, isPackageInstalled, installPackage };
export { executeTerminalCommand, isPackageInstalled, installPackage };
Comment thread
anasilva105 marked this conversation as resolved.
Loading