Skip to content

Commit 18afc6a

Browse files
committed
refactor(fx-core): delegate manifest macro resolution to app-manifest
Rewrite envFunctionUtils as a thin adapter over the resolver primitives now provided by @microsoft/app-manifest (reached transitively via @microsoft/teamsfx-api). expandVariableWithFunction keeps its signature and telemetry loop, delegating each call to resolveManifestFunctionCall and mapping the resolver's plain errors to localized UserError and fx-core FileNotFoundError via toFxError. Update the affected tests to drive resolution through real temp files instead of fs mocks, since file reads now happen inside app-manifest using node:fs/promises rather than the previously mocked fs-extra layer.
1 parent c57b031 commit 18afc6a

3 files changed

Lines changed: 102 additions & 162 deletions

File tree

packages/fx-core/src/component/utils/envFunctionUtils.ts

Lines changed: 49 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@ import {
88
Result,
99
UserError,
1010
UserErrorOptions,
11+
// Host-agnostic manifest-template resolution, extracted into @microsoft/app-manifest
12+
// and re-exported by @microsoft/teamsfx-api. fx-core stays the home for telemetry,
13+
// localized messages, and FxError mapping; the resolution logic lives one layer down.
14+
ManifestType,
15+
resolveManifestFunctionCall,
16+
UnsupportedFileFormatError as ManifestUnsupportedFileFormatError,
17+
InvalidFunctionError as ManifestInvalidFunctionError,
18+
InvalidFunctionParameterError as ManifestInvalidFunctionParameterError,
19+
ReadFileError as ManifestReadFileError,
20+
FileNotFoundError as ManifestFileNotFoundError,
1121
} from "@microsoft/teamsfx-api";
12-
import path from "path";
13-
import fs from "fs-extra";
14-
import stripBom from "strip-bom";
1522
import { FileNotFoundError } from "../../error";
16-
import { expandEnvironmentVariable } from "./common";
1723
import { getLocalizedString } from "../../common/localizeUtils";
1824
import { DriverContext } from "../driver/interface/commonArgs";
1925

@@ -26,12 +32,40 @@ enum TelemetryPropertyKey {
2632
functionCount = "function-count",
2733
}
2834

