Skip to content
Merged
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
4 changes: 2 additions & 2 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 15 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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": "...",
Expand Down Expand Up @@ -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",
Expand Down
8 changes: 7 additions & 1 deletion scripts/update-package-version.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
$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
1 change: 1 addition & 0 deletions src/constants/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
3 changes: 1 addition & 2 deletions src/constants/PackageManagers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// eslint-disable-next-line no-shadow
export enum PackageManagers {
npm = 'npm',
pnpm = 'pnpm',
yarn = 'yarn'
pnpm = 'pnpm'
}
5 changes: 5 additions & 0 deletions src/models/SiteAppCatalogExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface SiteAppCatalogExport {
url: string;
apps: Record<string, unknown>[];
error?: string;
}
1 change: 1 addition & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
112 changes: 103 additions & 9 deletions src/services/actions/CliActions.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -59,6 +60,9 @@ export class CliActions {
subscriptions.push(
commands.registerCommand(Commands.removeSiteAppCatalog, CliActions.removeSiteAppCatalog)
);
subscriptions.push(
commands.registerCommand(Commands.exportSiteAppCatalogs, CliActions.exportSiteAppCatalogs)
);
}

/**
Expand Down Expand Up @@ -175,15 +179,8 @@ export class CliActions {
*/
public static async getAppCatalogApps(appCatalogUrl?: string): Promise<AppCatalogApp[] | undefined> {
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,
Expand All @@ -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<any[]> {
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.
Expand Down Expand Up @@ -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
Expand Down
10 changes: 2 additions & 8 deletions src/services/actions/Dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: <manager> <add/install> -g <packages>
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');

Expand Down
3 changes: 1 addition & 2 deletions src/test/suite/packageManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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');
});
});
4 changes: 2 additions & 2 deletions src/utils/getInstallCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/utils/getPackageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PackageManagers>('packageManager', PackageManagers.npm);
Expand Down