Skip to content

Commit

Permalink
feat: SPFx add web part handling version (#10514)
Browse files Browse the repository at this point in the history
* feat: add web part

* refactor: error

refactor: telemetry

refactor: more

refactor: minor

refactor: cli

test: ut

test: ut

test: ut1

test: ut2

test: ut3

test: ut

test: ut

test: ut

refactor: minor

* refactor: string update
  • Loading branch information
yuqizhou77 authored Dec 15, 2023
1 parent 928e8b6 commit 3a2a715
Show file tree
Hide file tree
Showing 13 changed files with 883 additions and 104 deletions.
3 changes: 2 additions & 1 deletion packages/cli/src/commands/models/addSPFxWebpart.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { CLICommand } from "@microsoft/teamsfx-api";
import { CLICommand, Stage } from "@microsoft/teamsfx-api";
import { SPFxAddWebpartInputs, SPFxAddWebpartOptions } from "@microsoft/teamsfx-core";
import { getFxCore } from "../../activate";
import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents";
Expand All @@ -15,6 +15,7 @@ export const addSPFxWebpartCommand: CLICommand = {
},
handler: async (ctx) => {
const inputs = ctx.optionValues as SPFxAddWebpartInputs;
inputs.stage = Stage.addWebpart;
const core = getFxCore();
const res = await core.addWebpart(inputs);
return res;
Expand Down
13 changes: 12 additions & 1 deletion packages/fx-core/resource/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"plugins.spfx.scaffold.scaffoldProject": "Generate SPFx project using Yeoman CLI",
"plugins.spfx.scaffold.updateManifest": "Update webpart manifest",
"plugins.spfx.GetTenantFailedError": "Cannot get tenant %s %s",
"plugins.spfx.error.installLatestDependencyError": "Encountered unknown issues when setting up SPFx environment in %s folder. You could follow [Set up your SharePoint Framework development environment | Microsoft Learn](%s) to set up global SPFx environment.",
"plugins.spfx.error.installLatestDependencyError": "Encountered unknown issues when setting up SPFx environment in %s folder. Follow [Set up your SharePoint Framework development environment | Microsoft Learn](%s) to set up global SPFx environment.",
"_plugins.spfx.error.installLatestDependencyError.comment": "'Microsoft Learn' and 'SharePoint' are the product brand names which should not be localized.",
"plugins.spfx.error.scaffoldError": "Project creation failed. A possible reason could be from Yeoman SharePoint Generator. Check [Output panel](%s) for details.",
"plugins.spfx.error.import.retrieveSolutionInfo": "Failed to retrieve existing SPFx solution information. Please make sure your SPFx solution is valid.",
Expand All @@ -111,6 +111,17 @@
"plugins.spfx.import.success": "Your SPFx solution has been successfully imported to %s.",
"plugins.spfx.import.log.success": "Teams Toolkit has imported your SPFx solution successfully. A complete log of import details can be found in %s.",
"plugins.spfx.import.log.fail": "Teams Toolkit failed to import your SPFx solution. A complete log of import details can be found in %s.",
"plugins.spfx.addWebPart.confirmInstall": "SPFx version in your solution is %s, not yet installed on your machine. Do you want to install SPFx %s in Teams Toolkit directory to continue adding web part?",
"plugins.spfx.addWebPart.install": "Install",
"plugins.spfx.addWebPart.confirmUpgrade": "Teams Toolkit is using SPFx version %s, but SPFx version in your solution is %s. Do you want to upgrade SPFx version in Teams Toolkit directory to %s and add web part?",
"plugins.spfx.addWebPart.upgrade": "Upgrade",
"plugins.spfx.addWebPart.versionMismatch.continueConfirm": "SPFx version in your solution is %s, not yet installed on this machine. Teams Toolkit uses SPFx installed in Teams Toolkit directory by default (%s). The version mismatch may cause unexpected error. Do you want to continue?",
"plugins.spfx.addWebPart.versionMismatch.help": "Help",
"plugins.spfx.addWebPart.versionMismatch.continue": "Continue",
"plugins.spfx.addWebPart.versionMismatch.output": "SPFx version in your solution is %s. You have %s installed globally and %s installed in Teams Toolkit directory. Teams Toolkit uses SPFx installed in Teams Toolkit directory by default (%s). The version mismatch may cause unexpected error. Find possible solutions in %s.",
"plugins.spfx.addWebPart.versionMismatch.localOnly.output": "SPFx version in your solution is %s. You have %s installed in Teams Toolkit directory. Teams Toolkit uses SPFx installed in Teams Toolkit directory by default (%s). The version mismatch may cause unexpected error. Find possible solutions in %s.",
"plugins.spfx.addWebPart.cannotFindSolutionVersion": "Unable to find SPFx version in your solution in %s",
"plugins.spfx.error.installDependencyError": "Encountered unknown issues when setting up SPFx environment in %s folder. Follow %s to install %s to set up global SPFx environment.",
"plugins.frontend.checkNetworkTip": "Check your network connection.",
"plugins.frontend.checkFsPermissionsTip": "Check if you have Read/Write permissions to your file system.",
"plugins.frontend.checkStoragePermissionsTip": "Check if you have permissions to your Azure Storage Account.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
// Licensed under the MIT license.

export interface DependencyChecker {
install(): Promise<void>;
install(targetVersion: string): Promise<void>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Constants } from "../utils/constants";
import { getExecCommand, Utils } from "../utils/utils";

const name = Constants.GeneratorPackageName;
const displayName = `${name}@${Constants.LatestVersion}`;
const displayName = `${name}`;
const timeout = 6 * 60 * 1000;

export class GeneratorChecker implements DependencyChecker {
Expand All @@ -34,28 +34,30 @@ export class GeneratorChecker implements DependencyChecker {
this._logger = logger;
}

public async ensureLatestDependency(ctx: Context): Promise<Result<boolean, FxError>> {
telemetryHelper.sendSuccessEvent(ctx, TelemetryEvents.EnsureLatestSharepointGeneratorStart);
public async ensureDependency(
ctx: Context,
targetVersion: string
): Promise<Result<boolean, FxError>> {
telemetryHelper.sendSuccessEvent(ctx, TelemetryEvents.EnsureSharepointGeneratorStart);

try {
void this._logger.info(`${displayName} not found, installing...`);
await this.install();
void this._logger.info(`Successfully installed ${displayName}`);
void this._logger.info(`${displayName}@${targetVersion} not found, installing...`);
await this.install(targetVersion);
void this._logger.info(`Successfully installed ${displayName}@${targetVersion}`);

telemetryHelper.sendSuccessEvent(ctx, TelemetryEvents.EnsureLatestSharepointGenerator);
telemetryHelper.sendSuccessEvent(ctx, TelemetryEvents.EnsureSharepointGenerator);
} catch (error) {
telemetryHelper.sendErrorEvent(
ctx,
TelemetryEvents.EnsureLatestSharepointGenerator,
TelemetryEvents.EnsureSharepointGenerator,
error as UserError | SystemError,
{
[TelemetryProperty.EnsureLatestSharepointGeneratorReason]: (
error as UserError | SystemError
).name,
[TelemetryProperty.EnsureSharepointGeneratorReason]: (error as UserError | SystemError)
.name,
}
);
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
this._logger.error(`Failed to install ${displayName}, error = '${error}'`);
this._logger.error(`Failed to install ${displayName}@${targetVersion}, error = '${error}'`);
return err(error as UserError | SystemError);
}

Expand All @@ -64,19 +66,28 @@ export class GeneratorChecker implements DependencyChecker {

public async isLatestInstalled(loadedLatestVersion: string | undefined): Promise<boolean> {
try {
const generatorVersion = await this.queryVersion();
const generatorVersion = await this.findLocalInstalledVersion();
const latestGeneratorVersion = loadedLatestVersion ?? (await this.findLatestVersion(5));
const hasSentinel = await fs.pathExists(this.getSentinelPath());
return !!latestGeneratorVersion && generatorVersion === latestGeneratorVersion && hasSentinel;
return !!latestGeneratorVersion && generatorVersion === latestGeneratorVersion;
} catch (error) {
return false;
}
}

public async install(): Promise<void> {
public async findLocalInstalledVersion(): Promise<string | undefined> {
try {
const generatorVersion = await this.queryVersion();
const hasSentinel = await fs.pathExists(this.getSentinelPath());
return hasSentinel ? generatorVersion : undefined;
} catch (error) {
return undefined;
}
}

public async install(targetVersion: string): Promise<void> {
void this._logger.info("Start installing...");
await this.cleanup();
await this.installGenerator();
await this.installGenerator(targetVersion);

void this._logger.info("Validating package...");
if (!(await this.validate())) {
Expand All @@ -100,9 +111,15 @@ export class GeneratorChecker implements DependencyChecker {
}

public async findGloballyInstalledVersion(
timeoutInSeconds?: number
timeoutInSeconds?: number,
shouldThrowIfNotFind?: boolean
): Promise<string | undefined> {
return await Utils.findGloballyInstalledVersion(this._logger, name, timeoutInSeconds ?? 0);
return await Utils.findGloballyInstalledVersion(
this._logger,
name,
timeoutInSeconds ?? 0,
shouldThrowIfNotFind
);
}

public async findLatestVersion(timeoutInSeconds?: number): Promise<string | undefined> {
Expand Down Expand Up @@ -152,9 +169,9 @@ export class GeneratorChecker implements DependencyChecker {
}
}

private async installGenerator(): Promise<void> {
private async installGenerator(targetVersion: string): Promise<void> {
const version = targetVersion ?? Constants.LatestVersion;
try {
const version = Constants.LatestVersion;
await fs.ensureDir(path.join(this.getDefaultInstallPath(), "node_modules"));
await cpUtils.executeCommand(
undefined,
Expand All @@ -171,7 +188,7 @@ export class GeneratorChecker implements DependencyChecker {

await fs.ensureFile(this.getSentinelPath());
} catch (error) {
void this._logger.error(`Failed to execute npm install ${displayName}`);
void this._logger.error(`Failed to execute npm install ${displayName}@${version}`);
throw NpmInstallError(error as Error);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { getExecCommand, Utils } from "../utils/utils";
import { Constants } from "../utils/constants";

const name = Constants.YeomanPackageName;
const displayName = `${name}@${Constants.LatestVersion}`;
const displayName = `${name}`;
const timeout = 6 * 60 * 1000;

export class YoChecker implements DependencyChecker {
Expand All @@ -34,47 +34,59 @@ export class YoChecker implements DependencyChecker {
this._logger = logger;
}

public async ensureLatestDependency(ctx: Context): Promise<Result<boolean, FxError>> {
telemetryHelper.sendSuccessEvent(ctx, TelemetryEvents.EnsureLatestYoStart);
public async ensureDependency(
ctx: Context,
targetVersion: string
): Promise<Result<boolean, FxError>> {
telemetryHelper.sendSuccessEvent(ctx, TelemetryEvents.EnsureYoStart);
try {
void this._logger.info(`${displayName} not found, installing...`);
await this.install();
void this._logger.info(`Successfully installed ${displayName}`);
void this._logger.info(`${displayName}@${targetVersion} not found, installing...`);
await this.install(targetVersion);
void this._logger.info(`Successfully installed ${displayName}@${targetVersion}`);

telemetryHelper.sendSuccessEvent(ctx, TelemetryEvents.EnsureLatestYo);
telemetryHelper.sendSuccessEvent(ctx, TelemetryEvents.EnsureYo);
} catch (error) {
telemetryHelper.sendErrorEvent(
ctx,
TelemetryEvents.EnsureLatestYo,
TelemetryEvents.EnsureYo,
error as UserError | SystemError,
{
[TelemetryProperty.EnsureLatestYoReason]: (error as UserError | SystemError).name,
[TelemetryProperty.EnsureYoReason]: (error as UserError | SystemError).name,
}
);
this._logger.error(
`Failed to install ${displayName}, error = '${error.toString() as string}'`
`Failed to install ${displayName}@${targetVersion}, error = '${error.toString() as string}'`
);
return err(error as UserError | SystemError);
}

return ok(true);
}

public async isLatestInstalled(): Promise<boolean> {
public async findLocalInstalledVersion(): Promise<string | undefined> {
try {
const yoVersion = await this.queryVersion();
const latestYeomanVersion = await this.findLatestVersion(10);
const hasSentinel = await fs.pathExists(this.getSentinelPath());
return !!latestYeomanVersion && yoVersion === latestYeomanVersion && hasSentinel;
return hasSentinel ? yoVersion : undefined;
} catch (error) {
return undefined;
}
}

public async isLatestInstalled(): Promise<boolean> {
try {
const yoVersion = await this.findLocalInstalledVersion();
const latestYeomanVersion = await this.findLatestVersion(10);
return !!latestYeomanVersion && yoVersion === latestYeomanVersion;
} catch (error) {
return false;
}
}

public async install(): Promise<void> {
public async install(targetVersion: string): Promise<void> {
void this._logger.info("Start installing...");
await this.cleanup();
await this.installYo();
await this.installYo(targetVersion);

void this._logger.info("Validating package...");
if (!(await this.validate())) {
Expand Down Expand Up @@ -161,9 +173,9 @@ export class YoChecker implements DependencyChecker {
}
}

private async installYo(): Promise<void> {
private async installYo(targetVersion: string): Promise<void> {
const version = targetVersion ?? Constants.LatestVersion;
try {
const version = Constants.LatestVersion;
await fs.ensureDir(path.join(this.getDefaultInstallPath(), "node_modules"));
await cpUtils.executeCommand(
undefined,
Expand All @@ -180,7 +192,7 @@ export class YoChecker implements DependencyChecker {

await fs.ensureFile(this.getSentinelPath());
} catch (error) {
void this._logger.error("Failed to execute npm install yo");
void this._logger.error(`Failed to execute npm install ${displayName}@${version}`);
throw NpmInstallError(error as Error);
}
}
Expand Down
33 changes: 33 additions & 0 deletions packages/fx-core/src/component/generator/spfx/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,30 @@ export function LatestPackageInstallError(): SystemError {
);
}

export function PackageTargetVersionInstallError(
packageName: string,
version: string
): SystemError {
const fxFolderPath = "HOME/.fx";
const packageInfo = `${packageName}@${version}`;
return new SystemError(
Constants.PLUGIN_NAME,
"PackageTargetVersionInstallError",
getDefaultString(
"plugins.spfx.error.installDependencyError",
fxFolderPath,
Constants.AddWebpartHelpLink,
packageInfo
),
getLocalizedString(
"plugins.spfx.error.installDependencyError",
fxFolderPath,
Constants.AddWebpartHelpLink,
packageInfo
)
);
}

export function YoGeneratorScaffoldError(): UserError {
return new UserError({
source: Constants.PLUGIN_NAME,
Expand Down Expand Up @@ -128,3 +152,12 @@ export function PathAlreadyExistsError(path: string): UserError {
displayMessage: getLocalizedString("core.QuestionAppName.validation.pathExist", path),
});
}

export function SolutionVersionMissingError(path: string): UserError {
return new UserError({
source: Constants.PLUGIN_NAME,
name: "SolutionVersionMissing",
message: getDefaultString("plugins.spfx.addWebPart.cannotFindSolutionVersion", path),
displayMessage: getLocalizedString("plugins.spfx.addWebPart.cannotFindSolutionVersion", path),
});
}
Loading

0 comments on commit 3a2a715

Please sign in to comment.