@@ -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" ;
1522import { FileNotFoundError } from "../../error" ;
16- import { expandEnvironmentVariable } from "./common" ;
1723import { getLocalizedString } from "../../common/localizeUtils" ;
1824import { 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
3771export 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-
170111class UnsupportedFileFormatError extends UserError {
171112 constructor ( platform : Platform | undefined ) {
172113 const message =
0 commit comments