29-
export enum ManifestType {
30-
TeamsManifest = "teams-manifest",
31-
PluginManifest = "plugin-manifest",
32-
DeclarativeCopilotManifest = "declarative-copilot-manifest",
33-
ApiSpec = "api-spec",
34-
EmbeddedKnowledgeFile = "embedded-knowledge-file",
35+
// Re-exported for existing importers (ManifestUtils, PluginManifestUtils, utils, createAppPackage).
36+
export { ManifestType };
37+
38+
// Map a plain error thrown by @microsoft/app-manifest's resolver to the localized
39+
// FxError surface fx-core drivers expect, emitting the same diagnostic logs as before.
40+
function toFxError(e: unknown, ctx: DriverContext): FxError {
41+
if (e instanceof ManifestUnsupportedFileFormatError) {
42+
ctx.logProvider.error(
43+
getLocalizedString("core.envFunc.unsupportedFile.errorLog", e.filePath, "txt")
44+
);
45+
return new UnsupportedFileFormatError(ctx.platform);
46+
}
47+
if (e instanceof ManifestInvalidFunctionError) {
48+
ctx.logProvider.error(
49+
getLocalizedString("core.envFunc.unsupportedFunction.errorLog", e.token, "file")
50+
);
51+
return new InvalidFunctionError(ctx.platform);
52+
}
53+
if (e instanceof ManifestInvalidFunctionParameterError) {
54+
ctx.logProvider.error(
55+
getLocalizedString("core.envFunc.invalidFunctionParameter.errorLog", e.token, "file")
56+
);
57+
return new InvalidFunctionParameter(ctx.platform);
58+
}
59+
if (e instanceof ManifestReadFileError) {
60+
ctx.logProvider.error(
61+
getLocalizedString("core.envFunc.readFile.errorLog", e.filePath, e.cause?.toString())
62+
);
63+
return new ReadFileError(ctx.platform, e.filePath);
64+
}
65+
if (e instanceof ManifestFileNotFoundError) {
66+
return new FileNotFoundError(source, e.filePath);
67+
}
68+
throw e;
3569
}
3670

3771
export async function expandVariableWithFunction(
@@ -50,16 +84,12 @@ export async function expandVariableWithFunction(
5084
}
5185
let count = 0;
5286
for (const placeholder of matches) {
53-
const processedRes = await processFunction(
54-
placeholder.slice(2, -1).trim(),
55-
ctx,
56-
envs,
57-
fromPath
58-
);
59-
if (processedRes.isErr()) {
60-
return err(processedRes.error);
87+
let value: string;
88+
try {
89+
value = await resolveManifestFunctionCall(placeholder.slice(2, -1).trim(), envs, fromPath);
90+
} catch (e) {
91+
return err(toFxError(e, ctx));
6192
}
62-
let value = processedRes.value;
6393
if (isJson && value) {
6494
value = JSON.stringify(value).slice(1, -1);
6595
}
@@ -78,95 +108,6 @@ export async function expandVariableWithFunction(
78108
return ok(content);
79109
}
80110

81-
async function processFunction(
82-
content: string,
83-
ctx: DriverContext,
84-
envs: { [key in string]: string } | undefined,
85-
path: string
86-
): Promise<Result<string, FxError>> {
87-
const firstTrimmedContent = content.trim();
88-
if (!firstTrimmedContent.startsWith("file(") || !firstTrimmedContent.endsWith(")")) {
89-
ctx.logProvider.error(
90-
getLocalizedString("core.envFunc.unsupportedFunction.errorLog", firstTrimmedContent, "file")
91-
);
92-
return err(new InvalidFunctionError(ctx.platform));
93-
}
94-
95-
// file()
96-
const trimmedParameter = content.slice(5, -1).trim();
97-
if (trimmedParameter[0] === "'" && trimmedParameter[trimmedParameter.length - 1] === "'") {
98-
// static string as function parameter
99-
const res = await readFileContent(
100-
trimmedParameter.substring(1, trimmedParameter.length - 1),
101-
ctx,
102-
envs,
103-
path
104-
);
105-
return res;
106-
} else if (trimmedParameter.startsWith("${{") && trimmedParameter.endsWith("}}")) {
107-
// env variable inside
108-
const resolvedParameter = expandEnvironmentVariable(trimmedParameter, envs);
109-
110-
const res = readFileContent(resolvedParameter, ctx, envs, path);
111-
return res;
112-
} else if (trimmedParameter.startsWith("file(") && trimmedParameter.endsWith(")")) {
113-
// nested function inside
114-
const processsedRes = await processFunction(trimmedParameter, ctx, envs, path);
115-
116-
if (processsedRes.isErr()) {
117-
return err(processsedRes.error);
118-
}
119-
120-
const readFileRes = await readFileContent(processsedRes.value, ctx, envs, path);
121-
return readFileRes;
122-
} else {
123-
// invalid content inside function
124-
ctx.logProvider.error(
125-
getLocalizedString("core.envFunc.invalidFunctionParameter.errorLog", trimmedParameter, "file")
126-
);
127-
return err(new InvalidFunctionParameter(ctx.platform));
128-
}
129-
}
130-
131-
async function readFileContent(
132-
filePath: string,
133-
ctx: DriverContext,
134-
envs: { [key in string]: string } | undefined,
135-
fromPath: string
136-
): Promise<Result<string, FxError>> {
137-
const ext = path.extname(filePath);
138-
if (ext.toLowerCase() !== ".txt" && ext.toLowerCase() !== ".md") {
139-
ctx.logProvider.error(
140-
getLocalizedString("core.envFunc.unsupportedFile.errorLog", filePath, "txt")
141-
);
142-
return err(new UnsupportedFileFormatError(ctx.platform));
143-
}
144-
145-
const absolutePath = getAbsolutePath(filePath, fromPath);
146-
if (await fs.pathExists(absolutePath)) {
147-
try {
148-
let fileContent = await fs.readFile(absolutePath, "utf8");
149-
fileContent = stripBom(fileContent);
150-
let processedFileContent = expandEnvironmentVariable(fileContent, envs);
151-
processedFileContent = processedFileContent.replace(/\r\n/g, "\n");
152-
return ok(processedFileContent);
153-
} catch (e) {
154-
ctx.logProvider.error(
155-
getLocalizedString("core.envFunc.readFile.errorLog", absolutePath, e?.toString())
156-
);
157-
return err(new ReadFileError(ctx.platform, absolutePath));
158-
}
159-
} else {
160-
return err(new FileNotFoundError(source, filePath));
161-
}
162-
}
163-
164-
function getAbsolutePath(relativeOrAbsolutePath: string, fromPath: string): string {
165-
return path.isAbsolute(relativeOrAbsolutePath)
166-
? relativeOrAbsolutePath
167-
: path.join(path.dirname(fromPath), relativeOrAbsolutePath);
168-
}
169-
170111
class UnsupportedFileFormatError extends UserError {
171112
constructor(platform: Platform | undefined) {
172113
const message =

packages/fx-core/tests/component/driver/teamsApp/manifestUtils.test.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "@microsoft/teamsfx-api";
1111
import fs from "fs-extra";
1212
import mockedEnv, { RestoreFn } from "mocked-env";
13+
import os from "os";
1314
import path from "path";
1415
import { assert, vi } from "vitest";
1516
import {
@@ -527,13 +528,18 @@ describe("trimManifestShortName", () => {
527528
describe("resolveLocFile", () => {
528529
const sandbox = vi;
529530
let mockedEnvRestore: RestoreFn;
531+
let tmpDir: string | undefined;
530532

531-
afterEach(() => {
533+
afterEach(async () => {
532534
if (mockedEnvRestore) {
533535
mockedEnvRestore();
534536
}
535537
vi.restoreAllMocks();
536538
vi.restoreAllMocks();
539+
if (tmpDir) {
540+
await fs.remove(tmpDir);
541+
tmpDir = undefined;
542+
}
537543
});
538544

539545
it("returns error when loc file doesn't exist", async () => {
@@ -582,21 +588,17 @@ describe("resolveLocFile", () => {
582588
});
583589

584590
it("resolves $[file(...)] when context is provided", async () => {
585-
vi.spyOn(fs, "pathExists").mockImplementation(async (filePath) => {
586-
return filePath === "loc_file_path" || filePath === "instruction.txt";
587-
});
588-
vi.spyOn(fs, "readFile").mockImplementation(((filePath: number | fs.PathLike) => {
589-
if (filePath === "loc_file_path") {
590-
return Promise.resolve(
591-
JSON.stringify({
592-
name: {
593-
short: "$[file('instruction.txt')]",
594-
},
595-
})
596-
);
597-
}
598-
return Promise.resolve("localized short name");
599-
}) as any);
591+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "resolveloc-"));
592+
const locFilePath = path.join(tmpDir, "loc_file.json");
593+
await fs.writeFile(
594+
locFilePath,
595+
JSON.stringify({
596+
name: {
597+
short: "$[file('instruction.txt')]",
598+
},
599+
})
600+
);
601+
await fs.writeFile(path.join(tmpDir, "instruction.txt"), "localized short name");
600602

601603
const context: any = {
602604
platform: Platform.VSCode,
@@ -608,7 +610,7 @@ describe("resolveLocFile", () => {
608610
},
609611
};
610612

611-
const locFile = await manifestUtils.resolveLocFile("loc_file_path", context);
613+
const locFile = await manifestUtils.resolveLocFile(locFilePath, context);
612614

613615
assert.isTrue(locFile.isOk());
614616
if (locFile.isOk()) {

0 commit comments

Comments
 (0)