Skip to content

Commit fac0cb8

Browse files
feat(tsp): run npm/install and typeSpec/compile before package for TSP projects (#14194)
* perf(tsp): run npm/install and typeSpec/compile before package for TSP projects * perf: fix comment
1 parent 4480c85 commit fac0cb8

File tree

6 files changed

+226
-0
lines changed

6 files changed

+226
-0
lines changed

packages/fx-core/src/common/projectTypeChecker.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
MetadataV3,
1414
MetadataV4,
1515
} from "./versionMetadata";
16+
import { pathUtils } from "../component/utils/pathUtils";
1617

1718
export const TeamsJsModule = "@microsoft/teams-js";
1819

@@ -304,4 +305,15 @@ export function IsDeclarativeAgentManifest(manifest: any): boolean {
304305
manifest.copilotAgents?.declarativeAgents && manifest.copilotAgents.declarativeAgents.length > 0
305306
);
306307
}
308+
309+
export function isTypeSpecProject(projectPath: string): boolean {
310+
const yamlFilePath = pathUtils.getYmlFilePath(projectPath);
311+
if (!yamlFilePath) {
312+
return false;
313+
}
314+
315+
const yamlContent = fs.readFileSync(yamlFilePath, "utf8");
316+
return yamlContent.includes("typeSpec/compile");
317+
}
318+
307319
export const projectTypeChecker = new ProjectTypeChecker();

packages/fx-core/src/common/tools.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { GraphReadUserScopes, SPFxScopes } from "./constants";
1313
import fs from "fs-extra";
1414
import path from "path";
1515
import { MetadataV3, MetadataV4 } from "./versionMetadata";
16+
import { pathUtils } from "../component/utils/pathUtils";
17+
import { TypeSpecCompileArgs } from "../component/driver/typeSpec/interface/typeSpecCompileArgs";
18+
import { parseDocument } from "yaml";
1619

1720
export async function getSideloadingStatus(token: string): Promise<boolean | undefined> {
1821
return teamsDevPortalClient.getSideloadingStatus(token);
@@ -114,3 +117,38 @@ export function isTestToolEnabledProject(projectPath: string): boolean {
114117
}
115118
return false;
116119
}
120+
121+
export function getTypeSpecArgs(projectPath: string): TypeSpecCompileArgs {
122+
const defaultArgs = {
123+
path: "./main.tsp",
124+
manifestPath: "./appPackage/manifest.json",
125+
outputDir: "./appPackage/.generated",
126+
typeSpecConfigPath: "./tspconfig.yaml",
127+
};
128+
const yamlFilePath = pathUtils.getYmlFilePath(projectPath);
129+
if (!yamlFilePath) {
130+
return defaultArgs;
131+
}
132+
133+
const yamlContent = fs.readFileSync(yamlFilePath, "utf8");
134+
const document = parseDocument(yamlContent);
135+
const provisionNode = document.get("provision") as any;
136+
if (!provisionNode) {
137+
return defaultArgs;
138+
}
139+
140+
const tspCompileAction = provisionNode.items.find(
141+
(item: any) => item.get("uses") === "typeSpec/compile"
142+
);
143+
if (!tspCompileAction) {
144+
return defaultArgs;
145+
}
146+
147+
const args = tspCompileAction.get("with");
148+
return {
149+
path: args.get("path") ?? defaultArgs.path,
150+
manifestPath: args.get("manifestPath") ?? defaultArgs.manifestPath,
151+
outputDir: args.get("outputDir") ?? defaultArgs.outputDir,
152+
typeSpecConfigPath: args.get("typeSpecConfigPath") ?? defaultArgs.typeSpecConfigPath,
153+
};
154+
}

