diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 671dd7f5..6946bd69 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "viva-connections-toolkit", - "version": "4.20.0", + "version": "4.20.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "viva-connections-toolkit", - "version": "4.20.0", + "version": "4.20.1", "license": "MIT", "dependencies": { "@grconrad/vscode-extension-feedback": "1.0.0", diff --git a/package.json b/package.json index cd69ba6f..7782b357 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "viva-connections-toolkit", "displayName": "SharePoint Framework Toolkit", "description": "SharePoint Framework Toolkit aims to boost your productivity in developing and managing SharePoint Framework solutions helping at every stage of your development flow, from setting up your development workspace to deploying a solution straight to your tenant without the need to leave VS Code and now even create a CI/CD pipeline to introduce automate deployment of your app. This toolkit is provided by the community.", - "version": "4.20.0", + "version": "4.20.1", "publisher": "m365pnp", "preview": false, "homepage": "https://pnp.github.io/vscode-viva", @@ -833,10 +833,9 @@ "default": "npm", "enum": [ "npm", - "pnpm", - "yarn" + "pnpm" ], - "description": "Choose your preferred Node package manager. Choose between `npm`, `pnpm`, or `yarn`." + "description": "Choose your preferred Node package manager. Choose between `npm` and `pnpm`." }, "spfx-toolkit.showServiceIncidentList": { "title": "Show service health incidents", @@ -1297,6 +1296,12 @@ "category": "SPFx Toolkit", "icon": "$(trash)" }, + { + "command": "spfx-toolkit.exportSiteAppCatalogs", + "title": "Export Site App Catalogs", + "category": "SPFx Toolkit", + "icon": "$(export)" + }, { "command": "spfx-toolkit.moreTenantWideExtensionActions", "title": "...", @@ -1451,7 +1456,12 @@ { "command": "spfx-toolkit.addSiteAppCatalog", "when": "viewItem == sp-app-catalog-root", - "group": "inline" + "group": "inline@1" + }, + { + "command": "spfx-toolkit.exportSiteAppCatalogs", + "when": "viewItem == sp-app-catalog-root", + "group": "inline@2" }, { "command": "spfx-toolkit.removeSiteAppCatalog", diff --git a/scripts/update-package-version.ps1 b/scripts/update-package-version.ps1 index 447fc2e6..440d1914 100644 --- a/scripts/update-package-version.ps1 +++ b/scripts/update-package-version.ps1 @@ -8,4 +8,10 @@ $oldVersion = $packageJson.version $oldVersionParts = $oldVersion.Split(".") $newVersion = "$($oldVersionParts[0]).$($oldVersionParts[1]).$([int]$oldVersionParts[2] + 1)" $packageJson.version = $newVersion -$packageJson | ConvertTo-Json -Depth 10 | Out-File "$PathToWorkspace\package.json" -Encoding utf8 -Force \ No newline at end of file +$packageJson | ConvertTo-Json -Depth 10 | Out-File "$PathToWorkspace\package.json" -Encoding utf8 -Force + +$shrinkwrapPath = "$PathToWorkspace\npm-shrinkwrap.json" +$shrinkwrap = Get-Content $shrinkwrapPath -Raw | ConvertFrom-Json -AsHashtable +$shrinkwrap['version'] = $newVersion +$shrinkwrap['packages']['']['version'] = $newVersion +$shrinkwrap | ConvertTo-Json -Depth 100 | Out-File $shrinkwrapPath -Encoding utf8 -Force diff --git a/src/constants/Commands.ts b/src/constants/Commands.ts index b8f73f67..bdb2421c 100644 --- a/src/constants/Commands.ts +++ b/src/constants/Commands.ts @@ -101,6 +101,7 @@ export const Commands = { addTenantAppCatalog: `${EXTENSION_NAME}.addTenantAppCatalog`, addSiteAppCatalog: `${EXTENSION_NAME}.addSiteAppCatalog`, removeSiteAppCatalog: `${EXTENSION_NAME}.removeSiteAppCatalog`, + exportSiteAppCatalogs: `${EXTENSION_NAME}.exportSiteAppCatalogs`, // Set form customizer setFormCustomizer: `${EXTENSION_NAME}.setFormCustomizer`, diff --git a/src/constants/PackageManagers.ts b/src/constants/PackageManagers.ts index 66ddb285..1c06e718 100644 --- a/src/constants/PackageManagers.ts +++ b/src/constants/PackageManagers.ts @@ -1,6 +1,5 @@ // eslint-disable-next-line no-shadow export enum PackageManagers { npm = 'npm', - pnpm = 'pnpm', - yarn = 'yarn' + pnpm = 'pnpm' } \ No newline at end of file diff --git a/src/models/SiteAppCatalogExport.ts b/src/models/SiteAppCatalogExport.ts new file mode 100644 index 00000000..92847042 --- /dev/null +++ b/src/models/SiteAppCatalogExport.ts @@ -0,0 +1,5 @@ +export interface SiteAppCatalogExport { + url: string; + apps: Record[]; + error?: string; +} diff --git a/src/models/index.ts b/src/models/index.ts index 1434317e..b00875a4 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -5,6 +5,7 @@ export * from './GenerateWorkflowCommandInput'; export * from './Sample'; export * from './ServeConfig'; export * from './SiteAppCatalog'; +export * from './SiteAppCatalogExport'; export * from './solution-add-result'; export * from './SpfxAddComponentCommandInput'; export * from './SpfxDoctorOutput'; diff --git a/src/services/actions/CliActions.ts b/src/services/actions/CliActions.ts index 6ca4a159..761d8774 100644 --- a/src/services/actions/CliActions.ts +++ b/src/services/actions/CliActions.ts @@ -1,8 +1,9 @@ import { readFileSync, writeFileSync } from 'fs'; +import { homedir } from 'os'; import { Folders } from '../check/Folders'; import { commands, Progress, ProgressLocation, Uri, window, workspace, WorkspaceFolder } from 'vscode'; import { Commands, SpfxCompatibilityMatrix, WebViewType, WebviewCommand, WorkflowType } from '../../constants'; -import { AppCatalogApp, GenerateWorkflowCommandInput, SiteAppCatalog, SolutionAddResult, SpfxDoctorOutput, Subscription } from '../../models'; +import { AppCatalogApp, GenerateWorkflowCommandInput, SiteAppCatalog, SiteAppCatalogExport, SolutionAddResult, SpfxDoctorOutput, Subscription } from '../../models'; import { Extension } from '../dataType/Extension'; import { CliExecuter } from '../executeWrappers/CliCommandExecuter'; import { Notifications } from '../dataType/Notifications'; @@ -59,6 +60,9 @@ export class CliActions { subscriptions.push( commands.registerCommand(Commands.removeSiteAppCatalog, CliActions.removeSiteAppCatalog) ); + subscriptions.push( + commands.registerCommand(Commands.exportSiteAppCatalogs, CliActions.exportSiteAppCatalogs) + ); } /** @@ -175,15 +179,8 @@ export class CliActions { */ public static async getAppCatalogApps(appCatalogUrl?: string): Promise { try { - const commandOptions: any = appCatalogUrl && appCatalogUrl.trim() !== '' ? { - appCatalogScope: 'sitecollection', - appCatalogUrl: appCatalogUrl - } : {}; - - const response = (await CliExecuter.execute('spo app list', 'json', commandOptions)); - const apps = response?.stdout || '[]'; + const appsJson = await CliActions.getAppCatalogAppsRaw(appCatalogUrl); - const appsJson: any[] = JSON.parse(apps); const appList = appsJson.map(({ ID, Title, Deployed, IsEnabled }) => { return { ID, @@ -200,6 +197,23 @@ export class CliActions { } } + /** + * Retrieves the unmapped 'spo app list' output for the tenant or site app catalog. + * + * @param appCatalogUrl The URL of the tenant or site app catalog. + * @returns A promise that resolves to the app objects as returned by the CLI, with all their properties. + */ + public static async getAppCatalogAppsRaw(appCatalogUrl?: string): Promise { + const commandOptions: any = appCatalogUrl && appCatalogUrl.trim() !== '' ? { + appCatalogScope: 'sitecollection', + appCatalogUrl: appCatalogUrl + } : {}; + + const response = (await CliExecuter.execute('spo app list', 'json', commandOptions)); + + return JSON.parse(response?.stdout || '[]'); + } + /** * Retrieves the tenant-wide extensions from the specified tenant app catalog URL. * @param tenantAppCatalogUrl The URL of the tenant app catalog. @@ -671,6 +685,86 @@ export class CliActions { } } + /** + * Exports an inventory of all site collection app catalogs and the apps they contain. + */ + public static async exportSiteAppCatalogs() { + try { + const appCatalogUrls = await CliActions.appCatalogUrlsGet(); + // the first entry is the tenant app catalog, which is out of scope for this export + const siteAppCatalogUrls = appCatalogUrls?.slice(1) ?? []; + + if (siteAppCatalogUrls.length === 0) { + Notifications.warning('No site app catalogs found to export.'); + return; + } + + const siteAppCatalogs = await window.withProgress({ + location: ProgressLocation.Notification, + title: `Collecting the site app catalog details... Check [output window](command:${Commands.showOutputChannel}) to follow the progress.`, + cancellable: false + }, async () => { + const catalogs: SiteAppCatalogExport[] = []; + + for (const siteAppCatalogUrl of siteAppCatalogUrls) { + try { + catalogs.push({ + url: siteAppCatalogUrl, + apps: await CliActions.getAppCatalogAppsRaw(siteAppCatalogUrl) + }); + } catch (e: any) { + catalogs.push({ + url: siteAppCatalogUrl, + apps: [], + error: e?.error?.message || e?.message || 'Failed to retrieve the apps of this site app catalog.' + }); + } + } + + return catalogs; + }); + + const timestamp = new Date().toISOString().slice(0, 16).replace(/[:T]/g, '-'); + const defaultPath = join(workspace.workspaceFolders?.[0]?.uri.fsPath || homedir(), `site-app-catalogs-${timestamp}.json`); + + const targetUri = await window.showSaveDialog({ + defaultUri: Uri.file(defaultPath), + filters: { JSON: ['json'] }, + saveLabel: 'Export' + }); + + if (!targetUri) { + return; + } + + writeFileSync(targetUri.fsPath, CliActions.buildSiteAppCatalogsExport(siteAppCatalogs), 'utf8'); + + const openFile = 'Open file'; + Notifications.info(`Exported ${siteAppCatalogs.length} site app catalog(s) to '${basename(targetUri.fsPath)}'.`, openFile).then((selectedOption) => { + if (selectedOption === openFile) { + commands.executeCommand('vscode.open', targetUri); + } + }); + } catch (e: any) { + const message = e?.error?.message || e?.message || 'An unexpected error occurred during the export.'; + Notifications.error(message); + } + } + + /** + * Builds the content of the site app catalogs export file. + * + * @param siteAppCatalogs The site app catalogs to include in the export. + * @returns The serialized export content. + */ + private static buildSiteAppCatalogsExport(siteAppCatalogs: SiteAppCatalogExport[]): string { + return JSON.stringify({ + generatedOn: new Date().toISOString(), + tenantUrl: EnvironmentInformation.tenantUrl, + siteAppCatalogs + }, null, 2); + } + /** * Upgrades the project by generating the upgrade steps and displaying them in a Markdown preview. * @private diff --git a/src/services/actions/Dependencies.ts b/src/services/actions/Dependencies.ts index 7aab61b3..0c076b81 100644 --- a/src/services/actions/Dependencies.ts +++ b/src/services/actions/Dependencies.ts @@ -87,14 +87,8 @@ export class Dependencies { const packageManager = getPackageManager(); let installCommand: string; - // Handle different global installation syntax for each package manager - if (packageManager === PackageManagers.yarn) { - installCommand = `${packageManager} global add ${dependencies} @microsoft/generator-sharepoint@${spfxVersion.Version}`; - } else { - // npm and pnpm both use: -g - const cmd = packageManager === PackageManagers.npm ? 'install' : 'add'; - installCommand = `${packageManager} ${cmd} -g ${dependencies} @microsoft/generator-sharepoint@${spfxVersion.Version}`; - } + const cmd = packageManager === PackageManagers.npm ? 'install' : 'add'; + installCommand = `${packageManager} ${cmd} -g ${dependencies} @microsoft/generator-sharepoint@${spfxVersion.Version}`; await TerminalCommandExecuter.runCommandAndWait(installCommand, 'Installing dependencies', 'cloud-download'); diff --git a/src/test/suite/packageManager.test.ts b/src/test/suite/packageManager.test.ts index 33728cef..f1a06ec9 100644 --- a/src/test/suite/packageManager.test.ts +++ b/src/test/suite/packageManager.test.ts @@ -25,7 +25,7 @@ suite('Package Manager Settings', () => { const packageManagerSetting = contributedSettings['spfx-toolkit.packageManager']; assert(packageManagerSetting, 'Package Manager setting is not contributed'); - assert.deepStrictEqual(packageManagerSetting.enum, ['npm', 'pnpm', 'yarn']); + assert.deepStrictEqual(packageManagerSetting.enum, ['npm', 'pnpm']); }); test('should verify that package manager setting can be read', async () => { @@ -39,6 +39,5 @@ suite('Package Manager Settings', () => { test('should verify that PackageManagers enum has correct values', () => { assert.strictEqual(PackageManagers.npm, 'npm'); assert.strictEqual(PackageManagers.pnpm, 'pnpm'); - assert.strictEqual(PackageManagers.yarn, 'yarn'); }); }); diff --git a/src/utils/getInstallCommand.ts b/src/utils/getInstallCommand.ts index e5418a4a..73280a14 100644 --- a/src/utils/getInstallCommand.ts +++ b/src/utils/getInstallCommand.ts @@ -3,9 +3,9 @@ import { PackageManagers } from '../constants'; /** * Helper function to get the appropriate command for adding specific packages. * When installing all dependencies (no package names), all package managers use 'install'. - * When adding specific packages, npm uses 'install' while pnpm and yarn use 'add'. + * When adding specific packages, npm uses 'install' while pnpm use 'add'. * @param packageManager - The package manager to use - * @returns The command for adding specific packages ('install' for npm, 'add' for pnpm/yarn) + * @returns The command for adding specific packages ('install' for npm, 'add' for pnpm) */ export const getInstallCommand = (packageManager: PackageManagers): string => { return packageManager === PackageManagers.npm ? 'install' : 'add'; diff --git a/src/utils/getPackageManager.ts b/src/utils/getPackageManager.ts index f86efee1..0e16ada8 100644 --- a/src/utils/getPackageManager.ts +++ b/src/utils/getPackageManager.ts @@ -4,7 +4,7 @@ import { PackageManagers } from '../constants'; /** * Retrieves the package manager setting from VS Code extension settings. * If the setting is not found, defaults to 'npm'. - * @returns The package manager to use (npm, pnpm, or yarn) + * @returns The package manager to use (npm, pnpm) */ export const getPackageManager = (): PackageManagers => { return getExtensionSettings('packageManager', PackageManagers.npm);