mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Enforce vscode and ads version check when installing extensions (#4267)
* engine check when install extension * gallery install/update and vsix install * FIX COMMENTS * Fix the detail not loading issue when version is invalid * add more comments and adress PR comments * add install telemetry for install from vsix scenario * correct the name of the version property for telemetry
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"publisher": "Microsoft",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"sqlops": "*"
|
||||
"azdata": "*"
|
||||
},
|
||||
"extensionPack": [
|
||||
"Microsoft.agent",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"preview": true,
|
||||
"engines": {
|
||||
"vscode": "^1.25.0",
|
||||
"sqlops": "*"
|
||||
"azdata": "*"
|
||||
},
|
||||
"activationEvents": [
|
||||
"*"
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"icon": "images/sqlserver.png",
|
||||
"aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412",
|
||||
"engines": {
|
||||
"vscode": "0.10.x"
|
||||
"vscode": "*",
|
||||
"azdata": "^1.4.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"*"
|
||||
@@ -27,51 +28,51 @@
|
||||
"type": "object",
|
||||
"title": "Kubernetes configuration",
|
||||
"properties": {
|
||||
"mssql-bdc": {
|
||||
"type": "object",
|
||||
"description": "Kubernetes configuration",
|
||||
"properties": {
|
||||
"mssql-bdc.kubectl-path": {
|
||||
"type": "string",
|
||||
"description": "File path to a kubectl binary."
|
||||
},
|
||||
"mssql-bdc.kubectl-path.windows": {
|
||||
"type": "string",
|
||||
"description": "File path to a kubectl binary."
|
||||
},
|
||||
"mssql-bdc.kubectl-path.mac": {
|
||||
"type": "string",
|
||||
"description": "File path to a kubectl binary."
|
||||
},
|
||||
"mssql-bdc.kubectl-path.linux": {
|
||||
"type": "string",
|
||||
"description": "File path to a kubectl binary."
|
||||
},
|
||||
"mssql-bdc.kubeconfig": {
|
||||
"type": "string",
|
||||
"description": "File path to the kubeconfig file."
|
||||
},
|
||||
"mssql-bdc.knownKubeconfigs": {
|
||||
"type": "array",
|
||||
"description": "File paths to kubeconfig files from which you can select."
|
||||
},
|
||||
"mssql-bdc.outputFormat": {
|
||||
"enum": [
|
||||
"json",
|
||||
"yaml"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Output format for Kubernetes specs. One of 'json' or 'yaml' (default)."
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"mssql-bdc.namespace": "",
|
||||
"mssql-bdc.kubectl-path": "",
|
||||
"mssql-bdc.kubeconfig": "",
|
||||
"mssql-bdc.knownKubeconfigs": []
|
||||
}
|
||||
"mssql-bdc": {
|
||||
"type": "object",
|
||||
"description": "Kubernetes configuration",
|
||||
"properties": {
|
||||
"mssql-bdc.kubectl-path": {
|
||||
"type": "string",
|
||||
"description": "File path to a kubectl binary."
|
||||
},
|
||||
"mssql-bdc.kubectl-path.windows": {
|
||||
"type": "string",
|
||||
"description": "File path to a kubectl binary."
|
||||
},
|
||||
"mssql-bdc.kubectl-path.mac": {
|
||||
"type": "string",
|
||||
"description": "File path to a kubectl binary."
|
||||
},
|
||||
"mssql-bdc.kubectl-path.linux": {
|
||||
"type": "string",
|
||||
"description": "File path to a kubectl binary."
|
||||
},
|
||||
"mssql-bdc.kubeconfig": {
|
||||
"type": "string",
|
||||
"description": "File path to the kubeconfig file."
|
||||
},
|
||||
"mssql-bdc.knownKubeconfigs": {
|
||||
"type": "array",
|
||||
"description": "File paths to kubeconfig files from which you can select."
|
||||
},
|
||||
"mssql-bdc.outputFormat": {
|
||||
"enum": [
|
||||
"json",
|
||||
"yaml"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Output format for Kubernetes specs. One of 'json' or 'yaml' (default)."
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"mssql-bdc.namespace": "",
|
||||
"mssql-bdc.kubectl-path": "",
|
||||
"mssql-bdc.kubeconfig": "",
|
||||
"mssql-bdc.knownKubeconfigs": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
@@ -83,7 +84,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-nls": "^3.2.1",
|
||||
"download": "^6.2.5"
|
||||
"download": "^6.2.5",
|
||||
"shelljs": "^0.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha-junit-reporter": "^1.17.0",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"preview": true,
|
||||
"engines": {
|
||||
"vscode": "^1.25.0",
|
||||
"sqlops": "*"
|
||||
"azdata": "*"
|
||||
},
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/extensions/import/Microsoft_SQL_Server_Import_Extension_and_Tools_Import_Flat_File_Preview.docx",
|
||||
"icon": "images/sqlserver.png",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"private": true,
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"sqlops": "*"
|
||||
"azdata": "*"
|
||||
},
|
||||
"activationEvents": [
|
||||
"*"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"publisher": "Microsoft",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"sqlops": "*"
|
||||
"azdata": "*"
|
||||
},
|
||||
"main": "./out/extension",
|
||||
"activationEvents": [
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"publisher": "Microsoft",
|
||||
"engines": {
|
||||
"vscode": "^1.26.0",
|
||||
"sqlops": "*"
|
||||
"azdata": "*"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"repository": "https://github.com/Microsoft/azuredatastudio",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"preview": true,
|
||||
"engines": {
|
||||
"vscode": "^1.26.0",
|
||||
"sqlops": "*"
|
||||
"azdata": "*"
|
||||
},
|
||||
"icon": "images/sqlserver.png",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"preview": true,
|
||||
"engines": {
|
||||
"vscode": "^1.26.0",
|
||||
"sqlops": "*"
|
||||
"azdata": "*"
|
||||
},
|
||||
"icon": "images/sqlserver.png",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"publisher": "demo",
|
||||
"engines": {
|
||||
"vscode": "^1.26.0",
|
||||
"sqlops": "*"
|
||||
"azdata": "*"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
|
||||
@@ -117,7 +117,7 @@ export interface IExtensionManifest {
|
||||
publisher: string;
|
||||
version: string;
|
||||
// {{SQL CARBON EDIT}}
|
||||
engines: { vscode: string; sqlops?: string };
|
||||
engines: { vscode: string; azdata?: string };
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
main?: string;
|
||||
@@ -141,6 +141,8 @@ export interface IGalleryExtensionProperties {
|
||||
dependencies?: string[];
|
||||
extensionPack?: string[];
|
||||
engine?: string;
|
||||
// {{SQL CARBON EDIT}}
|
||||
azDataEngine?: string;
|
||||
localizedLanguages?: string[];
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,9 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any
|
||||
publisherId: extension.metadata ? extension.metadata.publisherId : null,
|
||||
publisherName: extension.manifest.publisher,
|
||||
publisherDisplayName: extension.metadata ? extension.metadata.publisherDisplayName : null,
|
||||
dependencies: extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length > 0
|
||||
dependencies: extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length > 0,
|
||||
// {{SQL CARBON EDIT}}
|
||||
extensionVersion: extension.manifest.version
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +101,8 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension):
|
||||
publisherName: extension.publisher,
|
||||
publisherDisplayName: extension.publisherDisplayName,
|
||||
dependencies: !!(extension.properties.dependencies && extension.properties.dependencies.length > 0),
|
||||
// {{SQL CARBON EDIT}}
|
||||
extensionVersion: extension.version,
|
||||
...extension.telemetryData
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,6 +123,8 @@ const PropertyType = {
|
||||
Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies',
|
||||
ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack',
|
||||
Engine: 'Microsoft.VisualStudio.Code.Engine',
|
||||
// {{SQL CARBON EDIT}}
|
||||
AzDataEngine: 'Microsoft.AzDataEngine',
|
||||
LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages'
|
||||
};
|
||||
|
||||
@@ -298,6 +300,12 @@ function getEngine(version: IRawGalleryExtensionVersion): string {
|
||||
return (values.length > 0 && values[0].value) || '';
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
function getAzureDataStudioEngine(version: IRawGalleryExtensionVersion): string {
|
||||
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.AzDataEngine) : [];
|
||||
return (values.length > 0 && values[0].value) || '';
|
||||
}
|
||||
|
||||
function getLocalizedLanguages(version: IRawGalleryExtensionVersion): string[] {
|
||||
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.LocalizedLanguages) : [];
|
||||
const value = (values.length > 0 && values[0].value) || '';
|
||||
@@ -343,6 +351,8 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
|
||||
dependencies: getExtensions(version, PropertyType.Dependency),
|
||||
extensionPack: getExtensions(version, PropertyType.ExtensionPack),
|
||||
engine: getEngine(version),
|
||||
// {{SQL CARBON EDIT}}
|
||||
azDataEngine: getAzureDataStudioEngine(version),
|
||||
localizedLanguages: getLocalizedLanguages(version)
|
||||
},
|
||||
/* __GDPR__FRAGMENT__
|
||||
@@ -724,7 +734,12 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
|
||||
}
|
||||
|
||||
loadCompatibleVersion(extension: IGalleryExtension, fromVersion: string = extension.version): Promise<IGalleryExtension> {
|
||||
if (extension.version === fromVersion && extension.properties.engine && isEngineValid(extension.properties.engine)) {
|
||||
// {{SQL CARBON EDIT}}
|
||||
// Change to original version: removed the extension version validation
|
||||
// Reason: This method is used to find the matching gallery extension for the locally installed extension,
|
||||
// since we only have one entry for each extension (not in-scope to enable mutiple version support for now),
|
||||
// if the new version of extension is not compatible, the extension won't be displayed properly.
|
||||
if (extension.version === fromVersion) {
|
||||
return Promise.resolve(extension);
|
||||
}
|
||||
const query = new Query()
|
||||
|
||||
@@ -46,6 +46,9 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
|
||||
|
||||
// {{SQL CARBON EDIT}
|
||||
import product from 'vs/platform/node/product';
|
||||
|
||||
const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem';
|
||||
const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser';
|
||||
const INSTALL_ERROR_UNSET_UNINSTALLED = 'unsetUninstalled';
|
||||
@@ -188,6 +191,9 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
}
|
||||
|
||||
install(vsix: URI, type: LocalExtensionType = LocalExtensionType.User): Promise<IExtensionIdentifier> {
|
||||
// {{SQL CARBON EDIT}}
|
||||
let startTime = new Date().getTime();
|
||||
|
||||
this.logService.trace('ExtensionManagementService#install', vsix.toString());
|
||||
return createCancelablePromise(token => {
|
||||
return this.downloadVsix(vsix)
|
||||
@@ -197,10 +203,11 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
return getManifest(zipPath)
|
||||
.then(manifest => {
|
||||
const identifier = { id: getLocalExtensionIdFromManifest(manifest) };
|
||||
// {{SQL CARBON EDIT - Remove VS Code version check}}
|
||||
// if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode)) {
|
||||
// return Promise.reject(new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", identifier.id, pkg.version)));
|
||||
// }
|
||||
// {{SQL CARBON EDIT - Check VSCode and ADS version}}
|
||||
if (manifest.engines && (!isEngineValid(manifest.engines.vscode, product.vscodeVersion)
|
||||
|| (manifest.engines.azdata && !isEngineValid(manifest.engines.azdata, pkg.version)))) {
|
||||
return Promise.reject(new Error(nls.localize('incompatible', "Unable to install version '{2}' of extension '{0}' as it is not compatible with Azure Data Studio '{1}'.", identifier.id, pkg.version, manifest.version)));
|
||||
}
|
||||
return this.removeIfExists(identifier.id)
|
||||
.then(
|
||||
() => {
|
||||
@@ -216,10 +223,13 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
// {{SQL CARBON EDIT}}
|
||||
// Until there's a gallery for SQL Ops Studio, skip retrieving the metadata from the gallery
|
||||
return this.installExtension({ zipPath, id: identifier.id, metadata: null }, type, token)
|
||||
.then(
|
||||
local => this._onDidInstallExtension.fire({ identifier, zipPath, local, operation: InstallOperation.Install }),
|
||||
error => { this._onDidInstallExtension.fire({ identifier, zipPath, error, operation: InstallOperation.Install }); return Promise.reject(error); }
|
||||
);
|
||||
.then(
|
||||
local => {
|
||||
this.reportTelemetry(this.getTelemetryEvent(InstallOperation.Install), getLocalExtensionTelemetryData(local), new Date().getTime() - startTime, void 0);
|
||||
this._onDidInstallExtension.fire({ identifier, zipPath, local, operation: InstallOperation.Install });
|
||||
},
|
||||
error => { this._onDidInstallExtension.fire({ identifier, zipPath, error, operation: InstallOperation.Install }); return Promise.reject(error); }
|
||||
);
|
||||
// return this.getMetadata(getGalleryExtensionId(manifest.publisher, manifest.name))
|
||||
// .then(
|
||||
// metadata => this.installFromZipPath(identifier, zipPath, metadata, type, token),
|
||||
|
||||
@@ -209,7 +209,7 @@ export interface IReducedExtensionDescription {
|
||||
engines: {
|
||||
vscode: string;
|
||||
// {{SQL CARBON EDIT}}
|
||||
sqlops?: string;
|
||||
azdata?: string;
|
||||
};
|
||||
main?: string;
|
||||
}
|
||||
@@ -222,12 +222,13 @@ export function isValidExtensionVersion(version: string, extensionDesc: IReduced
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
return (extensionDesc.engines.sqlops && extensionDesc.engines.sqlops === '*') || isVersionValid(version, extensionDesc.engines.sqlops, notices);
|
||||
return (extensionDesc.engines.azdata && extensionDesc.engines.azdata === '*') || isVersionValid(version, extensionDesc.engines.azdata, notices);
|
||||
}
|
||||
|
||||
export function isEngineValid(engine: string): boolean {
|
||||
// {{SQL CARBON EDIT}}
|
||||
export function isEngineValid(engine: string, version: string = pkg.version): boolean {
|
||||
// TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version
|
||||
return engine === '*' || isVersionValid(pkg.version, engine);
|
||||
return engine === '*' || isVersionValid(version, engine);
|
||||
}
|
||||
|
||||
export function isVersionValid(currentVersion: string, requestedVersion: string, notices: string[] = []): boolean {
|
||||
|
||||
@@ -191,6 +191,13 @@ export class InstallAction extends Action {
|
||||
return this.notificationService.error(err);
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
// Prompt the user that the current ADS version is not compatible with the extension,
|
||||
// return here as in this scenario it doesn't make sense for the user to download manually.
|
||||
if(err && err.code === INSTALL_ERROR_INCOMPATIBLE) {
|
||||
return this.notificationService.error(err);
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
|
||||
return promptDownloadManually(extension.gallery, localize('failedToInstall', "Failed to install \'{0}\'.", extension.id), err, this.instantiationService, this.notificationService, this.openerService);
|
||||
@@ -418,6 +425,13 @@ export class UpdateAction extends Action {
|
||||
return this.notificationService.error(err);
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
// Prompt the user that the current ADS version is not compatible with the extension,
|
||||
// return here as in this scenario it doesn't make sense for the user to download manually.
|
||||
if(err && err.code === INSTALL_ERROR_INCOMPATIBLE) {
|
||||
return this.notificationService.error(err);
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
|
||||
return promptDownloadManually(extension.gallery, localize('failedToUpdate', "Failed to update \'{0}\'.", extension.id), err, this.instantiationService, this.notificationService, this.openerService);
|
||||
|
||||
@@ -13,9 +13,10 @@ import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
// {{SQL CARBON EDIT}}
|
||||
import {
|
||||
IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, IExtensionManifest,
|
||||
InstallExtensionEvent, DidInstallExtensionEvent, LocalExtensionType, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionIdentifier, EnablementState, IExtensionManagementServerService
|
||||
InstallExtensionEvent, DidInstallExtensionEvent, LocalExtensionType, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionIdentifier, EnablementState, IExtensionManagementServerService, INSTALL_ERROR_INCOMPATIBLE
|
||||
} from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { getGalleryExtensionIdFromLocal, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, getMaliciousExtensionsSet, getLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
@@ -39,6 +40,11 @@ import * as resources from 'vs/base/common/resources';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
import pkg from 'vs/platform/node/package';
|
||||
import { isEngineValid } from 'vs/platform/extensions/node/extensionValidator';
|
||||
import { ExtensionManagementError } from 'vs/platform/extensionManagement/node/extensionManagementService';
|
||||
|
||||
interface IExtensionStateProvider<T> {
|
||||
(extension: Extension): T;
|
||||
}
|
||||
@@ -703,6 +709,13 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService,
|
||||
if (extensionIdentifier) {
|
||||
this.checkAndEnableDisabledDependencies(extensionIdentifier);
|
||||
}
|
||||
}
|
||||
// {{SQL CARBON EDIT}}
|
||||
// This is the error handler when installing local VSIX file.
|
||||
// Prompt the user about the error detail.
|
||||
, (error) => {
|
||||
this.notificationService.error(error);
|
||||
return Promise.reject(error);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -721,6 +734,15 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService,
|
||||
return Promise.reject(new Error('Missing gallery'));
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
// This is the execution path for install/update extension from marketplace.
|
||||
// Check both the vscode version and azure data studio version
|
||||
// The check is added here because we want to fail fast instead of downloading the VSIX and then fail.
|
||||
if (gallery.properties.engine && (!isEngineValid(gallery.properties.engine, product.vscodeVersion)
|
||||
|| (gallery.properties.azDataEngine && !isEngineValid(gallery.properties.azDataEngine, pkg.version)))) {
|
||||
return Promise.reject(new ExtensionManagementError(nls.localize('incompatible', "Unable to install version '{2}' of extension '{0}' as it is not compatible with Azure Data Studio '{1}'.", extension.id, pkg.version, gallery.version), INSTALL_ERROR_INCOMPATIBLE));
|
||||
}
|
||||
|
||||
return this.installWithProgress(
|
||||
// {{SQL CARBON EDIT}}
|
||||
() => {
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface IExtensionDescription {
|
||||
readonly engines: {
|
||||
vscode: string;
|
||||
// {{SQL CARBON EDIT}}
|
||||
sqlops?: string;
|
||||
azdata?: string;
|
||||
};
|
||||
readonly main?: string;
|
||||
readonly contributes?: { [point: string]: any; };
|
||||
|
||||
@@ -89,8 +89,8 @@ class ExtensionManifestParser extends ExtensionManifestHandler {
|
||||
manifest.uuid = manifest.__metadata.id;
|
||||
}
|
||||
// {{SQL CARBON EDIT}}
|
||||
if (manifest.engines && !manifest.engines.sqlops) {
|
||||
manifest.engines.sqlops = '*';
|
||||
if (manifest.engines && !manifest.engines.azdata) {
|
||||
manifest.engines.azdata = '*';
|
||||
}
|
||||
delete manifest.__metadata;
|
||||
return manifest;
|
||||
|
||||
Reference in New Issue
Block a user