packages/fx-core/src/core/FxCore.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
import {
7070
IsDeclarativeAgentManifest,
7171
ProjectTypeResult,
72+
isTypeSpecProject,
7273
projectTypeChecker,
7374
} from "../common/projectTypeChecker";
7475
import { TelemetryEvent, TelemetryProperty, telemetryUtils } from "../common/telemetry";
@@ -205,6 +206,10 @@ import { CoreTelemetryEvent, CoreTelemetryProperty } from "./telemetry";
205206
import { CoreHookContext, PreProvisionResForVS, VersionCheckRes } from "./types";
206207
import { InstallAppArgs } from "../component/driver/devChannel/interfaces/InstallAppArgs";
207208
import { TemplateNames } from "../component/generator/templates/templateNames";
209+
import { getTypeSpecArgs } from "../common/tools";
210+
import { NpmBuildDriver } from "../component/driver/script/npmBuildDriver";
211+
import { TypeSpecCompileDriver } from "../component/driver/typeSpec/compile";
212+
import { TypeSpecCompileArgs } from "../component/driver/typeSpec/interface/typeSpecCompileArgs";
208213

209214
export class FxCore {
210215
constructor(tools: Tools) {
@@ -1259,6 +1264,30 @@ export class FxCore {
12591264

12601265
const context: DriverContext = createDriverContext(inputs);
12611266

1267+
// For TSP projects
1268+
const isTspProject = isTypeSpecProject(inputs.projectPath!);
1269+
if (isTspProject) {
1270+
// Call npm/install
1271+
const npmInstallDriver: NpmBuildDriver = Container.get("cli/runNpmCommand");
1272+
const npmInstallArgs = {
1273+
args: "install --no-audit --progress=false",
1274+
};
1275+
const npmInstallResult = (await npmInstallDriver.execute(npmInstallArgs, context)).result;
1276+
if (npmInstallResult.isErr()) {
1277+
throw err(npmInstallResult.error);
1278+
}
1279+
1280+
// call typespec/compile
1281+
const typeSpecCompileDriver: TypeSpecCompileDriver = Container.get("typeSpec/compile");
1282+
const typeSpecCompileArgs: TypeSpecCompileArgs = getTypeSpecArgs(inputs.projectPath!);
1283+
const typeSpecCompileResult = (
1284+
await typeSpecCompileDriver.execute(typeSpecCompileArgs, context)
1285+
).result;
1286+
if (typeSpecCompileResult.isErr()) {
1287+
throw err(typeSpecCompileResult.error);
1288+
}
1289+
}
1290+
12621291
const teamsAppManifestFilePath = inputs?.[QuestionNames.TeamsAppManifestFilePath] as string;
12631292

12641293
const driver: CreateAppPackageDriver = Container.get("teamsApp/zipAppPackage");

packages/fx-core/tests/common/projectTypeChecker.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ import {
1111
SPFxKey,
1212
TeamsfxVersionState,
1313
getCapabilities,
14+
isTypeSpecProject,
1415
projectTypeChecker,
1516
} from "../../src/common/projectTypeChecker";
1617
import { MetadataV2, MetadataV3 } from "../../src/common/versionMetadata";
1718
import { IsDeclarativeAgentManifest } from "../../build/common/projectTypeChecker";
19+
import { pathUtils } from "../../src/component/utils/pathUtils";
20+
import * as chai from "chai";
1821

1922
describe("ProjectTypeChecker", () => {
2023
const sandbox = sinon.createSandbox();
@@ -547,4 +550,31 @@ describe("ProjectTypeChecker", () => {
547550
assert.isFalse(isDeclarativeAgent);
548551
});
549552
});
553+
554+
describe("isTypeSpecProject", () => {
555+
const sandbox = sinon.createSandbox();
556+
afterEach(() => {
557+
sandbox.restore();
558+
});
559+
560+
it("should return true if TypeSpec project", () => {
561+
sandbox.stub(pathUtils, "getYmlFilePath").returns("m365agents.yml");
562+
sandbox.stub(fs, "readFileSync").returns("provision: typeSpec/compile with: []");
563+
const result = isTypeSpecProject("test-project-path");
564+
chai.expect(result).to.be.true;
565+
});
566+
567+
it("should return false if no yaml file", () => {
568+
sandbox.stub(pathUtils, "getYmlFilePath").returns(undefined);
569+
const result = isTypeSpecProject("test-project-path");
570+
chai.expect(result).to.be.false;
571+
});
572+
573+
it("should return false if not TypeSpec project", () => {
574+
sandbox.stub(pathUtils, "getYmlFilePath").returns("m365agents.yml");
575+
sandbox.stub(fs, "readFileSync").returns("provision: aadApp/create with: []");
576+
const result = isTypeSpecProject("test-project-path");
577+
chai.expect(result).to.be.false;
578+
});
579+
});
550580
});

packages/fx-core/tests/common/tools.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ import {
1919
listDevTunnels,
2020
isTestToolEnabledProject,
2121
isSandboxedEnabled,
22+
getTypeSpecArgs,
2223
} from "../../src/common/tools";
2324
import { PackageService } from "../../src/component/m365/packageService";
2425
import { isVideoFilterProject } from "../../src/core/middleware/videoFilterAppBlocker";
2526
import { isUserCancelError } from "../../src/error/common";
2627
import { MockedM365Provider, MockTools } from "../core/utils";
2728
import { GraphClient } from "../../src/client/graphClient";
2829
import { setTools } from "../../src/common/globalVars";
30+
import { pathUtils } from "../../src";
2931

3032
chai.use(chaiAsPromised);
3133

@@ -446,4 +448,78 @@ projectId: 00000000-0000-0000-0000-000000000000`;
446448
chai.expect(result).to.be.false;
447449
});
448450
});
451+
452+
describe("getTypeSpecArgs", () => {
453+
const sandbox = sinon.createSandbox();
454+
afterEach(() => {
455+
sandbox.restore();
456+
});
457+
458+
it("should return default args if no yaml file", () => {
459+
sandbox.stub(pathUtils, "getYmlFilePath").returns(undefined);
460+
const result = getTypeSpecArgs("test-project-path");
461+
chai.expect(result).to.deep.equal({
462+
path: "./main.tsp",
463+
manifestPath: "./appPackage/manifest.json",
464+
outputDir: "./appPackage/.generated",
465+
typeSpecConfigPath: "./tspconfig.yaml",
466+
});
467+
});
468+
469+
it("should return default args if no provision node", () => {
470+
sandbox.stub(pathUtils, "getYmlFilePath").returns("m365agents.yml");
471+
sandbox.stub(fs, "readFileSync").returns("version: 1.0.0");
472+
const result = getTypeSpecArgs("test-project-path");
473+
chai.expect(result).to.deep.equal({
474+
path: "./main.tsp",
475+
manifestPath: "./appPackage/manifest.json",
476+
outputDir: "./appPackage/.generated",
477+
typeSpecConfigPath: "./tspconfig.yaml",
478+
});
479+
});
480+
481+
it("should return default args if no tspCompileAction", () => {
482+
sandbox.stub(pathUtils, "getYmlFilePath").returns("m365agents.yml");
483+
sandbox.stub(fs, "readFileSync").returns("provision: []");
484+
const result = getTypeSpecArgs("test-project-path");
485+
chai.expect(result).to.deep.equal({
486+
path: "./main.tsp",
487+
manifestPath: "./appPackage/manifest.json",
488+
outputDir: "./appPackage/.generated",
489+
typeSpecConfigPath: "./tspconfig.yaml",
490+
});
491+
});
492+
493+
it("should return args from tspCompileAction", () => {
494+
sandbox.stub(pathUtils, "getYmlFilePath").returns("m365agents.yml");
495+
sandbox
496+
.stub(fs, "readFileSync")
497+
.returns(
498+
"provision:\n - uses: typeSpec/compile\n with:\n path: ./custom.tsp\n manifestPath: ./customManifest.json\n outputDir: ./customOutputDir\n typeSpecConfigPath: ./customTspconfig.yaml"
499+
);
500+
const result = getTypeSpecArgs("test-project-path");
501+
chai.expect(result).to.deep.equal({
502+
path: "./custom.tsp",
503+
manifestPath: "./customManifest.json",
504+
outputDir: "./customOutputDir",
505+
typeSpecConfigPath: "./customTspconfig.yaml",
506+
});
507+
});
508+
509+
it("should return args from default if missing parameter", () => {
510+
sandbox.stub(pathUtils, "getYmlFilePath").returns("m365agents.yml");
511+
sandbox
512+
.stub(fs, "readFileSync")
513+
.returns(
514+
"provision:\n - uses: typeSpec/compile\n with:\n path2: ./custom.tsp\n manifestPath2: ./customManifest.json\n outputDir2: ./customOutputDir\n typeSpecConfigPath2: ./customTspconfig.yaml"
515+
);
516+
const result = getTypeSpecArgs("test-project-path");
517+
chai.expect(result).to.deep.equal({
518+
path: "./main.tsp",
519+
manifestPath: "./appPackage/manifest.json",
520+
outputDir: "./appPackage/.generated",
521+
typeSpecConfigPath: "./tspconfig.yaml",
522+
});
523+
});
524+
});
449525
});

packages/fx-core/tests/core/FxCore.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ import { validationUtils } from "../../src/ui/validationUtils";
126126
import { MockTools, MockUserInteraction, randomAppName } from "./utils";
127127
import { TabCapabilityOptions } from "../../src/question/scaffold/vsc/CapabilityOptions";
128128
import { InstallAppToChannelDriver } from "../../src/component/driver/devChannel/installApp";
129+
import * as CommonTools from "../../src/common/tools";
130+
import * as ProjecTypeChecker from "../../src/common/projectTypeChecker";
131+
import { NpmBuildDriver } from "../../src/component/driver/script/npmBuildDriver";
132+
import { TypeSpecCompileDriver } from "../../src/component/driver/typeSpec/compile";
129133

130134
const tools = new MockTools();
131135

@@ -2283,6 +2287,43 @@ describe("Teams app APIs", async () => {
22832287
sinon.assert.calledOnce(showMessageStub);
22842288
});
22852289

2290+
it("create app package with TypeSpec project", async () => {
2291+
setTools(tools);
2292+
const appName = await mockV3Project();
2293+
const inputs: Inputs = {
2294+
platform: Platform.VSCode,
2295+
[QuestionNames.Folder]: os.tmpdir(),
2296+
[QuestionNames.TeamsAppManifestFilePath]: ".\\appPackage\\manifest.json",
2297+
projectPath: path.join(os.tmpdir(), appName),
2298+
[QuestionNames.OutputZipPathParamName]: ".\\build\\appPackage\\appPackage.dev.zip",
2299+
};
2300+
const isTypeSpecProjectStub = sinon.stub(ProjecTypeChecker, "isTypeSpecProject").returns(true);
2301+
const getTypeSpecArgsStub = sinon.stub(CommonTools, "getTypeSpecArgs").returns({
2302+
path: "./main.tsp",
2303+
manifestPath: "./appPackage/manifest.json",
2304+
outputDir: "./appPackage/.generated",
2305+
typeSpecConfigPath: "./tspconfig.yaml",
2306+
});
2307+
const npmInstallStub = sinon
2308+
.stub(NpmBuildDriver.prototype, "execute")
2309+
.resolves({ result: ok(new Map()), summaries: [] });
2310+
const typeSpecCompileStub = sinon
2311+
.stub(TypeSpecCompileDriver.prototype, "execute")
2312+
.resolves({ result: ok(new Map()), summaries: [] });
2313+
sinon.stub(process, "platform").value("win32");
2314+
const runStub = sinon
2315+
.stub(CreateAppPackageDriver.prototype, "execute")
2316+
.resolves({ result: ok(new Map()), summaries: [] });
2317+
const showMessageStub = sinon.stub(tools.ui, "showMessage");
2318+
await core.createAppPackage(inputs);
2319+
sinon.assert.calledOnce(runStub);
2320+
sinon.assert.calledOnce(showMessageStub);
2321+
sinon.assert.calledOnce(isTypeSpecProjectStub);
2322+
sinon.assert.calledOnce(getTypeSpecArgsStub);
2323+
sinon.assert.calledOnce(npmInstallStub);
2324+
sinon.assert.calledOnce(typeSpecCompileStub);
2325+
});
2326+
22862327
it("publish application", async () => {
22872328
const appName = await mockV3Project();
22882329
const inputs: Inputs = {

0 commit comments

Comments
 (0)